// @vitest-environment happy-dom
// SearchBar is the top-level component — it owns the dialog, the
// input, the keyboard shortcut, and navigation. We need both the
// act-aware env flag AND a MemoryRouter so `useNavigate` doesn't
// blow up at render time.
(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 { MemoryRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { describe, expect, it, vi } from "vitest";

import { SearchBar } from "./SearchBar";
import { useAppStore } from "@/store";
import type { Activity, Claim, Remittance } from "@/types";

// ---------------------------------------------------------------------------
// Mocks — same as useSearch.test.ts: useSearch falls back to the
// in-memory store when `api.isConfigured` is false. We rely on that
// for the SearchBar tests too so we don't need to wire a fake fetch.
// ---------------------------------------------------------------------------

vi.mock("@/lib/api", () => ({
  api: {
    isConfigured: false,
    listClaims: vi.fn(),
    listRemittances: vi.fn(),
    listActivity: vi.fn(),
  },
}));

// ---------------------------------------------------------------------------
// Fixtures — minimal: enough to make sure hits show up.
// ---------------------------------------------------------------------------

const CLAIMS: Claim[] = [
  {
    id: "CLM-10428",
    patientName: "Avery Nguyen",
    providerNpi: "1730187395",
    payerName: "Blue Cross Blue Shield",
    cptCode: "99213",
    billedAmount: 240,
    receivedAmount: 240,
    status: "paid",
    submissionDate: "2026-05-01T00:00:00Z",
  },
];

const REMITS: Remittance[] = [
  {
    id: "REM-7782",
    claimId: "CLM-10428",
    payerName: "Blue Cross Blue Shield",
    paidAmount: 240,
    adjustmentAmount: 0,
    receivedDate: "2026-05-10T00:00:00Z",
    checkNumber: "EFT-100123",
    status: "reconciled",
  },
];

const ACTIVITY: Activity[] = [
  {
    id: "ACT-CLM-10428",
    kind: "claim_paid",
    message: "Paid CLM-10428 · Avery Nguyen",
    timestamp: "2026-05-10T00:00:00Z",
    npi: "1730187395",
    amount: 240,
  },
];

/**
 * Set an input's value the way React's synthetic event system expects.
 *
 * React 17+ installs an internal value tracker on `<input>` /
 * `<textarea>` elements and only fires `onChange` when the tracker
 * sees a change. Direct assignment (`input.value = "x"`) updates the
 * DOM but skips the tracker, so React's onChange never fires in the
 * test. The canonical workaround is to call the *native* value setter
 * from the input's prototype — that update goes through React's
 * tracker and the next dispatched `input` event fires onChange.
 */
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
  window.HTMLInputElement.prototype,
  "value"
)!.set!;
function typeInto(input: HTMLInputElement, value: string): void {
  nativeInputValueSetter.call(input, value);
  input.dispatchEvent(new Event("input", { bubbles: true }));
}

// ---------------------------------------------------------------------------
// Render helpers
// ---------------------------------------------------------------------------

function renderIntoContainer(
  element: React.ReactElement,
  initialEntries: string[] = ["/claims"]
): {
  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(
      <MemoryRouter initialEntries={initialEntries}>
        <QueryClientProvider client={qc}>{element}</QueryClientProvider>
      </MemoryRouter>
    );
  });
  return {
    container,
    unmount: () => {
      act(() => root.unmount());
      container.remove();
    },
  };
}

/**
 * Wait for the predicate over `document.body` to hold. The search
 * dialog's results panel renders into a Radix portal mounted on
 * `document.body`, so we poll there rather than at the React
 * container.
 */
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;
}

/** Dispatch a keydown on the given target (window by default). */
function pressKey(
  key: string,
  target: EventTarget = window,
  init: KeyboardEventInit = {}
): void {
  target.dispatchEvent(
    new KeyboardEvent("keydown", {
      key,
      bubbles: true,
      cancelable: true,
      ...init,
    })
  );
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("SearchBar", () => {
  it("renders the trigger button in the top bar", () => {
    const { container, unmount } = renderIntoContainer(<SearchBar />);
    expect(
      container.querySelector('[data-testid="search-trigger"]')
    ).not.toBeNull();
    unmount();
  });

  it("does not render the dialog until the trigger is clicked", () => {
    const { unmount } = renderIntoContainer(<SearchBar />);
    expect(
      document.body.querySelector('[data-testid="search-dialog"]')
    ).toBeNull();
    unmount();
  });

  it("opens the dialog when the trigger is clicked", async () => {
    const { container, unmount } = renderIntoContainer(<SearchBar />);
    const trigger = container.querySelector<HTMLButtonElement>(
      '[data-testid="search-trigger"]'
    )!;
    act(() => {
      trigger.click();
    });
    await settle((b) =>
      b.querySelector('[data-testid="search-dialog"]') !== null
    );
    unmount();
  });

  it("opens the dialog when Cmd+K is pressed on Mac", async () => {
    const originalPlatform = navigator.platform;
    Object.defineProperty(navigator, "platform", {
      value: "MacIntel",
      configurable: true,
    });
    try {
      const { unmount } = renderIntoContainer(<SearchBar />);
      act(() => {
        pressKey("k", window, { metaKey: true });
      });
      await settle((b) =>
        b.querySelector('[data-testid="search-dialog"]') !== null
      );
      unmount();
    } finally {
      Object.defineProperty(navigator, "platform", {
        value: originalPlatform,
        configurable: true,
      });
    }
  });

  it("opens the dialog when Ctrl+K is pressed on non-Mac", async () => {
    const originalPlatform = navigator.platform;
    Object.defineProperty(navigator, "platform", {
      value: "Win32",
      configurable: true,
    });
    try {
      const { unmount } = renderIntoContainer(<SearchBar />);
      act(() => {
        pressKey("k", window, { ctrlKey: true });
      });
      await settle((b) =>
        b.querySelector('[data-testid="search-dialog"]') !== null
      );
      unmount();
    } finally {
      Object.defineProperty(navigator, "platform", {
        value: originalPlatform,
        configurable: true,
      });
    }
  });

  it("renders results when the user types a matching query", async () => {
    // Seed the store BEFORE the SearchBar reads it. (We seed once per
    // file scope because happy-dom persists across tests; instead we
    // seed inside each test that needs it.)
    useAppStore.setState({
      claims: CLAIMS,
      remittances: REMITS,
      activity: ACTIVITY,
    });

    const { container, unmount } = renderIntoContainer(<SearchBar />);
    const trigger = container.querySelector<HTMLButtonElement>(
      '[data-testid="search-trigger"]'
    )!;
    act(() => {
      trigger.click();
    });
    await settle((b) =>
      b.querySelector('[data-testid="search-dialog"]') !== null
    );

    const input = document.body.querySelector<HTMLInputElement>(
      '[data-testid="search-input"]'
    )!;
    act(() => {
      typeInto(input, "Nguyen");
    });

    // Debounce is 150ms; flush and then settle for the row to appear.
    await act(async () => {
      await new Promise((r) => setTimeout(r, 200));
    });
    await settle((b) =>
      b.querySelector('[data-result-kind="claim"]') !== null
    );

    const claimRow = document.body.querySelector(
      '[data-result-kind="claim"]'
    );
    expect(claimRow).not.toBeNull();
    expect(claimRow!.getAttribute("data-result-id")).toBe("CLM-10428");
    unmount();
  });

  it("moves the selection with ArrowDown / ArrowUp on the input", async () => {
    useAppStore.setState({
      claims: [
        { ...CLAIMS[0]!, id: "CLM-A" },
        { ...CLAIMS[0]!, id: "CLM-B" },
        { ...CLAIMS[0]!, id: "CLM-C" },
      ],
      remittances: [],
      activity: [],
    });

    const { container, unmount } = renderIntoContainer(<SearchBar />);
    act(() => {
      container
        .querySelector<HTMLButtonElement>('[data-testid="search-trigger"]')!
        .click();
    });
    await settle((b) =>
      b.querySelector('[data-testid="search-dialog"]') !== null
    );

    const input = document.body.querySelector<HTMLInputElement>(
      '[data-testid="search-input"]'
    )!;
    act(() => {
      typeInto(input, "nguyen");
    });
    await act(async () => {
      await new Promise((r) => setTimeout(r, 200));
    });
    await settle((b) => b.querySelectorAll('[data-result-kind="claim"]').length >= 3);

    // Selection starts at 0.
    let rows = document.body.querySelectorAll('[data-result-kind="claim"]');
    expect(rows[0]!.getAttribute("data-selected")).not.toBeNull();
    expect(rows[1]!.getAttribute("data-selected")).toBeNull();

    // ArrowDown → selection 1.
    act(() => {
      pressKey("ArrowDown", input);
    });
    rows = document.body.querySelectorAll('[data-result-kind="claim"]');
    expect(rows[0]!.getAttribute("data-selected")).toBeNull();
    expect(rows[1]!.getAttribute("data-selected")).not.toBeNull();

    // ArrowUp → selection 0 again.
    act(() => {
      pressKey("ArrowUp", input);
    });
    rows = document.body.querySelectorAll('[data-result-kind="claim"]');
    expect(rows[0]!.getAttribute("data-selected")).not.toBeNull();
    unmount();
  });

  it("navigates to /claims?claim=<id> when Enter is pressed on a claim row", async () => {
    useAppStore.setState({
      claims: CLAIMS,
      remittances: REMITS,
      activity: ACTIVITY,
    });

    const { container, unmount } = renderIntoContainer(<SearchBar />);
    act(() => {
      container
        .querySelector<HTMLButtonElement>('[data-testid="search-trigger"]')!
        .click();
    });
    await settle((b) =>
      b.querySelector('[data-testid="search-dialog"]') !== null
    );

    const input = document.body.querySelector<HTMLInputElement>(
      '[data-testid="search-input"]'
    )!;
    act(() => {
      typeInto(input, "Nguyen");
    });
    await act(async () => {
      await new Promise((r) => setTimeout(r, 200));
    });
    await settle((b) =>
      b.querySelector('[data-result-kind="claim"]') !== null
    );

    act(() => {
      pressKey("Enter", input);
    });

    // The dialog should close in response to the selection — when
    // it closes, Radix removes the portal content. We assert on the
    // dialog-testid disappearing rather than the router URL (which
    // we'd need a navigation spy to observe, and MemoryRouter
    // doesn't expose one).
    await settle(
      (b) => b.querySelector('[data-testid="search-dialog"]') === null
    );
    unmount();
  });

  it("resets the query and selection when the dialog closes", async () => {
    useAppStore.setState({
      claims: CLAIMS,
      remittances: REMITS,
      activity: ACTIVITY,
    });

    const { container, unmount } = renderIntoContainer(<SearchBar />);
    const trigger = container.querySelector<HTMLButtonElement>(
      '[data-testid="search-trigger"]'
    )!;
    act(() => {
      trigger.click();
    });
    await settle((b) =>
      b.querySelector('[data-testid="search-dialog"]') !== null
    );

    const input = document.body.querySelector<HTMLInputElement>(
      '[data-testid="search-input"]'
    )!;
    act(() => {
      typeInto(input, "Nguyen");
    });

    // Close via Escape. Radix's Dialog listens on `document`
    // (capture phase) via `@radix-ui/react-use-escape-keydown`, so
    // dispatching on document matches real-user behavior. Window
    // bubbles to document anyway, but explicit `document` matches
    // the capture-phase handler more clearly.
    act(() => {
      pressKey("Escape", document);
    });
    await settle(
      (b) => b.querySelector('[data-testid="search-dialog"]') === null
    );

    // Reopen — input should be empty.
    act(() => {
      trigger.click();
    });
    await settle((b) =>
      b.querySelector('[data-testid="search-dialog"]') !== null
    );
    const reopened = document.body.querySelector<HTMLInputElement>(
      '[data-testid="search-input"]'
    )!;
    expect(reopened.value).toBe("");
    unmount();
  });
});