import { act, fireEvent, render, screen } from "@testing-library/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { useEffect } from "react";
import { EvidenceProvider, useEvidenceSubject } from "./evidenceContext";
import { RightInspector } from "./RightInspector";
import { createTestQueryClient } from "../test/createTestQueryClient";
import type { ChatTurnResult, ProposalDetail, VaultRecall } from "../types/api";

const MOCK_TURN: ChatTurnResult = {
  prompt: "What is alpha?",
  surface: "alpha causes beta",
  articulation_surface: "alpha causes beta",
  walk_surface: "alpha -> beta",
  grounding_source: "teaching",
  epistemic_state: "decoded",
  normative_clearance: "cleared",
  normative_detail: "",
  trace_hash: "sha256:abc123def456",
  refusal_emitted: false,
  hedge_injected: false,
  mutation_mode: "runtime_turn",
  identity_verdict: null,
  safety_verdict: null,
  ethics_verdict: null,
  proposal_candidates: [],
  turn_cost_ms: 17,
  checkpoint_emitted: true,
};

function SetSubjectAndRender({
  kind,
}: {
  kind: "turn" | "none";
}) {
  const { setSubject } = useEvidenceSubject();
  // Render-phase setSubject created an infinite synchronous render loop
  // (new subject object every pass) — the 100%-CPU spin previously
  // misdiagnosed as a teardown hang. State updates belong in effects.
  useEffect(() => {
    if (kind === "turn") {
      setSubject({ kind: "turn", turnId: 1, data: MOCK_TURN });
    }
  }, [kind, setSubject]);
  return <RightInspector />;
}

function renderInspector(kind: "turn" | "none" = "none") {
  return render(
    <EvidenceProvider>
      <SetSubjectAndRender kind={kind} />
    </EvidenceProvider>,
  );
}

describe("RightInspector", () => {
  it("renders inspector region", () => {
    renderInspector();
    expect(document.querySelector('[data-region="inspector"]')).toBeInTheDocument();
  });

  it("shows empty hint when no subject selected", () => {
    renderInspector("none");
    expect(screen.getByText("Select an item to inspect its evidence.")).toBeInTheDocument();
  });

  it("shows turn data when turn subject is set", () => {
    renderInspector("turn");
    expect(screen.getByText("Turn #1")).toBeInTheDocument();
    expect(screen.getByText("17ms")).toBeInTheDocument();
  });

  it("shows keyboard shortcut hint", () => {
    renderInspector();
    expect(screen.getByText("⌘I")).toBeInTheDocument();
  });

  it("renders an honest not-loaded state for an identity-only subject", () => {
    function SetIdentityOnly() {
      const { setSubject } = useEvidenceSubject();
      useEffect(() => {
        setSubject({ kind: "proposal", proposalId: "proposal-xyz" });
      }, [setSubject]);
      return <RightInspector />;
    }
    render(
      <EvidenceProvider>
        <SetIdentityOnly />
      </EvidenceProvider>,
    );
    expect(screen.getByText("proposal-xyz")).toBeInTheDocument();
    expect(screen.getByText(/Detail not loaded in this session/)).toBeInTheDocument();
  });

  it("renders calibration class evidence instead of raw unknown", () => {
    function SetCalibrationClass() {
      const { setSubject } = useEvidenceSubject();
      useEffect(() => {
        setSubject({
          kind: "calibration_class",
          className: "additive",
          data: {
            class_name: "additive",
            correct: 95,
            wrong: 5,
            refused: 50,
            committed: 100,
            reliability_floor: 0.86084162,
            coverage: 0.666666667,
            propose_required: 0.85,
            propose_licensed: true,
            serve_required: 0.99,
            serve_licensed: false,
            source_path: "evals/gsm8k_math/practice/v1/report.json",
            source_digest: "sha256:practice",
          },
        });
      }, [setSubject]);
      return <RightInspector />;
    }
    render(
      <EvidenceProvider>
        <SetCalibrationClass />
      </EvidenceProvider>,
    );
    expect(screen.getByText("Calibration Class")).toBeInTheDocument();
    expect(screen.getByText("additive")).toBeInTheDocument();
    expect(screen.getByText(/licensed at θ 85\.0%/)).toBeInTheDocument();
    expect(screen.getByText("sha256:practice")).toBeInTheDocument();
  });

  it("shows vault entry depth: distinct status/state, curated metadata, handle, raw drawer", () => {
    function SetVaultEntry() {
      const { setSubject } = useEvidenceSubject();
      useEffect(() => {
        setSubject({
          kind: "vault_entry",
          entryIndex: 7,
          data: {
            entry_index: 7,
            epistemic_status: "coherent",
            epistemic_state: "decoded",
            versor_digest: "sha256:vvvvvvvvvvvv",
            metadata: {
              turn: 3,
              role: "assistant",
              energy_class: "warm",
              coherence_residual: 0.01,
              propositional_form: "alpha causes beta",
              promotion_certificate_digest: "sha256:certcertcert",
            },
          },
        });
      }, [setSubject]);
      return <RightInspector />;
    }
    render(
      <EvidenceProvider>
        <SetVaultEntry />
      </EvidenceProvider>,
    );

    expect(screen.getByText("Vault Entry")).toBeInTheDocument();
    expect(screen.getByText("#7")).toBeInTheDocument();
    // epistemic_status (tier) and epistemic_state (trust display) are distinct
    expect(screen.getByText("epistemic_status")).toBeInTheDocument();
    expect(screen.getByText("epistemic_state")).toBeInTheDocument();
    expect(screen.getByText("coherent")).toBeInTheDocument();
    expect(screen.getByText("decoded")).toBeInTheDocument();
    // core + present-only metadata rows
    expect(screen.getByText("role")).toBeInTheDocument();
    expect(screen.getByText("assistant")).toBeInTheDocument();
    expect(screen.getByText("energy_class")).toBeInTheDocument();
    expect(screen.getByText("promotion_certificate_digest")).toBeInTheDocument();
    // propositional_form headline
    expect(screen.getByText("alpha causes beta")).toBeInTheDocument();
    // copyable evidence-address handle
    expect(screen.getByText("vault:7")).toBeInTheDocument();
    // raw metadata drawer present
    expect(screen.getByText("Raw metadata")).toBeInTheDocument();
    // exact-recall doctrine: never a similarity/relevance proxy
    expect(screen.queryByText(/similarity|relevance|score/i)).not.toBeInTheDocument();
  });

  it("renders 'not recorded' for absent core vault fields, not silent disappearance", () => {
    function SetSparseVaultEntry() {
      const { setSubject } = useEvidenceSubject();
      useEffect(() => {
        setSubject({
          kind: "vault_entry",
          entryIndex: 9,
          data: { entry_index: 9, versor_digest: null },
        });
      }, [setSubject]);
      return <RightInspector />;
    }
    render(
      <EvidenceProvider>
        <SetSparseVaultEntry />
      </EvidenceProvider>,
    );

    expect(screen.getByText("Vault Entry")).toBeInTheDocument();
    // epistemic_status, epistemic_state, turn, role all absent → 4 "not recorded"
    expect(screen.getAllByText("not recorded").length).toBe(4);
    // handle still present; no raw drawer when metadata is absent
    expect(screen.getByText("vault:9")).toBeInTheDocument();
    expect(screen.queryByText("Raw metadata")).not.toBeInTheDocument();
  });

  it("shows a transient Copied confirmation after an address copy", () => {
    vi.useFakeTimers();
    try {
      function CopySignal() {
        const { notifyAddressCopied } = useEvidenceSubject();
        return (
          <>
            <button type="button" onClick={notifyAddressCopied}>
              signal-copy
            </button>
            <RightInspector />
          </>
        );
      }
      render(
        <EvidenceProvider>
          <CopySignal />
        </EvidenceProvider>,
      );

      expect(screen.queryByTestId("address-copied")).not.toBeInTheDocument();
      fireEvent.click(screen.getByText("signal-copy"));
      expect(screen.getByTestId("address-copied")).toHaveTextContent("Copied");

      act(() => {
        vi.advanceTimersByTime(2000);
      });
      expect(screen.queryByTestId("address-copied")).not.toBeInTheDocument();
    } finally {
      vi.useRealTimers();
    }
  });
});

const RECALL_FIXTURE: VaultRecall = {
  entry_index: 7,
  query_versor_digest: "sha256:qqqqqqqqqqqq",
  top_k: 5,
  hits: [
    {
      entry_index: 7,
      rank: 0,
      cga_inner: 1.2e-7,
      exact_self_match: true,
      epistemic_status: "coherent",
      epistemic_state: "decoded",
      versor_digest: "sha256:qqqqqqqqqqqq",
    },
    {
      entry_index: 3,
      rank: 1,
      cga_inner: 0.4212,
      exact_self_match: false,
      epistemic_status: "coherent",
      epistemic_state: "decoded",
      versor_digest: "sha256:zzzzzzzzzzzz",
    },
  ],
  self_hit_rank: 0,
  self_hit_found: true,
  exact_cga: true,
  approximate: false,
  source_path: "engine_state/session_state.json",
};

function stubRecallFetch() {
  vi.stubGlobal(
    "fetch",
    vi.fn((input: unknown) => {
      const path = new URL(String(input)).pathname;
      if (path === "/vault/entries/7/recall") {
        return Promise.resolve({
          json: () => Promise.resolve({ ok: true, generated_at: "now", data: RECALL_FIXTURE }),
        });
      }
      return Promise.resolve({
        json: () =>
          Promise.resolve({
            ok: false,
            generated_at: "now",
            error: { code: "not_found", message: `unexpected ${path}` },
          }),
      });
    }),
  );
}

function renderVaultInspector() {
  function SetVaultEntry() {
    const { setSubject } = useEvidenceSubject();
    useEffect(() => {
      setSubject({
        kind: "vault_entry",
        entryIndex: 7,
        data: {
          entry_index: 7,
          epistemic_status: "coherent",
          epistemic_state: "decoded",
          versor_digest: "sha256:qqqqqqqqqqqq",
          metadata: { turn: 3, role: "assistant" },
        },
      });
    }, [setSubject]);
    return <RightInspector />;
  }
  return render(
    <QueryClientProvider client={createTestQueryClient()}>
      <EvidenceProvider>
        <SetVaultEntry />
      </EvidenceProvider>
    </QueryClientProvider>,
  );
}

describe("VaultEntryInspector — exact CGA recall", () => {
  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it("keeps recall opt-in: collapsed by default, no fetch, no banned language", () => {
    stubRecallFetch();
    renderVaultInspector();
    // The disclosure is present but the recall body has not mounted.
    expect(screen.getByText("Show exact CGA recall")).toBeInTheDocument();
    expect(screen.queryByText(/recalls itself/)).not.toBeInTheDocument();
    expect(fetch).not.toHaveBeenCalled();
    // Exact-recall doctrine holds on the collapsed surface too.
    expect(screen.queryByText(/similarity|relevance|cosine|approximat|score/i)).not.toBeInTheDocument();
  });

  it("expands to show exact CGA recall evidence — self-recall proof, never a similarity proxy", async () => {
    stubRecallFetch();
    renderVaultInspector();

    fireEvent.click(screen.getByText("Show exact CGA recall"));

    // The self-recall proof: the entry recalls itself at rank 0 as an exact match.
    expect(await screen.findByText(/recalls itself at rank 0/)).toBeInTheDocument();
    expect(screen.getByText(/exact match/)).toBeInTheDocument();
    // The exact primitive is named; no fabricated similarity/relevance/score.
    expect(screen.getAllByText(/cga_inner/).length).toBeGreaterThan(0);
    expect(screen.getByText("query_versor")).toBeInTheDocument();
    expect(
      screen.queryByText(/similarity|relevance|cosine|approximat|score|\bann\b/i),
    ).not.toBeInTheDocument();
  });
});
