import { afterEach, describe, expect, it } from 'vitest';
import {
  ACTIVE_RULE_CAP,
  DRESS_CODE_RULES,
  SHUFFLE_EVERY_MIN,
  SHUFFLE_RULE_COUNT,
  activeRules,
  announcedRules,
  setDressCodeShuffle,
  shuffleWindow,
  violations,
} from '../src/rules/dressCode';
import { generatePatron, _resetPatronSerial } from '../src/patrons/generator';
import { SeededRNG } from '../src/core/SeededRNG';

// Module-level mode: ALWAYS clear after each test or the whole suite goes to ROOM.
afterEach(() => setDressCodeShuffle(null));

const ids = (clockMin: number): string[] => activeRules(clockMin).map((r) => r.id);

describe('ROOM dress-code shuffle', () => {
  it('windows turn every SHUFFLE_EVERY_MIN', () => {
    expect(shuffleWindow(0)).toBe(0);
    expect(shuffleWindow(SHUFFLE_EVERY_MIN - 1)).toBe(0);
    expect(shuffleWindow(SHUFFLE_EVERY_MIN)).toBe(1);
    expect(shuffleWindow(SHUFFLE_EVERY_MIN * 4 + 3)).toBe(4);
  });

  it('serves SHUFFLE_RULE_COUNT real rules, stable within a window', () => {
    setDressCodeShuffle(4207);
    const a = ids(0);
    expect(a).toHaveLength(SHUFFLE_RULE_COUNT);
    expect(SHUFFLE_RULE_COUNT).toBeLessThanOrEqual(ACTIVE_RULE_CAP);
    for (const id of a) expect(DRESS_CODE_RULES.some((r) => r.id === id)).toBe(true);
    // re-reading the board mid-window must not reroll it
    expect(ids(SHUFFLE_EVERY_MIN - 1)).toEqual(a);
  });

  it('rewrites across windows and is deterministic per seed', () => {
    setDressCodeShuffle(4207);
    const windows = Array.from({ length: 8 }, (_, w) => ids(w * SHUFFLE_EVERY_MIN));
    // Some consecutive pair must differ — a board that never changes isn't ROOM.
    expect(windows.some((w, i) => i > 0 && w.join() !== windows[i - 1]!.join())).toBe(true);
    // Same seed replays the same night.
    setDressCodeShuffle(4207);
    expect(ids(SHUFFLE_EVERY_MIN * 3)).toEqual(windows[3]);
    // A different seed is a different board.
    setDressCodeShuffle(999);
    const other = Array.from({ length: 8 }, (_, w) => ids(w * SHUFFLE_EVERY_MIN));
    expect(other.join('|')).not.toBe(windows.join('|'));
  });

  it('announcedRules mirrors the live set — nothing greyed, nothing historical', () => {
    setDressCodeShuffle(4207);
    expect(announcedRules(200).map((r) => r.id)).toEqual(ids(200));
  });

  it('clearing the mode restores the Dazza chronology', () => {
    setDressCodeShuffle(4207);
    setDressCodeShuffle(null);
    expect(activeRules(0)).toEqual(DRESS_CODE_RULES.filter((r) => r.activeFrom <= 0).slice(-ACTIVE_RULE_CAP));
  });

  it('violations() judges against the shuffled set', () => {
    _resetPatronSerial();
    const rng = new SeededRNG(7);
    const ctx = { rng, nightDate: new Date('2026-07-16T00:00:00') };
    setDressCodeShuffle(4207);
    for (let i = 0; i < 40; i++) {
      const p = generatePatron(ctx, 30);
      const live = new Set(ids(30));
      for (const v of violations(p, 30)) expect(live.has(v.id)).toBe(true);
    }
  });
});
