// F8-04 profile screen — end-to-end coverage. Mocks the gateway so the
// lobby boots with an account aggregate, then exercises the sidebar
// navigation into the profile, the edit form, the save-stay flow, and
// the time-zone dropdown.

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

interface ProfileMocks {
	pendingSubscribes: Array<() => void>;
	account: AccountFixture;
	accountGetCount: number;
	profileUpdates: Array<{ displayName: string }>;
	settingsUpdates: Array<{ preferredLanguage: string; timeZone: string }>;
}

async function mockGateway(
	page: Page,
	initial: AccountFixture,
): Promise<ProfileMocks> {
	const mocks: ProfileMocks = {
		pendingSubscribes: [],
		account: { ...initial },
		accountGetCount: 0,
		profileUpdates: [],
		settingsUpdates: [],
	};

	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("**/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":
				mocks.accountGetCount += 1;
				payload = buildAccountResponsePayload(mocks.account);
				break;
			case "user.profile.update": {
				const decoded = UpdateMyProfileRequest.getRootAsUpdateMyProfileRequest(
					new ByteBuffer(req.payloadBytes),
				);
				const next = decoded.displayName() ?? "";
				mocks.profileUpdates.push({ displayName: next });
				mocks.account = { ...mocks.account, displayName: next };
				payload = buildAccountResponsePayload(mocks.account);
				break;
			}
			case "user.settings.update": {
				const decoded = UpdateMySettingsRequest.getRootAsUpdateMySettingsRequest(
					new ByteBuffer(req.payloadBytes),
				);
				const preferredLanguage = decoded.preferredLanguage() ?? "";
				const timeZone = decoded.timeZone() ?? "";
				mocks.settingsUpdates.push({ preferredLanguage, timeZone });
				mocks.account = { ...mocks.account, preferredLanguage, timeZone };
				payload = buildAccountResponsePayload(mocks.account);
				break;
			}
			case "lobby.my.games.list":
				payload = buildMyGamesListPayload([]);
				break;
			case "lobby.public.games.list":
				payload = buildPublicGamesListPayload([]);
				break;
			case "lobby.my.invites.list":
				payload = buildMyInvitesListPayload([]);
				break;
			case "lobby.my.applications.list":
				payload = buildMyApplicationsListPayload([]);
				break;
			default:
				payload = new Uint8Array();
		}
		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 expect(page.getByTestId("login-email-input")).toBeVisible();
	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.getByTestId("lobby-account-name")).toBeVisible();
}

test.describe("F8-04 — profile screen", () => {
	test("clicking the identity strip opens the profile and renders the form", async ({
		page,
	}) => {
		const mocks = await mockGateway(page, {
			userId: "user-1",
			email: "pilot@example.com",
			userName: "player-abc12345",
			displayName: "Pilot",
		});
		await completeLogin(page);
		await expect(page.getByTestId("lobby-account-name")).toContainText("Pilot");

		await page.getByTestId("lobby-account-name").click();
		await expect(page.getByTestId("profile-form")).toBeVisible();
		await expect(page.getByTestId("profile-display-name")).toHaveValue("Pilot");
		await expect(page.getByTestId("profile-identity")).toContainText(
			"player-abc12345",
		);
		await expect(page.getByTestId("profile-identity")).toContainText(
			"pilot@example.com",
		);

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

	test("saving an edited display name posts user.profile.update, stays on the form, and refreshes the identity strip", async ({
		page,
	}) => {
		const mocks = await mockGateway(page, {
			userId: "user-1",
			email: "pilot@example.com",
			userName: "player-abc12345",
			displayName: "Pilot",
		});
		await completeLogin(page);
		await page.getByTestId("lobby-account-name").click();
		await expect(page.getByTestId("profile-form")).toBeVisible();

		await page.getByTestId("profile-display-name").fill("Captain");
		await page.getByTestId("profile-save").click();

		// Form stays on screen; the saved notice surfaces and the
		// shell-level identity strip picks up the new name without a
		// second `user.account.get`.
		await expect(page.getByTestId("profile-saved-notice")).toBeVisible();
		await expect(page.getByTestId("profile-form")).toBeVisible();
		await expect(page.getByTestId("lobby-account-name")).toContainText(
			"Captain",
		);
		expect(mocks.profileUpdates).toEqual([{ displayName: "Captain" }]);
		expect(mocks.settingsUpdates).toEqual([]);

		// Editing the form again clears the notice so a follow-up save is
		// unambiguous.
		await page.getByTestId("profile-display-name").fill("Pilot");
		await expect(page.getByTestId("profile-saved-notice")).toHaveCount(0);

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

	test("changing the language posts user.settings.update, stays on the form, and switches the active locale", async ({
		page,
	}) => {
		const mocks = await mockGateway(page, {
			userId: "user-1",
			email: "pilot@example.com",
			userName: "player-abc12345",
			displayName: "Pilot",
		});
		await completeLogin(page);
		await page.getByTestId("lobby-account-name").click();

		await page.getByTestId("profile-preferred-language").selectOption("ru");
		await page.getByTestId("profile-save").click();

		// Profile stays on screen; the Russian dictionary now drives the
		// form copy. The save button label is the visible signal.
		await expect(page.getByTestId("profile-form")).toBeVisible();
		await expect(page.getByTestId("profile-save")).toHaveText("сохранить");
		await expect(page.getByTestId("profile-saved-notice")).toBeVisible();
		expect(mocks.settingsUpdates).toHaveLength(1);
		expect(mocks.settingsUpdates[0]?.preferredLanguage).toBe("ru");

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

	test("cancel returns to the lobby without posting anything", async ({
		page,
	}) => {
		const mocks = await mockGateway(page, {
			userId: "user-1",
			email: "pilot@example.com",
			userName: "player-abc12345",
			displayName: "Pilot",
		});
		await completeLogin(page);
		await page.getByTestId("lobby-account-name").click();
		await expect(page.getByTestId("profile-form")).toBeVisible();

		await page.getByTestId("profile-display-name").fill("ignored");
		await page.getByTestId("profile-cancel").click();

		// Cancel returns to the lobby (resolver navigates to the first
		// visible games sub-panel — recruitment for a no-games session).
		await expect(page.getByTestId("lobby-recruitment-empty")).toBeVisible();
		expect(mocks.profileUpdates).toEqual([]);
		expect(mocks.settingsUpdates).toEqual([]);

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

	test("time zone is a continent-grouped <select>; saving an edited zone posts user.settings.update", async ({
		page,
	}) => {
		const mocks = await mockGateway(page, {
			userId: "user-1",
			email: "pilot@example.com",
			userName: "player-abc12345",
			displayName: "Pilot",
			preferredLanguage: "en",
			timeZone: "Europe/London",
		});
		await completeLogin(page);
		await page.getByTestId("lobby-account-name").click();
		await expect(page.getByTestId("profile-form")).toBeVisible();

		const select = page.getByTestId("profile-time-zone");
		// The field renders as a <select> with at least the Europe and
		// America optgroups present and the stored zone selected.
		expect(await select.evaluate((el) => el.tagName)).toBe("SELECT");
		const optgroupLabels = await select.evaluate((el) =>
			Array.from((el as HTMLSelectElement).querySelectorAll("optgroup")).map(
				(g) => g.label,
			),
		);
		expect(optgroupLabels).toContain("Europe");
		expect(optgroupLabels).toContain("America");
		await expect(select).toHaveValue("Europe/London");

		await select.selectOption("America/New_York");
		await page.getByTestId("profile-save").click();

		await expect(page.getByTestId("profile-saved-notice")).toBeVisible();
		expect(mocks.settingsUpdates).toEqual([
			{ preferredLanguage: "en", timeZone: "America/New_York" },
		]);

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

	test("the identity strip persists across Overview ⇄ Profile without a second user.account.get", async ({
		page,
	}) => {
		const mocks = await mockGateway(page, {
			userId: "user-1",
			email: "pilot@example.com",
			userName: "player-abc12345",
			displayName: "Pilot",
		});
		await completeLogin(page);
		await expect(page.getByTestId("lobby-account-name")).toContainText("Pilot");
		const firstCount = mocks.accountGetCount;
		expect(firstCount).toBeGreaterThanOrEqual(1);

		// Navigate Games → Profile: identity must NOT flash the
		// loading placeholder, and the cache must answer without a
		// second gateway call.
		await page.getByTestId("lobby-nav-profile").click();
		await expect(page.getByTestId("profile-form")).toBeVisible();
		await expect(page.getByTestId("lobby-account-name")).toContainText("Pilot");

		// Navigate back to the games section. F8-04b replaced the bare
		// `Overview` page with a `games` parent + sub-panels; the dev
		// nav surface goes straight to the canonical landing
		// (`games-recruitment`) so the assertion is viewport-agnostic.
		await page.evaluate(() => window.__galaxyNav!.go("games-recruitment"));
		await expect(page.getByTestId("lobby-recruitment-empty")).toBeVisible();
		await expect(page.getByTestId("lobby-account-name")).toContainText("Pilot");

		expect(mocks.accountGetCount).toBe(firstCount);

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