import { ordinal, rating } from "openskill";
import { describe, expect, test } from "vitest";
import type { AllMatchResult } from "~/features/tournament-match/TournamentMatchRepository.server";
import invariant from "~/utils/invariant";
import type { Tables } from "../../../db/tables";
import type * as Progression from "./Progression";
import { tournamentSummary } from "./summarizer.server";
import type { TournamentDataTeam } from "./Tournament.server";

const createOpponent = (
	id: number,
	result: "win" | "loss",
	score: number,
	droppedOut = false,
	activeRosterUserIds: number[] | null = null,
	memberUserIds: number[] = [],
): AllMatchResult["opponentOne"] => ({
	id,
	result,
	score,
	droppedOut,
	activeRosterUserIds,
	memberUserIds,
});

describe("tournamentSummary()", () => {
	const createTeam = (
		teamId: number,
		userIds: number[],
	): TournamentDataTeam => ({
		checkIns: [],
		createdAt: 0,
		id: teamId,
		inviteCode: null,
		avgSeedingSkillOrdinal: null,
		startingBracketIdx: null,
		abDivision: null,
		mapPool: [],
		members: userIds.map((userId) => ({
			country: null,
			customUrl: null,
			discordAvatar: null,
			discordId: "123",
			username: "test",
			inGameName: "test",
			twitch: null,
			plusTier: null,
			createdAt: 0,
			userId,
			streamTwitch: null,
			streamViewerCount: null,
			streamThumbnailUrl: null,
			role: "REGULAR",
			isSub: 0,
			customAvatarUrl: null,
		})),
		name: `Team ${teamId}`,
		prefersNotToHost: 0,
		droppedOut: 0,
		team: null,
		seed: 1,
		activeRosterUserIds: [],
		avatarImgId: null,
		pickupAvatarUrl: null,
	});

	function summarize({
		results,
		seedingSkillCountsFor,
		withMemberInTwoTeams = false,
		teamsWithStartingBrackets,
		teamsWithAbDivisions,
		progression,
		finalStandings,
	}: {
		results?: AllMatchResult[];
		seedingSkillCountsFor?: Tables["SeedingSkill"]["type"];
		withMemberInTwoTeams?: boolean;
		teamsWithStartingBrackets?: Array<{
			id: number;
			startingBracketIdx: number | null;
		}>;
		teamsWithAbDivisions?: Array<{
			id: number;
			abDivision: 0 | 1;
		}>;
		progression?: Progression.ParsedBracket[];
		finalStandings?: Array<{
			placement: number;
			team: TournamentDataTeam;
		}>;
	} = {}) {
		const defaultTeams = [
			{
				id: 1,
				members: [
					{ userId: 1 },
					{ userId: 2 },
					{ userId: 3 },
					{ userId: 4 },
					{ userId: 20 },
				],
			},
			{
				id: 2,
				members: [{ userId: 5 }, { userId: 6 }, { userId: 7 }, { userId: 8 }],
			},
			{
				id: 3,
				members: [
					{ userId: 9 },
					{ userId: 10 },
					{ userId: 11 },
					{ userId: 12 },
				],
			},
			{
				id: 4,
				members: [
					{ userId: 13 },
					{ userId: 14 },
					{ userId: 15 },
					{ userId: 16 },
				],
			},
		];

		const teams = defaultTeams.map((team) => {
			const startingBracket = teamsWithStartingBrackets?.find(
				(t) => t.id === team.id,
			);
			const abDivisionEntry = teamsWithAbDivisions?.find(
				(t) => t.id === team.id,
			);
			return {
				...team,
				startingBracketIdx: startingBracket?.startingBracketIdx ?? null,
				abDivision: abDivisionEntry?.abDivision ?? null,
			};
		});

		return tournamentSummary({
			finalStandings: finalStandings ?? [
				{
					placement: 1,
					team: createTeam(
						1,
						withMemberInTwoTeams ? [1, 2, 3, 4, 5] : [1, 2, 3, 4],
					),
				},
				{
					placement: 2,
					team: createTeam(2, [5, 6, 7, 8]),
				},
				{
					placement: 3,
					team: createTeam(3, [9, 10, 11, 12]),
				},
				{
					placement: 4,
					team: createTeam(4, [13, 14, 15, 16]),
				},
			],
			results: results ?? [
				{
					maps: [
						{
							mode: "SZ",
							stageId: 1,
							participants: [
								{ tournamentTeamId: 1, userId: 1 },
								{ tournamentTeamId: 1, userId: 2 },
								{ tournamentTeamId: 1, userId: 3 },
								{ tournamentTeamId: 1, userId: 4 },
								{ tournamentTeamId: 2, userId: 5 },
								{ tournamentTeamId: 2, userId: 6 },
								{ tournamentTeamId: 2, userId: 7 },
								{ tournamentTeamId: 2, userId: 8 },
							],
							winnerTeamId: 1,
						},
						{
							mode: "TC",
							stageId: 2,
							participants: [
								{ tournamentTeamId: 1, userId: 1 },
								{ tournamentTeamId: 1, userId: 2 },
								{ tournamentTeamId: 1, userId: 3 },
								{ tournamentTeamId: 1, userId: 4 },
								{ tournamentTeamId: 2, userId: 5 },
								{ tournamentTeamId: 2, userId: 6 },
								{ tournamentTeamId: 2, userId: 7 },
								{ tournamentTeamId: 2, userId: 8 },
							],
							winnerTeamId: 1,
						},
					],
					opponentOne: createOpponent(1, "win", 2),
					opponentTwo: createOpponent(2, "loss", 0),
					roundMaps: {
						count: 3,
						type: "BEST_OF",
					},
				},
			],
			teams,
			queryCurrentTeamRating: () => rating(),
			queryCurrentUserRating: () => ({ rating: rating(), matchesCount: 0 }),
			queryTeamPlayerRatingAverage: () => rating(),
			queryCurrentSeedingRating: () => rating(),
			seedingSkillCountsFor: seedingSkillCountsFor ?? null,
			progression: progression ?? [
				{
					name: "Main Bracket",
					type: "single_elimination",
					settings: {},
					requiresCheckIn: false,
				},
			],
		});
	}

	test("calculates final standings", () => {
		const summary = summarize();
		expect(summary.tournamentResults.length).toBe(4 * 4);
	});

	test("calculates final standings, handling a player in two teams", () => {
		const summary = summarize({ withMemberInTwoTeams: true });
		expect(
			summary.tournamentResults.some(
				(result) => result.tournamentTeamId === 1 && result.userId === 5,
			),
		).toBeTruthy();
		expect(summary.tournamentResults.length).toBe(4 * 4);
	});

	test("winners skill should go up, losers skill should go down", () => {
		const summary = summarize();
		const winnerSkill = summary.skills.find((s) => s.userId === 1);
		const loserSkill = summary.skills.find((s) => s.userId === 5);

		invariant(winnerSkill, "winnerSkill should be defined");
		invariant(loserSkill, "loserSkill should be defined");
		expect(ordinal(winnerSkill)).toBeGreaterThan(ordinal(loserSkill));
	});

	test("seeding skill is calculated the same as normal skill", () => {
		const summary = summarize({ seedingSkillCountsFor: "RANKED" });
		const winnerSkill = summary.skills.find((s) => s.userId === 1);
		const winnerSeedingSkill = summary.skills.find((s) => s.userId === 1);

		invariant(winnerSkill, "winnerSkill should be defined");
		invariant(winnerSeedingSkill, "winnerSeedingSkill should be defined");

		expect(ordinal(winnerSkill)).toBe(ordinal(winnerSeedingSkill));
	});

	test("no seeding skill calculated if seedingSkillCountsFor is null", () => {
		const summary = summarize();

		expect(summary.seedingSkills.length).toBe(0);
	});

	test("seeding skills type matches the given seedingSkillCountsFor", () => {
		const summary = summarize({ seedingSkillCountsFor: "RANKED" });
		expect(summary.seedingSkills[0].type).toBe("RANKED");

		const summary2 = summarize({ seedingSkillCountsFor: "UNRANKED" });
		expect(summary2.seedingSkills[0].type).toBe("UNRANKED");
	});

	const resultsWith20: AllMatchResult[] = [
		{
			maps: [
				{
					mode: "SZ",
					stageId: 1,
					participants: [
						{ tournamentTeamId: 1, userId: 1 },
						{ tournamentTeamId: 1, userId: 2 },
						{ tournamentTeamId: 1, userId: 3 },
						{ tournamentTeamId: 1, userId: 4 },
						{ tournamentTeamId: 2, userId: 5 },
						{ tournamentTeamId: 2, userId: 6 },
						{ tournamentTeamId: 2, userId: 7 },
						{ tournamentTeamId: 2, userId: 8 },
					],
					winnerTeamId: 1,
				},
				{
					mode: "TC",
					stageId: 2,
					participants: [
						{ tournamentTeamId: 1, userId: 1 },
						{ tournamentTeamId: 1, userId: 2 },
						{ tournamentTeamId: 1, userId: 3 },
						{ tournamentTeamId: 1, userId: 4 },
						{ tournamentTeamId: 2, userId: 5 },
						{ tournamentTeamId: 2, userId: 6 },
						{ tournamentTeamId: 2, userId: 7 },
						{ tournamentTeamId: 2, userId: 8 },
					],
					winnerTeamId: 1,
				},
			],
			opponentOne: createOpponent(1, "win", 2),
			opponentTwo: createOpponent(2, "loss", 0),
			roundMaps: {
				count: 3,
				type: "BEST_OF",
			},
		},
		{
			maps: [
				{
					mode: "SZ",
					stageId: 1,
					participants: [
						{ tournamentTeamId: 1, userId: 1 },
						{ tournamentTeamId: 1, userId: 20 },
						{ tournamentTeamId: 1, userId: 3 },
						{ tournamentTeamId: 1, userId: 4 },
						{ tournamentTeamId: 2, userId: 5 },
						{ tournamentTeamId: 2, userId: 6 },
						{ tournamentTeamId: 2, userId: 7 },
						{ tournamentTeamId: 2, userId: 8 },
					],
					winnerTeamId: 1,
				},
				{
					mode: "TC",
					stageId: 2,
					participants: [
						{ tournamentTeamId: 1, userId: 1 },
						{ tournamentTeamId: 1, userId: 20 },
						{ tournamentTeamId: 1, userId: 3 },
						{ tournamentTeamId: 1, userId: 4 },
						{ tournamentTeamId: 2, userId: 5 },
						{ tournamentTeamId: 2, userId: 6 },
						{ tournamentTeamId: 2, userId: 7 },
						{ tournamentTeamId: 2, userId: 8 },
					],
					winnerTeamId: 1,
				},
			],
			opponentOne: createOpponent(1, "win", 2),
			opponentTwo: createOpponent(2, "loss", 0),
			roundMaps: {
				count: 3,
				type: "BEST_OF",
			},
		},
	];

	test("winning more than once makes the skill go up more", () => {
		const summary = summarize({
			results: resultsWith20,
		});
		const twoTimeWinnerSkill = summary.skills.find((s) => s.userId === 1);
		const oneTimeWinnerSkill = summary.skills.find((s) => s.userId === 2);

		invariant(twoTimeWinnerSkill, "twoTimeWinnerSkill should be defined");
		invariant(oneTimeWinnerSkill, "oneTimeWinnerSkill should be defined");
		expect(ordinal(twoTimeWinnerSkill)).toBeGreaterThan(
			ordinal(oneTimeWinnerSkill),
		);
	});

	test("calculates team skills (many rosters for same team)", () => {
		const summary = summarize({
			results: resultsWith20,
		});
		const teamOneRosterOne = summary.skills.find(
			(s) => s.identifier === "1-2-3-4",
		);
		const teamOneRosterTwo = summary.skills.find(
			(s) => s.identifier === "1-3-4-20",
		);
		expect(teamOneRosterOne).toBeTruthy();
		expect(teamOneRosterTwo).toBeTruthy();
	});

	const resultsWithSubbedRoster: AllMatchResult[] = [
		{
			maps: [
				{
					mode: "SZ",
					stageId: 1,
					participants: [
						{ tournamentTeamId: 1, userId: 1 },
						{ tournamentTeamId: 1, userId: 2 },
						{ tournamentTeamId: 1, userId: 3 },
						{ tournamentTeamId: 1, userId: 4 },
						{ tournamentTeamId: 2, userId: 5 },
						{ tournamentTeamId: 2, userId: 6 },
						{ tournamentTeamId: 2, userId: 7 },
						{ tournamentTeamId: 2, userId: 8 },
					],
					winnerTeamId: 1,
				},
				{
					mode: "TC",
					stageId: 2,
					participants: [
						{ tournamentTeamId: 1, userId: 1 },
						{ tournamentTeamId: 1, userId: 2 },
						{ tournamentTeamId: 1, userId: 3 },
						{ tournamentTeamId: 1, userId: 4 },
						{ tournamentTeamId: 2, userId: 5 },
						{ tournamentTeamId: 2, userId: 6 },
						{ tournamentTeamId: 2, userId: 7 },
						{ tournamentTeamId: 2, userId: 8 },
					],
					winnerTeamId: 2,
				},
				{
					mode: "TC",
					stageId: 2,
					participants: [
						{ tournamentTeamId: 1, userId: 1 },
						{ tournamentTeamId: 1, userId: 20 },
						{ tournamentTeamId: 1, userId: 3 },
						{ tournamentTeamId: 1, userId: 4 },
						{ tournamentTeamId: 2, userId: 5 },
						{ tournamentTeamId: 2, userId: 6 },
						{ tournamentTeamId: 2, userId: 7 },
						{ tournamentTeamId: 2, userId: 8 },
					],
					winnerTeamId: 1,
				},
			],
			opponentOne: createOpponent(1, "win", 2),
			opponentTwo: createOpponent(2, "loss", 1),
			roundMaps: {
				count: 3,
				type: "BEST_OF",
			},
		},
	];

	test("In the case of sub calculates skill based on the most common roster", () => {
		const summary = summarize({
			results: resultsWithSubbedRoster,
		});
		const teamOneRosterOne = summary.skills.find(
			(s) => s.identifier === "1-2-3-4",
		);
		const teamOneRosterTwo = summary.skills.find(
			(s) => s.identifier === "1-3-4-20",
		);
		expect(teamOneRosterOne).toBeTruthy();
		expect(teamOneRosterTwo).toBeFalsy();
	});

	test("In the case of sub calculates player results based on the most common roster", () => {
		const summary = summarize({
			results: resultsWithSubbedRoster,
		});
		expect(
			summary.playerResultDeltas.find(
				(p) =>
					p.ownerUserId === 5 &&
					p.otherUserId === 20 &&
					(p.setWins > 0 || p.setLosses > 0),
			),
		).toBeFalsy();
	});

	test("calculates results of mates", () => {
		const summary = summarize();
		const result = summary.playerResultDeltas.find(
			(r) => r.ownerUserId === 1 && r.otherUserId === 2,
		);

		invariant(result, "result should be defined");
		expect(result.setWins).toBe(1);
		expect(result.setLosses).toBe(0);
		expect(result.mapWins).toBe(2);
		expect(result.mapLosses).toBe(0);
		expect(result.type).toBe("MATE");
	});

	test("calculates results of opponents", () => {
		const summary = summarize();
		const result = summary.playerResultDeltas.find(
			(r) => r.ownerUserId === 1 && r.otherUserId === 5,
		);

		invariant(result, "result should be defined");
		expect(result.setWins).toBe(1);
		expect(result.setLosses).toBe(0);
		expect(result.mapWins).toBe(2);
		expect(result.mapLosses).toBe(0);
		expect(result.type).toBe("ENEMY");
	});

	test("calculates results of opponents (losing side)", () => {
		const summary = summarize();
		const result = summary.playerResultDeltas.find(
			(r) => r.ownerUserId === 5 && r.otherUserId === 1,
		);

		invariant(result, "result should be defined");
		expect(result.setWins).toBe(0);
		expect(result.setLosses).toBe(1);
		expect(result.mapWins).toBe(0);
		expect(result.mapLosses).toBe(2);
		expect(result.type).toBe("ENEMY");
	});

	test("calculates map results", () => {
		const summary = summarize();
		const result = summary.mapResultDeltas.filter((r) => r.userId === 1);
		expect(result.length).toBe(2);
		expect(result.every((r) => r.wins === 1 && r.losses === 0)).toBeTruthy();
	});

	test("calculates set results array", () => {
		const summary = summarize();

		const winner = summary.setResults.get(1);
		const loser = summary.setResults.get(5);
		const sub = summary.setResults.get(20);

		invariant(winner, "winner should be defined");
		invariant(loser, "loser should be defined");
		invariant(sub, "sub should be defined");

		expect(winner).toEqual(["W"]);
		expect(loser).toEqual(["L"]);
		expect(sub).toEqual([null]);
	});

	test("playing for many teams should include combined sets in the set results array", () => {
		const summary = summarize({
			withMemberInTwoTeams: true,
			results: resultsWith20,
		});

		const results = summary.setResults.get(20);

		// only sub for the first team (null) and winning for the second team (W)
		expect(results).toEqual([null, "W"]);
	});

	test("playing minority of maps in a set should not be count for set results", () => {
		const summary = summarize({
			results: [
				{
					maps: [
						{
							mode: "SZ",
							stageId: 1,
							participants: [
								{ tournamentTeamId: 1, userId: 1 },
								{ tournamentTeamId: 1, userId: 2 },
								{ tournamentTeamId: 1, userId: 3 },
								{ tournamentTeamId: 1, userId: 4 },
								{ tournamentTeamId: 2, userId: 5 },
								{ tournamentTeamId: 2, userId: 6 },
								{ tournamentTeamId: 2, userId: 7 },
								{ tournamentTeamId: 2, userId: 8 },
							],
							winnerTeamId: 1,
						},
						{
							mode: "SZ",
							stageId: 1,
							participants: [
								{ tournamentTeamId: 1, userId: 20 },
								{ tournamentTeamId: 1, userId: 2 },
								{ tournamentTeamId: 1, userId: 3 },
								{ tournamentTeamId: 1, userId: 4 },
								{ tournamentTeamId: 2, userId: 5 },
								{ tournamentTeamId: 2, userId: 6 },
								{ tournamentTeamId: 2, userId: 7 },
								{ tournamentTeamId: 2, userId: 8 },
							],
							winnerTeamId: 1,
						},
						{
							mode: "SZ",
							stageId: 1,
							participants: [
								{ tournamentTeamId: 1, userId: 20 },
								{ tournamentTeamId: 1, userId: 2 },
								{ tournamentTeamId: 1, userId: 3 },
								{ tournamentTeamId: 1, userId: 4 },
								{ tournamentTeamId: 2, userId: 5 },
								{ tournamentTeamId: 2, userId: 6 },
								{ tournamentTeamId: 2, userId: 7 },
								{ tournamentTeamId: 2, userId: 8 },
							],
							winnerTeamId: 1,
						},
					],
					opponentOne: createOpponent(1, "win", 3),
					opponentTwo: createOpponent(2, "loss", 0),
					roundMaps: {
						count: 3,
						type: "BEST_OF",
					},
				},
			],
		});

		const results = summary.setResults.get(1);
		expect(results).toEqual([null]);
	});

	test("playing in half the maps should be enough to count for set results", () => {
		const summary = summarize({
			results: [
				{
					maps: [
						{
							mode: "SZ",
							stageId: 1,
							participants: [
								{ tournamentTeamId: 1, userId: 1 },
								{ tournamentTeamId: 1, userId: 2 },
								{ tournamentTeamId: 1, userId: 3 },
								{ tournamentTeamId: 1, userId: 4 },
								{ tournamentTeamId: 2, userId: 5 },
								{ tournamentTeamId: 2, userId: 6 },
								{ tournamentTeamId: 2, userId: 7 },
								{ tournamentTeamId: 2, userId: 8 },
							],
							winnerTeamId: 1,
						},
						{
							mode: "SZ",
							stageId: 1,
							participants: [
								{ tournamentTeamId: 1, userId: 20 },
								{ tournamentTeamId: 1, userId: 2 },
								{ tournamentTeamId: 1, userId: 3 },
								{ tournamentTeamId: 1, userId: 4 },
								{ tournamentTeamId: 2, userId: 5 },
								{ tournamentTeamId: 2, userId: 6 },
								{ tournamentTeamId: 2, userId: 7 },
								{ tournamentTeamId: 2, userId: 8 },
							],
							winnerTeamId: 1,
						},
					],
					opponentOne: createOpponent(1, "win", 2),
					opponentTwo: createOpponent(2, "loss", 0),
					roundMaps: {
						count: 3,
						type: "BEST_OF",
					},
				},
			],
		});

		for (const userId of [1, 20]) {
			const results = summary.setResults.get(userId);
			invariant(results, `results for user ${userId} should be defined`);
			expect(results).toEqual(["W"]);
		}
	});

	test("div is null when teams have no startingBracketIdx", () => {
		const summary = summarize();

		for (const result of summary.tournamentResults) {
			expect(result.div).toBeNull();
		}
	});

	test("div is set correctly for teams with startingBracketIdx", () => {
		const summary = summarize({
			teamsWithStartingBrackets: [
				{ id: 1, startingBracketIdx: 0 },
				{ id: 2, startingBracketIdx: 1 },
				{ id: 3, startingBracketIdx: 0 },
				{ id: 4, startingBracketIdx: 1 },
			],
			progression: [
				{
					name: "Division 1",
					type: "single_elimination",
					settings: {},
					requiresCheckIn: false,
				},
				{
					name: "Division 2",
					type: "single_elimination",
					settings: {},
					requiresCheckIn: false,
				},
			],
			finalStandings: [
				{
					placement: 1,
					team: createTeam(1, [1, 2, 3, 4]),
				},
				{
					placement: 1,
					team: createTeam(2, [5, 6, 7, 8]),
				},
				{
					placement: 2,
					team: createTeam(3, [9, 10, 11, 12]),
				},
				{
					placement: 2,
					team: createTeam(4, [13, 14, 15, 16]),
				},
			],
		});

		const team1Results = summary.tournamentResults.filter(
			(r) => r.tournamentTeamId === 1,
		);
		const team2Results = summary.tournamentResults.filter(
			(r) => r.tournamentTeamId === 2,
		);

		expect(team1Results.every((r) => r.div === "Division 1")).toBeTruthy();
		expect(team2Results.every((r) => r.div === "Division 2")).toBeTruthy();
	});

	test("participantCount is correct for multi-division tournaments", () => {
		const summary = summarize({
			teamsWithStartingBrackets: [
				{ id: 1, startingBracketIdx: 0 },
				{ id: 2, startingBracketIdx: 1 },
				{ id: 3, startingBracketIdx: 0 },
				{ id: 4, startingBracketIdx: 1 },
			],
			progression: [
				{
					name: "Division 1",
					type: "single_elimination",
					settings: {},
					requiresCheckIn: false,
				},
				{
					name: "Division 2",
					type: "single_elimination",
					settings: {},
					requiresCheckIn: false,
				},
			],
			finalStandings: [
				{
					placement: 1,
					team: createTeam(1, [1, 2, 3, 4]),
				},
				{
					placement: 1,
					team: createTeam(2, [5, 6, 7, 8]),
				},
				{
					placement: 2,
					team: createTeam(3, [9, 10, 11, 12]),
				},
				{
					placement: 2,
					team: createTeam(4, [13, 14, 15, 16]),
				},
			],
		});

		const team1Results = summary.tournamentResults.filter(
			(r) => r.tournamentTeamId === 1,
		);
		const team2Results = summary.tournamentResults.filter(
			(r) => r.tournamentTeamId === 2,
		);
		const team3Results = summary.tournamentResults.filter(
			(r) => r.tournamentTeamId === 3,
		);
		const team4Results = summary.tournamentResults.filter(
			(r) => r.tournamentTeamId === 4,
		);

		expect(team1Results.every((r) => r.participantCount === 2)).toBeTruthy();
		expect(team2Results.every((r) => r.participantCount === 2)).toBeTruthy();
		expect(team3Results.every((r) => r.participantCount === 2)).toBeTruthy();
		expect(team4Results.every((r) => r.participantCount === 2)).toBeTruthy();
	});

	test("div is set from abDivision when finals is an A/B divisions round robin", () => {
		const summary = summarize({
			teamsWithAbDivisions: [
				{ id: 1, abDivision: 0 },
				{ id: 2, abDivision: 1 },
				{ id: 3, abDivision: 0 },
				{ id: 4, abDivision: 1 },
			],
			progression: [
				{
					name: "Groups stage",
					type: "round_robin",
					settings: { hasAbDivisions: true, teamsPerGroup: 4 },
					requiresCheckIn: false,
				},
			],
			finalStandings: [
				{
					placement: 1,
					team: createTeam(1, [1, 2, 3, 4]),
				},
				{
					placement: 1,
					team: createTeam(2, [5, 6, 7, 8]),
				},
				{
					placement: 2,
					team: createTeam(3, [9, 10, 11, 12]),
				},
				{
					placement: 2,
					team: createTeam(4, [13, 14, 15, 16]),
				},
			],
		});

		const team1Results = summary.tournamentResults.filter(
			(r) => r.tournamentTeamId === 1,
		);
		const team2Results = summary.tournamentResults.filter(
			(r) => r.tournamentTeamId === 2,
		);
		const team3Results = summary.tournamentResults.filter(
			(r) => r.tournamentTeamId === 3,
		);
		const team4Results = summary.tournamentResults.filter(
			(r) => r.tournamentTeamId === 4,
		);

		expect(team1Results.every((r) => r.div === "A")).toBeTruthy();
		expect(team2Results.every((r) => r.div === "B")).toBeTruthy();
		expect(team3Results.every((r) => r.div === "A")).toBeTruthy();
		expect(team4Results.every((r) => r.div === "B")).toBeTruthy();
	});

	test("participantCount counts teams per abDivision for A/B finals", () => {
		const summary = summarize({
			teamsWithAbDivisions: [
				{ id: 1, abDivision: 0 },
				{ id: 2, abDivision: 1 },
				{ id: 3, abDivision: 0 },
				{ id: 4, abDivision: 1 },
			],
			progression: [
				{
					name: "Groups stage",
					type: "round_robin",
					settings: { hasAbDivisions: true, teamsPerGroup: 4 },
					requiresCheckIn: false,
				},
			],
			finalStandings: [
				{
					placement: 1,
					team: createTeam(1, [1, 2, 3, 4]),
				},
				{
					placement: 1,
					team: createTeam(2, [5, 6, 7, 8]),
				},
				{
					placement: 2,
					team: createTeam(3, [9, 10, 11, 12]),
				},
				{
					placement: 2,
					team: createTeam(4, [13, 14, 15, 16]),
				},
			],
		});

		for (const result of summary.tournamentResults) {
			expect(result.participantCount).toBe(2);
		}
	});

	test("excludes matches ended early by organizer from calculations", () => {
		const summary = summarize({
			results: [
				{
					maps: [
						{
							mode: "SZ",
							stageId: 1,
							participants: [
								{ tournamentTeamId: 1, userId: 1 },
								{ tournamentTeamId: 1, userId: 2 },
								{ tournamentTeamId: 1, userId: 3 },
								{ tournamentTeamId: 1, userId: 4 },
								{ tournamentTeamId: 2, userId: 5 },
								{ tournamentTeamId: 2, userId: 6 },
								{ tournamentTeamId: 2, userId: 7 },
								{ tournamentTeamId: 2, userId: 8 },
							],
							winnerTeamId: 1,
						},
					],
					opponentOne: createOpponent(1, "win", 0),
					opponentTwo: createOpponent(2, "loss", 0),
					roundMaps: {
						count: 3,
						type: "BEST_OF",
					},
				},
			],
		});

		expect(summary.skills.length).toBe(0);
		expect(summary.mapResultDeltas.length).toBe(0);
		expect(summary.playerResultDeltas.length).toBe(0);
	});

	test("includes normal matches but excludes early-ended matches from calculations", () => {
		const summary = summarize({
			results: [
				{
					maps: [
						{
							mode: "SZ",
							stageId: 1,
							participants: [
								{ tournamentTeamId: 1, userId: 1 },
								{ tournamentTeamId: 1, userId: 2 },
								{ tournamentTeamId: 1, userId: 3 },
								{ tournamentTeamId: 1, userId: 4 },
								{ tournamentTeamId: 2, userId: 5 },
								{ tournamentTeamId: 2, userId: 6 },
								{ tournamentTeamId: 2, userId: 7 },
								{ tournamentTeamId: 2, userId: 8 },
							],
							winnerTeamId: 1,
						},
					],
					opponentOne: createOpponent(1, "win", 1),
					opponentTwo: createOpponent(2, "loss", 0),
					roundMaps: {
						count: 3,
						type: "BEST_OF",
					},
				},
				{
					maps: [
						{
							mode: "TC",
							stageId: 2,
							participants: [
								{ tournamentTeamId: 3, userId: 9 },
								{ tournamentTeamId: 3, userId: 10 },
								{ tournamentTeamId: 3, userId: 11 },
								{ tournamentTeamId: 3, userId: 12 },
								{ tournamentTeamId: 4, userId: 13 },
								{ tournamentTeamId: 4, userId: 14 },
								{ tournamentTeamId: 4, userId: 15 },
								{ tournamentTeamId: 4, userId: 16 },
							],
							winnerTeamId: 3,
						},
					],
					opponentOne: createOpponent(3, "win", 0),
					opponentTwo: createOpponent(4, "loss", 0),
					roundMaps: {
						count: 3,
						type: "BEST_OF",
					},
				},
			],
		});

		const skillsFromTeam1 = summary.skills.filter((s) =>
			[1, 2, 3, 4].includes(s.userId ?? 0),
		);
		const skillsFromTeam2 = summary.skills.filter((s) =>
			[5, 6, 7, 8].includes(s.userId ?? 0),
		);
		const skillsFromTeam3 = summary.skills.filter((s) =>
			[9, 10, 11, 12].includes(s.userId ?? 0),
		);
		const skillsFromTeam4 = summary.skills.filter((s) =>
			[13, 14, 15, 16].includes(s.userId ?? 0),
		);

		expect(skillsFromTeam1.length).toBe(0);
		expect(skillsFromTeam2.length).toBe(0);
		expect(skillsFromTeam3.length).toBe(0);
		expect(skillsFromTeam4.length).toBe(0);
	});

	test("includes early-ended matches from dropped teams in skill calculations", () => {
		const summary = summarize({
			results: [
				{
					maps: [
						{
							mode: "SZ",
							stageId: 1,
							participants: [
								{ tournamentTeamId: 1, userId: 1 },
								{ tournamentTeamId: 1, userId: 2 },
								{ tournamentTeamId: 1, userId: 3 },
								{ tournamentTeamId: 1, userId: 4 },
								{ tournamentTeamId: 2, userId: 5 },
								{ tournamentTeamId: 2, userId: 6 },
								{ tournamentTeamId: 2, userId: 7 },
								{ tournamentTeamId: 2, userId: 8 },
							],
							winnerTeamId: 1,
						},
					],
					opponentOne: createOpponent(1, "win", 1, false),
					opponentTwo: createOpponent(2, "loss", 0, true),
					roundMaps: {
						count: 3,
						type: "BEST_OF",
					},
				},
			],
		});

		const skillsFromTeam1 = summary.skills.filter((s) =>
			[1, 2, 3, 4].includes(s.userId ?? 0),
		);
		const skillsFromTeam2 = summary.skills.filter((s) =>
			[5, 6, 7, 8].includes(s.userId ?? 0),
		);

		expect(skillsFromTeam1.length).toBe(4);
		expect(skillsFromTeam2.length).toBe(4);
	});

	test("includes dropped team sets without maps using active roster", () => {
		const summary = summarize({
			results: [
				{
					maps: [],
					opponentOne: createOpponent(1, "win", 0, false, [1, 2, 3, 4]),
					opponentTwo: createOpponent(2, "loss", 0, true, [5, 6, 7, 8]),
					roundMaps: {
						count: 3,
						type: "BEST_OF",
					},
				},
			],
		});

		const skillsFromTeam1 = summary.skills.filter((s) =>
			[1, 2, 3, 4].includes(s.userId ?? 0),
		);
		const skillsFromTeam2 = summary.skills.filter((s) =>
			[5, 6, 7, 8].includes(s.userId ?? 0),
		);

		expect(skillsFromTeam1.length).toBe(4);
		expect(skillsFromTeam2.length).toBe(4);
	});

	test("includes dropped team sets without maps using memberUserIds as fallback", () => {
		const summary = summarize({
			results: [
				{
					maps: [],
					// No activeRosterUserIds, but memberUserIds is set
					opponentOne: createOpponent(1, "win", 0, false, null, [1, 2, 3, 4]),
					opponentTwo: createOpponent(2, "loss", 0, true, null, [5, 6, 7, 8]),
					roundMaps: {
						count: 3,
						type: "BEST_OF",
					},
				},
			],
		});

		const skillsFromTeam1 = summary.skills.filter((s) =>
			[1, 2, 3, 4].includes(s.userId ?? 0),
		);
		const skillsFromTeam2 = summary.skills.filter((s) =>
			[5, 6, 7, 8].includes(s.userId ?? 0),
		);

		expect(skillsFromTeam1.length).toBe(4);
		expect(skillsFromTeam2.length).toBe(4);
	});
});
