import { describe, expect, it } from "vitest";
import type { TournamentStageSettings } from "~/db/tables";
import { Tournament } from "~/features/tournament-bracket/core/Tournament";
import {
	LOW_INK_AUGUST_2025,
	RUSH_WEEKEND_3,
} from "~/features/tournament-bracket/core/tests/mocks-swiss";
import { ZONES_WEEKLY_38 } from "~/features/tournament-bracket/core/tests/mocks-zones-weekly";
import invariant from "~/utils/invariant";
import * as Engine from "./engine";
import { pairUp } from "./engine/swiss/pairing";
import * as TeamStatus from "./engine/swiss/team-status";

const Swiss = {
	...TeamStatus,
	pairUp,
	create: (args: {
		tournamentId: number;
		name: string;
		seeding: number[];
		settings?: TournamentStageSettings;
	}) =>
		Engine.create({
			...args,
			type: "swiss",
			settings: args.settings ?? null,
		}),
};

describe("Swiss", () => {
	const createArgsWithDefaults = (
		args: Partial<Parameters<typeof Swiss.create>[0]> = {},
	): Parameters<typeof Swiss.create>[0] => {
		return {
			name: "Swiss Tournament",
			seeding: [1, 2, 3, 4],
			settings: {},
			tournamentId: 1,
			...args,
		};
	};

	describe("create()", () => {
		it("attaches the correct tournament id to the data", () => {
			const data = Swiss.create(createArgsWithDefaults());

			expect(data.stage[0].tournament_id).toBe(1);
		});

		it("creates a swiss bracket with correct amount of initial matches", () => {
			const data = Swiss.create(createArgsWithDefaults());

			expect(data.match).toHaveLength(2);
		});

		it("creates a swiss bracket with correct amount of rounds as default", () => {
			const data = Swiss.create(createArgsWithDefaults());

			expect(data.round).toHaveLength(5);
		});

		it("creates a swiss bracket with correct amount of rounds as parameter", () => {
			const data = Swiss.create(
				createArgsWithDefaults({
					settings: {
						groupCount: 1,
						roundCount: 4,
					},
				}),
			);

			expect(data.round).toHaveLength(4);
		});

		it("creates a swiss bracket with two groups", () => {
			const data = Swiss.create(
				createArgsWithDefaults({
					settings: {
						groupCount: 2,
						roundCount: 5,
					},
				}),
			);

			expect(data.round).toHaveLength(10);

			const matchGroupIds = data.match.map((m) => m.group_id);
			expect(matchGroupIds).toContain(0);
			expect(matchGroupIds).toContain(1);
		});

		it("every team has a match", () => {
			const data = Swiss.create(createArgsWithDefaults());

			for (const teamId of [1, 2, 3, 4]) {
				expect(
					data.match.some(
						(match) =>
							match.opponent1?.id === teamId || match.opponent2?.id === teamId,
					),
				).toBe(true);
			}
		});

		it("assigns a BYE if odd number of teams", () => {
			const data = Swiss.create(
				createArgsWithDefaults({
					seeding: [1, 2, 3, 4, 5],
				}),
			);

			const byes = data.match.filter((match) => match.opponent2 === null);
			expect(byes).toHaveLength(1);
		});

		it("if no teams, should generate a bracket data with no matches", () => {
			const data = Swiss.create(createArgsWithDefaults({ seeding: [] }));

			expect(data.match).toHaveLength(0);
		});
	});

	describe("generateMatchUps()", () => {
		describe("Zones Weekly 38", () => {
			const tournament = new Tournament({
				...ZONES_WEEKLY_38(),
				simulateBrackets: false,
			});

			const bracket = tournament.bracketByIdx(0)!;

			const matches = Engine.generateRound(bracket.data as Engine.BracketData, {
				groupId: 4443,
				standings: bracket.standings,
				settings: bracket.settings,
			})._unsafeUnwrap().matches;

			it("finds new opponents for each team in the last round", () => {
				for (const match of matches) {
					if (match.opponent2 === null) continue;

					const opponent1 = match.opponent1!.id as number;
					const opponent2 = match.opponent2.id as number;

					const existingMatch = bracket.data.match.find(
						(m) =>
							(m.opponent1?.id === opponent1 &&
								m.opponent2?.id === opponent2) ||
							(m.opponent1?.id === opponent2 && m.opponent2?.id === opponent1),
					);

					expect(existingMatch).toBeUndefined();
				}
			});

			it("generates a bye", () => {
				const byes = matches.filter((match) => match.opponent2 === null);
				expect(byes).toHaveLength(1);
			});

			it("every pair is max one set win from each other", () => {
				for (const match of matches) {
					if (match.opponent2 === null) continue;

					const opponent1 = match.opponent1!.id as number;
					const opponent2 = match.opponent2.id as number;

					const opponent1Stats = bracket.standings.find(
						(s) => s.team.id === opponent1,
					)?.stats;
					const opponent2Stats = bracket.standings.find(
						(s) => s.team.id === opponent2,
					)?.stats;

					invariant(opponent1Stats, "Opponent 1 not found in standings");
					invariant(opponent2Stats, "Opponent 2 not found in standings");

					expect(
						Math.abs(opponent1Stats.setWins - opponent2Stats.setWins),
					).toBeLessThanOrEqual(1);
				}
			});
		});
	});

	const PAIR_UP_TEST_CASES = [RUSH_WEEKEND_3, LOW_INK_AUGUST_2025];

	describe("pairUp()", () => {
		it.for(
			PAIR_UP_TEST_CASES,
		)("all teams have matches (pair up test cases idx %#)", (testCase) => {
			const result = Swiss.pairUp(testCase);

			const inputTeams = testCase.map((team) => team.id).sort((a, b) => a - b);
			const resultTeams = result
				.flatMap((match) => [match.opponentOne, match.opponentTwo])
				.filter((val) => val !== null)
				.sort((a, b) => a - b);

			expect(inputTeams).toEqual(resultTeams);
		});

		it.for(
			PAIR_UP_TEST_CASES,
		)("every pair is max one set win from each other (pair up test cases idx %#)", (testCase) => {
			const result = Swiss.pairUp(testCase);

			for (const match of result) {
				if (match.opponentOne === null || match.opponentTwo === null) continue;

				const opponentOneScore = testCase.find(
					(t) => t.id === match.opponentOne,
				)!.score;
				const opponentTwoScore = testCase.find(
					(t) => t.id === match.opponentTwo,
				)!.score;

				expect(
					Math.abs(opponentOneScore - opponentTwoScore),
					`Teams ${match.opponentOne} and ${match.opponentTwo} have too large score difference (${opponentOneScore} vs ${opponentTwoScore})`,
				).toBeLessThanOrEqual(1);
			}
		});

		it.for(
			PAIR_UP_TEST_CASES,
		)("should match perfect records against each other as much as possible (pair up test cases idx %#)", (testCase) => {
			const result = Swiss.pairUp(testCase);

			const maxScore = testCase.reduce(
				(max, team) => Math.max(max, team.score),
				0,
			);
			const perfectRecordsCount = testCase.filter(
				(team) => team.score === maxScore,
			).length;

			let perfectRecordsPlayingEachOtherCount = 0;

			for (const match of result) {
				if (match.opponentOne === null || match.opponentTwo === null) continue;

				const oneIsPerfectScore = testCase.some(
					(team) => team.id === match.opponentOne && team.score === maxScore,
				);
				const twoIsPerfectScore = testCase.some(
					(team) => team.id === match.opponentTwo && team.score === maxScore,
				);

				if (oneIsPerfectScore && twoIsPerfectScore) {
					perfectRecordsPlayingEachOtherCount++;
				}
			}

			expect(perfectRecordsPlayingEachOtherCount).toBe(
				Math.floor(perfectRecordsCount / 2),
			);
		});

		it.for(
			PAIR_UP_TEST_CASES,
		)("generates max one bye (pair up test cases idx %#)", (testCase) => {
			const result = Swiss.pairUp(testCase);

			let byes = 0;
			for (const match of result) {
				if (match.opponentOne === null || match.opponentTwo === null) byes++;
			}

			expect(byes).toBeLessThanOrEqual(1);
		});
	});

	describe("calculateTeamStatus()", () => {
		it("returns 'advanced' when team has enough wins", () => {
			expect(
				Swiss.calculateTeamStatus({
					wins: 3,
					losses: 0,
					advanceThreshold: 3,
					roundCount: 5,
				}),
			).toBe("advanced");
			expect(
				Swiss.calculateTeamStatus({
					wins: 3,
					losses: 1,
					advanceThreshold: 3,
					roundCount: 5,
				}),
			).toBe("advanced");
			expect(
				Swiss.calculateTeamStatus({
					wins: 4,
					losses: 1,
					advanceThreshold: 3,
					roundCount: 5,
				}),
			).toBe("advanced");
		});

		it("returns 'eliminated' when team has too many losses", () => {
			expect(
				Swiss.calculateTeamStatus({
					wins: 0,
					losses: 3,
					advanceThreshold: 3,
					roundCount: 5,
				}),
			).toBe("eliminated");
			expect(
				Swiss.calculateTeamStatus({
					wins: 1,
					losses: 3,
					advanceThreshold: 3,
					roundCount: 5,
				}),
			).toBe("eliminated");
			expect(
				Swiss.calculateTeamStatus({
					wins: 2,
					losses: 3,
					advanceThreshold: 3,
					roundCount: 5,
				}),
			).toBe("eliminated");
		});

		it("returns 'active' when team can still advance or be eliminated", () => {
			expect(
				Swiss.calculateTeamStatus({
					wins: 2,
					losses: 2,
					advanceThreshold: 3,
					roundCount: 5,
				}),
			).toBe("active");
			expect(
				Swiss.calculateTeamStatus({
					wins: 1,
					losses: 1,
					advanceThreshold: 3,
					roundCount: 5,
				}),
			).toBe("active");
			expect(
				Swiss.calculateTeamStatus({
					wins: 0,
					losses: 0,
					advanceThreshold: 3,
					roundCount: 5,
				}),
			).toBe("active");
			expect(
				Swiss.calculateTeamStatus({
					wins: 2,
					losses: 1,
					advanceThreshold: 3,
					roundCount: 5,
				}),
			).toBe("active");
		});

		it("handles different tournament configurations", () => {
			// 4-round tournament with advance threshold 2
			expect(
				Swiss.calculateTeamStatus({
					wins: 2,
					losses: 0,
					advanceThreshold: 2,
					roundCount: 4,
				}),
			).toBe("advanced");
			expect(
				Swiss.calculateTeamStatus({
					wins: 0,
					losses: 3,
					advanceThreshold: 2,
					roundCount: 4,
				}),
			).toBe("eliminated");
			expect(
				Swiss.calculateTeamStatus({
					wins: 1,
					losses: 2,
					advanceThreshold: 2,
					roundCount: 4,
				}),
			).toBe("active");

			// 6-round tournament with advance threshold 4
			expect(
				Swiss.calculateTeamStatus({
					wins: 4,
					losses: 1,
					advanceThreshold: 4,
					roundCount: 6,
				}),
			).toBe("advanced");
			expect(
				Swiss.calculateTeamStatus({
					wins: 2,
					losses: 3,
					advanceThreshold: 4,
					roundCount: 6,
				}),
			).toBe("eliminated");
			expect(
				Swiss.calculateTeamStatus({
					wins: 3,
					losses: 2,
					advanceThreshold: 4,
					roundCount: 6,
				}),
			).toBe("active");
		});

		it("handles edge cases correctly", () => {
			// Team reaches advance threshold exactly
			expect(
				Swiss.calculateTeamStatus({
					wins: 3,
					losses: 2,
					advanceThreshold: 3,
					roundCount: 5,
				}),
			).toBe("advanced");

			// Team reaches elimination threshold exactly
			expect(
				Swiss.calculateTeamStatus({
					wins: 2,
					losses: 3,
					advanceThreshold: 3,
					roundCount: 5,
				}),
			).toBe("eliminated");

			// Tournament where advance threshold equals round count
			expect(
				Swiss.calculateTeamStatus({
					wins: 3,
					losses: 0,
					advanceThreshold: 3,
					roundCount: 3,
				}),
			).toBe("advanced");
			expect(
				Swiss.calculateTeamStatus({
					wins: 0,
					losses: 3,
					advanceThreshold: 3,
					roundCount: 3,
				}),
			).toBe("eliminated");
		});
	});

	describe("Threshold validation utilities", () => {
		describe("maxAdvanceThreshold()", () => {
			it("calculates maximum advance threshold correctly", () => {
				expect(Swiss.maxAdvanceThreshold({ roundCount: 3 })).toBe(3); // ceil(3/2) + 1 = 2 + 1 = 3
				expect(Swiss.maxAdvanceThreshold({ roundCount: 4 })).toBe(3); // ceil(4/2) + 1 = 2 + 1 = 3
				expect(Swiss.maxAdvanceThreshold({ roundCount: 5 })).toBe(4); // ceil(5/2) + 1 = 3 + 1 = 4
				expect(Swiss.maxAdvanceThreshold({ roundCount: 6 })).toBe(4); // ceil(6/2) + 1 = 3 + 1 = 4
				expect(Swiss.maxAdvanceThreshold({ roundCount: 7 })).toBe(5); // ceil(7/2) + 1 = 4 + 1 = 5
			});
		});

		describe("isValidAdvanceThreshold()", () => {
			it("validates correct thresholds", () => {
				expect(
					Swiss.isValidAdvanceThreshold({ roundCount: 5, advanceThreshold: 3 }),
				).toBe(true);
				expect(
					Swiss.isValidAdvanceThreshold({ roundCount: 4, advanceThreshold: 2 }),
				).toBe(true);
				expect(
					Swiss.isValidAdvanceThreshold({ roundCount: 6, advanceThreshold: 4 }),
				).toBe(true);
				expect(
					Swiss.isValidAdvanceThreshold({ roundCount: 3, advanceThreshold: 2 }),
				).toBe(true);
			});

			it("rejects invalid thresholds", () => {
				// Threshold too high
				expect(
					Swiss.isValidAdvanceThreshold({ roundCount: 5, advanceThreshold: 5 }),
				).toBe(false); // equals round count
				expect(
					Swiss.isValidAdvanceThreshold({ roundCount: 5, advanceThreshold: 6 }),
				).toBe(false); // exceeds round count

				// Threshold too low
				expect(
					Swiss.isValidAdvanceThreshold({ roundCount: 5, advanceThreshold: 0 }),
				).toBe(false);
				expect(
					Swiss.isValidAdvanceThreshold({
						roundCount: 3,
						advanceThreshold: -1,
					}),
				).toBe(false);
			});

			it("handles edge cases", () => {
				expect(
					Swiss.isValidAdvanceThreshold({ roundCount: 3, advanceThreshold: 2 }),
				).toBe(true); // minimum valid
				expect(
					Swiss.isValidAdvanceThreshold({ roundCount: 5, advanceThreshold: 4 }),
				).toBe(true); // maximum valid for 5 rounds
			});
		});

		describe("validAdvanceThresholdOptions()", () => {
			it("returns correct options for different round counts", () => {
				expect(Swiss.validAdvanceThresholdOptions({ roundCount: 3 })).toEqual([
					2, 3,
				]);
				expect(Swiss.validAdvanceThresholdOptions({ roundCount: 5 })).toEqual([
					2, 3, 4,
				]);
				expect(Swiss.validAdvanceThresholdOptions({ roundCount: 8 })).toEqual([
					2, 3, 4, 5,
				]);
			});

			it("handles minimal round counts", () => {
				expect(Swiss.validAdvanceThresholdOptions({ roundCount: 2 })).toEqual([
					2,
				]);
				expect(Swiss.validAdvanceThresholdOptions({ roundCount: 1 })).toEqual([
					2,
				]);
			});

			it("includes thresholds up to the calculated maximum for large round counts", () => {
				const roundCount = 9;
				const max = Swiss.maxAdvanceThreshold({ roundCount });

				expect(Swiss.validAdvanceThresholdOptions({ roundCount })).toContain(
					max,
				);
				expect(
					Swiss.isValidAdvanceThreshold({ roundCount, advanceThreshold: max }),
				).toBe(true);
			});
		});
	});
});
