import { beforeEach, describe, expect, it } from 'vitest';
import { clearLobby, getLobby, patchLobbyGame, patchLobbyInvitation, setLobby } from './lobbycache';
import type { AccountRef, GameView, Invitation } from './model';

function invitation(id: string, status: string): Invitation {
  return {
    id,
    inviter: { accountId: 'inv', displayName: 'Inv' },
    invitees: [],
    variant: 'scrabble_en',
    turnTimeoutSecs: 300,
    hintsAllowed: true,
    hintsPerPlayer: 0,
    multipleWordsPerTurn: true,
        dropoutTiles: 'remove',
    status,
    gameId: '',
    expiresAtUnix: 0,
  };
}

function gameView(id: string, status: GameView['status'] = 'active', toMove = 0): GameView {
  return {
    id,
    variant: 'scrabble_en',
    dictVersion: 'v1',
    vsAi: false,
    unreadChat: false,
    unreadMessages: false,
    kind: 0,
    status,
    players: 2,
    toMove,
    turnTimeoutSecs: 300,
    multipleWordsPerTurn: true,
    noRepeatWords: false,
    moveCount: 0,
    endReason: '',
    lastActivityUnix: 0,
    seats: [],
  };
}

beforeEach(() => clearLobby());

describe('patchLobbyGame', () => {
  it('replaces the matching game by id and leaves the others untouched', () => {
    setLobby({ games: [gameView('a', 'active', 0), gameView('b', 'active', 0)], invitations: [], incoming: [] });
    // The player's own move flipped game "a" to the opponent's turn.
    patchLobbyGame(gameView('a', 'active', 1));
    const snap = getLobby();
    expect(snap?.games.map((g) => g.id)).toEqual(['a', 'b']);
    expect(snap?.games.find((g) => g.id === 'a')?.toMove).toBe(1);
    expect(snap?.games.find((g) => g.id === 'b')?.toMove).toBe(0);
  });

  it('preserves invitations and incoming when patching a game', () => {
    const incoming: AccountRef[] = [{ accountId: 'u9', displayName: 'Nine' }];
    setLobby({ games: [gameView('a')], invitations: [], incoming });
    patchLobbyGame(gameView('a', 'finished'));
    expect(getLobby()?.incoming).toEqual(incoming);
    expect(getLobby()?.games[0].status).toBe('finished');
  });

  it('adds the game when it is not yet in the cached lobby (a game started elsewhere)', () => {
    setLobby({ games: [gameView('a')], invitations: [], incoming: [] });
    patchLobbyGame(gameView('z', 'active'));
    // Order is irrelevant — the lobby re-groups and re-sorts on render — so assert membership.
    expect(getLobby()?.games.map((g) => g.id).sort()).toEqual(['a', 'z']);
  });

  it('is a no-op when there is no cached lobby yet', () => {
    patchLobbyGame(gameView('a'));
    expect(getLobby()).toBeNull();
  });

  it('does not mutate the previous snapshot array', () => {
    const games = [gameView('a', 'active', 0)];
    setLobby({ games, invitations: [], incoming: [] });
    patchLobbyGame(gameView('a', 'active', 1));
    expect(games[0].toMove).toBe(0); // the original array/object is left intact
  });
});

describe('patchLobbyInvitation', () => {
  it('adds a new pending invitation to the cached lobby', () => {
    setLobby({ games: [], invitations: [], incoming: [] });
    patchLobbyInvitation(invitation('i1', 'pending'));
    expect(getLobby()?.invitations.map((x) => x.id)).toEqual(['i1']);
  });

  it('replaces a pending invitation already in the list (e.g. an updated response)', () => {
    setLobby({ games: [], invitations: [invitation('i1', 'pending'), invitation('i2', 'pending')], incoming: [] });
    const updated = { ...invitation('i1', 'pending'), turnTimeoutSecs: 999 };
    patchLobbyInvitation(updated);
    expect(getLobby()?.invitations.map((x) => x.id)).toEqual(['i1', 'i2']);
    expect(getLobby()?.invitations.find((x) => x.id === 'i1')?.turnTimeoutSecs).toBe(999);
  });

  it('removes an invitation that reached a terminal status', () => {
    for (const terminal of ['declined', 'cancelled', 'started', 'expired']) {
      setLobby({ games: [], invitations: [invitation('i1', 'pending'), invitation('i2', 'pending')], incoming: [] });
      patchLobbyInvitation(invitation('i1', terminal));
      expect(getLobby()?.invitations.map((x) => x.id)).toEqual(['i2']);
    }
  });

  it('is a no-op for a terminal invitation that is not in the list', () => {
    setLobby({ games: [], invitations: [invitation('i2', 'pending')], incoming: [] });
    patchLobbyInvitation(invitation('i1', 'declined'));
    expect(getLobby()?.invitations.map((x) => x.id)).toEqual(['i2']);
  });

  it('is a no-op when there is no cached lobby yet', () => {
    patchLobbyInvitation(invitation('i1', 'pending'));
    expect(getLobby()).toBeNull();
  });

  it('preserves games and incoming when patching an invitation', () => {
    const incoming: AccountRef[] = [{ accountId: 'u9', displayName: 'Nine' }];
    setLobby({ games: [gameView('g1')], invitations: [], incoming });
    patchLobbyInvitation(invitation('i1', 'pending'));
    expect(getLobby()?.games.map((g) => g.id)).toEqual(['g1']);
    expect(getLobby()?.incoming).toEqual(incoming);
  });
});

describe('the unified snapshot (one lobby cache — offline reuses its server games, greyed)', () => {
  it('replaces the whole snapshot on each store, so the latest merged lists win', () => {
    setLobby({ games: [gameView('online1')], invitations: [], incoming: [] });
    expect(getLobby()?.games.map((g) => g.id)).toEqual(['online1']);
    setLobby({ games: [gameView('local1'), gameView('online2')], invitations: [], incoming: [] });
    expect(getLobby()?.games.map((g) => g.id)).toEqual(['local1', 'online2']);
  });
});
