/** Spec §6 unique constraints: the database itself rejects double awards and double submits. */
import { randomUUID } from 'node:crypto';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import type { Db } from '../src/client';
import {
  createPlayer,
  createSessionFixture,
  createTestDb,
  createVenueFixture,
  deleteVenues,
  ensureTestSchema,
  type SessionFixture,
  type VenueFixture,
} from './helpers';

let db: Db;
let venue: VenueFixture;
let session: SessionFixture;
let playerId: string;

const UNIQUE_VIOLATION = { code: 'P2002' };

beforeAll(async () => {
  ensureTestSchema();
  db = createTestDb();
  venue = await createVenueFixture(db, 'constraints');
  session = await createSessionFixture(db, venue);
  playerId = (await createPlayer(db, venue, session)).id;
});

afterAll(async () => {
  await deleteVenues(db, [venue.venueId]);
  await db.$disconnect();
});

describe('AnswerSubmission', () => {
  const answer = (questionId: string, idempotencyKey: string) => ({
    venueId: venue.venueId,
    playerSessionId: playerId,
    roundId: session.roundId,
    questionId,
    optionId: 'a',
    openedAt: new Date(1_000),
    receivedAt: new Date(3_000),
    elapsedMs: 2_000,
    idempotencyKey,
  });

  it('allows one answer per player, round and question', async () => {
    const questionId = randomUUID();
    await db.answerSubmission.create({ data: answer(questionId, randomUUID()) });
    await expect(db.answerSubmission.create({ data: answer(questionId, randomUUID()) })).rejects.toMatchObject(UNIQUE_VIOLATION);
    // A different question in the same round is fine.
    await expect(db.answerSubmission.create({ data: answer(randomUUID(), randomUUID()) })).resolves.toBeTruthy();
  });

  it('rejects a reused idempotency key', async () => {
    const key = randomUUID();
    await db.answerSubmission.create({ data: answer(randomUUID(), key) });
    await expect(db.answerSubmission.create({ data: answer(randomUUID(), key) })).rejects.toMatchObject(UNIQUE_VIOLATION);
  });

  it('makes write-behind inserts idempotent with skipDuplicates (ON CONFLICT DO NOTHING)', async () => {
    const row = answer(randomUUID(), randomUUID());
    expect((await db.answerSubmission.createMany({ data: [row], skipDuplicates: true })).count).toBe(1);
    expect((await db.answerSubmission.createMany({ data: [row], skipDuplicates: true })).count).toBe(0);
  });
});

describe('BoardTile', () => {
  it('allows one tile per board position', async () => {
    const board = await db.playerBoard.create({
      data: { venueId: venue.venueId, playerSessionId: playerId, roundId: session.roundId, templateKey: 'identity', baseLayoutRow: 0 },
    });
    const tile = { venueId: venue.venueId, playerBoardId: board.id, position: 4, category: 'science' };
    await db.boardTile.create({ data: tile });
    await expect(db.boardTile.create({ data: { ...tile, category: 'history' } })).rejects.toMatchObject(UNIQUE_VIOLATION);
    await expect(db.boardTile.create({ data: { ...tile, position: 5 } })).resolves.toBeTruthy();
    // One board per player per round, too.
    await expect(
      db.playerBoard.create({
        data: { venueId: venue.venueId, playerSessionId: playerId, roundId: session.roundId, templateKey: 'rot90', baseLayoutRow: 0 },
      }),
    ).rejects.toMatchObject(UNIQUE_VIOLATION);
  });
});

describe('ScoreEvent', () => {
  it('awards each (player, session, kind, refId) once', async () => {
    const refId = `LINE:${session.roundId}:line:3`;
    const score = { venueId: venue.venueId, playerSessionId: playerId, gameSessionId: session.gameSessionId, roundId: session.roundId, kind: 'LINE' as const, refId, points: 250 };
    await db.scoreEvent.create({ data: score });
    await expect(db.scoreEvent.create({ data: score })).rejects.toMatchObject(UNIQUE_VIOLATION);
    await expect(db.scoreEvent.create({ data: { ...score, kind: 'FULL_BOARD', points: 500 } })).resolves.toBeTruthy();
    await expect(db.scoreEvent.create({ data: { ...score, refId: `LINE:${session.roundId}:line:4` } })).resolves.toBeTruthy();
  });
});

describe('PowerUpUse', () => {
  it('allows each power-up type once per player per event', async () => {
    const use = { venueId: venue.venueId, playerSessionId: playerId, eventId: session.eventId, roundId: session.roundId, type: 'DOUBLE_DOWN' as const };
    await db.powerUpUse.create({ data: { ...use, idempotencyKey: randomUUID() } });
    await expect(db.powerUpUse.create({ data: { ...use, idempotencyKey: randomUUID() } })).rejects.toMatchObject(UNIQUE_VIOLATION);
    await expect(db.powerUpUse.create({ data: { ...use, type: 'SECOND_CHANCE', idempotencyKey: randomUUID() } })).resolves.toBeTruthy();
  });
});

describe('PowerUpGrant', () => {
  it('grants one token per line source', async () => {
    const grant = {
      venueId: venue.venueId,
      playerSessionId: playerId,
      eventId: session.eventId,
      gameSessionId: session.gameSessionId,
      roundId: session.roundId,
      sourceRef: `${session.roundId}:line:0`,
    };
    await db.powerUpGrant.create({ data: grant });
    await expect(db.powerUpGrant.create({ data: grant })).rejects.toMatchObject(UNIQUE_VIOLATION);
    await expect(db.powerUpGrant.create({ data: { ...grant, sourceRef: `${session.roundId}:line:1` } })).resolves.toBeTruthy();
  });
});

describe('PlayerSession', () => {
  it('keeps nicknames unique per game session (case-insensitive key)', async () => {
    await createPlayer(db, venue, session, { nickname: 'Mike' });
    await expect(createPlayer(db, venue, session, { nickname: 'MIKE' })).rejects.toMatchObject(UNIQUE_VIOLATION);
  });
});
