import { describe, it, expect, vi } from 'vitest';
import type { StateView } from '../model';

// getDawg is mocked to the committed sample dictionary (the store's IndexedDB is absent under node,
// so games live in the source's in-memory cache — created once, then driven without reload).
vi.mock('../dict', () => ({
  getDawg: async () => {
    const { Dawg } = await import('../dict/dawg');
    const { readFileSync } = await import('node:fs');
    return new Dawg(new Uint8Array(readFileSync(new URL('../dict/testdata/sample_en.dawg', import.meta.url))));
  },
}));

import { LocalSource } from './source';
import type { Seat } from './serialize';
import { newLock } from '../pin';

async function hotseat(id: string, seats: Seat[], hostDigits = '9999'): Promise<LocalSource> {
  const src = new LocalSource();
  await src.create({
    id,
    variant: 'scrabble_en',
    dictVersion: 'sample',
    seed: 3n,
    multipleWords: true,
    seats,
    hotseat: true,
    hostPin: await newLock(hostDigits),
  });
  return src;
}

describe('LocalSource hotseat', () => {
  it('creates a pass-and-play game: not vs_ai, board visible, first seat to move', async () => {
    const src = await hotseat('local:h1', [
      { kind: 'human', name: 'Ann' },
      { kind: 'human', name: 'Bob' },
    ]);
    const st = await src.gameState('local:h1');
    expect(st.game.vsAi).toBe(false);
    expect(st.game.hotseat).toBe(true);
    expect(st.game.toMove).toBe(0);
    expect(st.locked).toBeFalsy(); // seat 0 has no PIN
    expect(st.rack.length).toBe(7);
    expect(st.game.seats.map((s) => s.displayName)).toEqual(['Ann', 'Bob']);
  });

  it('hides the rack of a PIN-locked seat until the right PIN unlocks it', async () => {
    const src = await hotseat('local:h2', [
      { kind: 'human', name: 'Ann', pin: await newLock('1234') },
      { kind: 'human', name: 'Bob' },
    ]);
    const st = await src.gameState('local:h2');
    expect(st.locked).toBe(true);
    expect(st.rack).toEqual([]);

    await expect(src.unlockSeat('local:h2', '0000')).rejects.toMatchObject({ code: 'wrong_pin' });

    const opened = await src.unlockSeat('local:h2', '1234');
    expect(opened.locked).toBe(false);
    expect(opened.rack.length).toBe(7);
  });

  it('re-locks the seat when the turn advances and again when it comes back', async () => {
    const src = await hotseat('local:h3', [
      { kind: 'human', name: 'A', pin: await newLock('1111') },
      { kind: 'human', name: 'B' },
      { kind: 'human', name: 'C' },
    ]);
    await src.unlockSeat('local:h3', '1111');
    await src.pass('local:h3'); // seat 0 -> 1
    let st = await src.gameState('local:h3');
    expect(st.game.toMove).toBe(1);
    expect(st.locked).toBeFalsy(); // seat 1 has no PIN

    await src.pass('local:h3'); // 1 -> 2
    await src.pass('local:h3'); // 2 -> 0
    st = await src.gameState('local:h3');
    expect(st.game.toMove).toBe(0);
    expect(st.locked).toBe(true); // seat 0 needs re-unlocking
  });

  it('lets the host skip the current turn, gated by the master PIN', async () => {
    const src = await hotseat('local:h4', [
      { kind: 'human', name: 'A' },
      { kind: 'human', name: 'B' },
    ], '4321');
    expect((await src.gameState('local:h4')).game.toMove).toBe(0);
    await expect(src.hostAction('local:h4', '0000', 'skip')).rejects.toMatchObject({ code: 'wrong_pin' });
    const st = await src.hostAction('local:h4', '4321', 'skip');
    expect(st).not.toBeNull();
    expect((st as StateView).game.toMove).toBe(1);
  });

  it('lets the host resign a seat (ending a two-player game)', async () => {
    const src = await hotseat('local:h5', [
      { kind: 'human', name: 'A' },
      { kind: 'human', name: 'B' },
    ], '4321');
    const st = await src.hostAction('local:h5', '4321', 'resign', 1);
    expect((st as StateView).game.status).toBe('finished');
  });

  it('exposes a host-excluded seat as resigned in the view (for the medal ranking)', async () => {
    const src = await hotseat('local:hR', [
      { kind: 'human', name: 'A' },
      { kind: 'human', name: 'B' },
      { kind: 'human', name: 'C' },
    ], '4321');
    await src.hostAction('local:hR', '4321', 'resign', 1); // exclude seat 1 (game continues: 2 active)
    const st = await src.gameState('local:hR');
    expect(st.game.seats[1].resigned).toBe(true);
    expect(st.game.seats[0].resigned).toBe(false);
    expect(st.game.seats[2].resigned).toBe(false);
  });

  it('lets the host terminate the game, deleting it', async () => {
    const src = await hotseat('local:h6', [
      { kind: 'human', name: 'A' },
      { kind: 'human', name: 'B' },
    ], '4321');
    const r = await src.hostAction('local:h6', '4321', 'terminate');
    expect(r).toBeNull();
    await expect(src.gameState('local:h6')).rejects.toMatchObject({ code: 'game_not_found' });
  });

  it('re-locks an unlocked seat on relock (returning to the lobby)', async () => {
    const src = await hotseat('local:h9', [
      { kind: 'human', name: 'Ann', pin: await newLock('1234') },
      { kind: 'human', name: 'Bob' },
    ]);
    const opened = await src.unlockSeat('local:h9', '1234');
    expect(opened.locked).toBe(false);
    src.relock('local:h9');
    const st = await src.gameState('local:h9');
    expect(st.locked).toBe(true); // returning to the game re-prompts the seat PIN
    expect(st.rack).toEqual([]);
  });

  it('verifies the master PIN', async () => {
    const src = await hotseat('local:h7', [
      { kind: 'human', name: 'A' },
      { kind: 'human', name: 'B' },
    ], '4321');
    expect(await src.verifyHostPin('local:h7', '4321')).toBe(true);
    expect(await src.verifyHostPin('local:h7', '0000')).toBe(false);
  });

  it('persists a naturally finished game (scoreless run) as finished, and unlocked', async () => {
    const src = await hotseat('local:h8', [
      { kind: 'human', name: 'A', pin: await newLock('1111') },
      { kind: 'human', name: 'B', pin: await newLock('2222') },
    ]);
    for (let i = 0; i < 6; i++) await src.pass('local:h8');
    const st = await src.gameState('local:h8');
    expect(st.game.status).toBe('finished');
    expect(st.locked).toBeFalsy(); // a finished game shows its final rack, never an unlock button
  });
});
