import { beforeEach, describe, expect, it } from 'vitest';
import { SeededRNG } from '../src/core/SeededRNG';
import { ageOn } from '../src/core/GameClock';
import { generatePatron, _resetPatronSerial, type GeneratorContext } from '../src/patrons/generator';
import type { Patron } from '../src/data/types';

const NIGHT = new Date('2026-07-18T00:00:00');
const ctx = (seed: number): GeneratorContext => ({ rng: new SeededRNG(seed), nightDate: NIGHT });

const batch = (seed: number, n: number, clockMin = 0): Patron[] => {
  _resetPatronSerial();
  const c = ctx(seed);
  return Array.from({ length: n }, () => generatePatron(c, clockMin));
};

beforeEach(() => _resetPatronSerial());

describe('generatePatron', () => {
  it('is deterministic: same seed → identical batch', () => {
    expect(batch(42, 50)).toEqual(batch(42, 50));
  });

  it('different seeds → different batches', () => {
    const a = batch(1, 20).map((p) => p.dollSeed);
    const b = batch(2, 20).map((p) => p.dollSeed);
    expect(a).not.toEqual(b);
  });

  it('real IDs never carry fake tells and their DOB matches true age', () => {
    for (const p of batch(7, 300)) {
      if (!p.idCard.fake) {
        expect(ageOn(p.idCard.dob, NIGHT)).toBe(p.age);
        expect(p.age).toBeGreaterThanOrEqual(18);
      }
    }
  });

  it('every fake ID has at least one detectable tell', () => {
    const fakes = batch(11, 600).filter((p) => p.idCard.fake);
    expect(fakes.length).toBeGreaterThan(0);
    for (const p of fakes) {
      const f = p.idCard.fake!;
      const expired = new Date(`${p.idCard.expiry}T00:00:00`) < NIGHT;
      const hasTell =
        !!f.peelingLaminate || !!f.wrongHologram || !!f.jokeName || !!f.photoMismatch || expired;
      expect(hasTell).toBe(true);
    }
  });

  it('almost18s are 17 with fake IDs claiming 18+; fresh18s are genuinely 18', () => {
    _resetPatronSerial();
    const c = ctx(13);
    for (let i = 0; i < 40; i++) {
      const kid = generatePatron(c, 0, 'almost18');
      expect(kid.age).toBe(17);
      expect(kid.idCard.fake).toBeDefined();
      expect(ageOn(kid.idCard.dob, NIGHT)).toBeGreaterThanOrEqual(18);
      const fresh = generatePatron(c, 0, 'fresh18');
      expect(fresh.age).toBe(18);
      expect(fresh.idCard.fake).toBeUndefined();
    }
  });

  it('photoMismatch is the only case where photoSeed differs from dollSeed', () => {
    for (const p of batch(17, 600)) {
      if (p.idCard.fake?.photoMismatch) expect(p.idCard.photoSeed).not.toBe(p.dollSeed);
      else expect(p.idCard.photoSeed).toBe(p.dollSeed);
    }
  });

  it('late crowd arrives drunker on average', () => {
    const early = batch(23, 200, 0);
    const late = batch(23, 200, 300);
    const avg = (ps: Patron[]) => ps.reduce((s, p) => s + p.intoxication, 0) / ps.length;
    expect(avg(late)).toBeGreaterThan(avg(early) + 0.05);
  });

  it('outfits fill every slot and intoxication/tolerance stay in 0..1', () => {
    for (const p of batch(29, 100)) {
      expect(p.outfit.map((l) => l.slot).sort()).toEqual(
        ['accessory', 'hair', 'legs', 'outer', 'shoes', 'top'],
      );
      expect(p.intoxication).toBeGreaterThanOrEqual(0);
      expect(p.intoxication).toBeLessThanOrEqual(1);
      expect(p.tolerance).toBeGreaterThanOrEqual(0);
      expect(p.tolerance).toBeLessThanOrEqual(1);
    }
  });

  it('guest-list truth implies the claim (liars exist, honest listers do not hide)', () => {
    for (const p of batch(31, 400)) {
      if (p.flags.onGuestList) expect(p.flags.claimsGuestList).toBe(true);
    }
  });
});

describe('venue archetype overlays', () => {
  it('weight overrides reshape the crowd without touching the defaults', () => {
    _resetPatronSerial();
    const base = ctx(51);
    const heavy = {
      rng: new SeededRNG(51),
      nightDate: NIGHT,
      archetypeWeights: { influencer: 60, punter: 5 },
    };
    const count = (c: typeof base, n: number): Record<string, number> => {
      const tally: Record<string, number> = {};
      for (let i = 0; i < n; i++) {
        const p = generatePatron(c, 0);
        tally[p.archetype] = (tally[p.archetype] ?? 0) + 1;
      }
      return tally;
    };
    const plain = count(base, 400);
    _resetPatronSerial();
    const vip = count(heavy, 400);
    expect((vip['influencer'] ?? 0)).toBeGreaterThan((plain['influencer'] ?? 0) * 3);
    expect((vip['punter'] ?? 0)).toBeLessThan(plain['punter'] ?? 0);
  });

  it('an override map with no entry leaves that archetype at its default weight', () => {
    _resetPatronSerial();
    const c = { rng: new SeededRNG(52), nightDate: NIGHT, archetypeWeights: {} };
    expect(() => {
      for (let i = 0; i < 50; i++) generatePatron(c, 0);
    }).not.toThrow();
  });
});
