import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { getDeviceId, loadTokens, saveToken } from '../game/storage';
import { KeyRing, answerScope, deviceId, forgetNickname, recallNickname, rememberNickname, resetDeviceId, syncTokenNickname } from './device';

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

function memoryStorage(): Storage {
  const data = new Map<string, string>();
  return {
    get length() {
      return data.size;
    },
    clear: () => data.clear(),
    getItem: (k) => data.get(k) ?? null,
    key: (i) => [...data.keys()][i] ?? null,
    removeItem: (k) => void data.delete(k),
    setItem: (k, v) => void data.set(k, String(v)),
  };
}

let storage: Storage;
beforeEach(() => {
  storage = memoryStorage();
  vi.stubGlobal('window', { localStorage: storage });
});
afterEach(() => {
  vi.unstubAllGlobals();
});

describe('answer idempotency keys', () => {
  it('are stable for one question in one round, and new when the question repeats in a later round', () => {
    const ring = new KeyRing();
    const first = ring.for(answerScope('round-1', 'q-7'));
    expect(ring.for(answerScope('round-1', 'q-7'))).toBe(first);
    expect(ring.for(answerScope('round-23', 'q-7'))).not.toBe(first);
  });
});

describe('Start over', () => {
  it('writes a fresh device id to the key getDeviceId() reads', () => {
    const before = getDeviceId();
    expect(storage.getItem('pintrush:device')).toBe(before);
    resetDeviceId();
    const after = storage.getItem('pintrush:device');
    expect(after).toMatch(UUID);
    expect(after).not.toBe(before);
    expect(getDeviceId()).toBe(after);
    expect(deviceId()).toBe(after);
  });

  it('keeps the fresh id for this tab when storage refuses writes', () => {
    const before = deviceId();
    vi.stubGlobal('window', {
      localStorage: {
        ...storage,
        getItem: (k: string) => storage.getItem(k),
        setItem: () => {
          throw new Error('QuotaExceededError');
        },
      },
    });
    resetDeviceId();
    const after = deviceId();
    expect(after).toMatch(UUID);
    expect(after).not.toBe(before);
    // Once storage works again, a later reset goes back to the stored id.
    vi.stubGlobal('window', { localStorage: storage });
    resetDeviceId();
    expect(deviceId()).toBe(storage.getItem('pintrush:device'));
  });

  it('forgets the remembered nickname for that room only', () => {
    rememberNickname('room-a', 'Pinty');
    rememberNickname('room-b', 'Barfly');
    forgetNickname('room-a');
    expect(recallNickname('room-a')).toBeNull();
    expect(recallNickname('room-b')).toBe('Barfly');
  });
});

describe('session handoff', () => {
  it('names a token the server issued without a nickname', () => {
    const exp = Date.now() + 60_000;
    saveToken('room-a', { token: 't-champ', gameSessionId: 'champ', playerSessionId: 'p1', expiresAt: exp, nickname: 'Pinty' });
    saveToken('room-a', { token: 't-always', gameSessionId: 'always', playerSessionId: 'p2', expiresAt: exp });
    expect(loadTokens('room-a')[0]?.nickname).toBeUndefined();
    syncTokenNickname('room-a', 'always', 'Pinty 2');
    const [latest] = loadTokens('room-a');
    expect(latest).toMatchObject({ gameSessionId: 'always', token: 't-always', nickname: 'Pinty 2' });
    expect(recallNickname('room-a')).toBe('Pinty 2');
  });
});
