import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { db } from "~/db/sql";
import type { Tables, TournamentAuditLogMetadata } from "~/db/tables";
import { dbInsertUsers, dbReset, withUserId } from "~/utils/Test";
import * as TournamentAuditLogRepository from "./TournamentAuditLogRepository.server";

const createTournament = () =>
	db
		.insertInto("Tournament")
		.values({
			mapPickingStyle: "TO",
			settings: JSON.stringify({ bracketProgression: [] }),
		})
		.returning("id")
		.executeTakeFirstOrThrow();

const createTeam = (tournamentId: number, name: string) =>
	db
		.insertInto("TournamentTeam")
		.values({
			tournamentId,
			name,
			inviteCode: `inv-${tournamentId}-${name}`,
		})
		.returning("id")
		.executeTakeFirstOrThrow();

const insertEvent = ({
	actorUserId,
	...args
}: {
	type: Tables["TournamentAuditLog"]["type"];
	actorUserId: number;
	tournamentTeamId: number;
	subjectUserId?: number;
	metadata?: TournamentAuditLogMetadata;
}) =>
	withUserId(actorUserId, () =>
		db
			.transaction()
			.execute((trx) => TournamentAuditLogRepository.insert(trx, args)),
	);

describe("TournamentAuditLogRepository", () => {
	beforeEach(async () => {
		await dbInsertUsers(3);
	});

	afterEach(() => {
		dbReset();
	});

	test("insert creates a stable history row from the live team", async () => {
		const tournament = await createTournament();
		const team = await createTeam(tournament.id, "Team Olive");

		await insertEvent({
			type: "TEAM_REGISTERED",
			actorUserId: 1,
			tournamentTeamId: team.id,
		});

		const teams = await TournamentAuditLogRepository.findTeamsByTournamentId(
			tournament.id,
		);

		expect(teams).toHaveLength(1);
		expect(teams[0].tournamentTeamId).toBe(team.id);
		expect(teams[0].name).toBe("Team Olive");
	});

	test("findByTournamentId returns events newest first with resolved relations", async () => {
		const tournament = await createTournament();
		const team = await createTeam(tournament.id, "Team Olive");

		await insertEvent({
			type: "TEAM_REGISTERED",
			actorUserId: 1,
			tournamentTeamId: team.id,
			subjectUserId: 1,
		});
		await insertEvent({
			type: "MEMBER_ADDED",
			actorUserId: 1,
			tournamentTeamId: team.id,
			subjectUserId: 2,
		});

		const events = await TournamentAuditLogRepository.findByTournamentId({
			tournamentId: tournament.id,
			limit: 30,
			offset: 0,
		});

		expect(events).toHaveLength(2);
		// newest first
		expect(events[0].type).toBe("MEMBER_ADDED");
		expect(events[0].actor?.id).toBe(1);
		expect(events[0].subject?.id).toBe(2);
		expect(events[0].team?.name).toBe("Team Olive");
		expect(events[1].type).toBe("TEAM_REGISTERED");
	});

	test("team name survives the team being deleted", async () => {
		const tournament = await createTournament();
		const team = await createTeam(tournament.id, "Team Olive");

		await insertEvent({
			type: "TEAM_UNREGISTERED",
			actorUserId: 1,
			tournamentTeamId: team.id,
		});

		await db
			.deleteFrom("TournamentTeam")
			.where("TournamentTeam.id", "=", team.id)
			.execute();

		const events = await TournamentAuditLogRepository.findByTournamentId({
			tournamentId: tournament.id,
			limit: 30,
			offset: 0,
		});

		expect(events).toHaveLength(1);
		expect(events[0].team?.name).toBe("Team Olive");
	});

	test("a reused team id does not collapse two teams into one history", async () => {
		const tournament = await createTournament();
		const teamA = await createTeam(tournament.id, "Team A");

		await insertEvent({
			type: "TEAM_UNREGISTERED",
			actorUserId: 1,
			tournamentTeamId: teamA.id,
		});

		await db
			.deleteFrom("TournamentTeam")
			.where("TournamentTeam.id", "=", teamA.id)
			.execute();

		const teamB = await createTeam(tournament.id, "Team B");
		// SQLite reuses the highest deleted rowid for the next insert
		expect(teamB.id).toBe(teamA.id);

		await insertEvent({
			type: "TEAM_REGISTERED",
			actorUserId: 1,
			tournamentTeamId: teamB.id,
		});

		const teams = await TournamentAuditLogRepository.findTeamsByTournamentId(
			tournament.id,
		);
		expect(teams).toHaveLength(2);
		expect(teams.map((team) => team.name).sort()).toEqual(["Team A", "Team B"]);

		const events = await TournamentAuditLogRepository.findByTournamentId({
			tournamentId: tournament.id,
			limit: 30,
			offset: 0,
		});
		const eventByType = new Map(events.map((event) => [event.type, event]));
		expect(eventByType.get("TEAM_UNREGISTERED")?.team?.name).toBe("Team A");
		expect(eventByType.get("TEAM_REGISTERED")?.team?.name).toBe("Team B");
	});

	test("filters by event type and by team", async () => {
		const tournament = await createTournament();
		const teamA = await createTeam(tournament.id, "Team A");
		const teamB = await createTeam(tournament.id, "Team B");

		await insertEvent({
			type: "TEAM_REGISTERED",
			actorUserId: 1,
			tournamentTeamId: teamA.id,
		});
		await insertEvent({
			type: "TEAM_CHECKED_IN",
			actorUserId: 1,
			tournamentTeamId: teamA.id,
		});
		await insertEvent({
			type: "TEAM_REGISTERED",
			actorUserId: 1,
			tournamentTeamId: teamB.id,
		});

		const byType = await TournamentAuditLogRepository.findByTournamentId({
			tournamentId: tournament.id,
			type: "TEAM_REGISTERED",
			limit: 30,
			offset: 0,
		});
		expect(byType).toHaveLength(2);

		const teams = await TournamentAuditLogRepository.findTeamsByTournamentId(
			tournament.id,
		);
		const teamAHistoryId = teams.find(
			(team) => team.tournamentTeamId === teamA.id,
		)?.id;

		const byTeam = await TournamentAuditLogRepository.findByTournamentId({
			tournamentId: tournament.id,
			tournamentTeamHistoryId: teamAHistoryId,
			limit: 30,
			offset: 0,
		});
		expect(byTeam).toHaveLength(2);

		const count = await TournamentAuditLogRepository.countByTournamentId({
			tournamentId: tournament.id,
			type: "TEAM_REGISTERED",
		});
		expect(count).toBe(2);
	});

	test("paginates via limit and offset", async () => {
		const tournament = await createTournament();
		const team = await createTeam(tournament.id, "Team Olive");

		for (let i = 0; i < 3; i++) {
			await insertEvent({
				type: "TEAM_CHECKED_IN",
				actorUserId: 1,
				tournamentTeamId: team.id,
			});
		}

		const firstPage = await TournamentAuditLogRepository.findByTournamentId({
			tournamentId: tournament.id,
			limit: 2,
			offset: 0,
		});
		const secondPage = await TournamentAuditLogRepository.findByTournamentId({
			tournamentId: tournament.id,
			limit: 2,
			offset: 2,
		});

		expect(firstPage).toHaveLength(2);
		expect(secondPage).toHaveLength(1);
	});

	test("stores and reads back metadata", async () => {
		const tournament = await createTournament();
		const team = await createTeam(tournament.id, "Team Olive");

		await insertEvent({
			type: "TEAM_CHECKED_IN",
			actorUserId: 1,
			tournamentTeamId: team.id,
			metadata: { bracketIdx: 2 },
		});

		const events = await TournamentAuditLogRepository.findByTournamentId({
			tournamentId: tournament.id,
			limit: 30,
			offset: 0,
		});

		expect(events[0].metadata?.bracketIdx).toBe(2);
	});

	test("stores and reads back the in-game name for name change events", async () => {
		const tournament = await createTournament();
		const team = await createTeam(tournament.id, "Team Olive");

		await insertEvent({
			type: "UPDATE_IN_GAME_NAME",
			actorUserId: 1,
			tournamentTeamId: team.id,
			subjectUserId: 2,
			metadata: { inGameName: "New IGN#1234" },
		});

		const events = await TournamentAuditLogRepository.findByTournamentId({
			tournamentId: tournament.id,
			limit: 30,
			offset: 0,
		});

		expect(events[0].type).toBe("UPDATE_IN_GAME_NAME");
		expect(events[0].subject?.id).toBe(2);
		expect(events[0].metadata?.inGameName).toBe("New IGN#1234");
	});

	test("updateTeamHistoryName keeps the preserved name current", async () => {
		const tournament = await createTournament();
		const team = await createTeam(tournament.id, "Old Name");

		await insertEvent({
			type: "TEAM_REGISTERED",
			actorUserId: 1,
			tournamentTeamId: team.id,
		});

		await db.transaction().execute((trx) =>
			TournamentAuditLogRepository.updateTeamHistoryName(trx, {
				tournamentTeamId: team.id,
				name: "New Name",
			}),
		);

		const events = await TournamentAuditLogRepository.findByTournamentId({
			tournamentId: tournament.id,
			limit: 30,
			offset: 0,
		});

		expect(events[0].team?.name).toBe("New Name");
	});
});
