import { describe, expect, it } from 'vitest';
import { dazzaDue, nextDazza, type DazzaWorld } from '../src/scenes/door/dazzaSchedule';
import { DAZZA_TEXTS, type DazzaLine } from '../src/data/strings/dazza';

// Default world satisfies none of the mood gates, so rule-bearing timing can be
// tested without a gate accidentally letting commentary through.
const world = (over: Partial<DazzaWorld> = {}): DazzaWorld => ({
  vibe: 50,
  aggro: 0,
  queueLength: 0,
  insideCount: 50,
  fired: new Set<string>(),
  ...over,
});

const ids = (lines: readonly DazzaLine[]): string[] => lines.map((l) => l.id);
const dueIds = (clockMin: number, w: DazzaWorld): string[] => ids(dazzaDue(clockMin, w));

const lineById = (id: string): DazzaLine => {
  const line = DAZZA_TEXTS.find((l) => l.id === id);
  if (!line) throw new Error(`unknown dazza line: ${id}`);
  return line;
};

const RULE_LINES = DAZZA_TEXTS.filter((l) => l.ruleId);

describe('dazzaDue', () => {
  it('says nothing at the top of the night', () => {
    expect(dazzaDue(0, world())).toEqual([]);
  });

  it('delivers every rule-bearing text on its minute, never a minute early', () => {
    expect(RULE_LINES).toHaveLength(8);
    for (const line of RULE_LINES) {
      expect(dueIds(line.fromMin - 1, world())).not.toContain(line.id);
      expect(dueIds(line.fromMin, world())).toContain(line.id);
    }
  });

  it('puts the law ahead of the philosophy when several land at once', () => {
    const due = dazzaDue(360, world({ vibe: 20, queueLength: 8, insideCount: 2 }));
    const ruleIdx = due.flatMap((l, i) => (l.ruleId ? [i] : []));
    const moodIdx = due.flatMap((l, i) => (l.ruleId ? [] : [i]));
    expect(ruleIdx.length).toBeGreaterThan(0);
    expect(moodIdx.length).toBeGreaterThan(0);
    expect(Math.max(...ruleIdx)).toBeLessThan(Math.min(...moodIdx));
  });

  it('orders within each group by fromMin ascending', () => {
    const due = dazzaDue(360, world({ vibe: 20, queueLength: 8, insideCount: 2 }));
    expect(ids(due.filter((l) => l.ruleId))).toEqual([
      'rule-basics',
      'rule-logos',
      'rule-songRequest',
      'rule-sunnies',
      'rule-couples',
      'rule-sneakers',
      'rule-bucketHats',
      'rule-suits',
    ]);
    expect(ids(due.filter((l) => !l.ruleId))).toEqual([
      'vibe-empty',
      'vibe-mid',
      'vibe-queue',
      'kayden',
      'owner-scare',
      'philosophy-1',
      'philosophy-2',
    ]);
  });

  it('never repeats a line that has already been sent', () => {
    const w = world({ vibe: 20, queueLength: 8, insideCount: 2 });
    const first = dazzaDue(360, w);
    expect(first.length).toBeGreaterThan(0);
    expect(dazzaDue(360, { ...w, fired: new Set(ids(first)) })).toEqual([]);

    const partial = world({ fired: new Set(['rule-basics']) });
    expect(dueIds(90, partial)).not.toContain('rule-basics');
    expect(dueIds(90, partial)).toContain('rule-logos');
  });
});

describe('mood gates', () => {
  const cases: { id: string; yes: Partial<DazzaWorld>; no: Partial<DazzaWorld> }[] = [
    { id: 'vibe-mid', yes: { vibe: 34 }, no: { vibe: 35 } },
    { id: 'vibe-good', yes: { vibe: 73 }, no: { vibe: 72 } },
    { id: 'vibe-queue', yes: { queueLength: 6 }, no: { queueLength: 5 } },
    { id: 'vibe-empty', yes: { insideCount: 3 }, no: { insideCount: 4 } },
    { id: 'kayden', yes: { queueLength: 3 }, no: { queueLength: 2 } },
  ];

  for (const c of cases) {
    it(`${c.id} fires only when the room justifies it`, () => {
      const at = lineById(c.id).fromMin;
      expect(dueIds(at, world(c.yes))).toContain(c.id);
      expect(dueIds(at, world(c.no))).not.toContain(c.id);
      // gate satisfied but too early is still silence
      expect(dueIds(at - 1, world(c.yes))).not.toContain(c.id);
    });
  }

  it('ungated lines fire on time whatever the room is doing', () => {
    const rooms: Partial<DazzaWorld>[] = [
      { vibe: 0, aggro: 100, queueLength: 0, insideCount: 0 },
      { vibe: 100, aggro: 0, queueLength: 40, insideCount: 400 },
    ];
    for (const id of ['owner-scare', 'philosophy-1', 'philosophy-2']) {
      const at = lineById(id).fromMin;
      for (const room of rooms) {
        expect(dueIds(at, world(room))).toContain(id);
        expect(dueIds(at - 1, world(room))).not.toContain(id);
      }
    }
  });
});

describe('nextDazza', () => {
  it('hands back the head of the queue, one text at a time', () => {
    const w = world({ vibe: 20, queueLength: 8, insideCount: 2 });
    const due = dazzaDue(360, w);
    expect(nextDazza(360, w)).toBe(due[0]);
    expect(nextDazza(360, w)?.id).toBe('rule-basics');
  });

  it('returns undefined when he has nothing to say', () => {
    expect(nextDazza(0, world())).toBeUndefined();
    expect(nextDazza(360, world({ fired: new Set(ids(DAZZA_TEXTS)) }))).toBeUndefined();
  });
});

describe('a whole night, minute by minute', () => {
  it('delivers every line exactly once, rules on the minute they become law', () => {
    const fired = new Set<string>();
    const deliveredAt = new Map<string, number>();

    for (let m = 0; m <= 360; m++) {
      // vibe swings so both vibe-mid and vibe-good get a chance across the night
      const line = nextDazza(m, {
        vibe: m % 2 === 0 ? 20 : 90,
        aggro: 0,
        queueLength: 8,
        insideCount: 2,
        fired,
      });
      if (!line) continue;
      expect(deliveredAt.has(line.id)).toBe(false);
      fired.add(line.id);
      deliveredAt.set(line.id, m);
    }

    expect([...deliveredAt.keys()].sort()).toEqual(ids(DAZZA_TEXTS).sort());
    for (const line of RULE_LINES) {
      expect(deliveredAt.get(line.id)).toBe(line.fromMin);
    }
  });
});
