/**
 * Shared setup for the db integration tests.
 *
 * Tests run against `pintrush_test` in a dedicated Postgres schema (`db_tests`), so they never touch
 * the dev database and can run alongside other packages' suites that use the default schema.
 * The schema is synced with a non-destructive `prisma db push` once per vitest run (a marker file
 * in os.tmpdir() keyed by the vitest main process). Nothing is reset: every test creates its own
 * uniquely named venues and deletes them afterwards, so runs are independent of leftover rows.
 */
import { execFileSync } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { isMainThread } from 'node:worker_threads';
import { createDb, type Db } from '../src/client';

export const TEST_DATABASE_URL =
  process.env.PINTRUSH_DB_TEST_URL ?? 'postgresql://pintrush:pintrush@localhost:54329/pintrush_test?schema=db_tests';

const DB_ROOT = path.resolve(import.meta.dirname, '..');
const PRISMA_CLI = path.join(DB_ROOT, 'node_modules', 'prisma', 'build', 'index.js');
const MARKER_PREFIX = 'pintrush-db-tests-';
const STALE_MARKER_MS = 12 * 60 * 60 * 1000;

/** Forked workers see the vitest main process as their parent; thread workers share its pid. */
function vitestRunId(): number {
  if (!isMainThread) return process.pid;
  return typeof process.send === 'function' ? process.ppid : process.pid;
}

function removeStaleMarkers(): void {
  const now = Date.now();
  for (const name of fs.readdirSync(os.tmpdir())) {
    if (!name.startsWith(MARKER_PREFIX)) continue;
    const file = path.join(os.tmpdir(), name);
    try {
      if (now - fs.statSync(file).mtimeMs > STALE_MARKER_MS) fs.rmSync(file, { force: true });
    } catch {
      // another process removed it first
    }
  }
}

/** Sync the test schema (once per run) and point DATABASE_URL at it. */
export function ensureTestSchema(): void {
  process.env.DATABASE_URL = TEST_DATABASE_URL;
  const marker = path.join(os.tmpdir(), `${MARKER_PREFIX}${vitestRunId()}.pushed`);
  if (fs.existsSync(marker)) return;
  removeStaleMarkers();
  execFileSync(process.execPath, [PRISMA_CLI, 'db', 'push', '--url', TEST_DATABASE_URL], {
    cwd: DB_ROOT,
    env: { ...process.env, DATABASE_URL: TEST_DATABASE_URL },
    stdio: 'pipe',
  });
  fs.writeFileSync(marker, new Date().toISOString());
}

/** Prisma client connected to the test schema. Call `ensureTestSchema()` first. */
export function createTestDb(): Db {
  return createDb(TEST_DATABASE_URL);
}

export function uniqueSuffix(): string {
  return randomUUID().replace(/-/g, '').slice(0, 10);
}

export interface VenueFixture {
  venueId: string;
  roomId: string;
  slug: string;
}

/** A venue with one room; everything hangs off the venue so deleting it cleans up (cascade). */
export async function createVenueFixture(db: Db, label: string, timezone = 'America/New_York'): Promise<VenueFixture> {
  const suffix = uniqueSuffix();
  const slug = `${label}-${suffix}`;
  const venue = await db.venue.create({
    data: { slug, name: `${label} ${suffix}`, joinCode: `${label}${suffix}`.toUpperCase().slice(0, 24), timezone },
  });
  const room = await db.room.create({ data: { venueId: venue.id, name: 'Main', slug: 'main' } });
  return { venueId: venue.id, roomId: room.id, slug };
}

/** Platform-wide pack shared by test rounds (Round.packId is required; packs are never deleted). */
export async function testPackId(db: Db): Promise<string> {
  const pack = await db.questionPack.upsert({
    where: { key: 'db-test-pack' },
    create: { key: 'db-test-pack', name: 'DB test pack', categories: [] },
    update: {},
  });
  return pack.id;
}

export interface SessionFixture {
  eventId: string;
  gameSessionId: string;
  roundId: string;
}

export async function createSessionFixture(
  db: Db,
  venue: VenueFixture,
  options: { kind?: 'ALWAYS_ON' | 'CHAMPIONSHIP'; businessDate?: string; startedAt?: Date } = {},
): Promise<SessionFixture> {
  const kind = options.kind ?? 'CHAMPIONSHIP';
  const event = await db.event.create({
    data: {
      venueId: venue.venueId,
      roomId: venue.roomId,
      kind,
      name: `${kind} ${uniqueSuffix()}`,
      status: 'LIVE',
      businessDate: kind === 'ALWAYS_ON' ? (options.businessDate ?? null) : null,
      startedAt: options.startedAt ?? null,
    },
  });
  const session = await db.gameSession.create({
    data: { venueId: venue.venueId, roomId: venue.roomId, eventId: event.id, status: 'RUNNING' },
  });
  const round = await db.round.create({
    data: {
      venueId: venue.venueId,
      gameSessionId: session.id,
      eventId: event.id,
      index: 0,
      kind: 'RUSH',
      packId: await testPackId(db),
      seed: uniqueSuffix(),
      baseLayoutRow: 0,
      questionIds: [],
    },
  });
  return { eventId: event.id, gameSessionId: session.id, roundId: round.id };
}

export async function createPlayer(
  db: Db,
  venue: VenueFixture,
  session: SessionFixture,
  options: { nickname?: string; deviceHash?: string | null; joinedAt?: Date } = {},
) {
  const nickname = options.nickname ?? `Player ${uniqueSuffix()}`;
  return db.playerSession.create({
    data: {
      venueId: venue.venueId,
      roomId: venue.roomId,
      eventId: session.eventId,
      gameSessionId: session.gameSessionId,
      nickname,
      nicknameKey: nickname.toLowerCase(),
      deviceHash: options.deviceHash ?? null,
      ...(options.joinedAt ? { joinedAt: options.joinedAt } : {}),
    },
  });
}

export async function deleteVenues(db: Db, venueIds: string[]): Promise<void> {
  if (venueIds.length > 0) await db.venue.deleteMany({ where: { id: { in: venueIds } } });
}
