// @vitest-environment happy-dom
// RemitDrawer wires `useRemitDetail` (TanStack Query) + a window-level
// keyboard listener. Both paths need an act-aware environment or React
// logs noisy warnings. Mirror the convention from ClaimDrawer.test.ts.
(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, afterEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RemitDrawer } from "./RemitDrawer";
import { api, ApiError } from "@/lib/api";
import type { RemitDetail } from "@/hooks/useRemitDetail";

// Module-level mock — vitest hoists `vi.mock` above imports. We mock both
// `api.getRemittance` (the only method the hook calls) AND `ApiError`
// (the class used in the hook's retry predicate + here for instanceof
// checks in the orchestrator's error-kind branch). Same pattern as
// useRemitDetail.test.ts.
vi.mock("@/lib/api", () => {
  class ApiError extends Error {
    constructor(public status: number, message: string) {
      super(message);
    }
  }
  return {
    api: {
      getRemittance: vi.fn(),
    },
    ApiError,
  };
});

/**
 * Minimal valid RemitDetail fixture — every required key present so
 * the component typechecks. Mirrors the SAMPLE_DETAIL in
 * useRemitDetail.test.ts.
 */
const SAMPLE_DETAIL: RemitDetail = {
  id: "REM-1",
  claimId: "CLM-1",
  payerClaimControlNumber: "PCN-1",
  payerName: "Medicaid",
  paidAmount: 100.0,
  adjustmentAmount: 20.0,
  receivedDate: "2026-01-15T00:00:00Z",
  checkNumber: "EFT-12345",
  status: "received",
  paymentMethod: "EFT",
  paymentDate: "2026-01-14",
  adjustments: [
    {
      group: "CO",
      reason: "45",
      label: "Charge exceeds fee schedule",
      amount: 50.0,
      quantity: null,
    },
    {
      group: "PR",
      reason: "1",
      label: "Deductible",
      amount: 10.0,
      quantity: 1,
    },
  ],
};

type RenderArgs = {
  remitId: string | null;
  remits?: { id: string }[];
  onClose?: () => void;
  onNavigate?: (id: string) => void;
  onToggleHelp?: () => void;
};

/**
 * Test harness for `RemitDrawer`. Renders into a real DOM container
 * wrapped in `QueryClientProvider` (the hook uses TanStack Query) and
 * exposes a `fireKey` helper that dispatches keydown events on `window`
 * — `useDrawerKeyboard` listens there, not on a specific element.
 *
 * `detail` controls how the mocked `api.getRemittance` resolves:
 *   - RemitDetail → resolves with that object
 *   - Error       → rejects with that error (use a real `ApiError` for
 *                   404 or a plain `Error` for network failure)
 *   - null        → never resolves (for the loading state)
 */
function renderDrawer(
  props: RenderArgs,
  detail: RemitDetail | Error | null = SAMPLE_DETAIL
): {
  container: HTMLDivElement;
  unmount: () => void;
  fireKey: (key: string) => void;
} {
  const onClose = props.onClose ?? vi.fn<() => void>();
  const onNavigate = props.onNavigate ?? vi.fn<(id: string) => void>();
  const onToggleHelp = props.onToggleHelp ?? vi.fn<() => void>();
  const remits = props.remits ?? [{ id: "REM-1" }];

  const mock = api.getRemittance as unknown as ReturnType<typeof vi.fn>;
  if (detail === null) {
    mock.mockReturnValue(new Promise(() => {}));
  } else if (detail instanceof Error) {
    mock.mockRejectedValue(detail);
  } else {
    mock.mockResolvedValue(detail);
  }

  const container = document.createElement("div");
  document.body.appendChild(container);
  const qc = new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
        // Default retryDelay is exponential backoff (1s + 2s + 4s);
        // we override to fire retries immediately so the network-failure
        // test doesn't have to wait 7s for the retry loop to time out.
        retryDelay: 0,
      },
    },
  });
  const root: Root = createRoot(container);
  act(() => {
    root.render(
      React.createElement(
        QueryClientProvider,
        { client: qc },
        React.createElement(RemitDrawer, {
          remitId: props.remitId,
          remits,
          onClose,
          onNavigate,
          onToggleHelp,
        })
      )
    );
  });

  return {
    container,
    unmount: () => {
      act(() => root.unmount());
      container.remove();
    },
    fireKey: (key: string) => {
      act(() => {
        window.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true }));
      });
    },
  };
}

/**
 * Drive React Query's internal micro-task queue until it stops
 * transitioning (or we time out). Same shape as the ClaimDrawer
 * `settle` helper.
 */
async function settle(
  predicate: (body: HTMLElement) => boolean,
  timeoutMs = 2000
): Promise<HTMLElement> {
  const body = document.body;
  const start = Date.now();
  while (!predicate(body)) {
    if (Date.now() - start > timeoutMs) {
      throw new Error(
        `settle: predicate did not hold within ${timeoutMs}ms (body=${body.innerHTML.slice(0, 200)})`
      );
    }
    await act(async () => {
      await new Promise((r) => setTimeout(r, 0));
    });
  }
  return body;
}

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

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

  it("renders_nothing_when_remitId_is_null", () => {
    const { container, unmount } = renderDrawer({ remitId: null });

    // The component must render no DOM at all when closed.
    expect(container.children.length).toBe(0);
    // And must NOT hit the network — the hook short-circuits when
    // remitId is null (per useRemitDetail contract).
    expect(api.getRemittance).not.toHaveBeenCalled();

    unmount();
  });

  it("renders_skeleton_while_loading", async () => {
    // detail = null → never-resolving promise → permanent loading.
    const { unmount } = renderDrawer({ remitId: "REM-1" }, null);

    const body = await settle((b) =>
      b.querySelector('[data-testid="remit-drawer-skeleton"]') !== null
    );
    expect(body.querySelector('[data-testid="remit-drawer-skeleton"]'))
      .not.toBeNull();

    unmount();
  });

  it("renders_error_state_on_404", async () => {
    const { unmount } = renderDrawer(
      { remitId: "ghost" },
      new ApiError(404, "Remittance ghost not found")
    );

    const body = await settle((b) =>
      b.querySelector('[data-testid="remit-drawer-error-not_found"]') !== null
    );

    const errorEl = body.querySelector(
      '[data-testid="remit-drawer-error-not_found"]'
    );
    expect(errorEl).not.toBeNull();

    // Close button is wired up.
    const closeBtn = body.querySelector(
      '[data-testid="error-close"]'
    ) as HTMLButtonElement | null;
    expect(closeBtn).not.toBeNull();

    unmount();
  });

  it("renders_error_state_on_network_failure", async () => {
    const { unmount } = renderDrawer(
      { remitId: "REM-1" },
      new Error("network down")
    );

    // The hook retries non-404 errors up to 3 times; the error UI
    // only appears after the final retry gives up. The polling
    // helper waits out the retries (retryDelay: 0 means back-to-back).
    const body = await settle((b) =>
      b.querySelector('[data-testid="remit-drawer-error-network"]') !== null
    );

    const errorEl = body.querySelector(
      '[data-testid="remit-drawer-error-network"]'
    );
    expect(errorEl).not.toBeNull();

    // Retry button is shown for network failures.
    const retryBtn = body.querySelector(
      '[data-testid="error-retry"]'
    ) as HTMLButtonElement | null;
    expect(retryBtn).not.toBeNull();

    unmount();
  });

  it("renders_all_sections_on_success", async () => {
    const { unmount } = renderDrawer({ remitId: "REM-1" });

    // Wait for the header to render (it's the first section in the
    // success path). All other sections will be present by the time
    // the header shows up because they render in the same commit.
    const body = await settle((b) =>
      b.querySelector('[data-testid="remit-drawer-header"]') !== null
    );

    // Every section that renders unconditionally when data is present.
    expect(body.querySelector('[data-testid="remit-drawer-header"]'))
      .not.toBeNull();
    expect(body.querySelector('[data-testid="financial-summary"]'))
      .not.toBeNull();
    expect(body.querySelector('[data-testid="parties-grid"]'))
      .not.toBeNull();
    expect(body.querySelector('[data-testid="claim-payments-table"]'))
      .not.toBeNull();
    // The fixture has 2 CAS rows; the panel must be visible.
    expect(body.querySelector('[data-testid="cas-adjustments-panel"]'))
      .not.toBeNull();
    expect(body.querySelector('[data-testid="cas-adjustments-list"]'))
      .not.toBeNull();

    unmount();
  });

  it("omits_cas_panel_when_adjustments_is_empty", async () => {
    const { unmount } = renderDrawer(
      { remitId: "REM-1" },
      { ...SAMPLE_DETAIL, adjustments: [] }
    );

    const body = await settle((b) =>
      b.querySelector('[data-testid="remit-drawer-header"]') !== null
    );

    expect(body.querySelector('[data-testid="cas-adjustments-panel"]'))
      .toBeNull();
    // Header / financial / parties / claim-payments still render.
    expect(body.querySelector('[data-testid="financial-summary"]'))
      .not.toBeNull();
    expect(body.querySelector('[data-testid="claim-payments-table"]'))
      .not.toBeNull();

    unmount();
  });

  it("j_key_calls_onNavigate_with_next_remit_id", () => {
    const onNavigate = vi.fn<(id: string) => void>();
    const { fireKey, unmount } = renderDrawer({
      remitId: "B",
      remits: [{ id: "A" }, { id: "B" }, { id: "C" }],
      onNavigate,
    });

    fireKey("j");
    expect(onNavigate).toHaveBeenCalledTimes(1);
    expect(onNavigate).toHaveBeenCalledWith("C");

    unmount();
  });

  it("k_key_calls_onNavigate_with_previous_remit_id", () => {
    const onNavigate = vi.fn<(id: string) => void>();
    const { fireKey, unmount } = renderDrawer({
      remitId: "B",
      remits: [{ id: "A" }, { id: "B" }, { id: "C" }],
      onNavigate,
    });

    fireKey("k");
    expect(onNavigate).toHaveBeenCalledTimes(1);
    expect(onNavigate).toHaveBeenCalledWith("A");

    unmount();
  });

  it("j_wraps_around_at_end", () => {
    const onNavigate = vi.fn<(id: string) => void>();
    const { fireKey, unmount } = renderDrawer({
      remitId: "C",
      remits: [{ id: "A" }, { id: "B" }, { id: "C" }],
      onNavigate,
    });

    fireKey("j");
    expect(onNavigate).toHaveBeenCalledTimes(1);
    expect(onNavigate).toHaveBeenCalledWith("A");

    unmount();
  });

  it("k_wraps_around_at_start", () => {
    const onNavigate = vi.fn<(id: string) => void>();
    const { fireKey, unmount } = renderDrawer({
      remitId: "A",
      remits: [{ id: "A" }, { id: "B" }, { id: "C" }],
      onNavigate,
    });

    fireKey("k");
    expect(onNavigate).toHaveBeenCalledTimes(1);
    expect(onNavigate).toHaveBeenCalledWith("C");

    unmount();
  });

  it("escape_key_calls_onClose", () => {
    const onClose = vi.fn<() => void>();
    const { fireKey, unmount } = renderDrawer({
      remitId: "REM-1",
      onClose,
    });

    fireKey("Escape");
    expect(onClose).toHaveBeenCalledTimes(1);

    unmount();
  });

  it("question_mark_calls_onToggleHelp", () => {
    const onToggleHelp = vi.fn<() => void>();
    const { fireKey, unmount } = renderDrawer({
      remitId: "REM-1",
      onToggleHelp,
    });

    fireKey("?");
    expect(onToggleHelp).toHaveBeenCalledTimes(1);

    unmount();
  });

  it("keyboard_disabled_when_remitId_is_null", () => {
    const onNavigate = vi.fn<(id: string) => void>();
    const { fireKey, unmount } = renderDrawer({
      remitId: null,
      remits: [{ id: "A" }, { id: "B" }, { id: "C" }],
      onNavigate,
    });

    fireKey("j");
    fireKey("k");
    expect(onNavigate).not.toHaveBeenCalled();

    unmount();
  });

  it("current_remit_not_in_remits_list_does_nothing", () => {
    const onNavigate = vi.fn<(id: string) => void>();
    const { fireKey, unmount } = renderDrawer({
      remitId: "GHOST",
      remits: [{ id: "A" }, { id: "B" }, { id: "C" }],
      onNavigate,
    });

    fireKey("j");
    fireKey("k");
    expect(onNavigate).not.toHaveBeenCalled();

    unmount();
  });
});
