// Phase 29 persistence + new-turn reset coverage for the
// `GameStateStore.mapToggles` rune. The tests drive the store
// against a real `fake-indexeddb`-backed Cache (the same harness
// `game-state.test.ts` uses) so the JSON-blob round-trip and the
// stale-`lastResetTurn` branch are exercised end-to-end.

import "@testing-library/jest-dom/vitest";
import "fake-indexeddb/auto";
import {
	afterEach,
	beforeEach,
	describe,
	expect,
	test,
	vi,
} from "vitest";
import { Builder } from "flatbuffers";

import {
	DEFAULT_MAP_TOGGLES,
	GameStateStore,
	type MapToggles,
} from "../src/lib/game-state.svelte";
import type { GalaxyClient } from "../src/api/galaxy-client";
import type { Cache } from "../src/platform/store/index";
import { IDBCache } from "../src/platform/store/idb-cache";
import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb";
import type { IDBPDatabase } from "idb";
import { Report } from "../src/proto/galaxy/fbs/report";

const listMyGamesSpy = vi.fn();
vi.mock("../src/api/lobby", async () => {
	const actual = await vi.importActual<typeof import("../src/api/lobby")>(
		"../src/api/lobby",
	);
	return {
		...actual,
		listMyGames: (...args: unknown[]) => listMyGamesSpy(...args),
	};
});

let db: IDBPDatabase<GalaxyDB>;
let dbName: string;
let cache: Cache;

beforeEach(async () => {
	dbName = `galaxy-map-toggles-test-${crypto.randomUUID()}`;
	db = await openGalaxyDB(dbName);
	cache = new IDBCache(db);
	listMyGamesSpy.mockReset();
});

afterEach(async () => {
	db.close();
	await new Promise<void>((resolve) => {
		const req = indexedDB.deleteDatabase(dbName);
		req.onsuccess = () => resolve();
		req.onerror = () => resolve();
		req.onblocked = () => resolve();
	});
});

const GAME_ID = "11111111-2222-3333-4444-555555555555";

function makeGameSummary(currentTurn: number) {
	return {
		gameId: GAME_ID,
		gameName: "Test Game",
		gameType: "private",
		status: "running",
		ownerUserId: "owner-1",
		minPlayers: 2,
		maxPlayers: 8,
		enrollmentEndsAt: new Date(),
		createdAt: new Date(),
		updatedAt: new Date(),
		currentTurn,
	};
}

function buildReportPayload(turn: number): Uint8Array {
	const builder = new Builder(64);
	Report.startReport(builder);
	Report.addTurn(builder, BigInt(turn));
	Report.addWidth(builder, 4000);
	Report.addHeight(builder, 4000);
	Report.addPlanetCount(builder, 0);
	builder.finish(Report.endReport(builder));
	return builder.asUint8Array();
}

function makeFakeClient(turn: number): GalaxyClient {
	return {
		executeCommand: async () => ({
			resultCode: "ok",
			payloadBytes: buildReportPayload(turn),
		}),
	} as unknown as GalaxyClient;
}

describe("GameStateStore.mapToggles persistence", () => {
	test("defaults apply when no blob is persisted", async () => {
		listMyGamesSpy.mockResolvedValue([makeGameSummary(3)]);
		const store = new GameStateStore();
		await store.init({ client: makeFakeClient(3), cache, gameId: GAME_ID });
		expect(store.mapToggles).toEqual(DEFAULT_MAP_TOGGLES);
		store.dispose();
	});

	test("setMapToggle round-trips through Cache across instances", async () => {
		listMyGamesSpy.mockResolvedValue([makeGameSummary(3)]);
		const a = new GameStateStore();
		await a.init({ client: makeFakeClient(3), cache, gameId: GAME_ID });
		await a.setMapToggle("hyperspaceGroups", false);
		await a.setMapToggle("battleMarkers", false);
		await a.setMapToggle("visibleHyperspace", false);
		a.dispose();

		listMyGamesSpy.mockResolvedValue([makeGameSummary(3)]);
		const b = new GameStateStore();
		await b.init({ client: makeFakeClient(3), cache, gameId: GAME_ID });
		expect(b.mapToggles.hyperspaceGroups).toBe(false);
		expect(b.mapToggles.battleMarkers).toBe(false);
		expect(b.mapToggles.visibleHyperspace).toBe(false);
		// Untouched flags retain defaults.
		expect(b.mapToggles.bombingMarkers).toBe(true);
		b.dispose();
	});

	test("missing fields in a persisted blob fall back to defaults", async () => {
		// Simulate an older client persisting a partial blob — only
		// `hyperspaceGroups` is set, every other field must inherit
		// the current default.
		await cache.put("game-map-toggles", GAME_ID, {
			toggles: { hyperspaceGroups: false } as Partial<MapToggles>,
			lastResetTurn: 5,
		});
		listMyGamesSpy.mockResolvedValue([makeGameSummary(5)]);
		const store = new GameStateStore();
		await store.init({ client: makeFakeClient(5), cache, gameId: GAME_ID });
		expect(store.mapToggles.hyperspaceGroups).toBe(false);
		expect(store.mapToggles.battleMarkers).toBe(true);
		expect(store.mapToggles.bombingMarkers).toBe(true);
		expect(store.mapToggles.visibleHyperspace).toBe(true);
		store.dispose();
	});
});

describe("GameStateStore.mapToggles new-turn reset", () => {
	test("a server turn newer than lastResetTurn resets every flag", async () => {
		await cache.put("game-map-toggles", GAME_ID, {
			toggles: {
				...DEFAULT_MAP_TOGGLES,
				hyperspaceGroups: false,
				battleMarkers: false,
				visibleHyperspace: false,
			},
			lastResetTurn: 4,
		});
		listMyGamesSpy.mockResolvedValue([makeGameSummary(5)]);
		const store = new GameStateStore();
		await store.init({ client: makeFakeClient(5), cache, gameId: GAME_ID });
		expect(store.mapToggles).toEqual(DEFAULT_MAP_TOGGLES);
		// The reset write back to cache so a subsequent reload sees the
		// fresh state.
		const persisted = await cache.get<{
			toggles: MapToggles;
			lastResetTurn: number;
		}>("game-map-toggles", GAME_ID);
		expect(persisted?.toggles).toEqual(DEFAULT_MAP_TOGGLES);
		expect(persisted?.lastResetTurn).toBe(5);
		store.dispose();
	});

	test("matching lastResetTurn restores persisted overrides", async () => {
		await cache.put("game-map-toggles", GAME_ID, {
			toggles: { ...DEFAULT_MAP_TOGGLES, hyperspaceGroups: false },
			lastResetTurn: 5,
		});
		listMyGamesSpy.mockResolvedValue([makeGameSummary(5)]);
		const store = new GameStateStore();
		await store.init({ client: makeFakeClient(5), cache, gameId: GAME_ID });
		expect(store.mapToggles.hyperspaceGroups).toBe(false);
		store.dispose();
	});

	test("advanceToPending resets toggles after jumping onto the new turn", async () => {
		await cache.put("game-prefs", `${GAME_ID}/last-viewed-turn`, 4);
		await cache.put("game-map-toggles", GAME_ID, {
			toggles: DEFAULT_MAP_TOGGLES,
			lastResetTurn: 4,
		});
		// First setGame opens the user on turn 4 with currentTurn=5
		// (last-viewed-turn bookmark < currentTurn). The new-turn
		// reset path fires immediately because lastResetTurn=4 < 5.
		listMyGamesSpy.mockResolvedValue([makeGameSummary(5)]);
		const store = new GameStateStore();
		await store.init({ client: makeFakeClient(4), cache, gameId: GAME_ID });
		// Drift the toggles after the setGame reset so we can verify
		// that advanceToPending resets them again on the user's
		// explicit jump onto turn 5.
		await store.setMapToggle("battleMarkers", false);
		expect(store.mapToggles.battleMarkers).toBe(false);
		expect(store.pendingTurn).toBe(5);

		// User clicks "Return to current turn" — the store fetches the
		// turn-5 report and resets toggles.
		await store.advanceToPending();
		expect(store.mapToggles).toEqual(DEFAULT_MAP_TOGGLES);
		expect(store.pendingTurn).toBeNull();
		store.dispose();
	});
});
