// Phase 26 end-to-end coverage for history mode. The spec boots an
// authenticated session, mocks the gateway calls the in-game shell
// makes (`lobby.my.games.list`, `user.games.report`), pre-seeds a
// local order draft, and drives the new turn navigator + history
// banner.
//
// The active view is `/table/planets` rather than `/map`: the Pixi
// renderer can monopolise the headless Chromium main thread for
// hundreds of ms after a snapshot change, which lets the navigator
// click win the race against Svelte's reactive flush and the
// `toContainText` poll find the old "turn ?" state for the entire
// 5 s polling window. The table view exercises the same `GameReport`
// data pipeline and the same banner / sidebar wiring without that
// rendering tail, so the assertions stay deterministic.
//
// Gateway mock design notes:
//   - `user.games.order.get` always replies with a non-ok status so
//     `OrderDraftStore.hydrateFromServer` short-circuits into its
//     `syncStatus = "error"` branch without overwriting the local
//     cache. This keeps the pre-seeded draft in memory across the
//     boot path, which is what we need to assert "draft survives a
//     history round-trip".
//   - `user.games.report` answers any requested turn with a turn
//     stamp in the local-planet names so a future diagnostic can
//     prove the rendered snapshot matches the requested turn.
//   - `SubscribeEvents` is held open so the revocation watcher does
//     not bounce the test back to `/login`.

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 { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
import {
	buildMyGamesListPayload,
	type GameFixture,
} from "./fixtures/lobby-fbs";
import { buildReportPayload } from "./fixtures/report-fbs";

const SESSION_ID = "phase-26-history-session";
const GAME_ID = "11111111-2222-3333-4444-555555555555";
const CURRENT_TURN = 5;

const SEED_DRAFT = [
	{ kind: "placeholder" as const, id: "cmd-a", label: "first" },
	{ kind: "placeholder" as const, id: "cmd-b", label: "second" },
];

interface MockState {
	reportRequests: number[];
}

async function mockGateway(page: Page): Promise<MockState> {
	const state: MockState = { reportRequests: [] };

	const baseGame = (): GameFixture => ({
		gameId: GAME_ID,
		gameName: "Phase 26 Game",
		gameType: "private",
		status: "running",
		ownerUserId: "user-1",
		minPlayers: 2,
		maxPlayers: 8,
		enrollmentEndsAtMs: BigInt(Date.now() + 86_400_000),
		createdAtMs: BigInt(Date.now() - 86_400_000),
		updatedAtMs: BigInt(Date.now()),
		currentTurn: CURRENT_TURN,
	});

	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 = new Uint8Array(new ArrayBuffer(0));

			const errorPayload = (message: string): Uint8Array => {
				const text = new TextEncoder().encode(
					JSON.stringify({ code: "internal_error", message }),
				);
				const buf = new ArrayBuffer(text.byteLength);
				new Uint8Array(buf).set(text);
				return new Uint8Array(buf);
			};

			switch (req.messageType) {
				case "lobby.my.games.list":
					payload = buildMyGamesListPayload([baseGame()]);
					break;
				case "user.games.report": {
					const decoded = GameReportRequest.getRootAsGameReportRequest(
						new ByteBuffer(req.payloadBytes),
					);
					const turn = decoded.turn();
					state.reportRequests.push(turn);
					const localPlanets = [
						{
							number: 1,
							name: `Home-${turn}`,
							x: 1000,
							y: 1000,
						},
					];
					payload = buildReportPayload({
						turn,
						mapWidth: 4000,
						mapHeight: 4000,
						localPlanets,
					});
					break;
				}
				case "user.games.order.get": {
					// Force `hydrateFromServer` into its catch branch so
					// the seeded local draft survives the boot path.
					resultCode = "internal_error";
					payload = errorPayload("test stub");
					break;
				}
				default:
					resultCode = "internal_error";
					payload = errorPayload(`unstubbed ${req.messageType}`);
			}

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

	await page.route(
		"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
		async () => {
			await new Promise<void>(() => {});
		},
	);

	return state;
}

async function seedShell(page: Page): Promise<void> {
	await page.goto("/__debug/store");
	await expect(page.getByTestId("debug-store-ready")).toBeVisible();
	await page.waitForFunction(() => window.__galaxyDebug?.ready === true);
	await page.evaluate(() => window.__galaxyDebug!.clearSession());
	await page.evaluate(
		(id) => window.__galaxyDebug!.setDeviceSessionId(id),
		SESSION_ID,
	);
	await page.evaluate(
		({ gameId, commands }) =>
			window.__galaxyDebug!.clearOrderDraft(gameId).then(() =>
				window.__galaxyDebug!.seedOrderDraft(gameId, commands),
			),
		{ gameId: GAME_ID, commands: SEED_DRAFT },
	);
}

test("navigating to a past turn enters history mode and back-to-current restores the draft", async ({
	page,
	isMobile,
}) => {
	const state = await mockGateway(page);
	await seedShell(page);

	await page.goto(`/games/${GAME_ID}/table/planets`);
	await expect(page.getByTestId("turn-navigator-trigger")).toContainText(
		`turn ${CURRENT_TURN}`,
	);

	// Live mode: banner hidden, order tab reachable.
	await expect(page.getByTestId("history-banner")).toHaveCount(0);

	// Order tab is visible. We expect both Sidebar (desktop / tablet)
	// and BottomTabs (mobile) wirings — the Phase 12 prop pair flips
	// off together when historyMode goes true.
	if (isMobile) {
		await expect(page.getByTestId("bottom-tab-order")).toBeVisible();
	} else {
		await expect(page.getByTestId("sidebar-tab-order")).toBeVisible();
	}

	// Step back one turn with the prev arrow.
	await page.getByTestId("turn-navigator-prev").click();
	await expect(page.getByTestId("turn-navigator-trigger")).toContainText(
		`turn ${CURRENT_TURN - 1}`,
	);
	await expect(page.getByTestId("history-banner")).toBeVisible();
	await expect(page.getByTestId("history-banner")).toContainText(
		`Viewing turn ${CURRENT_TURN - 1}`,
	);

	// Order tab vanishes from both wirings in history mode.
	if (isMobile) {
		await expect(page.getByTestId("bottom-tab-order")).toHaveCount(0);
	} else {
		await expect(page.getByTestId("sidebar-tab-order")).toHaveCount(0);
	}

	// Open the navigator popover and jump to turn 2 directly.
	await page.getByTestId("turn-navigator-trigger").click();
	const list = page.getByTestId("turn-navigator-list");
	await expect(list).toBeVisible();
	await expect(
		list.getByTestId("turn-navigator-item-0"),
	).toBeVisible();
	await expect(
		list.getByTestId("turn-navigator-item-5"),
	).toBeVisible();
	await expect(
		list.getByTestId("turn-navigator-current-badge"),
	).toBeVisible();

	await page.getByTestId("turn-navigator-item-2").click();
	await expect(page.getByTestId("turn-navigator-trigger")).toContainText(
		"turn 2",
	);
	await expect(page.getByTestId("history-banner")).toContainText(
		"Viewing turn 2",
	);

	// Click the banner action; live mode resumes.
	await page.getByTestId("history-banner-return").click();
	await expect(page.getByTestId("history-banner")).toHaveCount(0);
	await expect(page.getByTestId("turn-navigator-trigger")).toContainText(
		`turn ${CURRENT_TURN}`,
	);

	// Order tab is back and the seeded draft survives the round-trip.
	if (isMobile) {
		await page.getByTestId("bottom-tab-order").click();
	} else {
		await page.getByTestId("sidebar-tab-order").click();
	}
	await expect(page.getByTestId("sidebar-tool-order")).toBeVisible();
	const list2 = page.getByTestId("order-list");
	await expect(list2).toBeVisible();
	for (let i = 0; i < SEED_DRAFT.length; i++) {
		await expect(page.getByTestId(`order-command-${i}`)).toBeVisible();
	}

	// The mock served every requested turn (5 on boot, 4 via arrow,
	// 2 via dropdown, 5 again on return). The exact sequence proves
	// `viewTurn` does not bypass the network for live turns and
	// historical fetches hit the gateway when no cache row is present.
	expect(state.reportRequests).toEqual([5, 4, 2, 5]);
});
