import { afterEach, describe, it } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import React from "react";

import { GraphPanel } from "../src/components/GraphPanel";
import { click, pressKey, render, screen, waitFor } from "./render";

describe("GraphPanel Paper shell", () => {
	const originalFetch = globalThis.fetch;

	afterEach(() => {
		globalThis.fetch = originalFetch;
	});

	it("renders the graph shell toolbar without adding app-level graph overlays", async () => {
		mockGraphFetch();

		render(
			<GraphPanel
				currentKnowledgeBaseName="AI 学习库"
				currentKnowledgeBasePath="/kb"
				theme="light"
			/>,
		);

		const toolbar = screen.getByRole("banner", { name: "图谱工具栏" });
		assert.equal(toolbar.classList.contains("graph-shell-toolbar"), true);
		assert.match(toolbar.textContent ?? "", /图谱活地图/);
		assert.ok(screen.getByRole("button", { name: /重置布局/ }));
		assert.ok(screen.getByRole("button", { name: /调参/ }));
		assert.ok(screen.getByRole("button", { name: /重构/ }));
		assert.notEqual(toolbar.querySelector(".graph-shell-legend"), null);

		await waitFor(() => {
			assert.match(toolbar.textContent ?? "", /1 节点 · 0 关联/);
		});

		const shell = document.querySelector(".graph-shell");
		const stage = document.querySelector(".graph-stage");
		assert.ok(shell);
		assert.ok(stage);
		assert.equal(shell?.contains(toolbar), true);
		assert.equal(shell?.contains(stage), true);
		assert.equal(stage?.contains(toolbar), false);
		assert.notEqual(screen.queryByLabelText("图谱图例"), null);
		assert.equal(document.querySelector(".graph-legend"), null);
		assert.ok(document.querySelector(".graph-host"));
	});

	it("opens graph edge tuning controls from the toolbar", async () => {
		mockGraphFetch();

		render(
			<GraphPanel
				currentKnowledgeBaseName="AI 学习库"
				currentKnowledgeBasePath="/kb"
				theme="light"
			/>,
		);

		const tuningButton = screen.getByRole("button", { name: /调参/ }) as HTMLButtonElement;
		await waitFor(() => {
			assert.equal(tuningButton.disabled, false);
		});
		await click(tuningButton);

		const panel = screen.getByRole("dialog", { name: "图谱调参" });
		assert.match(panel.textContent ?? "", /分主次/);
		const semanticToggle = screen.getByRole("checkbox", { name: "语义强调" });
		const focusToggle = screen.getByRole("checkbox", { name: "聚焦点亮" });
		assert.equal((semanticToggle as HTMLInputElement).checked, false);
		assert.equal((focusToggle as HTMLInputElement).checked, false);
		assert.equal(document.activeElement, semanticToggle);

		await click(semanticToggle);

		await waitFor(() => {
			assert.equal((semanticToggle as HTMLInputElement).checked, true);
			assert.equal(localStorage.getItem("llm-wiki.graph.edge-style"), "{\"semanticEmphasis\":true,\"focusHighlight\":false}");
		});

		await click(focusToggle);

		await waitFor(() => {
			assert.equal((focusToggle as HTMLInputElement).checked, true);
			assert.equal(localStorage.getItem("llm-wiki.graph.edge-style"), "{\"semanticEmphasis\":true,\"focusHighlight\":true}");
		});

		await pressKey(document, "Escape");

		assert.equal(screen.queryByRole("dialog", { name: "图谱调参" }), null);
		assert.equal(document.activeElement, tuningButton);
	});

	it("surfaces graph build errors in the graph panel", async () => {
		mockGraphFetch({ needsBuild: true });

		const { rerender } = render(
			<GraphPanel
				currentKnowledgeBaseName="AI 学习库"
				currentKnowledgeBasePath="/kb"
				theme="light"
			/>,
		);

		await waitFor(() => {
			assert.match(screen.getByText("图谱构建中").textContent ?? "", /图谱构建中/);
		});

		rerender(
			<GraphPanel
				currentKnowledgeBaseName="AI 学习库"
				currentKnowledgeBasePath="/kb"
				theme="light"
				graphBuildError={{ kbPath: "/kb", message: "构建失败", rebuiltAt: "2026-06-20T00:00:00.000Z" }}
			/>,
		);

		await waitFor(() => {
			assert.match(screen.getByText("图谱暂时不可用").textContent ?? "", /图谱暂时不可用/);
			assert.match(document.body.textContent ?? "", /构建失败/);
		});
	});

	it("keeps the GraphPanel Paper shell styling outside graph-engine internals", () => {
		const css = readFileSync(resolve(import.meta.dirname, "../src/index.css"), "utf8");

		assert.match(css, /\.graph-shell\s*\{[\s\S]*display:\s*grid/);
		assert.match(css, /\.graph-shell-toolbar[\s\S]*position:\s*relative/);
		assert.match(css, /\.graph-stage[\s\S]*min-height:\s*0/);
		assert.match(css, /\.graph-shell-legend[\s\S]*display:\s*flex/);
		assert.doesNotMatch(cssBlock(css, ".graph-shell-toolbar"), /position:\s*absolute/);
		assert.match(css, /\.graph-shell-toolbar[\s\S]*var\(--paper-grain\)/);
		assert.match(css, /\.graph-shell-toolbar-chip,[\s\S]*\.graph-shell-toolbar-button/);
		assert.match(css, /\.graph-stage[\s\S]*border-radius:\s*16px/);
		assert.doesNotMatch(css, /(^|\n)\s*\.graph-toolbar\b/);
		assert.doesNotMatch(css, /(^|\n)\s*\.graph-legend\b/);
		assert.doesNotMatch(css, /render-styles|sigma-node|sigma-edge/);
	});
});

function cssBlock(css: string, selector: string): string {
	const start = css.indexOf(`${selector} {`);
	if (start === -1) return "";
	const end = css.indexOf("\n  }", start);
	return end === -1 ? css.slice(start) : css.slice(start, end);
}

function mockGraphFetch(options: { needsBuild?: boolean } = {}) {
	globalThis.fetch = (async (input) => {
		const url = String(input);
		if (url.startsWith("/api/graph/layout")) {
			return jsonResponse({
				ok: true,
				layout: { version: 1, pins: {}, updatedAt: "2026-06-20T00:00:00.000Z" },
			});
		}
		if (url.startsWith("/api/graph?")) {
			if (options.needsBuild) {
				return jsonResponse({
					ok: true,
					needsBuild: true,
					graphPath: "/kb/.llm-wiki/graph.json",
				});
			}
			return jsonResponse({
				ok: true,
				needsBuild: false,
				data: {
					meta: { build_date: "2026-06-20T00:00:00.000Z" },
					nodes: [
						{
							id: "wiki/paper.md",
							label: "Paper",
							title: "Paper",
							type: "topic",
							community: "paper",
							path: "wiki/paper.md",
						},
					],
					edges: [],
					communities: [],
				},
				});
			}
		if (url.startsWith("/api/graph/rebuild")) {
			return jsonResponse({ ok: true, status: "started" });
		}
		return jsonResponse({ ok: false, error: `Unexpected request: ${url}` }, 500);
	}) as typeof fetch;
}

function jsonResponse(payload: unknown, status = 200) {
	return new Response(JSON.stringify(payload), {
		status,
		headers: { "Content-Type": "application/json" },
	});
}
