import { describe, expect, it, vi } from 'vitest';

// The functions under test are pure, but they live in modules that subclass
// Phaser.Scene, and Phaser touches `window` at import time under the node
// environment. Stubbing the module is the only way to reach them from a test.
vi.mock('phaser', () => ({ default: { Scene: class {} } }));

import { freshNightState } from '../src/core/meters';
import { DAZZA_SIGNOFF } from '../src/data/strings/door';
import { MAX_HEAT_STRIKES, shouldRecordStrike, type NightLog } from '../src/scenes/door/NightScene';
import { nightCash, signoffFor } from '../src/scenes/door/NightSummaryScene';
import type { NightState } from '../src/data/types';

const log = (over: Partial<NightLog> = {}): NightLog => ({
  vibeHistory: [50],
  verdicts: [],
  admitted: 0,
  denied: 0,
  tested: 0,
  pattedDown: 0,
  hypePeak: 1,
  heatStrikes: [],
  endReason: 'clock',
  seed: 1,
  ...over,
});

const stateWith = (...reasons: string[]): NightState => {
  const s = freshNightState('voltage', 0, 80);
  for (const reason of reasons) s.heatStrikes.push({ reason });
  return s;
};

describe('nightCash', () => {
  it('pays a flat wage for an uneventful night', () => {
    expect(nightCash(log(), stateWith())).toBe(180);
  });

  it('pays $2 per admit on top of the wage', () => {
    const clean = stateWith();
    expect(nightCash(log({ admitted: 10 }), clean)).toBe(nightCash(log(), clean) + 20);
    expect(nightCash(log({ admitted: 40 }), clean)).toBeGreaterThan(
      nightCash(log({ admitted: 10 }), clean),
    );
  });

  it('pays a hype bonus that scales with peak hype, not with final hype', () => {
    const clean = stateWith();
    const base = nightCash(log(), clean);
    expect(nightCash(log({ hypePeak: 2 }), clean)).toBe(base + 90);
    expect(nightCash(log({ hypePeak: 3 }), clean)).toBe(base + 180);
  });

  it('deducts $60 for every heat strike', () => {
    const l = log({ admitted: 20, hypePeak: 2 });
    const none = nightCash(l, stateWith());
    expect(nightCash(l, stateWith('over capacity'))).toBe(none - 60);
    expect(nightCash(l, stateWith('over capacity', 'minor admitted'))).toBe(none - 120);
  });

  it('floors at zero — a catastrophic night pays nothing, it does not invoice you', () => {
    const strikes = Array.from({ length: 20 }, (_, i) => `offence ${i}`);
    expect(nightCash(log(), stateWith(...strikes))).toBe(0);
  });

  it('returns a whole number even when hype is fractional', () => {
    for (const hypePeak of [1, 1.3, 1.75, 2.4, 3]) {
      const cash = nightCash(log({ hypePeak, admitted: 7 }), stateWith());
      expect(Number.isInteger(cash)).toBe(true);
    }
  });
});

describe('signoffFor', () => {
  it('gives the harshest line at two strikes no matter how good the room felt', () => {
    expect(signoffFor(100, 2)).toBe(DAZZA_SIGNOFF[0]);
    expect(signoffFor(100, 3)).toBe(DAZZA_SIGNOFF[0]);
    // one strike is survivable — a great room still earns praise
    expect(signoffFor(100, 1)).not.toBe(DAZZA_SIGNOFF[0]);
  });

  it('never moves to an earlier line as vibe rises (no strikes)', () => {
    for (const strikes of [0, 1]) {
      let lowest = 0;
      for (let vibe = 0; vibe <= 100; vibe++) {
        const idx = DAZZA_SIGNOFF.indexOf(signoffFor(vibe, strikes));
        expect(idx).toBeGreaterThanOrEqual(lowest);
        lowest = idx;
      }
    }
  });

  it('actually varies with vibe rather than always saying the same thing', () => {
    expect(signoffFor(0, 0)).not.toBe(signoffFor(100, 0));
  });

  it('returns a real signoff line across the whole 0..100 band, endpoints included', () => {
    for (let vibe = 0; vibe <= 100; vibe++) {
      const line = signoffFor(vibe, 0);
      expect(line.length).toBeGreaterThan(0);
      expect(DAZZA_SIGNOFF).toContain(line);
    }
  });
});

describe('shouldRecordStrike', () => {
  it('matches CONTRACTS.md §3: max 3 strikes', () => {
    expect(MAX_HEAT_STRIKES).toBe(3);
  });

  it('records a strike the night has not seen before', () => {
    expect(shouldRecordStrike(stateWith(), { reason: 'minor admitted' })).toBe(true);
    expect(shouldRecordStrike(stateWith('over capacity'), { reason: 'minor admitted' })).toBe(true);
  });

  it('stops recording once the licence is already gone', () => {
    const full = stateWith('a', 'b', 'c');
    expect(full.heatStrikes).toHaveLength(MAX_HEAT_STRIKES);
    expect(shouldRecordStrike(full, { reason: 'd' })).toBe(false);
  });

  it('treats a persistent breach as one offence, not one per patron', () => {
    const s = stateWith('over capacity');
    expect(shouldRecordStrike(s, { reason: 'over capacity' })).toBe(false);
    expect(shouldRecordStrike(s, { reason: 'over capacity', deferred: true })).toBe(false);
  });
});
