import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { TreeShellRustDomShellHost } from "./tree-shell-dom-host";
import { TreeShellHost } from "./tree-shell-host";
import type { KernelFileTreeProjectionItem } from "@/lib/kernel-file-tree";

(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

type PendingRuntimeRequest = {
  request: Record<string, unknown>;
  resolve: (runtimeResult?: Record<string, unknown> | null) => void;
};

describe("tree-shell-host", () => {
  let container: HTMLDivElement;
  let root: Root;
  let previousLegacyFlag: string | undefined;
  let originalFetch: typeof fetch | undefined;
  let pendingRuntimeRequests: PendingRuntimeRequest[];

  beforeEach(() => {
    previousLegacyFlag = process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST;
    delete process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST;
    container = document.createElement("div");
    document.body.appendChild(container);
    root = createRoot(container);
    originalFetch = global.fetch;
    pendingRuntimeRequests = [];
    global.fetch = vi.fn((_input: RequestInfo | URL, init?: RequestInit) => {
      const request = JSON.parse(String(init?.body ?? "{}")) as Record<string, unknown>;
      return new Promise((resolve) => {
        pendingRuntimeRequests.push({
          request,
          resolve: (runtimeResult = null) => {
            resolve({
              ok: true,
              json: async () => runtimeResult,
            } as Response);
          },
        });
      });
    }) as typeof fetch;
  });

  afterEach(() => {
    pendingRuntimeRequests.splice(0).forEach(({ resolve }) => resolve(null));
    act(() => {
      root.unmount();
    });
    container.remove();
    if (originalFetch) {
      global.fetch = originalFetch;
    } else {
      delete (globalThis as typeof globalThis & { fetch?: typeof fetch }).fetch;
    }
    if (previousLegacyFlag === undefined) {
      delete process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST;
    } else {
      process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST = previousLegacyFlag;
    }
  });

  const fileTreeItems: KernelFileTreeProjectionItem[] = [
    {
      projectionKind: "file_tree",
      rowId: "index:doc_1",
      rowKind: "index",
      nodeId: "doc_1:index",
      parentNodeId: null,
      title: "Doc 1 索引",
      depth: 0,
      childCount: 0,
      position: 0,
      expandedByDefault: false,
      iconHint: "page",
      capabilities: ["select"],
      resourceMeta: {
        resourceKind: "document",
        documentId: "doc_1",
        workspaceId: "ws_1",
        iconHint: "page",
      },
    },
    {
      projectionKind: "file_tree",
      rowId: "index:doc_2",
      rowKind: "index",
      nodeId: "doc_2:index",
      parentNodeId: null,
      title: "Doc 2 索引",
      depth: 0,
      childCount: 0,
      position: 1,
      expandedByDefault: false,
      iconHint: "page",
      capabilities: ["select"],
      resourceMeta: {
        resourceKind: "document",
        documentId: "doc_2",
        workspaceId: "ws_1",
        iconHint: "page",
      },
    },
    {
      projectionKind: "file_tree",
      rowId: "index:doc_3",
      rowKind: "index",
      nodeId: "doc_3:index",
      parentNodeId: null,
      title: "Doc 3 索引",
      depth: 0,
      childCount: 0,
      position: 2,
      expandedByDefault: false,
      iconHint: "page",
      capabilities: ["select"],
      resourceMeta: {
        resourceKind: "document",
        documentId: "doc_3",
        workspaceId: "ws_1",
        iconHint: "page",
      },
    },
    {
      projectionKind: "file_tree",
      rowId: "asset:asset_pdf",
      rowKind: "asset",
      nodeId: "asset_pdf",
      parentNodeId: "doc_2",
      title: "附件.pdf",
      depth: 1,
      childCount: 0,
      position: 3,
      expandedByDefault: false,
      iconHint: "pdf",
      capabilities: ["select"],
      resourceMeta: {
        resourceKind: "pdf",
        documentId: "doc_2",
        assetId: "asset_pdf",
        workspaceId: "ws_1",
        iconHint: "pdf",
      },
    },
  ];

  function queryFileTreeRow(rowId: string) {
    return container.querySelector(`[data-row-id="${rowId}"]`) as HTMLElement | null;
  }

  function expectSelectedRowIds(rowIds: string[]) {
    const selected = Array.from(container.querySelectorAll('[data-shell-mode="filetree"][data-selected="true"]'))
      .map((element) => element.getAttribute("data-row-id"))
      .filter((rowId): rowId is string => Boolean(rowId));
    expect(selected).toEqual(rowIds);
  }

  function renderDomHost(input: {
    activeDocumentId?: string | null;
    onFileTreeDeleteSelection?: (payload: {
      selectedRowIds: string[];
      anchorRowId: string | null;
      focusedRowId: string | null;
    }) => void;
    onTreeMutation?: (payload: any) => void;
  } = {}) {
    act(() => {
      root.render(
        <TreeShellRustDomShellHost
          mode="filetree"
          surfaceTestId="sidebar-file-tree-shell"
          workspaceId="ws_1"
          activeDocumentId={input.activeDocumentId ?? null}
          inlineFileTreeItems={fileTreeItems}
          onFileTreeDeleteSelection={input.onFileTreeDeleteSelection}
          onTreeMutation={input.onTreeMutation}
        />,
      );
    });
  }

  async function flushRuntimeDispatch() {
    await act(async () => {
      await Promise.resolve();
      await Promise.resolve();
    });
  }

  function renderHost() {
    act(() => {
      root.render(
        <TreeShellHost
          mode="page"
          surfaceTestId="sidebar-page-tree-shell"
          rendererFamily="rust_family"
          workspaceId="ws_1"
        >
          <div data-testid="legacy-react-fallback">旧 React fallback</div>
        </TreeShellHost>,
      );
    });
  }

  it("rust_family 默认选择 Rust/WASM DOM shell host，不能进入 iframe_srcdoc", () => {
    renderHost();

    const surface = container.querySelector('[data-testid="sidebar-page-tree-shell"]');
    const rustHost = container.querySelector('[data-testid="sidebar-page-tree-shell-rust-host"]');
    const domHost = container.querySelector('[data-testid="sidebar-page-tree-shell-dom-host"]');

    expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_host");
    expect(surface?.getAttribute("data-tree-renderer-contract")).toBe("rust_renderer_input_v1");
    expect(rustHost?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_host");
    expect(domHost?.getAttribute("data-tree-browser-bridge")).toBe("dom_wasm");
    expect(domHost?.getAttribute("data-tree-runtime-artifact-host")).toBe("rust_tree_shell_runtime_artifact_v1");
    expect(container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]')).toBeNull();
    expect(container.querySelector('[data-tree-browser-bridge="iframe_srcdoc"]')).toBeNull();
    expect(container.querySelector('[data-testid="legacy-react-fallback"]')).toBeNull();
  });

  it("只有显式 legacy flag 才允许进入旧 TreeShellIframeHost", async () => {
    process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST = "1";

    await act(async () => {
      renderHost();
      await Promise.resolve();
    });

    const surface = container.querySelector('[data-testid="sidebar-page-tree-shell"]');
    const iframe = container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]');

    expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_runtime_artifact_host");
    expect(container.querySelector('[data-testid="sidebar-page-tree-shell-dom-host"]')).toBeNull();
    expect(iframe).not.toBeNull();
    expect(iframe?.getAttribute("data-tree-browser-bridge")).toBe("iframe_srcdoc");
    expect(iframe?.getAttribute("srcdoc")).toContain('"browserBridge":"iframe_srcdoc"');
  });

  it("filetree DOM host 应先本地提交 Ctrl/Shift selection，再异步等 runtime 对账", async () => {
    renderDomHost();

    const row2 = queryFileTreeRow("index:doc_2");
    const row3 = queryFileTreeRow("index:doc_3");
    expect(row2).not.toBeNull();
    expect(row3).not.toBeNull();

    act(() => {
      row2?.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
    });
    expectSelectedRowIds(["index:doc_2"]);

    act(() => {
      row3?.dispatchEvent(new MouseEvent("click", { bubbles: true, shiftKey: true }));
    });
    expectSelectedRowIds(["index:doc_2", "index:doc_3"]);
  });

  it("旧 runtime selection 回写不应覆盖更新后的多选，Delete 应使用当前稳定 selection", async () => {
    const handleDeleteSelection = vi.fn();
    renderDomHost({ onFileTreeDeleteSelection: handleDeleteSelection });

    const row2 = queryFileTreeRow("index:doc_2");
    const row3 = queryFileTreeRow("index:doc_3");
    expect(row2).not.toBeNull();
    expect(row3).not.toBeNull();

    act(() => {
      row2?.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
    });
    await flushRuntimeDispatch();
    act(() => {
      row3?.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
    });
    expectSelectedRowIds(["index:doc_2", "index:doc_3"]);
    await flushRuntimeDispatch();

    act(() => {
      pendingRuntimeRequests[0]?.resolve({
        requestId: String(pendingRuntimeRequests[0]?.request.requestId ?? "filetree-stale"),
        mode: "fileTree",
        state: {
          mode: "fileTree",
          state: {
            activeRowId: null,
            selection: {
              selectedRowIds: ["index:doc_2"],
              anchorRowId: "index:doc_2",
              focusedRowId: "index:doc_2",
            },
            dragRowIds: [],
            dragEffect: null,
            dropTargetRowId: null,
          },
        },
      });
    });
    await flushRuntimeDispatch();
    expectSelectedRowIds(["index:doc_2", "index:doc_3"]);

    act(() => {
      row3?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Delete" }));
    });
    await flushRuntimeDispatch();

    expect(handleDeleteSelection).toHaveBeenCalledTimes(1);
    expect(handleDeleteSelection.mock.calls[0]?.[0]).toMatchObject({
      selectedRowIds: ["index:doc_2", "index:doc_3"],
      anchorRowId: "index:doc_3",
      focusedRowId: "index:doc_3",
    });
  });

  it("Backspace 也应复用当前稳定多选并触发删除回调", async () => {
    const handleDeleteSelection = vi.fn();
    renderDomHost({ onFileTreeDeleteSelection: handleDeleteSelection });

    const row2 = queryFileTreeRow("index:doc_2");
    const row3 = queryFileTreeRow("index:doc_3");
    expect(row2).not.toBeNull();
    expect(row3).not.toBeNull();

    act(() => {
      row2?.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
    });
    await flushRuntimeDispatch();
    act(() => {
      row3?.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
    });
    expectSelectedRowIds(["index:doc_2", "index:doc_3"]);

    act(() => {
      row3?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Backspace" }));
    });
    await flushRuntimeDispatch();

    expect(handleDeleteSelection).toHaveBeenCalledTimes(1);
    expect(handleDeleteSelection.mock.calls[0]?.[0]).toMatchObject({
      selectedRowIds: ["index:doc_2", "index:doc_3"],
      anchorRowId: "index:doc_3",
      focusedRowId: "index:doc_3",
    });
  });

  it("点击行内更多操作时不应因为事件冒泡把多选压成单选", async () => {
    renderDomHost();

    const row2 = queryFileTreeRow("index:doc_2");
    const row3 = queryFileTreeRow("index:doc_3");
    const row3Menu = container.querySelector(
      '[data-row-id="index:doc_3"] [data-testid="filetree-action-menu"]',
    ) as HTMLElement | null;
    expect(row2).not.toBeNull();
    expect(row3).not.toBeNull();
    expect(row3Menu).not.toBeNull();

    act(() => {
      row2?.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
    });
    act(() => {
      row3?.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
    });
    expectSelectedRowIds(["index:doc_2", "index:doc_3"]);

    act(() => {
      row3Menu?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    await flushRuntimeDispatch();

    expectSelectedRowIds(["index:doc_2", "index:doc_3"]);
  });

  it("activeDocumentId 变化时，已有 filetree selection 不应被无条件重置", () => {
    renderDomHost();

    const row2 = queryFileTreeRow("index:doc_2");
    const row3 = queryFileTreeRow("index:doc_3");
    expect(row2).not.toBeNull();
    expect(row3).not.toBeNull();

    act(() => {
      row2?.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
    });
    act(() => {
      row3?.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
    });
    expectSelectedRowIds(["index:doc_2", "index:doc_3"]);

    renderDomHost({ activeDocumentId: "doc_1" });

    expectSelectedRowIds(["index:doc_2", "index:doc_3"]);
  });

  it("F2 应进入 filetree 行内重命名，并用 tree.node.rename 提交页面标题", async () => {
    const handleTreeMutation = vi.fn();
    renderDomHost({ onTreeMutation: handleTreeMutation });

    const row2 = queryFileTreeRow("index:doc_2");
    expect(row2).not.toBeNull();

    act(() => {
      row2?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "F2" }));
    });
    await flushRuntimeDispatch();

    const input = container.querySelector('.tree-rename-input[data-rename-id="index:doc_2"]') as HTMLInputElement | null;
    expect(input).not.toBeNull();
    act(() => {
      input!.value = "Doc 2 Renamed";
      input!.dispatchEvent(new Event("input", { bubbles: true }));
    });
    await act(async () => {
      input!.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
      await Promise.resolve();
    });

    const renameRequest = pendingRuntimeRequests.find(
      (entry) => (entry.request as { action?: unknown }).action === "rename",
    );
    expect(renameRequest?.request).toMatchObject({
      action: "rename",
      documentId: "doc_2",
      title: "Doc 2 Renamed",
      workspaceId: "ws_1",
    });

    await act(async () => {
      renameRequest?.resolve({
        result: {
          documentId: "doc_2",
          title: "Doc 2 Renamed",
          workspaceId: "ws_1",
          updatedAt: "2026-05-15T00:00:00Z",
        },
      });
      await Promise.resolve();
    });

    expect(handleTreeMutation).toHaveBeenCalledWith(
      expect.objectContaining({
        type: "tree.node.renamed",
        documentId: "doc_2",
        title: "Doc 2 Renamed",
      }),
    );
  });

  it("F2 行内重命名应在输入框内阻断空名、非法字符和同级重名", async () => {
    renderDomHost();

    const row2 = queryFileTreeRow("index:doc_2");
    expect(row2).not.toBeNull();

    act(() => {
      row2?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "F2" }));
    });
    await flushRuntimeDispatch();

    const input = container.querySelector('.tree-rename-input[data-rename-id="index:doc_2"]') as HTMLInputElement | null;
    expect(input).not.toBeNull();

    act(() => {
      input!.value = "   ";
      input!.dispatchEvent(new Event("input", { bubbles: true }));
    });
    await act(async () => {
      input!.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
      await Promise.resolve();
    });
    expect(container.querySelector('[data-testid="tree-rename-validation"]')?.textContent).toContain("名称不能为空");
    expect(input?.getAttribute("aria-invalid")).toBe("true");
    expect(pendingRuntimeRequests.some((entry) => (entry.request as { action?: unknown }).action === "rename")).toBe(false);

    act(() => {
      input!.value = "非法/名称";
      input!.dispatchEvent(new Event("input", { bubbles: true }));
    });
    await act(async () => {
      input!.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
      await Promise.resolve();
    });
    expect(container.querySelector('[data-testid="tree-rename-validation"]')?.textContent).toContain("不能包含");
    expect(pendingRuntimeRequests.some((entry) => (entry.request as { action?: unknown }).action === "rename")).toBe(false);

    act(() => {
      input!.value = "Doc 1 索引";
      input!.dispatchEvent(new Event("input", { bubbles: true }));
    });
    await act(async () => {
      input!.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
      await Promise.resolve();
    });
    expect(container.querySelector('[data-testid="tree-rename-validation"]')?.textContent).toContain("同级已存在");
    expect(pendingRuntimeRequests.some((entry) => (entry.request as { action?: unknown }).action === "rename")).toBe(false);
  });

  it("F2 重命名附件未输入扩展名时应保留原扩展名", async () => {
    renderDomHost();

    const assetRow = queryFileTreeRow("asset:asset_pdf");
    expect(assetRow).not.toBeNull();

    act(() => {
      assetRow?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "F2" }));
    });
    await flushRuntimeDispatch();

    const input = container.querySelector('.tree-rename-input[data-rename-id="asset:asset_pdf"]') as HTMLInputElement | null;
    expect(input).not.toBeNull();

    act(() => {
      input!.value = "附件新名";
      input!.dispatchEvent(new Event("input", { bubbles: true }));
    });
    await act(async () => {
      input!.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
      await Promise.resolve();
    });

    const renameRequest = pendingRuntimeRequests.find((entry) => {
      const request = entry.request as { action?: unknown; assetIds?: unknown };
      return request.action === "rename" && Array.isArray(request.assetIds);
    });
    expect(renameRequest?.request).toMatchObject({
      action: "rename",
      assetIds: ["asset_pdf"],
      newName: "附件新名.pdf",
    });
  });
});
