// @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 { Remittances } from "./Remittances";
import { api } from "@/lib/api";
import { useTailStore } from "@/store/tail-store";

// Module-level mock: vitest hoists `vi.mock` calls above imports. We only
// stub the method this page touches via the `useRemittances` hook.
// `isConfigured: true` is critical — without it, `useRemittances` falls
// back to the empty zustand store instead of calling our mocked API.
vi.mock("@/lib/api", () => ({
  api: {
    isConfigured: true,
    listRemittances: vi.fn(),
    getRemittance: vi.fn(),
  },
  ApiError: class ApiError extends Error {
    constructor(public status: number, message: string) {
      super(message);
    }
  },
}));

// Mock the live-tail hook so the page renders the pill in the settled
// `live` state without driving the real streamTail parser. The hook's
// own test file (`useTailStream.test.ts`) covers the lifecycle in depth.
vi.mock("@/hooks/useTailStream", () => ({
  useTailStream: vi.fn(() => ({
    status: "live",
    lastEventAt: null,
    error: null,
    forceReconnect: vi.fn(),
  })),
}));

/**
 * Minimal `render` helper using react-dom/client + act(). Mirrors the
 * `renderIntoContainer` helper in `Reconciliation.test.tsx` — 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 `document.body.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();
    });
  }
}

/**
 * Poll a predicate until it holds or we time out. react-query's fetch
 * resolves asynchronously, and Radix portals content into
 * `document.body`, so the DOM doesn't reflect new state synchronously
 * after a keypress — we have to await a tick before asserting.
 */
async function settle(
  predicate: () => boolean,
  timeoutMs = 2000
): Promise<void> {
  const start = Date.now();
  while (!predicate()) {
    if (Date.now() - start > timeoutMs) {
      throw new Error("settle: predicate did not hold within timeout");
    }
    await act(async () => {
      await new Promise((r) => setTimeout(r, 0));
    });
  }
}

/**
 * Dispatch a synthetic keydown on `window`. Same pattern used in
 * `Claims.test.tsx` and `useDrawerKeyboard.test.ts`.
 */
function pressKey(key: string): void {
  act(() => {
    window.dispatchEvent(
      new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true })
    );
  });
}

/** The data row at `idx` (per `data-row-index={idx}`). */
function rowAt(idx: number): HTMLTableRowElement | null {
  return document.querySelector(
    `tbody tr[data-row-index="${idx}"]`
  ) as HTMLTableRowElement | null;
}

/**
 * Point happy-dom's URL at a known value. happy-dom v20 doesn't expose a
 * writable `window.location.search`, but `window.happyDOM.setURL` updates
 * the URL the window reports without triggering a navigation — exactly
 * what we want for mounting the page at `/remittances?remit=PCN-1` etc.
 * Same helper used by Claims.test.tsx and useRemitDrawerUrlState.test.ts.
 */
function setLocation(url: string): void {
  (window as unknown as { happyDOM: { setURL: (u: string) => void } }).happyDOM.setURL(url);
}

/** True iff exactly one row carries `data-state="selected"`. */
function hasExactlyOneSelectedRow(): boolean {
  const selected = document.querySelectorAll(
    'tbody tr[data-state="selected"]'
  );
  return selected.length === 1;
}

/** Fixture used by every test below so the page has rows to navigate. */
const SAMPLE_REMITS = [
  {
    id: "PCN-1",
    claimId: "CLM-1",
    payerName: "Colorado Medicaid",
    paidAmount: 100,
    adjustmentAmount: 60,
    receivedDate: "2026-06-19T12:00:00Z",
    checkNumber: "",
    status: "received",
    adjustments: [
      {
        group: "CO",
        reason: "45",
        label: "Charge exceeds fee schedule/maximum allowable",
        amount: 50,
        quantity: null,
      },
      {
        group: "PR",
        reason: "1",
        label: "Deductible amount",
        amount: 10,
        quantity: 1,
      },
    ],
  },
  {
    id: "PCN-2",
    claimId: "CLM-2",
    payerName: "Aetna",
    paidAmount: 200,
    adjustmentAmount: 0,
    receivedDate: "2026-06-18T12:00:00Z",
    checkNumber: "",
    status: "received",
    adjustments: [],
  },
  {
    id: "PCN-3",
    claimId: "CLM-3",
    payerName: "United",
    paidAmount: 300,
    adjustmentAmount: 0,
    receivedDate: "2026-06-17T12:00:00Z",
    checkNumber: "",
    status: "received",
    adjustments: [],
  },
] as const;

describe("Remittances", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // Reset URL to the bare remittances page between tests so a
    // `?remit=` leaked from a prior test (via pushState) doesn't
    // bleed into the next. happy-dom's URL survives across tests in
    // the same file unless explicitly reset.
    setLocation("http://localhost/remittances");
    // Singleton tail-store: clear the remittances slice between tests
    // so a tail-arrival case (if added later) doesn't see rows from a
    // previous test.
    useTailStore.getState().reset("remittances");
    (
      api.listRemittances as unknown as ReturnType<typeof vi.fn>
    ).mockResolvedValue({
      items: SAMPLE_REMITS,
      total: SAMPLE_REMITS.length,
      returned: SAMPLE_REMITS.length,
      has_more: false,
    });
    // 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
    // tests only assert the drawer mounts, not the loaded data.
    (
      api.getRemittance as unknown as ReturnType<typeof vi.fn>
    ).mockReturnValue(new Promise(() => {}));
  });

  it("clicking a row opens the RemitDrawer (no more inline expand)", async () => {
    // SP21 Phase 4 Task 4.3: the inline CAS expansion is gone — the
    // whole row now drills into the RemitDrawer via `?remit=ID`. The
    // CAS panel is now inside the drawer, not in a second <tr>.
    const { unmount } = renderIntoContainer(React.createElement(Remittances));
    await waitForText("PCN-1");

    // No drawer in the DOM yet — the URL has no `?remit=`.
    expect(document.body.querySelector('[data-testid="remit-drawer"]')).toBeNull();

    // Click the row containing PCN-1.
    const cell = Array.from(document.querySelectorAll("td")).find(
      (td) => td.textContent === "PCN-1"
    );
    const row = cell?.closest("tr");
    expect(row).toBeTruthy();
    await act(async () => {
      (row as HTMLTableRowElement).click();
    });

    // The drawer must mount into document.body via Radix's portal.
    await settle(
      () => document.body.querySelector('[data-testid="remit-drawer"]') !== null
    );
    expect(
      document.body.querySelector('[data-testid="remit-drawer"]')
    ).not.toBeNull();

    // URL must reflect the open remit.
    expect(window.location.search).toContain("remit=PCN-1");

    unmount();
  });

  it("deep-link ?remit=ID opens the drawer on mount", async () => {
    // Pre-set the URL so the hook reads PCN-1 off `window.location.search`
    // during its `useState` initializer — no click needed.
    setLocation("http://localhost/remittances?remit=PCN-1");

    const { unmount } = renderIntoContainer(React.createElement(Remittances));
    await settle(
      () => document.body.querySelector('[data-testid="remit-drawer"]') !== null
    );

    // Drawer should appear immediately, without user interaction.
    expect(
      document.body.querySelector('[data-testid="remit-drawer"]')
    ).not.toBeNull();

    unmount();
  });

  it("test_initial_render_shows_no_selection_highlight", async () => {
    const { unmount } = renderIntoContainer(React.createElement(Remittances));
    await waitForText("PCN-3");

    // The spec requires the initial render shows no selection
    // highlight — none of the three data rows should be marked
    // selected.
    expect(
      document.querySelectorAll('tbody tr[data-state="selected"]').length
    ).toBe(0);

    unmount();
  });

  it("test_pressing_j_increments_selection_with_wrap", async () => {
    const { unmount } = renderIntoContainer(React.createElement(Remittances));
    await waitForText("PCN-3");

    // From null, j lands on row 0.
    pressKey("j");
    await settle(() => rowAt(0)?.getAttribute("data-state") === "selected");
    expect(rowAt(0)?.getAttribute("data-state")).toBe("selected");

    // j again → row 1, then row 2.
    pressKey("j");
    await settle(() => rowAt(1)?.getAttribute("data-state") === "selected");
    expect(rowAt(1)?.getAttribute("data-state")).toBe("selected");

    pressKey("j");
    await settle(() => rowAt(2)?.getAttribute("data-state") === "selected");
    expect(rowAt(2)?.getAttribute("data-state")).toBe("selected");

    // j from the last row wraps to row 0 (wrap policy documented in
    // Remittances.tsx — matches Claims' drawer behavior).
    pressKey("j");
    await settle(() => rowAt(0)?.getAttribute("data-state") === "selected");
    expect(rowAt(0)?.getAttribute("data-state")).toBe("selected");
    expect(hasExactlyOneSelectedRow()).toBe(true);

    unmount();
  });

  it("test_pressing_k_decrements_selection_with_wrap", async () => {
    const { unmount } = renderIntoContainer(React.createElement(Remittances));
    await waitForText("PCN-3");

    // From null, k jumps to the last row (one-keystroke "bottom of
    // list" gesture).
    pressKey("k");
    await settle(() => rowAt(2)?.getAttribute("data-state") === "selected");
    expect(rowAt(2)?.getAttribute("data-state")).toBe("selected");

    // k → row 1, then row 0.
    pressKey("k");
    await settle(() => rowAt(1)?.getAttribute("data-state") === "selected");
    expect(rowAt(1)?.getAttribute("data-state")).toBe("selected");

    pressKey("k");
    await settle(() => rowAt(0)?.getAttribute("data-state") === "selected");
    expect(rowAt(0)?.getAttribute("data-state")).toBe("selected");

    // k from row 0 wraps to row 2 (last).
    pressKey("k");
    await settle(() => rowAt(2)?.getAttribute("data-state") === "selected");
    expect(rowAt(2)?.getAttribute("data-state")).toBe("selected");

    unmount();
  });

  it("test_j_does_not_trigger_expand_collapse", async () => {
    // j/k selection is a focus indicator, NOT a substitute for the
    // click-to-expand gesture. Pressing j through a row that has
    // adjustments must NOT open the detail row.
    const { unmount } = renderIntoContainer(React.createElement(Remittances));
    await waitForText("PCN-3");

    // Row 0 has adjustments — moving selection onto it should leave
    // the row collapsed (the user can still click to expand).
    pressKey("j");
    await settle(() => rowAt(0)?.getAttribute("data-state") === "selected");
    expect(document.body.textContent).not.toContain("Adjustments (2)");

    pressKey("j");
    await settle(() => rowAt(1)?.getAttribute("data-state") === "selected");
    expect(document.body.textContent).not.toContain("Adjustments (2)");

    unmount();
  });

  it("test_question_mark_toggles_cheatsheet_and_escape_closes", async () => {
    const { unmount } = renderIntoContainer(React.createElement(Remittances));
    await waitForText("PCN-3");

    // No cheatsheet initially.
    expect(
      document.body.querySelector('[data-testid="keyboard-cheatsheet"]')
    ).toBeNull();

    // `?` opens it.
    pressKey("?");
    await settle(
      () =>
        document.body.querySelector('[data-testid="keyboard-cheatsheet"]') !==
        null
    );
    expect(
      document.body.querySelector('[data-testid="keyboard-cheatsheet"]')
    ).not.toBeNull();

    // Escape closes it.
    pressKey("Escape");
    await settle(
      () =>
        document.body.querySelector('[data-testid="keyboard-cheatsheet"]') ===
        null
    );
    expect(
      document.body.querySelector('[data-testid="keyboard-cheatsheet"]')
    ).toBeNull();

    // `?` again re-opens (toggle works in both directions).
    pressKey("?");
    await settle(
      () =>
        document.body.querySelector('[data-testid="keyboard-cheatsheet"]') !==
        null
    );
    expect(
      document.body.querySelector('[data-testid="keyboard-cheatsheet"]')
    ).not.toBeNull();

    unmount();
  });

  it("test_j_does_not_navigate_while_cheatsheet_is_open", async () => {
    // `enabled: !helpOpen` is the recommended pairing with
    // useRowKeyboard: j/k nav yields to the cheatsheet while it's
    // open. Pressing j while the cheatsheet is open closes the
    // cheatsheet (its own dismiss listener handles any non-`?`
    // keypress) but the row should NOT have moved.
    const { unmount } = renderIntoContainer(React.createElement(Remittances));
    await waitForText("PCN-3");

    // Move selection to row 0 first.
    pressKey("j");
    await settle(() => rowAt(0)?.getAttribute("data-state") === "selected");

    // Open cheatsheet.
    pressKey("?");
    await settle(
      () =>
        document.body.querySelector('[data-testid="keyboard-cheatsheet"]') !==
        null
    );

    // j while cheatsheet open: closes cheatsheet, row stays at 0.
    pressKey("j");
    await settle(
      () =>
        document.body.querySelector('[data-testid="keyboard-cheatsheet"]') ===
        null
    );
    expect(
      document.body.querySelector('[data-testid="keyboard-cheatsheet"]')
    ).toBeNull();
    expect(hasExactlyOneSelectedRow()).toBe(true);
    expect(rowAt(0)?.getAttribute("data-state")).toBe("selected");

    unmount();
  });

  // -------------------------------------------------------------------
  // Live-tail status pill (sub-project 5, Phase 5 Task 23, optional
  // page-level test). Cheap smoke check: the toolbar renders the pill
  // with the `live` label whenever the mocked stream is settled. The
  // hook's lifecycle is covered exhaustively in its own test file.
  // -------------------------------------------------------------------

  it("test_status_pill_shows_live_in_toolbar", async () => {
    (
      api.listRemittances as unknown as ReturnType<typeof vi.fn>
    ).mockResolvedValue({
      items: [],
      total: 0,
      returned: 0,
      has_more: false,
    });

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

    // Wait for the empty-state to mount — once the page body is rendered,
    // the toolbar (and thus the pill) is in the DOM.
    await waitForText("Remittances · awaiting first 835");

    const pill = document.body.querySelector(
      '[data-testid="tail-status-pill"]',
    );
    expect(pill).not.toBeNull();
    expect(pill?.textContent).toContain("Live");

    unmount();
  });
});