import { describe, expect, it } from 'vitest';
import { SeededRNG } from '../../src/core/SeededRNG';
import { PATDOWN_CLEAR, PATDOWN_MISSED } from '../../src/data/strings/floor';
import {
  foundLine,
  layoutPockets,
  scorePatDown,
  POCKET_COUNT,
  type Pocket,
} from '../../src/scenes/floor/patDown';
import type { Patron } from '../../src/data/types';

const patronWith = (contraband: string[]): Patron => ({
  id: 'p1',
  dollSeed: 42,
  archetype: 'punter',
  age: 24,
  idCard: { name: 'Shazza Nguyen', dob: '2002-03-04', expiry: '2029-03-04', photoSeed: 42 },
  outfit: [],
  intoxication: 0.3,
  tolerance: 0.5,
  flags: contraband.length > 0 ? { contraband } : {},
});

const stream = (seed: number) => new SeededRNG(seed).stream('patdown');
const binAll = (pockets: Pocket[]): Pocket[] => pockets.map((p) => ({ ...p, binned: p.itemId !== undefined }));

describe('layoutPockets', () => {
  it('returns POCKET_COUNT pockets with unique ids at distinct positions', () => {
    const pockets = layoutPockets(patronWith(['flask']), stream(1));
    expect(pockets).toHaveLength(POCKET_COUNT);
    expect(new Set(pockets.map((p) => p.id)).size).toBe(POCKET_COUNT);
    expect(new Set(pockets.map((p) => `${p.x},${p.y}`)).size).toBe(POCKET_COUNT);
  });

  it('keeps pockets inside the 64x96 outline and clear of each other', () => {
    const pockets = layoutPockets(patronWith([]), stream(2));
    for (const p of pockets) {
      expect(p.x).toBeGreaterThanOrEqual(0);
      expect(p.y).toBeGreaterThanOrEqual(0);
      expect(p.x).toBeLessThanOrEqual(64);
      expect(p.y).toBeLessThanOrEqual(96);
    }
    for (let i = 0; i < pockets.length; i++) {
      for (let j = i + 1; j < pockets.length; j++) {
        const a = pockets[i]!;
        const b = pockets[j]!;
        const clear = Math.abs(a.x - b.x) >= 12 || Math.abs(a.y - b.y) >= 12;
        expect(clear, `${a.label} overlaps ${b.label}`).toBe(true);
      }
    }
  });

  it('places every contraband id in a distinct pocket', () => {
    const items = ['flask', 'kebab', 'budgie'];
    const pockets = layoutPockets(patronWith(items), stream(3));
    const filled = pockets.filter((p) => p.itemId !== undefined);
    expect(filled).toHaveLength(items.length);
    expect(new Set(filled.map((p) => p.id)).size).toBe(items.length);
    expect(filled.map((p) => p.itemId).sort()).toEqual([...items].sort());
  });

  it('leaves an empty-handed patron with no items and nothing revealed or binned', () => {
    const pockets = layoutPockets(patronWith([]), stream(4));
    expect(pockets.every((p) => p.itemId === undefined)).toBe(true);
    expect(pockets.every((p) => !p.revealed && !p.binned)).toBe(true);
  });

  it('places only what fits when there is more contraband than pockets', () => {
    const items = ['flask', 'kebab', 'budgie', 'vapes', 'marker', 'snags', 'tinnies', 'goonSack'];
    const pockets = layoutPockets(patronWith(items), stream(5));
    expect(pockets.filter((p) => p.itemId !== undefined)).toHaveLength(POCKET_COUNT);
  });

  it('is deterministic for the same seed and divergent across seeds', () => {
    const items = ['flask', 'kebab', 'budgie'];
    const a = layoutPockets(patronWith(items), stream(7)).map((p) => p.itemId);
    const b = layoutPockets(patronWith(items), stream(7)).map((p) => p.itemId);
    expect(a).toEqual(b);

    const seeds = [11, 12, 13, 14, 15, 16].map((s) =>
      layoutPockets(patronWith(items), stream(s))
        .map((p) => p.itemId ?? '-')
        .join(),
    );
    expect(new Set(seeds).size).toBeGreaterThan(1);
  });
});

describe('scorePatDown', () => {
  it('rewards a full sweep: every item found, positive vibe, no aggro', () => {
    const patron = patronWith(['flask', 'kebab']);
    const result = scorePatDown(patron, binAll(layoutPockets(patron, stream(21))));
    expect(result.foundIds.sort()).toEqual(['flask', 'kebab']);
    expect(result.missedIds).toEqual([]);
    expect(result.vibeDelta).toBe(4);
    expect(result.aggroDelta).toBe(0);
    expect(result.summary).not.toContain(PATDOWN_MISSED);
  });

  it('leaks vibe for items left in the pockets when time runs out', () => {
    const patron = patronWith(['flask', 'kebab']);
    const result = scorePatDown(patron, layoutPockets(patron, stream(22)));
    expect(result.foundIds).toEqual([]);
    expect(result.missedIds.sort()).toEqual(['flask', 'kebab']);
    expect(result.vibeDelta).toBe(-3);
    expect(result.summary).toBe(PATDOWN_MISSED);
    expect(result.eject).toBe(false);
  });

  it('mixes found and missed, and still flags the miss', () => {
    const patron = patronWith(['flask', 'kebab']);
    const pockets = layoutPockets(patron, stream(23)).map((p) => ({
      ...p,
      binned: p.itemId === 'flask',
    }));
    const result = scorePatDown(patron, pockets);
    expect(result.foundIds).toEqual(['flask']);
    expect(result.missedIds).toEqual(['kebab']);
    expect(result.vibeDelta).toBe(0.5);
    expect(result.summary).toContain(PATDOWN_MISSED);
  });

  it('ejects on a binned severity-3 item but not a severity-1 one', () => {
    const baggie = patronWith(['baggie']);
    expect(scorePatDown(baggie, binAll(layoutPockets(baggie, stream(31)))).eject).toBe(true);

    const kebab = patronWith(['kebab']);
    const kebabResult = scorePatDown(kebab, binAll(layoutPockets(kebab, stream(32))));
    expect(kebabResult.eject).toBe(false);
    expect(kebabResult.foundIds).toEqual(['kebab']);
  });

  it('does not eject for a severity-3 item that was never binned', () => {
    const patron = patronWith(['baggie']);
    const result = scorePatDown(patron, layoutPockets(patron, stream(33)));
    expect(result.eject).toBe(false);
    expect(result.missedIds).toEqual(['baggie']);
  });

  it('charges aggro and reads PATDOWN_CLEAR when they had nothing on them', () => {
    const patron = patronWith([]);
    const result = scorePatDown(patron, layoutPockets(patron, stream(41)));
    expect(result.foundIds).toEqual([]);
    expect(result.missedIds).toEqual([]);
    expect(result.aggroDelta).toBe(1.5);
    expect(result.vibeDelta).toBe(0);
    expect(result.eject).toBe(false);
    expect(result.summary).toBe(PATDOWN_CLEAR);
  });
});

describe('foundLine', () => {
  it('uses the scripted line for a known item', () => {
    expect(foundLine('budgie')).toContain('budgie');
  });

  it('falls back gracefully for an unknown id', () => {
    const line = foundLine('a-second-budgie');
    expect(typeof line).toBe('string');
    expect(line.length).toBeGreaterThan(0);
  });
});
