// @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 { useReconciliation } from "./useReconciliation";
import { api } from "@/lib/api";

// Module-level mock: vitest hoists `vi.mock` calls above imports.
vi.mock("@/lib/api", () => ({
  api: {
    listUnmatched: vi.fn(),
    matchRemit: vi.fn(),
    unmatchClaim: vi.fn(),
  },
}));

/**
 * Minimal renderHook equivalent using react-dom/client + react-dom/test-utils.
 * Lets the test assert on the hook's latest return value after one or more
 * `act()`-wrapped state updates. We use this instead of
 * `@testing-library/react` because the project doesn't ship a DOM test
 * library yet — adding one just for this hook would inflate the dev-deps
 * tree for a single call site.
 */
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();
    },
  };
}

/** Flush microtasks + react state updates until the predicate holds 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();
    });
  }
}

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

  it("returns the unmatched query + match + unmatch mutations", async () => {
    (
      api.listUnmatched as unknown as ReturnType<typeof vi.fn>
    ).mockResolvedValue({ claims: [], remittances: [] });

    const { result, unmount } = renderHook(() => useReconciliation());

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

    expect(result.current?.unmatched.data).toEqual({ claims: [], remittances: [] });
    expect(typeof result.current?.match.mutate).toBe("function");
    expect(typeof result.current?.match.mutateAsync).toBe("function");
    expect(typeof result.current?.unmatch.mutate).toBe("function");
    expect(typeof result.current?.unmatch.mutateAsync).toBe("function");
    unmount();
  });

  it("match.mutateAsync calls api.matchRemit with the claim + remit ids", async () => {
    (
      api.listUnmatched as unknown as ReturnType<typeof vi.fn>
    ).mockResolvedValue({ claims: [], remittances: [] });
    (
      api.matchRemit as unknown as ReturnType<typeof vi.fn>
    ).mockResolvedValue({
      claim: { id: "c1" },
      match: { id: 1, strategy: "manual" },
    });

    const { result, unmount } = renderHook(() => useReconciliation());
    await waitForState(() => result.current?.unmatched.isSuccess === true);

    let mutateResult: unknown;
    await act(async () => {
      mutateResult = await result.current!.match.mutateAsync({
        claimId: "c1",
        remitId: "r1",
      });
    });

    expect(api.matchRemit).toHaveBeenCalledWith("c1", "r1");
    expect(mutateResult).toEqual({
      claim: { id: "c1" },
      match: { id: 1, strategy: "manual" },
    });
    await waitForState(() => result.current?.match.isSuccess === true);
    unmount();
  });

  it("unmatch.mutateAsync calls api.unmatchClaim with the claim id", async () => {
    (
      api.listUnmatched as unknown as ReturnType<typeof vi.fn>
    ).mockResolvedValue({ claims: [], remittances: [] });
    (
      api.unmatchClaim as unknown as ReturnType<typeof vi.fn>
    ).mockResolvedValue({
      claim: { id: "c1" },
    });

    const { result, unmount } = renderHook(() => useReconciliation());
    await waitForState(() => result.current?.unmatched.isSuccess === true);

    await act(async () => {
      await result.current!.unmatch.mutateAsync({ claimId: "c1" });
    });

    expect(api.unmatchClaim).toHaveBeenCalledWith("c1");
    await waitForState(() => result.current?.unmatch.isSuccess === true);
    unmount();
  });
});
