// Phase 8 lobby end-to-end coverage. The gateway is mocked through
// `page.route(...)` like in the Phase 7 spec; this spec dispatches by
// `messageType` so each lobby command can return its own forged
// FlatBuffers payload. The flows under test:
//
//   1) Land on /lobby with empty lists; create a private game; verify
//      the new game appears in My Games after the redirect.
//   2) Submit an application to a public game; verify the application
//      shows up in My Applications.
//   3) Accept an invitation; verify the invite card disappears.

import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { GameCreateRequest } from "../../src/proto/galaxy/fbs/lobby";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
import {
	buildAccountResponsePayload,
	buildApplicationSubmitResponsePayload,
	buildGameCreateResponsePayload,
	buildInviteRedeemResponsePayload,
	buildMyApplicationsListPayload,
	buildMyGamesListPayload,
	buildMyInvitesListPayload,
	buildPublicGamesListPayload,
	type ApplicationFixture,
	type GameFixture,
	type InviteFixture,
} from "./fixtures/lobby-fbs";

interface LobbyState {
	myGames: GameFixture[];
	publicGames: GameFixture[];
	invitations: InviteFixture[];
	applications: ApplicationFixture[];
}

interface LobbyMocks {
	state: LobbyState;
	pendingSubscribes: Array<() => void>;
	createGameCalls: GameFixture[];
	applicationSubmitCalls: Array<{ gameId: string; raceName: string }>;
	inviteRedeemCalls: Array<{ gameId: string; inviteId: string }>;
}

async function mockGateway(page: Page, initial: Partial<LobbyState> = {}): Promise<LobbyMocks> {
	const mocks: LobbyMocks = {
		state: {
			myGames: initial.myGames ?? [],
			publicGames: initial.publicGames ?? [],
			invitations: initial.invitations ?? [],
			applications: initial.applications ?? [],
		},
		pendingSubscribes: [],
		createGameCalls: [],
		applicationSubmitCalls: [],
		inviteRedeemCalls: [],
	};

	await page.route("**/api/v1/public/auth/send-email-code", async (route) => {
		await route.fulfill({
			status: 200,
			contentType: "application/json",
			body: JSON.stringify({ challenge_id: "ch-test-1" }),
		});
	});

	await page.route("**/api/v1/public/auth/confirm-email-code", async (route) => {
		await route.fulfill({
			status: 200,
			contentType: "application/json",
			body: JSON.stringify({ device_session_id: "dev-test-1" }),
		});
	});

	await page.route("**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", async (route) => {
		const reqText = route.request().postData();
		if (reqText === null) {
			await route.fulfill({ status: 400 });
			return;
		}
		const req = fromJson(
			ExecuteCommandRequestSchema,
			JSON.parse(reqText) as JsonValue,
		);

		let resultCode = "ok";
		let payload: Uint8Array;
		switch (req.messageType) {
			case "user.account.get":
				payload = buildAccountResponsePayload({
					userId: "user-1",
					email: "pilot@example.com",
					userName: "pilot",
					displayName: "Pilot",
				});
				break;
			case "lobby.my.games.list":
				payload = buildMyGamesListPayload(mocks.state.myGames);
				break;
			case "lobby.public.games.list":
				payload = buildPublicGamesListPayload(mocks.state.publicGames);
				break;
			case "lobby.my.invites.list":
				payload = buildMyInvitesListPayload(mocks.state.invitations);
				break;
			case "lobby.my.applications.list":
				payload = buildMyApplicationsListPayload(mocks.state.applications);
				break;
			case "lobby.game.create": {
				const decoded = GameCreateRequest.getRootAsGameCreateRequest(
					new ByteBuffer(req.payloadBytes),
				);
				const created: GameFixture = {
					gameId: "private-newly-created",
					gameName: decoded.gameName() ?? "",
					gameType: "private",
					status: "draft",
					ownerUserId: "user-1",
					minPlayers: decoded.minPlayers(),
					maxPlayers: decoded.maxPlayers(),
					enrollmentEndsAtMs: decoded.enrollmentEndsAtMs(),
					createdAtMs: BigInt(Date.now()),
					updatedAtMs: BigInt(Date.now()),
				};
				mocks.createGameCalls.push(created);
				mocks.state.myGames = [...mocks.state.myGames, created];
				payload = buildGameCreateResponsePayload(created);
				break;
			}
			case "lobby.application.submit": {
				const builder = req.payloadBytes;
				const submitReq = await import("../../src/proto/galaxy/fbs/lobby");
				const decoded = submitReq.ApplicationSubmitRequest.getRootAsApplicationSubmitRequest(
					new ByteBuffer(builder),
				);
				const application: ApplicationFixture = {
					applicationId: `app-${mocks.applicationSubmitCalls.length + 1}`,
					gameId: decoded.gameId() ?? "",
					applicantUserId: "user-1",
					raceName: decoded.raceName() ?? "",
					status: "pending",
					createdAtMs: BigInt(Date.now()),
				};
				mocks.applicationSubmitCalls.push({
					gameId: application.gameId,
					raceName: application.raceName,
				});
				mocks.state.applications = [application, ...mocks.state.applications];
				payload = buildApplicationSubmitResponsePayload(application);
				break;
			}
			case "lobby.invite.redeem": {
				const redeemMod = await import("../../src/proto/galaxy/fbs/lobby");
				const decoded = redeemMod.InviteRedeemRequest.getRootAsInviteRedeemRequest(
					new ByteBuffer(req.payloadBytes),
				);
				const gameId = decoded.gameId() ?? "";
				const inviteId = decoded.inviteId() ?? "";
				mocks.inviteRedeemCalls.push({ gameId, inviteId });
				const original = mocks.state.invitations.find((i) => i.inviteId === inviteId);
				const invite: InviteFixture = {
					...(original ?? {
						inviteId,
						gameId,
						inviterUserId: "user-host",
						invitedUserId: "user-1",
						raceName: "",
					}),
					status: "accepted",
					decidedAtMs: BigInt(Date.now()),
				};
				mocks.state.invitations = mocks.state.invitations.filter(
					(i) => i.inviteId !== inviteId,
				);
				const newGame: GameFixture = {
					gameId,
					gameName: "Invited Game",
					gameType: "private",
					status: "enrollment_open",
					ownerUserId: "user-host",
					minPlayers: 2,
					maxPlayers: 8,
					enrollmentEndsAtMs: BigInt(Date.now() + 1_000_000),
				};
				mocks.state.myGames = [...mocks.state.myGames, newGame];
				payload = buildInviteRedeemResponsePayload(invite);
				break;
			}
			default:
				resultCode = "internal_error";
				payload = new Uint8Array();
				break;
		}

		const responseJson = await forgeExecuteCommandResponseJson({
			requestId: req.requestId,
			timestampMs: BigInt(Date.now()),
			resultCode,
			payloadBytes: payload,
		});
		await route.fulfill({
			status: 200,
			contentType: "application/json",
			body: responseJson,
		});
	});

	await page.route(
		"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
		async (route) => {
			const action = await new Promise<"endOfStream" | "abort">((resolve) => {
				mocks.pendingSubscribes.push(() => resolve("endOfStream"));
			});
			if (action === "abort") {
				await route.abort();
				return;
			}
			const body = new TextEncoder().encode("{}");
			const frame = new Uint8Array(5 + body.length);
			frame[0] = 0x02;
			new DataView(frame.buffer).setUint32(1, body.length, false);
			frame.set(body, 5);
			await route.fulfill({
				status: 200,
				contentType: "application/connect+json",
				body: Buffer.from(frame),
			});
		},
	);

	return mocks;
}

async function completeLogin(page: Page): Promise<void> {
	await page.goto("/");
	await expect(page).toHaveURL(/\/login$/);
	// The login page renders the inputs `readonly` as a Safari
	// autofill-suppression workaround; the readonly attribute is
	// dropped on first focus. Playwright's `fill()` checks editability
	// before its own focus call, so emulate the user gesture explicitly:
	// click the input (focus → readonly drops), then fill.
	await page.getByTestId("login-email-input").click();
	await page.getByTestId("login-email-input").fill("pilot@example.com");
	await page.getByTestId("login-email-submit").click();
	await expect(page.getByTestId("login-code-input")).toBeVisible();
	await page.getByTestId("login-code-input").click();
	await page.getByTestId("login-code-input").fill("123456");
	await page.getByTestId("login-code-submit").click();
	await expect(page).toHaveURL(/\/lobby$/);
}

test.describe("Phase 8 — lobby flow", () => {
	test("create-game flow lands the new game in My Games", async ({ page }) => {
		const mocks = await mockGateway(page);
		await completeLogin(page);

		await expect(page.getByTestId("lobby-my-games-empty")).toBeVisible();
		await expect(page.getByTestId("lobby-public-games-empty")).toBeVisible();

		await page.getByTestId("lobby-create-button").click();
		await expect(page).toHaveURL(/\/lobby\/create$/);

		await page.getByTestId("lobby-create-game-name").click();
		await page.getByTestId("lobby-create-game-name").fill("First Contact");
		await page.getByTestId("lobby-create-turn-schedule").click();
		await page.getByTestId("lobby-create-turn-schedule").fill("0 0 * * *");
		await page
			.getByTestId("lobby-create-enrollment-ends-at")
			.fill("2026-06-01T12:00");
		await page.getByTestId("lobby-create-submit").click();

		await expect(page).toHaveURL(/\/lobby$/);
		await expect(page.getByTestId("lobby-my-game-card")).toContainText("First Contact");
		expect(mocks.createGameCalls.length).toBe(1);
		expect(mocks.createGameCalls[0]!.gameName).toBe("First Contact");

		mocks.pendingSubscribes.forEach((resolve) => resolve());
	});

	test("submitting an application produces a pending applications card", async ({
		page,
	}) => {
		const mocks = await mockGateway(page, {
			publicGames: [
				{
					gameId: "public-1",
					gameName: "Open Lobby",
					gameType: "public",
					status: "enrollment_open",
				},
			],
		});
		await completeLogin(page);

		await expect(page.getByTestId("lobby-public-game-apply")).toBeVisible();
		await page.getByTestId("lobby-public-game-apply").click();
		await page
			.getByTestId("lobby-application-race-name")
			.fill("Vegan Federation");
		await page.getByTestId("lobby-application-submit").click();

		await expect(page.getByTestId("lobby-application-card")).toBeVisible();
		expect(mocks.applicationSubmitCalls).toEqual([
			{ gameId: "public-1", raceName: "Vegan Federation" },
		]);

		mocks.pendingSubscribes.forEach((resolve) => resolve());
	});

	test("accepting an invitation removes it and adds the game to My Games", async ({
		page,
	}) => {
		const mocks = await mockGateway(page, {
			invitations: [
				{
					inviteId: "invite-1",
					gameId: "private-1",
					inviterUserId: "user-host",
					invitedUserId: "user-1",
					raceName: "Vegan Federation",
					status: "pending",
				},
			],
		});
		await completeLogin(page);

		await expect(page.getByTestId("lobby-invite-accept")).toBeVisible();
		await page.getByTestId("lobby-invite-accept").click();

		await expect(page.getByTestId("lobby-invite-accept")).toBeHidden();
		await expect(page.getByTestId("lobby-my-game-card")).toContainText("Invited Game");
		expect(mocks.inviteRedeemCalls).toEqual([
			{ gameId: "private-1", inviteId: "invite-1" },
		]);

		mocks.pendingSubscribes.forEach((resolve) => resolve());
	});
});
