// @vitest-environment happy-dom
// React Query's internal state updates need an `act`-aware environment
// or it logs noisy warnings. Mirror the useClaimDetail /
// useReconciliation / useTailStream test convention.
(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 { useClaimAcks } from "./useClaimAcks";
import { api } from "@/lib/api";

// Module-level mock: vitest hoists `vi.mock` above imports. We mock the
// single `api.listClaimAcks` method the hook calls.
vi.mock("@/lib/api", () => ({
  api: {
    listClaimAcks: vi.fn(),
  },
}));

/** Minimal `renderHook` shim — same pattern as `useClaimDetail.test.ts`. */
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);
  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();
    });
  }
}

/** Sample fixture matching the camelCase `ClaimAck` shape (the api
 * layer's `mapClaimAck` does the snake_case→camelCase conversion, so
 * the mock returns the post-conversion shape directly). */
const SAMPLE_ROWS = [
  {
    id: 1,
    claimId: "CLM-1",
    batchId: null,
    ackId: 99,
    ackKind: "999" as const,
    ak2Index: 0,
    setControlNumber: "991102989",
    setAcceptRejectCode: "A",
    linkedAt: "2026-07-02T12:00:00Z",
    linkedBy: "auto" as const,
    claimState: "received",
  },
  {
    id: 2,
    claimId: "CLM-1",
    batchId: null,
    ackId: 99,
    ackKind: "999" as const,
    ak2Index: 1,
    setControlNumber: "991102990",
    setAcceptRejectCode: "R",
    linkedAt: "2026-07-02T12:00:01Z",
    linkedBy: "auto" as const,
    claimState: "received",
  },
];

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

  it("test_returns_idle_state_and_does_not_fetch_when_claimId_is_null", () => {
    // Drawer closed → no network call, no loading, no error. The hook
    // must short-circuit BEFORE the queryFn is ever invoked so a
    // closed drawer is free.
    const { result, unmount } = renderHook(() => useClaimAcks(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.listClaimAcks).not.toHaveBeenCalled();

    unmount();
  });

  it("test_fetches_and_returns_claim_ack_rows_for_a_valid_claim_id", async () => {
    (api.listClaimAcks as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
      SAMPLE_ROWS,
    );

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

    await waitForState(
      () => result.current?.data !== null && result.current?.data !== undefined,
    );

    // The hook must have invoked the API exactly once with the claim id.
    expect(api.listClaimAcks).toHaveBeenCalledTimes(1);
    expect(api.listClaimAcks).toHaveBeenCalledWith("CLM-1");

    const data = result.current?.data;
    expect(data).not.toBeNull();
    expect(data).toHaveLength(2);
    // Both rows belong to the same claim (per-AK2 granularity for 999).
    expect(data?.[0]?.claimId).toBe("CLM-1");
    expect(data?.[0]?.ackKind).toBe("999");
    expect(data?.[0]?.setAcceptRejectCode).toBe("A");
    expect(data?.[1]?.setAcceptRejectCode).toBe("R");
    expect(data?.[1]?.ak2Index).toBe(1);

    // isLoading must flip false once the fetch resolves.
    expect(result.current?.isLoading).toBe(false);
    expect(result.current?.isError).toBe(false);

    unmount();
  });

  it("test_returns_empty_array_when_claim_has_no_acks", async () => {
    (api.listClaimAcks as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
      [],
    );

    const { result, unmount } = renderHook(() => useClaimAcks("CLM-empty"));

    await waitForState(
      () => result.current?.data !== null && result.current?.data !== undefined,
    );

    // Empty list must be surfaced as [] (NOT null), so the ClaimDrawer
    // panel can render its <EmptyState /> without a null check.
    expect(result.current?.data).toEqual([]);

    unmount();
  });
});