// Phase 27 — Playwright coverage for the Battle Viewer.
//
// Mocks the Connect-RPC `user.games.report` (report with battles +
// bombings) and `user.games.battle` (Phase 28 migration; the viewer
// fetches the full BattleReport through the signed envelope instead
// of the old REST passthrough).
// Drives three flows:
//   1. Reports view → click battle UUID → viewer renders.
//   2. Playback controls: play / step back.
//   3. Reports view → click bombing marker proxy → row scrolls
//      (here approximated by clicking the link in Reports — the
//      map e2e flow is exercised separately by `map-roundtrip`).

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 { UUID } from "../../src/proto/galaxy/fbs/common";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { GameBattleRequest } from "../../src/proto/galaxy/fbs/battle";

import {
	buildOrderResponsePayload,
	buildOrderGetResponsePayload,
} from "./fixtures/order-fbs";
import { buildMyGamesListPayload, type GameFixture } from "./fixtures/lobby-fbs";
import { buildReportPayload } from "./fixtures/report-fbs";
import {
	buildBattlePayload,
	type BattleFixture,
} from "./fixtures/battle-fbs";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";

const GAME_ID = "00000000-0000-0000-0000-000000000010";
const BATTLE_ID = "11111111-1111-1111-1111-111111111111";
const SESSION_ID = "device-session-battle";
const RACE_A = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
const RACE_B = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb";

const SAMPLE_BATTLE: BattleFixture = {
	id: BATTLE_ID,
	planet: 1,
	planetName: "Earth",
	races: { "0": RACE_A, "1": RACE_B },
	ships: [
		{
			key: 10,
			race: "Earthlings",
			className: "Cruiser",
			tech: { WEAPONS: 1 },
			num: 3,
			numLeft: 2,
			loadType: "EMP",
			loadQuantity: 0,
			inBattle: true,
		},
		{
			key: 20,
			race: "Bajori",
			className: "Hawk",
			tech: { SHIELDS: 1 },
			num: 2,
			numLeft: 0,
			loadType: "EMP",
			loadQuantity: 0,
			inBattle: true,
		},
	],
	protocol: [
		{ a: 0, sa: 10, d: 1, sd: 20, x: false },
		{ a: 0, sa: 10, d: 1, sd: 20, x: true },
		{ a: 1, sa: 20, d: 0, sd: 10, x: true },
		{ a: 0, sa: 10, d: 1, sd: 20, x: true },
	],
};

async function mockGatewayAndBattle(page: Page): Promise<void> {
	const game: GameFixture = {
		gameId: GAME_ID,
		gameName: "Phase 27 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: 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 resultCode = "ok";
			let payload: Uint8Array;
			switch (req.messageType) {
				case "lobby.my.games.list":
					payload = buildMyGamesListPayload([game]);
					break;
				case "user.games.report": {
					GameReportRequest.getRootAsGameReportRequest(
						new ByteBuffer(req.payloadBytes),
					).gameId(new UUID());
					payload = buildReportPayload({
						turn: 1,
						mapWidth: 4000,
						mapHeight: 4000,
						race: "Earthlings",
						localPlanets: [
							{
								number: 1,
								name: "Earth",
								x: 2000,
								y: 2000,
								size: 1000,
								resources: 5,
								population: 4000,
								industry: 3000,
								capital: 0,
								material: 0,
								colonists: 100,
								freeIndustry: 800,
								production: "Cruiser",
							},
						],
						battles: [{ id: BATTLE_ID, planet: 1, shots: 4 }],
						localShipClass: [
							{
								name: "Cruiser",
								drive: 10,
								armament: 2,
								weapons: 5,
								shields: 5,
								cargo: 2,
							},
						],
						otherShipClass: [
							{
								race: "Bajori",
								name: "Hawk",
								drive: 12,
								armament: 1,
								weapons: 4,
								shields: 2,
								cargo: 0,
								mass: 75,
							},
						],
					});
					break;
				}
				case "user.games.order":
					payload = buildOrderResponsePayload(GAME_ID, [], Date.now());
					break;
				case "user.games.order.get":
					payload = buildOrderGetResponsePayload(GAME_ID, [], Date.now(), false);
					break;
				case "user.games.battle": {
					const fb = GameBattleRequest.getRootAsGameBattleRequest(
						new ByteBuffer(req.payloadBytes),
					);
					const id = fb.battleId();
					if (
						id !== null &&
						id.hi() === BigInt("0x1111111111111111") &&
						id.lo() === BigInt("0x1111111111111111")
					) {
						payload = buildBattlePayload(SAMPLE_BATTLE);
					} else {
						resultCode = "not_found";
						payload = new Uint8Array();
					}
					break;
				}
				default:
					resultCode = "internal_error";
					payload = new Uint8Array();
			}

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

}

async function bootSession(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,
	);
}

test.describe("Phase 27 battle viewer", () => {
	test("Reports UUID link opens the battle viewer", async ({ page }, testInfo) => {
		test.skip(
			testInfo.project.name.startsWith("chromium-mobile"),
			"desktop variant covers the link flow",
		);

		await mockGatewayAndBattle(page);
		await bootSession(page);
		await page.goto("/");
		await page.waitForFunction(() => window.__galaxyNav !== undefined);
		await page.evaluate(
			(id) => window.__galaxyNav!.enterGame(id, "report", {}),
			GAME_ID,
		);

		await expect(page.getByTestId("active-view-report")).toBeVisible();
		const row = page.getByTestId("report-battle-row").first();
		await expect(row).toBeVisible();
		await row.click();

		// The battle row switches the active view in place (the address
		// bar stays at the app base); the viewer chrome is the signal.
		await expect(page.getByTestId("battle-viewer")).toBeVisible();
		await expect(page.getByTestId("battle-scene")).toBeVisible();
		await expect(page.getByTestId("battle-frame-index")).toContainText("0 / 4");
	});

	test("playback play + step back updates the frame counter", async ({
		page,
	}, testInfo) => {
		test.skip(
			testInfo.project.name.startsWith("chromium-mobile"),
			"desktop variant covers playback controls",
		);

		await mockGatewayAndBattle(page);
		await bootSession(page);
		await page.goto("/");
		await page.waitForFunction(() => window.__galaxyNav !== undefined);
		await page.evaluate(
			([id, battleId]) =>
				window.__galaxyNav!.enterGame(id, "battle", { battleId, turn: 1 }),
			[GAME_ID, BATTLE_ID] as const,
		);

		await expect(page.getByTestId("battle-viewer")).toBeVisible();
		await expect(page.getByTestId("battle-frame-index")).toContainText("0 / 4");

		// Step forward once → 1 / 4.
		await page.getByTestId("battle-control-step-forward").click();
		await expect(page.getByTestId("battle-frame-index")).toContainText("1 / 4");

		// Step back to 0 / 4.
		await page.getByTestId("battle-control-step-back").click();
		await expect(page.getByTestId("battle-frame-index")).toContainText("0 / 4");
	});

	test("missing battle id surfaces the not-found state", async ({
		page,
	}, testInfo) => {
		test.skip(
			testInfo.project.name.startsWith("chromium-mobile"),
			"desktop variant covers the negative path",
		);

		await mockGatewayAndBattle(page);
		await bootSession(page);
		await page.goto("/");
		await page.waitForFunction(() => window.__galaxyNav !== undefined);
		await page.evaluate(
			(id) =>
				window.__galaxyNav!.enterGame(id, "battle", {
					battleId: "22222222-2222-2222-2222-222222222222",
					turn: 1,
				}),
			GAME_ID,
		);

		await expect(page.getByTestId("battle-not-found")).toBeVisible();
	});

	test("viewer fits the desktop viewport without a vertical scroll", async ({
		page,
	}, testInfo) => {
		test.skip(
			testInfo.project.name.startsWith("chromium-mobile"),
			"desktop-only height-fit check",
		);

		await page.setViewportSize({ width: 1280, height: 720 });
		await mockGatewayAndBattle(page);
		await bootSession(page);
		await page.goto("/");
		await page.waitForFunction(() => window.__galaxyNav !== undefined);
		await page.evaluate(
			([id, battleId]) =>
				window.__galaxyNav!.enterGame(id, "battle", { battleId, turn: 1 }),
			[GAME_ID, BATTLE_ID] as const,
		);

		await expect(page.getByTestId("battle-viewer")).toBeVisible();
		await expect(page.getByTestId("battle-scene")).toBeVisible();

		// Phase 27 refinement: viewer + log fit the viewport; the
		// internal log scrolls inside its own pane rather than
		// growing the page. Allow a small tolerance for fractional
		// pixel rounding around flex math, but reject any
		// scrollable overflow beyond a couple of pixels.
		// Phase 27 refinement: viewer + log fit the viewport; the
		// internal log scrolls inside its own pane rather than
		// growing the page. Allow a small tolerance for fractional
		// pixel rounding around flex math.
		const overflow = await page.evaluate(
			() => document.documentElement.scrollHeight - window.innerHeight,
		);
		expect(overflow).toBeLessThanOrEqual(4);
	});
});
