import { describe, expect, it } from 'vitest';
import { CAREFUL, DAWDLER, SLOPPY, simulateNight, type NightMetrics } from './economy';
import { VENUES } from '../../src/data/venues';

// The feel, as numbers. Phase-1/Phase-3 both measured the same failures:
// vibe pinned at ~100 by 10 PM, aggro flat at 0, hype a pure ratchet, dawdling
// dominant, the slam fictional. These tests are the tuning pass's contract —
// if a knob change breaks a band, the feel regressed, not just a constant.

const SEEDS = [4207, 4208, 4209, 7, 99];

const runAll = (policy: typeof CAREFUL): NightMetrics[] => SEEDS.map((s) => simulateNight(s, policy));

const avg = (xs: number[]): number => xs.reduce((a, b) => a + b, 0) / xs.length;

const summarise = (runs: NightMetrics[]): string =>
  `${runs[0]!.policy}: end=${runs.map((r) => `${r.endReason}@${r.endClockMin}`).join(',')} ` +
  `admit=${avg(runs.map((r) => r.admitted)).toFixed(0)} deny=${avg(runs.map((r) => r.denied)).toFixed(0)} ` +
  `vibePin=${avg(runs.map((r) => r.vibePinnedShare)).toFixed(2)} aggroMax=${avg(runs.map((r) => r.aggroMax)).toFixed(0)} ` +
  `slamQ(avg/max)=${avg(runs.map((r) => r.slamQueueAvg)).toFixed(1)}/${Math.max(...runs.map((r) => r.slamQueueMax))} ` +
  `hype=${avg(runs.map((r) => r.hypePeak)).toFixed(1)} strikes=${avg(runs.map((r) => r.heatStrikes)).toFixed(1)} inside=${avg(runs.map((r) => r.inside)).toFixed(0)}`;

describe('door economy (bot nights, 5 seeds)', () => {
  const careful = runAll(CAREFUL);
  const sloppy = runAll(SLOPPY);
  const dawdler = runAll(DAWDLER);

  it('prints the current shape (baseline visibility)', () => {
    console.log('\n' + [careful, sloppy, dawdler].map(summarise).join('\n'));
    expect(careful.length).toBe(SEEDS.length);
  });

  it('careful play survives to 3 AM', () => {
    for (const r of careful) expect(r.endReason).toBe('clock');
  });

  it('careful play keeps vibe ALIVE — not pinned at the ceiling', () => {
    // The meter must stay a live pressure: less than half of the post-11PM
    // samples may sit at 95+, and the night must actually visit the low band.
    expect(avg(careful.map((r) => r.vibePinnedShare))).toBeLessThan(0.5);
  });

  it('careful play fills the room — admits comfortably beat denies', () => {
    const a = avg(careful.map((r) => r.admitted));
    const d = avg(careful.map((r) => r.denied));
    expect(a).toBeGreaterThan(d * 1.2);
  });

  it('the slam is real — the queue visibly backs up mid-night', () => {
    expect(Math.max(...careful.map((r) => r.slamQueueMax))).toBeGreaterThanOrEqual(6);
    expect(avg(careful.map((r) => r.slamQueueAvg))).toBeGreaterThan(1.5);
  });

  it('aggro breathes for a careful player — pressure without a riot', () => {
    const peaks = careful.map((r) => r.aggroMax);
    expect(avg(peaks)).toBeGreaterThan(15);
    for (const p of peaks) expect(p).toBeLessThan(95);
  });

  it('sloppy play is punished: strikes pile up', () => {
    expect(avg(sloppy.map((r) => r.heatStrikes))).toBeGreaterThanOrEqual(3);
  });

  it('dawdling is NOT dominant — a stalled rope riots before it profits', () => {
    // Dying is worth nothing: a night that ends early scores zero. The cash
    // proxy mirrors nightCash's shape (final vibe + hype bonus), gated on
    // actually surviving to 3 AM.
    const score = (r: NightMetrics): number =>
      r.endReason === 'clock' ? r.vibeSamples[r.vibeSamples.length - 1]! + (r.hypePeak - 1) * 30 : 0;
    expect(avg(dawdler.map(score))).toBeLessThanOrEqual(avg(careful.map(score)) + 1);
  });

  it('careful play does not bleed licence points to invisible information', () => {
    // Strikes must be EARNABLE knowledge: a bot that checks everything checkable
    // should end most nights clean (scripted encounters may cost the odd one).
    expect(avg(careful.map((r) => r.heatStrikes))).toBeLessThan(1);
  });
});

describe('the venue ladder changes the pressure (design §6)', () => {
  it('The Royal is a gentler week than Voltage for the same careful hands', () => {
    const royal = VENUES[0]!;
    const seeds = [4207, 4208, 4209];
    const atRoyal = seeds.map((s) =>
      simulateNight(s, CAREFUL, { arrivalScale: royal.arrivalScale, licensed: royal.licensed }),
    );
    const atVoltage = seeds.map((s) => simulateNight(s, CAREFUL));
    const avgOf = (xs: number[]): number => xs.reduce((a, b) => a + b, 0) / xs.length;
    // Fewer arrivals -> a visibly calmer slam and a survivable night.
    expect(avgOf(atRoyal.map((r) => r.slamQueueAvg))).toBeLessThan(
      avgOf(atVoltage.map((r) => r.slamQueueAvg)),
    );
    for (const r of atRoyal) expect(r.endReason).toBe('clock');
  });
});
