// Vitest coverage for the Phase 23 Report View's table of contents.
// Smokes the anchor list, the active-link state, the back-to-map
// navigation, and the mobile <select> fallback. The
// IntersectionObserver-driven active-section computation lives in
// the orchestrator (`report.svelte`); this test only checks the
// presentational pieces of the TOC.

import "@testing-library/jest-dom/vitest";
import { fireEvent, render } from "@testing-library/svelte";
import { beforeEach, describe, expect, test, vi } from "vitest";

import { i18n } from "../src/lib/i18n/index.svelte";
import type { TranslationKey } from "../src/lib/i18n/index.svelte";

const gotoMock = vi.hoisted(() => vi.fn());
vi.mock("$app/navigation", () => ({
	goto: gotoMock,
}));

import ReportToc, {
	type TocEntry,
} from "../src/lib/active-view/report/report-toc.svelte";

const ENTRIES: readonly TocEntry[] = [
	{ slug: "galaxy-summary", titleKey: "game.report.section.galaxy_summary.title" },
	{ slug: "votes", titleKey: "game.report.section.votes.title" },
	{ slug: "bombings", titleKey: "game.report.section.bombings.title" },
];

beforeEach(() => {
	i18n.resetForTests("en");
	gotoMock.mockClear();
});

describe("report TOC", () => {
	test("renders one anchor per entry and one option in the mobile select", () => {
		const ui = render(ReportToc, {
			props: { entries: ENTRIES, activeSlug: "galaxy-summary", gameId: "g-1" },
		});
		for (const e of ENTRIES) {
			expect(ui.getByTestId(`report-toc-${e.slug}`)).toBeInTheDocument();
		}
		const mobile = ui.getByTestId("report-toc-mobile") as HTMLSelectElement;
		expect(mobile.options).toHaveLength(ENTRIES.length);
		expect(mobile.value).toBe("galaxy-summary");
	});

	test("marks the active anchor with aria-current=location and a class", () => {
		const ui = render(ReportToc, {
			props: { entries: ENTRIES, activeSlug: "bombings", gameId: "g-1" },
		});
		const active = ui.getByTestId("report-toc-bombings");
		expect(active).toHaveAttribute("aria-current", "location");
		expect(active).toHaveClass("active");

		const inactive = ui.getByTestId("report-toc-votes");
		expect(inactive).not.toHaveAttribute("aria-current");
		expect(inactive).not.toHaveClass("active");
	});

	test("back-to-map button calls goto with the active game's map URL", async () => {
		const ui = render(ReportToc, {
			props: {
				entries: ENTRIES,
				activeSlug: "galaxy-summary",
				gameId: "abc",
			},
		});
		const button = ui.getByTestId("report-back-to-map");
		await fireEvent.click(button);
		expect(gotoMock).toHaveBeenCalledWith("/games/abc/map");
	});

	test("anchor click cancels the default jump and calls scrollIntoView on the target", async () => {
		// Stub `scrollIntoView` on the target — jsdom does not
		// implement it. The TOC also reads
		// `prefers-reduced-motion`; the matchMedia stub forces a
		// stable `behavior: "auto"` so the assertion is reproducible.
		const scrollSpy = vi.fn();
		const target = document.createElement("section");
		target.id = "report-bombings";
		target.scrollIntoView = scrollSpy;
		document.body.appendChild(target);

		Object.defineProperty(window, "matchMedia", {
			writable: true,
			value: (query: string) => ({
				matches: query.includes("reduce"),
				media: query,
				onchange: null,
				addListener: () => {},
				removeListener: () => {},
				addEventListener: () => {},
				removeEventListener: () => {},
				dispatchEvent: () => false,
			}),
		});

		const ui = render(ReportToc, {
			props: { entries: ENTRIES, activeSlug: "galaxy-summary", gameId: "g" },
		});
		await fireEvent.click(ui.getByTestId("report-toc-bombings"));
		expect(scrollSpy).toHaveBeenCalledWith({
			behavior: "auto",
			block: "start",
		});
		target.remove();
	});

	test("mobile select scrolls to the chosen section without navigating", async () => {
		const scrollSpy = vi.fn();
		const target = document.createElement("section");
		target.id = "report-votes";
		target.scrollIntoView = scrollSpy;
		document.body.appendChild(target);

		Object.defineProperty(window, "matchMedia", {
			writable: true,
			value: () => ({
				matches: false,
				media: "(prefers-reduced-motion: no-preference)",
				onchange: null,
				addListener: () => {},
				removeListener: () => {},
				addEventListener: () => {},
				removeEventListener: () => {},
				dispatchEvent: () => false,
			}),
		});

		const ui = render(ReportToc, {
			props: {
				entries: ENTRIES,
				activeSlug: "galaxy-summary",
				gameId: "g",
			},
		});
		const select = ui.getByTestId("report-toc-mobile") as HTMLSelectElement;
		await fireEvent.change(select, { target: { value: "votes" } });
		expect(scrollSpy).toHaveBeenCalled();
		expect(gotoMock).not.toHaveBeenCalled();
		target.remove();
	});

	// Tests intentionally validate the *type* of the entries prop is
	// exposed correctly so future widening of the list does not
	// silently drop entries. TypeScript already enforces this through
	// `TocEntry`; the assertion below is a soft check so a stray
	// `as unknown as ...` cast surfaces fast.
	test("TocEntry exposes a slug and a TranslationKey", () => {
		const slug: string = ENTRIES[0]!.slug;
		const key: TranslationKey = ENTRIES[0]!.titleKey;
		expect(typeof slug).toBe("string");
		expect(typeof key).toBe("string");
	});
});
