// Vitest coverage for the Phase 22 races table active view. The
// component renders against a synthetic `RenderedReportSource` (no
// live `GameStateStore`) and a real `OrderDraftStore` (so the per-row
// stance toggle and the vote picker exercise the `add` path and the
// IndexedDB persistence end-to-end). The render path also flows
// through `applyOrderOverlay`, so the optimistic flips made by the
// component must keep the test fixture's report intact: each test
// passes the *raw* report and the helper recomputes the overlay on
// every snapshot.

import "@testing-library/jest-dom/vitest";
import "fake-indexeddb/auto";
import { fireEvent, render, waitFor } from "@testing-library/svelte";
import {
	afterEach,
	beforeEach,
	describe,
	expect,
	test,
	vi,
} from "vitest";

import { i18n } from "../src/lib/i18n/index.svelte";
import {
	applyOrderOverlay,
	type GameReport,
	type ReportOtherRace,
} from "../src/api/game-state";
import {
	ORDER_DRAFT_CONTEXT_KEY,
	OrderDraftStore,
} from "../src/sync/order-draft.svelte";
import { RENDERED_REPORT_CONTEXT_KEY } from "../src/lib/rendered-report.svelte";
import { IDBCache } from "../src/platform/store/idb-cache";
import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb";
import type { Cache } from "../src/platform/store/index";
import type { IDBPDatabase } from "idb";
import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups";

const GAME_ID = "11111111-2222-3333-4444-555555555555";

const pageMock = vi.hoisted(() => ({
	url: new URL("http://localhost/games/g1/table/races"),
	params: { id: "g1" } as Record<string, string>,
}));

const gotoMock = vi.hoisted(() => vi.fn());

vi.mock("$app/state", () => ({
	page: pageMock,
}));

vi.mock("$app/navigation", () => ({
	goto: gotoMock,
}));

import TableRaces from "../src/lib/active-view/table-races.svelte";

let db: IDBPDatabase<GalaxyDB>;
let dbName: string;
let cache: Cache;
let draft: OrderDraftStore;

beforeEach(async () => {
	dbName = `galaxy-table-races-${crypto.randomUUID()}`;
	db = await openGalaxyDB(dbName);
	cache = new IDBCache(db);
	draft = new OrderDraftStore();
	await draft.init({ cache, gameId: GAME_ID });
	i18n.resetForTests("en");
	pageMock.params = { id: "g1" };
	gotoMock.mockClear();
});

afterEach(async () => {
	draft.dispose();
	db.close();
	await new Promise<void>((resolve) => {
		const req = indexedDB.deleteDatabase(dbName);
		req.onsuccess = () => resolve();
		req.onerror = () => resolve();
		req.onblocked = () => resolve();
	});
});

function race(
	overrides: Partial<ReportOtherRace> & Pick<ReportOtherRace, "name">,
): ReportOtherRace {
	return {
		drive: 0,
		weapons: 0,
		shields: 0,
		cargo: 0,
		population: 0,
		industry: 0,
		planets: 0,
		relation: "PEACE",
		votesReceived: 0,
		...overrides,
	};
}

function makeReport(
	races: ReportOtherRace[],
	opts: { myVotes?: number; myVoteFor?: string } = {},
): GameReport {
	const baseEmpty = {
		...EMPTY_SHIP_GROUPS,
		races,
		myVotes: opts.myVotes ?? 0,
		myVoteFor: opts.myVoteFor ?? "",
	};
	return {
		turn: 1,
		mapWidth: 1000,
		mapHeight: 1000,
		planetCount: 0,
		planets: [],
		race: "Self",
		localShipClass: [],
		routes: [],
		localPlayerDrive: 0,
		localPlayerWeapons: 0,
		localPlayerShields: 0,
		localPlayerCargo: 0,
		...baseEmpty,
	};
}

function mountTable(report: GameReport | null) {
	const renderedReport = {
		get report() {
			if (report === null) return null;
			return applyOrderOverlay(report, draft.commands, draft.statuses);
		},
	};
	const context = new Map<unknown, unknown>([
		[ORDER_DRAFT_CONTEXT_KEY, draft],
		[RENDERED_REPORT_CONTEXT_KEY, renderedReport],
	]);
	return render(TableRaces, { context });
}

describe("races table", () => {
	test("renders a loading placeholder before the report lands", () => {
		const ui = mountTable(null);
		expect(ui.getByTestId("races-loading")).toBeInTheDocument();
	});

	test("renders an empty placeholder when no other races are known", () => {
		const ui = mountTable(makeReport([]));
		expect(ui.getByTestId("races-empty")).toBeInTheDocument();
		// vote picker stays mounted but disabled
		expect(ui.getByTestId("races-vote-target")).toBeDisabled();
	});

	test("renders one row per race with all ten columns populated", () => {
		const ui = mountTable(
			makeReport([
				race({
					name: "Andori",
					drive: 0.25,
					weapons: 0.5,
					shields: 0.75,
					cargo: 1.0,
					population: 12345,
					industry: 6789,
					planets: 4,
					relation: "WAR",
					votesReceived: 3.5,
				}),
			]),
		);
		const rows = ui.getAllByTestId("races-row");
		expect(rows).toHaveLength(1);
		expect(rows[0]).toHaveAttribute("data-name", "Andori");
		expect(ui.getByTestId("races-cell-name")).toHaveTextContent("Andori");
		expect(ui.getByTestId("races-cell-drive")).toHaveTextContent("25");
		expect(ui.getByTestId("races-cell-weapons")).toHaveTextContent("50");
		expect(ui.getByTestId("races-cell-shields")).toHaveTextContent("75");
		expect(ui.getByTestId("races-cell-cargo")).toHaveTextContent("100");
		expect(ui.getByTestId("races-cell-population")).toHaveTextContent(
			/12[,\s]345/,
		);
		expect(ui.getByTestId("races-cell-industry")).toHaveTextContent(
			/6[,\s]?789/,
		);
		expect(ui.getByTestId("races-cell-planets")).toHaveTextContent("4");
		expect(ui.getByTestId("races-cell-votes")).toHaveTextContent("3.5");
	});

	test("filters rows by case-insensitive name match", async () => {
		const ui = mountTable(
			makeReport([
				race({ name: "Alpha" }),
				race({ name: "Beta" }),
				race({ name: "Gamma" }),
			]),
		);
		await fireEvent.input(ui.getByTestId("races-filter"), {
			target: { value: "PH" },
		});
		const rows = ui.getAllByTestId("races-row");
		expect(rows).toHaveLength(1);
		expect(rows[0]).toHaveAttribute("data-name", "Alpha");
	});

	test("toggles sort direction when the same column is clicked twice", async () => {
		const ui = mountTable(
			makeReport([
				race({ name: "Alpha", votesReceived: 1 }),
				race({ name: "Beta", votesReceived: 5 }),
				race({ name: "Gamma", votesReceived: 3 }),
			]),
		);
		const header = ui.getByTestId("races-column-votesReceived");
		await fireEvent.click(header);
		let names = ui
			.getAllByTestId("races-row")
			.map((row) => row.getAttribute("data-name"));
		expect(names).toEqual(["Alpha", "Gamma", "Beta"]);
		await fireEvent.click(header);
		names = ui
			.getAllByTestId("races-row")
			.map((row) => row.getAttribute("data-name"));
		expect(names).toEqual(["Beta", "Gamma", "Alpha"]);
	});

	test("clicking the already-active stance is a no-op (no command queued)", async () => {
		const ui = mountTable(
			makeReport([race({ name: "Andori", relation: "WAR" })]),
		);
		await fireEvent.click(ui.getByTestId("races-stance-war"));
		// Give the async handler one microtask to settle, then assert
		// the draft remained empty — the click matched the current
		// stance, so nothing should land in the order queue.
		await Promise.resolve();
		expect(draft.commands).toHaveLength(0);
	});

	test("clicking PEACE on a WAR row appends setDiplomaticStance and flips the overlay", async () => {
		const ui = mountTable(
			makeReport([race({ name: "Andori", relation: "WAR" })]),
		);
		const peaceButton = ui.getByTestId("races-stance-peace");
		await fireEvent.click(peaceButton);
		await waitFor(() => expect(draft.commands).toHaveLength(1));
		const cmd = draft.commands[0]!;
		if (cmd.kind !== "setDiplomaticStance") {
			throw new Error("wrong kind");
		}
		expect(cmd.acceptor).toBe("Andori");
		expect(cmd.relation).toBe("PEACE");
		// After overlay the WAR button loses its `aria-pressed=true`.
		await waitFor(() => {
			expect(ui.getByTestId("races-stance-war")).toHaveAttribute(
				"aria-pressed",
				"false",
			);
			expect(ui.getByTestId("races-stance-peace")).toHaveAttribute(
				"aria-pressed",
				"true",
			);
		});
	});

	test("a second stance click for the same race collapses on acceptor", async () => {
		const ui = mountTable(
			makeReport([race({ name: "Andori", relation: "WAR" })]),
		);
		await fireEvent.click(ui.getByTestId("races-stance-peace"));
		await waitFor(() => expect(draft.commands).toHaveLength(1));
		const firstId = draft.commands[0]!.id;
		await fireEvent.click(ui.getByTestId("races-stance-war"));
		await waitFor(() => {
			expect(draft.commands).toHaveLength(1);
		});
		const cmd = draft.commands[0]!;
		if (cmd.kind !== "setDiplomaticStance") {
			throw new Error("wrong kind");
		}
		expect(cmd.id).not.toBe(firstId);
		expect(cmd.relation).toBe("WAR");
	});

	test("changing the vote picker appends setVoteRecipient", async () => {
		const ui = mountTable(
			makeReport(
				[race({ name: "Andori" }), race({ name: "Bajori" })],
				{ myVoteFor: "Andori" },
			),
		);
		await fireEvent.change(ui.getByTestId("races-vote-target"), {
			target: { value: "Bajori" },
		});
		await waitFor(() => expect(draft.commands).toHaveLength(1));
		const cmd = draft.commands[0]!;
		if (cmd.kind !== "setVoteRecipient") {
			throw new Error("wrong kind");
		}
		expect(cmd.acceptor).toBe("Bajori");
	});

	test("a second vote pick collapses singleton regardless of target", async () => {
		const ui = mountTable(
			makeReport(
				[
					race({ name: "Andori" }),
					race({ name: "Bajori" }),
					race({ name: "Cardassian" }),
				],
				{ myVoteFor: "Andori" },
			),
		);
		const select = ui.getByTestId("races-vote-target");
		await fireEvent.change(select, { target: { value: "Bajori" } });
		await waitFor(() => expect(draft.commands).toHaveLength(1));
		await fireEvent.change(select, { target: { value: "Cardassian" } });
		await waitFor(() => {
			expect(draft.commands).toHaveLength(1);
		});
		const cmd = draft.commands[0]!;
		if (cmd.kind !== "setVoteRecipient") {
			throw new Error("wrong kind");
		}
		expect(cmd.acceptor).toBe("Cardassian");
	});

	test("my votes summary reads from the report", () => {
		const ui = mountTable(
			makeReport([race({ name: "Andori" })], { myVotes: 7.5 }),
		);
		expect(ui.getByTestId("races-my-votes")).toHaveTextContent("7.5");
	});
});
