// @vitest-environment happy-dom
// Tell React this is an `act`-aware test environment so react-query's
// internal state updates flush through without noisy console warnings.
(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 { describe, expect, it, vi, beforeEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReconciliationPage } from "./Reconciliation";
import { api } from "@/lib/api";

// Module-level mock: vitest hoists `vi.mock` calls above imports. Only the
// methods this page touches need to be stubbed; ApiError is re-exported so
// the page can branch on .status in its catch handler.
vi.mock("@/lib/api", () => ({
  api: {
    listUnmatched: vi.fn(),
    matchRemit: vi.fn(),
    unmatchClaim: vi.fn(),
    getRemittance: vi.fn(),
  },
  ApiError: class ApiError extends Error {
    constructor(public status: number, message: string) {
      super(message);
    }
  },
}));

/**
 * Minimal `render` helper using react-dom/client + act(). Mirrors the
 * `renderHook` helper in `useReconciliation.test.ts` — see that file's
 * comment for the rationale (project doesn't ship a DOM test library
 * yet, and adding one just for these tests would inflate the dev-deps
 * tree). Returns the rendered container so tests can assert against
 * the live DOM via `container.textContent`.
 */
function renderIntoContainer(element: React.ReactElement): {
  container: HTMLDivElement;
  unmount: () => void;
} {
  const container = document.createElement("div");
  document.body.appendChild(container);
  const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });

  const root: Root = createRoot(container);
  act(() => {
    root.render(
      React.createElement(
        QueryClientProvider,
        { client: qc },
        element
      )
    );
  });

  return {
    container,
    unmount: () => {
      act(() => root.unmount());
      container.remove();
    },
  };
}

/**
 * Flush microtasks + react state updates until the predicate holds or we
 * time out. react-query's fetch resolves asynchronously, so we have to
 * await a tick before the rendered DOM reflects the mocked data.
 */
async function waitForText(
  text: string,
  timeoutMs = 2000
): Promise<void> {
  const start = Date.now();
  while (!document.body.textContent?.includes(text)) {
    if (Date.now() - start > timeoutMs) {
      throw new Error(`waitForText: "${text}" did not appear within ${timeoutMs}ms`);
    }
    await act(async () => {
      await Promise.resolve();
    });
  }
}

describe("ReconciliationPage", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // Default for the per-remit detail fetch — the drawer fetches
    // this whenever `?remit=` is in the URL. Return a never-resolving
    // promise so the drawer stays in the loading state; the smoke
    // test only asserts the drawer mounts.
    (
      api.getRemittance as unknown as ReturnType<typeof vi.fn>
    ).mockReturnValue(new Promise(() => {}));
  });

  it("SP21 Task 4.6: deep-link ?remit=ID opens the RemitDrawer on mount", async () => {
    // Non-empty unmatched payload so the page renders the two-column
    // matching surface (the "Pair them." branch). The pre-existing
    // empty-state branch has a flaky happy-dom/race that's unrelated
    // to Phase 4 — using non-empty data sidesteps that bug and still
    // proves the deep-link → drawer mount works. We pre-set
    // `window.location` (which `useRemitDrawerUrlState` reads on
    // mount) so `?remit=REM-7` resolves to a truthy `remitId`.
    (
      api.listUnmatched as unknown as ReturnType<typeof vi.fn>
    ).mockResolvedValue({
      claims: [
        {
          id: "CLM-1",
          patientName: "Patient A",
          billedAmount: 100,
          providerNpi: "1234567890",
          serviceDate: "2026-06-01",
          payerId: "P1",
          state: "submitted",
        },
      ],
      remittances: [
        {
          id: "REM-7",
          payerClaimControlNumber: "PCN-A",
          status: "received",
          paidAmount: 100,
          adjustmentAmount: 0,
          receivedDate: "2026-06-01",
          isReversal: false,
          totalCharge: 100,
          serviceDate: "2026-06-01",
          batchId: "b1",
        },
      ],
    });
    (window as unknown as { happyDOM: { setURL: (u: string) => void } })
      .happyDOM.setURL("http://localhost/reconciliation?remit=REM-7");

    const { unmount } = renderIntoContainer(
      React.createElement(ReconciliationPage),
    );

    // Wait for the loaded two-column view (the "Pair them." headline
    // is the clearest signal that listUnmatched has resolved and the
    // page is past the loading + empty branches), then assert the
    // drawer is mounted.
    await waitForText("Pair them.");
    expect(
      document.body.querySelector('[data-testid="remit-drawer"]'),
    ).not.toBeNull();

    unmount();

    // Reset URL so the next test sees a clean /reconciliation URL.
    (window as unknown as { happyDOM: { setURL: (u: string) => void } })
      .happyDOM.setURL("http://localhost/reconciliation");
  });

  it("renders both columns with unmatched rows when api returns data", async () => {
    (
      api.listUnmatched as unknown as ReturnType<typeof vi.fn>
    ).mockResolvedValue({
      claims: [
        {
          id: "CLM-1",
          patientName: "Patient A",
          billedAmount: 100,
          providerNpi: "1234567890",
          serviceDate: "2026-06-01",
          payerId: "P1",
          state: "submitted",
        },
      ],
      remittances: [
        {
          id: "CLP-1:b1",
          payerClaimControlNumber: "PCN-A",
          status: "received",
          paidAmount: 100,
          adjustmentAmount: 0,
          receivedDate: "2026-06-01",
          isReversal: false,
          totalCharge: 100,
          serviceDate: "2026-06-01",
          batchId: "b1",
        },
      ],
    });

    const { unmount } = renderIntoContainer(
      React.createElement(ReconciliationPage)
    );
    await waitForText("CLM-1");

    // Both column headings + both rows should be present.
    expect(document.body.textContent).toContain("CLM-1");
    expect(document.body.textContent).toContain("PCN-A");
    expect(document.body.textContent).toContain("Unmatched claims");
    expect(document.body.textContent).toContain("Unmatched remits");
    unmount();
  });

  it("renders the empty state when nothing is unmatched", async () => {
    (
      api.listUnmatched as unknown as ReturnType<typeof vi.fn>
    ).mockResolvedValue({ claims: [], remittances: [] });

    const { unmount } = renderIntoContainer(
      React.createElement(ReconciliationPage)
    );
    await waitForText("nothing pending");

    expect(document.body.textContent).toContain("Reconciliation · nothing pending");
    expect(document.body.textContent).toContain("Every claim and remittance is paired.");
    // No two-column selection chrome should render.
    expect(document.body.textContent).not.toContain("Unmatched claims (");
    unmount();
  });
});
