// Component tests for the Phase 8 lobby page. The lobby API and the
// gateway client are mocked at module level; the session singleton is
// wired to a per-test `SessionStore`-backing IndexedDB so the page's
// boot path settles on `authenticated` and constructs a real
// GalaxyClient (which is then never called because the lobby API
// wrappers are stubs). The tests assert the section rendering, the
// inline race-name form for public games, and the invitation Accept
// flow.

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

import { i18n } from "../src/lib/i18n/index.svelte";
import { session } from "../src/lib/session-store.svelte";
import { type GalaxyDB, openGalaxyDB } from "../src/platform/store/idb";
import { IDBCache } from "../src/platform/store/idb-cache";
import { WebCryptoKeyStore } from "../src/platform/store/webcrypto-keystore";

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

const listMyGamesSpy = vi.fn();
const listPublicGamesSpy = vi.fn();
const listMyInvitesSpy = vi.fn();
const listMyApplicationsSpy = vi.fn();
const submitApplicationSpy = vi.fn();
const redeemInviteSpy = vi.fn();
const declineInviteSpy = vi.fn();

vi.mock("../src/api/lobby", async () => {
	const actual = await vi.importActual<typeof import("../src/api/lobby")>(
		"../src/api/lobby",
	);
	return {
		...actual,
		listMyGames: (...args: unknown[]) => listMyGamesSpy(...args),
		listPublicGames: (...args: unknown[]) => listPublicGamesSpy(...args),
		listMyInvites: (...args: unknown[]) => listMyInvitesSpy(...args),
		listMyApplications: (...args: unknown[]) => listMyApplicationsSpy(...args),
		submitApplication: (...args: unknown[]) => submitApplicationSpy(...args),
		redeemInvite: (...args: unknown[]) => redeemInviteSpy(...args),
		declineInvite: (...args: unknown[]) => declineInviteSpy(...args),
	};
});

vi.mock("../src/lib/env", () => ({
	GATEWAY_BASE_URL: "http://gateway.test",
	GATEWAY_RESPONSE_PUBLIC_KEY: new Uint8Array(32).fill(0x55),
}));

vi.mock("../src/api/connect", () => ({
	createEdgeGatewayClient: vi.fn(() => ({})),
}));

vi.mock("../src/api/galaxy-client", () => {
	class FakeGalaxyClient {
		executeCommand = vi.fn(async () => ({
			resultCode: "ok",
			payloadBytes: new Uint8Array(),
		}));
	}
	return { GalaxyClient: FakeGalaxyClient };
});

vi.mock("../src/platform/core/index", () => ({
	loadCore: async () => ({
		signRequest: () => new Uint8Array(),
		verifyResponse: () => true,
		verifyEvent: () => true,
		verifyPayloadHash: () => true,
	}),
}));

let db: IDBPDatabase<GalaxyDB>;
let dbName: string;

beforeEach(async () => {
	dbName = `galaxy-ui-test-${crypto.randomUUID()}`;
	db = await openGalaxyDB(dbName);
	const store = {
		keyStore: new WebCryptoKeyStore(db),
		cache: new IDBCache(db),
	};
	session.resetForTests();
	session.setStoreLoaderForTests(async () => store);
	await session.init();
	await session.signIn("device-1");
	i18n.resetForTests("en");

	listMyGamesSpy.mockReset();
	listPublicGamesSpy.mockReset();
	listMyInvitesSpy.mockReset();
	listMyApplicationsSpy.mockReset();
	submitApplicationSpy.mockReset();
	redeemInviteSpy.mockReset();
	declineInviteSpy.mockReset();
});

afterEach(async () => {
	session.resetForTests();
	i18n.resetForTests("en");
	db.close();
	await new Promise<void>((resolve) => {
		const req = indexedDB.deleteDatabase(dbName);
		req.onsuccess = () => resolve();
		req.onerror = () => resolve();
		req.onblocked = () => resolve();
	});
});

async function importLobbyPage(): Promise<typeof import("../src/routes/lobby/+page.svelte")> {
	return import("../src/routes/lobby/+page.svelte");
}

const baseDate = new Date("2026-05-07T10:00:00Z");

function makeGame(id: string, name: string, status = "draft") {
	return {
		gameId: id,
		gameName: name,
		gameType: "private",
		status,
		ownerUserId: "user-1",
		minPlayers: 2,
		maxPlayers: 8,
		enrollmentEndsAt: baseDate,
		createdAt: baseDate,
		updatedAt: baseDate,
		currentTurn: 0,
	};
}

function makePublicGame(id: string, name: string) {
	return {
		gameId: id,
		gameName: name,
		gameType: "public",
		status: "enrollment_open",
		ownerUserId: "",
		minPlayers: 4,
		maxPlayers: 12,
		enrollmentEndsAt: baseDate,
		createdAt: baseDate,
		updatedAt: baseDate,
		currentTurn: 0,
	};
}

function makeInvite(id: string) {
	return {
		inviteId: id,
		gameId: "private-1",
		inviterUserId: "host",
		invitedUserId: "user-1",
		code: "",
		raceName: "Vegan Federation",
		status: "pending",
		createdAt: baseDate,
		expiresAt: baseDate,
		decidedAt: null,
	};
}

function makeApplication(id: string, status: string) {
	return {
		applicationId: id,
		gameId: "public-1",
		applicantUserId: "user-1",
		raceName: "Vegan Federation",
		status,
		createdAt: baseDate,
		decidedAt: status === "pending" ? null : baseDate,
	};
}

describe("lobby page", () => {
	test("renders empty states for every section when API returns no items", async () => {
		listMyGamesSpy.mockResolvedValue([]);
		listPublicGamesSpy.mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
		listMyInvitesSpy.mockResolvedValue([]);
		listMyApplicationsSpy.mockResolvedValue([]);

		const Page = (await importLobbyPage()).default;
		const ui = render(Page);

		await waitFor(() => {
			expect(ui.getByTestId("lobby-my-games-empty")).toBeInTheDocument();
			expect(ui.getByTestId("lobby-invitations-empty")).toBeInTheDocument();
			expect(ui.getByTestId("lobby-applications-empty")).toBeInTheDocument();
			expect(ui.getByTestId("lobby-public-games-empty")).toBeInTheDocument();
		});
	});

	test("renders my-game cards and public-game cards when items are present", async () => {
		listMyGamesSpy.mockResolvedValue([makeGame("private-1", "First Contact")]);
		listPublicGamesSpy.mockResolvedValue({
			items: [makePublicGame("public-1", "Open Lobby")],
			page: 1,
			pageSize: 50,
			total: 1,
		});
		listMyInvitesSpy.mockResolvedValue([]);
		listMyApplicationsSpy.mockResolvedValue([]);

		const Page = (await importLobbyPage()).default;
		const ui = render(Page);

		await waitFor(() => {
			expect(ui.getAllByTestId("lobby-my-game-card").length).toBe(1);
			expect(ui.getByText("First Contact")).toBeInTheDocument();
			expect(ui.getByText("Open Lobby")).toBeInTheDocument();
		});
	});

	test("submitting an application opens the inline form and posts race_name", async () => {
		listMyGamesSpy.mockResolvedValue([]);
		listPublicGamesSpy.mockResolvedValue({
			items: [makePublicGame("public-1", "Open Lobby")],
			page: 1,
			pageSize: 50,
			total: 1,
		});
		listMyInvitesSpy.mockResolvedValue([]);
		listMyApplicationsSpy.mockResolvedValue([]);
		submitApplicationSpy.mockResolvedValue(makeApplication("app-1", "pending"));

		const Page = (await importLobbyPage()).default;
		const ui = render(Page);

		await waitFor(() => {
			expect(ui.getByTestId("lobby-public-game-apply")).toBeInTheDocument();
		});

		await fireEvent.click(ui.getByTestId("lobby-public-game-apply"));

		await waitFor(() => {
			expect(ui.getByTestId("lobby-application-form")).toBeInTheDocument();
		});

		await fireEvent.input(ui.getByTestId("lobby-application-race-name"), {
			target: { value: "Vegan Federation" },
		});
		await fireEvent.click(ui.getByTestId("lobby-application-submit"));

		await waitFor(() => {
			expect(submitApplicationSpy).toHaveBeenCalledWith(
				expect.anything(),
				"public-1",
				"Vegan Federation",
			);
			expect(ui.getByTestId("lobby-application-card")).toBeInTheDocument();
		});
	});

	test("submitting an empty race name surfaces a validation error and does not call the API", async () => {
		listMyGamesSpy.mockResolvedValue([]);
		listPublicGamesSpy.mockResolvedValue({
			items: [makePublicGame("public-1", "Open Lobby")],
			page: 1,
			pageSize: 50,
			total: 1,
		});
		listMyInvitesSpy.mockResolvedValue([]);
		listMyApplicationsSpy.mockResolvedValue([]);

		const Page = (await importLobbyPage()).default;
		const ui = render(Page);

		await waitFor(() =>
			expect(ui.getByTestId("lobby-public-game-apply")).toBeInTheDocument(),
		);
		await fireEvent.click(ui.getByTestId("lobby-public-game-apply"));
		await fireEvent.click(ui.getByTestId("lobby-application-submit"));

		await waitFor(() => {
			expect(ui.getByTestId("lobby-application-error")).toBeInTheDocument();
			expect(submitApplicationSpy).not.toHaveBeenCalled();
		});
	});

	test("accepting an invitation calls redeemInvite and removes the card", async () => {
		listMyGamesSpy.mockResolvedValue([]);
		listPublicGamesSpy.mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
		listMyInvitesSpy.mockResolvedValue([makeInvite("invite-1")]);
		listMyApplicationsSpy.mockResolvedValue([]);
		redeemInviteSpy.mockResolvedValue(makeInvite("invite-1"));

		const Page = (await importLobbyPage()).default;
		const ui = render(Page);

		await waitFor(() =>
			expect(ui.getByTestId("lobby-invite-accept")).toBeInTheDocument(),
		);

		await fireEvent.click(ui.getByTestId("lobby-invite-accept"));

		await waitFor(() => {
			expect(redeemInviteSpy).toHaveBeenCalledWith(
				expect.anything(),
				"private-1",
				"invite-1",
			);
			expect(ui.queryByTestId("lobby-invite-accept")).not.toBeInTheDocument();
			expect(ui.getByTestId("lobby-invitations-empty")).toBeInTheDocument();
		});
	});

	test("declining an invitation calls declineInvite and removes the card", async () => {
		listMyGamesSpy.mockResolvedValue([]);
		listPublicGamesSpy.mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
		listMyInvitesSpy.mockResolvedValue([makeInvite("invite-2")]);
		listMyApplicationsSpy.mockResolvedValue([]);
		declineInviteSpy.mockResolvedValue({ ...makeInvite("invite-2"), status: "declined" });

		const Page = (await importLobbyPage()).default;
		const ui = render(Page);

		await waitFor(() =>
			expect(ui.getByTestId("lobby-invite-decline")).toBeInTheDocument(),
		);

		await fireEvent.click(ui.getByTestId("lobby-invite-decline"));

		await waitFor(() => {
			expect(declineInviteSpy).toHaveBeenCalledWith(
				expect.anything(),
				"private-1",
				"invite-2",
			);
			expect(ui.queryByTestId("lobby-invite-decline")).not.toBeInTheDocument();
		});
	});

	test("my-game cards are clickable for running/paused/finished and disabled otherwise", async () => {
		// Cover the live-able statuses (running, paused, finished) and a
		// representative non-playable mix (cancelled is the post-shutdown
		// terminal state developers see most often; draft is the lobby-
		// internal state before any membership exists).
		listMyGamesSpy.mockResolvedValue([
			makeGame("g-running", "Live", "running"),
			makeGame("g-paused", "Paused Run", "paused"),
			makeGame("g-finished", "Closed Run", "finished"),
			makeGame("g-cancelled", "Cancelled Run", "cancelled"),
			makeGame("g-draft", "Draft Run", "draft"),
		]);
		listPublicGamesSpy.mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
		listMyInvitesSpy.mockResolvedValue([]);
		listMyApplicationsSpy.mockResolvedValue([]);

		const Page = (await importLobbyPage()).default;
		const ui = render(Page);

		await waitFor(() => {
			expect(ui.getAllByTestId("lobby-my-game-card").length).toBe(5);
		});
		const cards = ui.getAllByTestId("lobby-my-game-card");
		const disabledByLabel: Record<string, boolean> = {};
		for (const card of cards) {
			const label = card.querySelector("strong")?.textContent ?? "";
			disabledByLabel[label] = (card as HTMLButtonElement).disabled;
		}
		expect(disabledByLabel["Live"]).toBe(false);
		expect(disabledByLabel["Paused Run"]).toBe(false);
		expect(disabledByLabel["Closed Run"]).toBe(false);
		expect(disabledByLabel["Cancelled Run"]).toBe(true);
		expect(disabledByLabel["Draft Run"]).toBe(true);
	});

	test("application status badges localise pending and approved states", async () => {
		listMyGamesSpy.mockResolvedValue([]);
		listPublicGamesSpy.mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
		listMyInvitesSpy.mockResolvedValue([]);
		listMyApplicationsSpy.mockResolvedValue([
			makeApplication("app-1", "pending"),
			makeApplication("app-2", "approved"),
		]);

		const Page = (await importLobbyPage()).default;
		const ui = render(Page);

		await waitFor(() => {
			const cards = ui.getAllByTestId("lobby-application-card");
			expect(cards.length).toBe(2);
			expect(cards[0]!.querySelector(".status")?.textContent?.trim()).toBe("pending");
			expect(cards[1]!.querySelector(".status")?.textContent?.trim()).toBe("approved");
		});
	});
});
