import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";

import { App } from "../src/client/App.js";
import type {
  ComponentStageResponse,
  DocumentComparisonResponse,
  RunSummaryResponse,
} from "../src/shared/api.js";

vi.mock("../src/client/DiffView.js", () => ({
  DiffView: ({ beforeLabel, afterLabel }: { beforeLabel: string; afterLabel: string }) => (
    <div data-testid="diff-view">
      {beforeLabel} / {afterLabel}
    </div>
  ),
}));

const components = [
  {
    component_position: 0,
    component_id: "paper.rule",
    version: "1.0.0",
    parameters: [],
    applicability: "替换测试单词。",
    change_count: 1,
  },
  {
    component_position: 1,
    component_id: "paper.zero",
    version: "1.0.0",
    parameters: [],
    applicability: "不修改当前测试文档。",
    change_count: 0,
  },
];

const documentSummary = {
  document_id: "paper",
  source_label: "inputs/paper.md",
  status: "success" as const,
  input_sha256: "1".repeat(64),
  current_sha256: "2".repeat(64),
  change_count: 1,
  source_available: true,
  output_available: true,
  availability_error: null,
};

const runResponse: RunSummaryResponse = {
  schema_version: 1,
  run: {
    run_id: "review-run",
    run_date: "2026-08-23",
    status: "success",
    started_at_utc: "2026-08-23T01:00:00Z",
    completed_at_utc: "2026-08-23T01:01:00Z",
    retention_until: "2026-09-22T01:01:00Z",
  },
  components,
  documents: [documentSummary],
  summary: {
    document_count: 1,
    success_count: 1,
    failed_count: 0,
    unstable_count: 0,
    change_count: 1,
  },
  original_run_location_changed: false,
};

const change = {
  component_id: "paper.rule",
  component_version: "1.0.0",
  component_position: 0,
  proposal_ref: {
    component_position: 0,
    snapshot_sha256: "1".repeat(64),
    proposal_index: 0,
  },
  edit_index: 0,
  reason: "替换测试单词",
  location: { line: 1, column: 3 },
  editor_range: { start: 0, end: 3 },
  before: "old",
  after: "new",
};

const documentResponse: DocumentComparisonResponse = {
  schema_version: 1,
  document: documentSummary,
  components,
  original_markdown: "old",
  cleaned_markdown: "new",
  changes: [change],
  errors: [],
  residual_proposals: [],
};

const stageResponse: ComponentStageResponse = {
  schema_version: 1,
  document_id: "paper",
  component: components[0]!,
  before_sha256: "1".repeat(64),
  after_sha256: "2".repeat(64),
  before_markdown: "old",
  after_markdown: "new",
  changes: [change],
};

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

describe("App", () => {
  beforeEach(() => {
    vi.restoreAllMocks();
  });

  it("shows the run, document comparison, component order and component stage", async () => {
    const fetchMock = vi.fn((input: string | URL | Request) => {
      const pathname =
        typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
      if (pathname === "/api/v1/run") {
        return Promise.resolve(response(runResponse));
      }
      if (pathname.endsWith("/components/0")) {
        return Promise.resolve(response(stageResponse));
      }
      return Promise.resolve(response(documentResponse));
    });
    vi.stubGlobal("fetch", fetchMock);

    render(<App />);

    expect(await screen.findByRole("heading", { name: "review-run" })).toBeInTheDocument();
    expect(await screen.findByTestId("diff-view")).toHaveTextContent("清洗前 / 清洗后");
    expect(screen.getByText("paper.rule")).toBeInTheDocument();
    expect(screen.getByText("paper.zero")).toBeInTheDocument();
    expect(screen.getByText("替换测试单词")).toBeInTheDocument();

    fireEvent.click(screen.getByRole("button", { name: /paper\.rule/ }));
    await waitFor(() => {
      expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining("components/0"), expect.anything());
    });
    expect(await screen.findByTestId("diff-view")).toHaveTextContent("组件 1 执行前 / 组件 1 执行后");
  });

  it("shows API failures without rendering document text", async () => {
    vi.stubGlobal(
      "fetch",
      vi.fn(() =>
        Promise.resolve(
          response({ error: { code: "unsupported_schema", message: "不支持这个产物版本。" } }, 409),
        ),
      ),
    );

    render(<App />);

    expect(await screen.findByRole("alert")).toHaveTextContent("不支持这个产物版本");
    expect(screen.queryByTestId("diff-view")).not.toBeInTheDocument();
  });

  it("shows failed audit evidence without inventing a cleaned result", async () => {
    const failedSummary = {
      ...documentSummary,
      status: "failed" as const,
      current_sha256: documentSummary.input_sha256,
      change_count: 0,
      output_available: false,
    };
    const failedRun: RunSummaryResponse = {
      ...runResponse,
      run: { ...runResponse.run, status: "failed" },
      documents: [failedSummary],
      summary: {
        document_count: 1,
        success_count: 0,
        failed_count: 1,
        unstable_count: 0,
        change_count: 0,
      },
    };
    const failedDocument: DocumentComparisonResponse = {
      schema_version: 1,
      document: failedSummary,
      components: components.map((component) => ({ ...component, change_count: 0 })),
      original_markdown: "原文",
      cleaned_markdown: null,
      changes: [],
      errors: [
        {
          component_id: "paper.rule",
          component_version: "1.0.0",
          component_position: 0,
          stage: "transform",
          error_type: "SyntheticError",
          message: "测试组件失败。",
        },
      ],
      residual_proposals: [],
    };
    vi.stubGlobal(
      "fetch",
      vi.fn((input: string | URL | Request) => {
        const pathname =
          typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
        return Promise.resolve(response(pathname === "/api/v1/run" ? failedRun : failedDocument));
      }),
    );

    render(<App />);

    expect(await screen.findByRole("heading", { name: "失败文档只展示审计证据" })).toBeInTheDocument();
    expect(screen.getByText("测试组件失败。")).toBeInTheDocument();
    expect(screen.queryByTestId("diff-view")).not.toBeInTheDocument();
  });

  it("keeps audit details visible when a successful run has lost its original source", async () => {
    const unavailableSummary = {
      ...documentSummary,
      source_available: false,
      availability_error: "原文路径已经失效。",
    };
    const unavailableRun: RunSummaryResponse = {
      ...runResponse,
      documents: [unavailableSummary],
    };
    const unavailableDocument: DocumentComparisonResponse = {
      ...documentResponse,
      document: unavailableSummary,
      original_markdown: null,
      changes: [{ ...change, editor_range: null }],
    };
    vi.stubGlobal(
      "fetch",
      vi.fn((input: string | URL | Request) => {
        const pathname =
          typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
        return Promise.resolve(
          response(pathname === "/api/v1/run" ? unavailableRun : unavailableDocument),
        );
      }),
    );

    render(<App />);

    expect(await screen.findByRole("alert")).toHaveTextContent("原文路径已经失效");
    expect(screen.getByText("替换测试单词")).toBeInTheDocument();
    expect(screen.queryByTestId("diff-view")).not.toBeInTheDocument();
  });
});
