// @vitest-environment happy-dom
// Set the act-aware env flag so React Query / state updates flush
// without noisy console warnings. Mirrors the convention in the
// sibling hook tests (useDrawerKeyboard, useReconciliation).
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";

import { useSearch, destinationFor, flattenResults } from "./useSearch";
import { useAppStore } from "@/store";
import { api } from "@/lib/api";
import type { Activity, Claim, Remittance } from "@/types";

// ---------------------------------------------------------------------------
// Mocks
// ---------------------------------------------------------------------------

// The hook reads from the in-memory zustand store via
// `useSyncExternalStore` when `api.isConfigured` is false. Default
// vitest env (`VITE_API_BASE_URL: "http://test.local"`) flips
// `api.isConfigured` to TRUE — we want to test the fallback code
// path here (it's the one the demo runs in), so mock `api` to report
// itself unconfigured.
vi.mock("@/lib/api", () => ({
  api: {
    isConfigured: false,
    listClaims: vi.fn(),
    listRemittances: vi.fn(),
    listActivity: vi.fn(),
  },
}));

// ---------------------------------------------------------------------------
// Test fixtures
// ---------------------------------------------------------------------------

const CLAIMS: Claim[] = [
  {
    id: "CLM-10428",
    patientName: "Avery Nguyen",
    providerNpi: "1730187395",
    payerName: "Blue Cross Blue Shield",
    cptCode: "99213",
    billedAmount: 240,
    receivedAmount: 240,
    status: "paid",
    submissionDate: "2026-05-01T00:00:00Z",
  },
  {
    id: "CLM-10429",
    patientName: "Jordan Patel",
    providerNpi: "1528471902",
    payerName: "United Healthcare",
    cptCode: "99214",
    billedAmount: 410,
    receivedAmount: 0,
    status: "denied",
    submissionDate: "2026-05-02T00:00:00Z",
    denialReason: "CO-97: Service included in another service",
  },
  {
    id: "CLM-10430",
    patientName: "Riley Garcia",
    providerNpi: "1982036471",
    payerName: "Aetna",
    cptCode: "85025",
    billedAmount: 95,
    receivedAmount: 0,
    status: "submitted",
    submissionDate: "2026-05-03T00:00:00Z",
  },
];

const REMITS: Remittance[] = [
  {
    id: "REM-7782",
    claimId: "CLM-10428",
    payerName: "Blue Cross Blue Shield",
    paidAmount: 240,
    adjustmentAmount: 0,
    receivedDate: "2026-05-10T00:00:00Z",
    checkNumber: "EFT-100123",
    status: "reconciled",
  },
  {
    id: "REM-7783",
    claimId: "CLM-10429",
    payerName: "United Healthcare",
    paidAmount: 0,
    adjustmentAmount: 410,
    receivedDate: "2026-05-11T00:00:00Z",
    checkNumber: "EFT-100124",
    status: "posted",
    denialReason: "CO-97: Service included in another service",
  },
];

const ACTIVITY: Activity[] = [
  {
    id: "ACT-CLM-10428",
    kind: "claim_paid",
    message: "Paid CLM-10428 · Avery Nguyen",
    timestamp: "2026-05-10T00:00:00Z",
    npi: "1730187395",
    amount: 240,
  },
  {
    id: "ACT-CLM-10429",
    kind: "claim_denied",
    message: "Denied CLM-10429 · Jordan Patel",
    timestamp: "2026-05-11T00:00:00Z",
    npi: "1528471902",
    amount: 410,
  },
  {
    id: "ACT-CLM-10430",
    kind: "claim_submitted",
    message: "Submitted CLM-10430 · Riley Garcia",
    timestamp: "2026-05-12T00:00:00Z",
    npi: "1982036471",
    amount: 95,
  },
];

// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------

/** Minimal renderHook shim using createRoot — matches the pattern
 * in the sibling hook tests. */
function renderHook<TResult>(
  useHook: () => TResult
): { result: { current: TResult | undefined }; unmount: () => void } {
  const result: { current: TResult | undefined } = { current: undefined };
  const container = document.createElement("div");
  document.body.appendChild(container);
  // A QueryClient is only strictly needed when `api.isConfigured` is
  // true, but supplying one is harmless either way and future-proofs
  // the test if the mock flips.
  const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });

  function Probe() {
    result.current = useHook();
    return null;
  }

  const root: Root = createRoot(container);
  act(() => {
    root.render(
      React.createElement(
        QueryClientProvider,
        { client: qc },
        React.createElement(Probe)
      )
    );
  });
  return {
    result,
    unmount: () => {
      act(() => root.unmount());
      container.remove();
    },
  };
}

/** Yield to the timer queue so the 150ms debounce in `useSearch`
 * can flush. `act` wraps the wait so React state updates triggered
 * by the debounced setter are visible to subsequent assertions. */
async function flushDebounce(): Promise<void> {
  await act(async () => {
    await new Promise((r) => setTimeout(r, 200));
  });
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("useSearch", () => {
  beforeEach(() => {
    // Seed the in-memory zustand store with our fixtures so the
    // hook's `useSyncExternalStore` snapshot returns them.
    useAppStore.setState({
      claims: CLAIMS,
      remittances: REMITS,
      activity: ACTIVITY,
    });
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("returns empty results for an empty query", () => {
    const { result, unmount } = renderHook(() => useSearch());
    expect(result.current?.query).toBe("");
    expect(result.current?.results.total).toBe(0);
    expect(result.current?.results.claims).toEqual([]);
    expect(result.current?.results.remittances).toEqual([]);
    expect(result.current?.results.activity).toEqual([]);
    unmount();
  });

  it("debounces the query — results stay empty within the debounce window", async () => {
    const { result, unmount } = renderHook(() => useSearch());
    act(() => {
      result.current?.setQuery("Nguyen");
    });
    // Query is updated synchronously (so the input can echo it),
    // but results should not have run yet — debounce is 150ms.
    expect(result.current?.query).toBe("Nguyen");
    expect(result.current?.results.total).toBe(0);
    unmount();
  });

  it("matches claims by patient name", async () => {
    const { result, unmount } = renderHook(() => useSearch());
    act(() => {
      result.current?.setQuery("Nguyen");
    });
    await flushDebounce();
    expect(result.current?.results.claims.length).toBeGreaterThan(0);
    expect(result.current?.results.claims[0]?.id).toBe("CLM-10428");
    unmount();
  });

  it("matches claims by id fragment", async () => {
    const { result, unmount } = renderHook(() => useSearch());
    act(() => {
      result.current?.setQuery("CLM-10430");
    });
    await flushDebounce();
    expect(result.current?.results.claims[0]?.id).toBe("CLM-10430");
    unmount();
  });

  it("matches claims by payer name", async () => {
    const { result, unmount } = renderHook(() => useSearch());
    act(() => {
      result.current?.setQuery("Aetna");
    });
    await flushDebounce();
    expect(result.current?.results.claims.length).toBeGreaterThan(0);
    expect(result.current?.results.claims[0]?.id).toBe("CLM-10430");
    unmount();
  });

  it("matches remittances by check number", async () => {
    const { result, unmount } = renderHook(() => useSearch());
    act(() => {
      result.current?.setQuery("EFT-100124");
    });
    await flushDebounce();
    expect(result.current?.results.remittances.length).toBeGreaterThan(0);
    expect(result.current?.results.remittances[0]?.id).toBe("REM-7783");
    unmount();
  });

  it("matches activity by message text", async () => {
    const { result, unmount } = renderHook(() => useSearch());
    act(() => {
      result.current?.setQuery("Garcia");
    });
    await flushDebounce();
    expect(result.current?.results.activity.length).toBeGreaterThan(0);
    expect(result.current?.results.activity[0]?.id).toBe("ACT-CLM-10430");
    unmount();
  });

  it("ranks exact-id matches above substring matches", async () => {
    const { result, unmount } = renderHook(() => useSearch());
    act(() => {
      result.current?.setQuery("CLM-10428");
    });
    await flushDebounce();
    const flat = flattenResults(result.current!.results);
    // The exact-id claim should be the first result.
    expect(flat[0]?.id).toBe("CLM-10428");
    unmount();
  });

  it("caps each group at MAX_PER_GROUP (10)", async () => {
    // Build a store with many claims all matching the same query.
    const many: Claim[] = Array.from({ length: 25 }, (_, i) => ({
      ...CLAIMS[0]!,
      id: `CLM-${20000 + i}`,
      patientName: `Patient ${i}`,
    }));
    useAppStore.setState({ claims: many, remittances: [], activity: [] });

    const { result, unmount } = renderHook(() => useSearch());
    act(() => {
      result.current?.setQuery("Patient");
    });
    await flushDebounce();
    // 25 matching claims, but the cap should hold at 10.
    expect(result.current?.results.claims.length).toBe(10);
    expect(result.current?.results.total).toBe(10);
    unmount();
  });

  it("exposes the loading flag (false in fallback mode)", () => {
    const { result, unmount } = renderHook(() => useSearch());
    expect(result.current?.isLoading).toBe(false);
    unmount();
  });

  it("returns total === 0 when nothing matches", async () => {
    const { result, unmount } = renderHook(() => useSearch());
    act(() => {
      result.current?.setQuery("zzzzzz-no-such-string");
    });
    await flushDebounce();
    expect(result.current?.results.total).toBe(0);
    expect(result.current?.results.claims).toEqual([]);
    expect(result.current?.results.remittances).toEqual([]);
    expect(result.current?.results.activity).toEqual([]);
    unmount();
  });

  it("flattens results in display order: claims → remittances → activity", async () => {
    const { result, unmount } = renderHook(() => useSearch());
    act(() => {
      // A query that hits all three entity types.
      result.current?.setQuery("10428");
    });
    await flushDebounce();
    const flat = flattenResults(result.current!.results);
    expect(flat.length).toBeGreaterThan(0);
    // The first three should be claims (we have one CLM-10428 match);
    // remits with claimId=CLM-10428 come next; activity with that
    // claim id comes last.
    const kinds = flat.map((r) => r.kind);
    // Find the first index of each kind, in order:
    const firstClaim = kinds.indexOf("claim");
    const firstRemit = kinds.indexOf("remittance");
    const firstActivity = kinds.indexOf("activity");
    if (firstRemit !== -1) expect(firstRemit).toBeGreaterThan(firstClaim);
    if (firstActivity !== -1) expect(firstActivity).toBeGreaterThan(firstClaim);
    unmount();
  });
});

describe("destinationFor", () => {
  it("routes claim results to /claims?claim=<id>", () => {
    const url = destinationFor({
      kind: "claim",
      id: "CLM-10428",
      title: "",
      subtitle: "",
      badge: "paid",
      score: 1,
    });
    expect(url).toBe("/claims?claim=CLM-10428");
  });

  it("URL-encodes claim ids with special characters", () => {
    const url = destinationFor({
      kind: "claim",
      id: "CLM/A B",
      title: "",
      subtitle: "",
      badge: "paid",
      score: 1,
    });
    expect(url).toBe("/claims?claim=CLM%2FA%20B");
  });

  it("routes remittance results to /remittances", () => {
    const url = destinationFor({
      kind: "remittance",
      id: "REM-7782",
      title: "",
      subtitle: "",
      badge: "reconciled",
      score: 1,
    });
    expect(url).toBe("/remittances");
  });

  it("routes activity results to /activity?kind=<kind>", () => {
    const url = destinationFor({
      kind: "activity",
      id: "ACT-CLM-10428",
      title: "",
      subtitle: "",
      badge: "claim_paid",
      score: 1,
    });
    expect(url).toBe("/activity?kind=claim_paid");
  });
});

// `api` is mocked at the top of the file — silence the "imported but
// unused" lint with this re-export, which also documents that the
// mock is the load-bearing piece of the test setup.
void api;