// @vitest-environment happy-dom
// React Query's internal state updates need an `act`-aware environment or
// it logs noisy warnings. The reconciliation test sets this up the same
// way; mirror it here for consistency.
(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 { useClaimDetail } from "./useClaimDetail";
import { api, ApiError } from "@/lib/api";

// Module-level mock: vitest hoists `vi.mock` above imports. We mock both
// `api.getClaimDetail` (the only method the hook calls) AND `ApiError`
// (the class used in the hook's retry predicate) so that `instanceof
// ApiError` in the hook and `new ApiError(404, ...)` in the test resolve
// to the same class instance at runtime.
vi.mock("@/lib/api", () => {
  class ApiError extends Error {
    constructor(public status: number, message: string) {
      super(message);
    }
  }
  return {
    api: {
      getClaimDetail: vi.fn(),
    },
    ApiError,
  };
});

/**
 * Minimal `renderHook` shim — same pattern as `useReconciliation.test.ts`.
 * The project doesn't ship `@testing-library/react`, so we wire a Probe
 * component into a real `createRoot` and read its return value through
 * a shared `result` object. `act` flushes React's state updates between
 * micro-tasks.
 */
function renderHook<TResult>(
  useCallback: () => TResult
): {
  result: { current: TResult | undefined };
  unmount: () => void;
} {
  const result: { current: TResult | undefined } = { current: undefined };
  const container = document.createElement("div");
  document.body.appendChild(container);
  // `retry: false` on the default options is just a safety net; the hook
  // installs its own retry predicate that short-circuits on 404.
  const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });

  function Probe() {
    result.current = useCallback();
    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();
    },
  };
}

/** Poll the result until `predicate` is true or we time out. */
async function waitForState(
  predicate: () => boolean,
  timeoutMs = 1000
): Promise<void> {
  const start = Date.now();
  while (!predicate()) {
    if (Date.now() - start > timeoutMs) {
      throw new Error(`waitForState: predicate did not hold within ${timeoutMs}ms`);
    }
    await act(async () => {
      await Promise.resolve();
    });
  }
}

// Minimal valid ClaimDetail — every required key present, with the
// typical `matchedRemittance: null` (unpaired claim, the dominant
// happy-path render). Same shape the api.test.ts getClaimDetail test uses.
const SAMPLE_DETAIL = {
  id: "CLM-1",
  batchId: "B-1",
  state: "submitted",
  stateLabel: "Submitted",
  billedAmount: 123.45,
  patientName: "Jane Doe",
  providerNpi: "1234567890",
  providerName: "Acme Clinic",
  payerName: "Medicaid",
  payerId: "MEDCO",
  submissionDate: "2026-01-15T00:00:00Z",
  serviceDateFrom: "2026-01-10",
  serviceDateTo: "2026-01-10",
  parsedAt: "2026-01-15T00:00:01Z",
  diagnoses: [{ code: "E11.9", qualifier: "ABK" }],
  serviceLines: [
    {
      lineNumber: 1,
      procedureQualifier: "HC",
      procedureCode: "99213",
      modifiers: [],
      charge: 100.0,
      units: 1,
      unitType: "UN",
      serviceDate: "2026-01-10",
    },
  ],
  parties: {
    billingProvider: {
      name: "Acme Clinic",
      npi: "1234567890",
      taxId: "12-3456789",
      address: {
        line1: "123 Main St",
        line2: null,
        city: "Denver",
        state: "CO",
        zip: "80202",
      },
    },
    subscriber: {
      firstName: "Jane",
      lastName: "Doe",
      memberId: "M-1",
      dob: null,
      gender: "F",
    },
    payer: { name: "Medicaid", id: "MEDCO" },
  },
  validation: { passed: true, errors: [], warnings: [] },
  rawSegments: [["ISA*00*"]],
  matchedRemittance: null,
  stateHistory: [
    {
      kind: "claim_submitted",
      ts: "2026-01-15T00:00:00Z",
      batchId: "B-1",
      remittanceId: null,
    },
  ],
} as const;

describe("useClaimDetail", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("returns the empty drawer state and does not fetch when id is null", () => {
    // The drawer is closed → no network call, no loading, no error.
    // The hook must short-circuit BEFORE the queryFn is ever invoked.
    const { result, unmount } = renderHook(() => useClaimDetail(null));

    expect(result.current).toBeDefined();
    expect(result.current?.data).toBeNull();
    expect(result.current?.isLoading).toBe(false);
    expect(result.current?.isError).toBe(false);
    expect(result.current?.error).toBeNull();
    expect(typeof result.current?.refetch).toBe("function");
    expect(api.getClaimDetail).not.toHaveBeenCalled();

    unmount();
  });

  it("fetches the claim and returns the parsed body for a valid id", async () => {
    (api.getClaimDetail as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
      SAMPLE_DETAIL
    );

    const { result, unmount } = renderHook(() => useClaimDetail("CLM-1"));

    // Wait until the query resolves — `data` transitions from null to the
    // sample detail. We also assert `isLoading: false` once that happens.
    await waitForState(
      () =>
        result.current?.data !== null &&
        result.current?.data !== undefined &&
        result.current?.isLoading === false
    );

    expect(result.current?.data).toEqual(SAMPLE_DETAIL);
    expect(result.current?.isError).toBe(false);
    expect(result.current?.error).toBeNull();
    expect(api.getClaimDetail).toHaveBeenCalledTimes(1);
    expect(api.getClaimDetail).toHaveBeenCalledWith("CLM-1");

    unmount();
  });

  it("surfaces a 404 ApiError as isError=true with the original error object", async () => {
    // The hook's retry predicate must give up immediately on 404 — the
    // drawer needs to render the "claim doesn't exist" state rather than
    // masking it with three back-to-back retries.
    (api.getClaimDetail as unknown as ReturnType<typeof vi.fn>).mockRejectedValue(
      new ApiError(404, "Claim ghost not found")
    );

    const { result, unmount } = renderHook(() => useClaimDetail("ghost"));

    await waitForState(() => result.current?.isError === true);

    expect(result.current?.data).toBeNull();
    expect(result.current?.isLoading).toBe(false);
    expect(result.current?.isError).toBe(true);
    expect(result.current?.error).toBeInstanceOf(ApiError);
    expect((result.current?.error as ApiError).status).toBe(404);
    // 404s must not retry — exactly one fetch attempt.
    expect(api.getClaimDetail).toHaveBeenCalledTimes(1);

    unmount();
  });
});
