// F8-04b regression spec: recruitment cards merge public games with
// the caller's applications and surface the application status as a
// chip. The inline race-name form must be visible when there is no
// application or when the latest application is `rejected` (re-apply
// flow). Pending / approved applications hide the form.

import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
import {
	buildAccountResponsePayload,
	buildMyApplicationsListPayload,
	buildMyGamesListPayload,
	buildMyInvitesListPayload,
	buildPublicGamesListPayload,
	type ApplicationFixture,
	type GameFixture,
} from "./fixtures/lobby-fbs";

interface BadgeMocks {
	pendingSubscribes: Array<() => void>;
}

async function mockGateway(
	page: Page,
	opts: { games: GameFixture[]; applications: ApplicationFixture[] },
): Promise<BadgeMocks> {
	const mocks: BadgeMocks = { pendingSubscribes: [] };

	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-badge-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-badge-1" }),
		});
	});

	await page.route("**/edge.v1.Gateway/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 payload: Uint8Array;
		switch (req.messageType) {
			case "user.account.get":
				payload = buildAccountResponsePayload({
					userId: "user-badge",
					email: "pilot+badge@example.com",
					userName: "pilot",
					displayName: "Pilot",
				});
				break;
			case "lobby.my.games.list":
				payload = buildMyGamesListPayload([]);
				break;
			case "lobby.public.games.list":
				payload = buildPublicGamesListPayload(opts.games);
				break;
			case "lobby.my.invites.list":
				payload = buildMyInvitesListPayload([]);
				break;
			case "lobby.my.applications.list":
				payload = buildMyApplicationsListPayload(opts.applications);
				break;
			default:
				payload = new Uint8Array();
				break;
		}
		const responseJson = await forgeExecuteCommandResponseJson({
			requestId: req.requestId,
			timestampMs: BigInt(Date.now()),
			resultCode: "ok",
			payloadBytes: payload,
		});
		await route.fulfill({
			status: 200,
			contentType: "application/json",
			body: responseJson,
		});
	});

	await page.route("**/edge.v1.Gateway/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 page.getByTestId("login-email-input").click();
	await page.getByTestId("login-email-input").fill("pilot+badge@example.com");
	await page.getByTestId("login-email-submit").click();
	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.getByTestId("lobby-account-name")).toBeVisible();
}

test.describe("F8-04b — recruitment status badges", () => {
	test("pending application hides the inline form and shows the chip", async ({
		page,
	}) => {
		const game: GameFixture = {
			gameId: "public-pending",
			gameName: "Pending Game",
			gameType: "public",
			status: "enrollment_open",
			ownerUserId: "other-owner",
		};
		const app: ApplicationFixture = {
			applicationId: "app-pending",
			gameId: "public-pending",
			applicantUserId: "user-badge",
			raceName: "Race Pending",
			status: "pending",
			createdAtMs: 1n,
		};
		const mocks = await mockGateway(page, { games: [game], applications: [app] });
		await completeLogin(page);

		const card = page.getByTestId("lobby-recruitment-card");
		await expect(card).toBeVisible();
		await expect(page.getByTestId("lobby-application-status-chip")).toContainText(
			/pending/i,
		);
		// Inline form is hidden for pending — re-apply not allowed.
		await expect(page.getByTestId("lobby-public-game-apply")).toBeHidden();
		await expect(page.getByTestId("lobby-application-form")).toBeHidden();

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

	test("rejected application shows the chip AND keeps the inline form visible", async ({
		page,
	}) => {
		const game: GameFixture = {
			gameId: "public-rejected",
			gameName: "Rejected Game",
			gameType: "public",
			status: "enrollment_open",
			ownerUserId: "other-owner",
		};
		const app: ApplicationFixture = {
			applicationId: "app-rejected",
			gameId: "public-rejected",
			applicantUserId: "user-badge",
			raceName: "Race Rejected",
			status: "rejected",
			createdAtMs: 1n,
		};
		const mocks = await mockGateway(page, { games: [game], applications: [app] });
		await completeLogin(page);

		await expect(page.getByTestId("lobby-recruitment-card")).toBeVisible();
		await expect(page.getByTestId("lobby-application-status-chip")).toContainText(
			/rejected/i,
		);
		// Re-apply button is visible for rejected — owner-confirmed F8-04b
		// behaviour.
		await expect(page.getByTestId("lobby-public-game-apply")).toBeVisible();

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

	test("approved application hides the inline form and shows the chip", async ({
		page,
	}) => {
		const game: GameFixture = {
			gameId: "public-approved",
			gameName: "Approved Game",
			gameType: "public",
			status: "enrollment_open",
			ownerUserId: "other-owner",
		};
		const app: ApplicationFixture = {
			applicationId: "app-approved",
			gameId: "public-approved",
			applicantUserId: "user-badge",
			raceName: "Race Approved",
			status: "approved",
			createdAtMs: 1n,
		};
		const mocks = await mockGateway(page, { games: [game], applications: [app] });
		await completeLogin(page);

		await expect(page.getByTestId("lobby-application-status-chip")).toContainText(
			/approved/i,
		);
		await expect(page.getByTestId("lobby-public-game-apply")).toBeHidden();

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

	test("no application leaves the inline race-name form visible", async ({
		page,
	}) => {
		const game: GameFixture = {
			gameId: "public-new",
			gameName: "New Game",
			gameType: "public",
			status: "enrollment_open",
			ownerUserId: "other-owner",
		};
		const mocks = await mockGateway(page, { games: [game], applications: [] });
		await completeLogin(page);

		await expect(page.getByTestId("lobby-recruitment-card")).toBeVisible();
		// No application → no chip, but the apply button is there.
		await expect(page.getByTestId("lobby-application-status-chip")).toBeHidden();
		await expect(page.getByTestId("lobby-public-game-apply")).toBeVisible();

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