// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, fireEvent, render, waitFor } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import Inbox from "./Inbox";
import * as inboxApi from "@/lib/inbox-api";
import * as downloadModule from "@/lib/download";

// SP21 Phase 4 Task 4.4: Inbox now uses `useNavigate` (for unmatched
// claim drilldown to /claims?claim=ID), so the render harness needs
// a Router context. We use a fresh QueryClient per test so the
// drawer's per-remit query doesn't leak cache between cases, and a
// MemoryRouter so `useNavigate` has a router to push into.
function renderInbox() {
  const qc = new QueryClient({
    defaultOptions: { queries: { retry: false, retryDelay: 0 } },
  });
  return render(
    <MemoryRouter initialEntries={["/inbox"]}>
      <QueryClientProvider client={qc}>
        <Inbox />
      </QueryClientProvider>
    </MemoryRouter>,
  );
}

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

describe("Inbox page", () => {
  it("renders the five lane headers", async () => {
    // SP14: the Payer-Rejected lane was added (277CA STC A4/A6/A7).
    // The Inbox now renders 5 lanes: REJECTED, PAYER REJECTED,
    // CANDIDATES, UNMATCHED, DONE.
    vi.stubGlobal(
      "fetch",
      vi.fn().mockResolvedValue({
        ok: true,
        json: async () => ({
          rejected: [],
          payer_rejected: [],
          candidates: [],
          unmatched: [],
          done_today: [],
        }),
      }),
    );
    const { container } = renderInbox();
    await waitFor(() => {
      expect(container.textContent).toContain("REJECTED");
      expect(container.textContent).toContain("PAYER REJECTED");
      expect(container.textContent).toContain("CANDIDATES");
      expect(container.textContent).toContain("UNMATCHED");
      expect(container.textContent).toContain("DONE");
    });
  });

  it("SP14: payer-rejected row count rolls up into the need-eyes header", async () => {
    // The "need eyes" total in the header is the operator's
    // anchor for the day. Payer-rejected claims are actionable
    // (operator must acknowledge or follow up), so they count
    // toward the need-eyes total alongside 999 rejected, candidates,
    // and unmatched.
    vi.stubGlobal(
      "fetch",
      vi.fn().mockResolvedValue({
        ok: true,
        json: async () => ({
          rejected: [],
          payer_rejected: [
            { id: "PR1", kind: "claim", patient_control_number: "PR1",
              charge_amount: 250, payer_id: "CO_TXIX", provider_npi: "1881068062",
              state: "submitted", rejection_reason: "A7 invalid dx",
              service_date_from: null, payer_rejected_status_code: "A7",
              payer_rejected_reason: "invalid diagnosis",
              payer_rejected_at: "2026-06-20T10:00:00Z" },
          ],
          candidates: [],
          unmatched: [],
          done_today: [],
        }),
      }),
    );
    const { container } = renderInbox();
    await waitFor(() => {
      expect(container.textContent).toContain("1");
      expect(container.textContent).toContain("items need eyes");
    });
  });

  it("SP14: acknowledging selected payer-rejected claims calls the API and refetches", async () => {
    // The user selects a payer-rejected claim, hits Acknowledge, and
    // the inbox calls /api/inbox/payer-rejected/acknowledge with the
    // claim id, then refetches the lane payload.
    const fetchMock = vi.fn().mockImplementation(async (url: string) => {
      if (url.includes("/api/inbox/lanes")) {
        return {
          ok: true,
          json: async () => ({
            rejected: [],
            payer_rejected: [
              { id: "PR1", kind: "claim", patient_control_number: "PR1",
                charge_amount: 250, payer_id: "CO_TXIX", provider_npi: "1881068062",
                state: "submitted", rejection_reason: "A7 invalid dx",
                service_date_from: null, payer_rejected_status_code: "A7",
                payer_rejected_reason: "invalid diagnosis",
                payer_rejected_at: "2026-06-20T10:00:00Z" },
            ],
            candidates: [],
            unmatched: [],
            done_today: [],
          }),
        };
      }
      if (url.includes("/api/inbox/payer-rejected/acknowledge")) {
        return {
          ok: true,
          json: async () => ({
            ok: true,
            transitioned: 1,
            already_acked: 0,
            not_found: 0,
            not_rejected: 0,
          }),
        };
      }
      return { ok: true, json: async () => ({}) };
    });
    vi.stubGlobal("fetch", fetchMock);

    const { container, getByTestId } = renderInbox();
    await waitFor(() => {
      expect(container.textContent).toContain("PR1");
    });

    // Select the PR1 claim via its checkbox.
    const checkbox = container.querySelector(
      'input[type="checkbox"][aria-label="Select PR1"]',
    ) as HTMLInputElement;
    expect(checkbox).toBeTruthy();
    await act(async () => {
      checkbox.click();
    });

    // Hit Acknowledge. The bulk action bar button is wired to the
    // /api/inbox/payer-rejected/acknowledge endpoint.
    const ackButton = getByTestId("payer-rejected-acknowledge");
    await act(async () => {
      ackButton.click();
    });

    // The mock was called with the acknowledge URL.
    const ackCall = fetchMock.mock.calls.find(
      (c) => typeof c[0] === "string" && c[0].includes("/api/inbox/payer-rejected/acknowledge"),
    );
    expect(ackCall).toBeTruthy();
    expect(ackCall).toBeDefined();
    const ackBody = JSON.parse(ackCall![1].body);
    expect(ackBody.claim_ids).toEqual(["PR1"]);
  });

  it("SP8: multi-select resubmit opens bundle modal; Resubmit + Download triggers api + downloadBlob", async () => {
    // Two rejected claims → user selects both → Re-submit → modal appears
    // → user clicks Resubmit + Download → backend is hit with
    // ?download=true and the returned blob is handed to downloadBlob.
    const fetchMock = vi.fn().mockImplementation(async (url: string) => {
      if (url.includes("/api/inbox/lanes")) {
        return {
          ok: true,
          json: async () => ({
            rejected: [
              { id: "C1", kind: "claim", payer_claim_control_number: "C1",
                charge_amount: 100, payer_id: "P1", provider_npi: "N1",
                state: "rejected", rejection_reason: "old", service_date: null,
                score: null },
              { id: "C2", kind: "claim", payer_claim_control_number: "C2",
                charge_amount: 200, payer_id: "P1", provider_npi: "N1",
                state: "rejected", rejection_reason: "old", service_date: null,
                score: null },
            ],
            payer_rejected: [],
            candidates: [],
            unmatched: [],
            done_today: [],
          }),
        };
      }
      if (url.includes("/api/inbox/rejected/resubmit")) {
        return {
          ok: true,
          blob: async () => new Blob(["PK\u0003\u0004fake-zip-bytes"], { type: "application/zip" }),
          headers: {
            get: (name: string) =>
              name.toLowerCase() === "content-disposition"
                ? 'attachment; filename="resubmit-2-claims.zip"'
                : name.toLowerCase() === "x-cyclone-serialize-errors"
                  ? null
                  : null,
          },
        };
      }
      return { ok: true, json: async () => ({}) };
    });
    vi.stubGlobal("fetch", fetchMock);

    // Spy on the two side-effects we care about: the api call + the
    // browser download. Both are module-scope so we install spies and
    // reset between tests (afterEach already calls vi.restoreAllMocks
    // but we mockReset to clear call history from other tests).
    const resubmitDownloadSpy = vi
      .spyOn(inboxApi, "resubmitRejectedWithDownload")
      .mockResolvedValue({
        blob: new Blob(["PK\u0003\u0004fake-zip-bytes"], { type: "application/zip" }),
        filename: "resubmit-2-claims.zip",
        serializeErrors: [],
      });
    const downloadSpy = vi
      .spyOn(downloadModule, "downloadBlob")
      .mockImplementation(() => {});

    const { container, getByText } = renderInbox();
    await waitFor(() => {
      expect(container.textContent).toContain("C1");
      expect(container.textContent).toContain("C2");
    });

    // Select both rejected rows (the checkbox is the only <input> per row).
    const checkboxes = container.querySelectorAll('input[type="checkbox"]');
    expect(checkboxes.length).toBeGreaterThanOrEqual(2);
    await act(async () => {
      fireEvent.click(checkboxes[0]);
      fireEvent.click(checkboxes[1]);
    });

    // BulkBar shows a Re-submit button for the rejected lane.
    const resubmitBtn = getByText(/Re-submit \(2\)/);
    await act(async () => {
      fireEvent.click(resubmitBtn);
    });

    // Modal appears with both options.
    const modal = container.querySelector(
      '[data-testid="resubmit-bundle-modal"]',
    );
    expect(modal).not.toBeNull();

    const downloadBtn = container.querySelector(
      '[data-testid="resubmit-modal-download"]',
    ) as HTMLButtonElement | null;
    expect(downloadBtn).not.toBeNull();
    await act(async () => {
      fireEvent.click(downloadBtn!);
    });

    // The api was called with the selected ids, and the blob was
    // handed to downloadBlob with the right filename.
    expect(resubmitDownloadSpy).toHaveBeenCalledTimes(1);
    expect(resubmitDownloadSpy).toHaveBeenCalledWith(["C1", "C2"]);
    expect(downloadSpy).toHaveBeenCalledTimes(1);
    const [filename, blob] = downloadSpy.mock.calls[0];
    expect(filename).toBe("resubmit-2-claims.zip");
    expect(blob).toBeInstanceOf(Blob);
  });

  it("SP21 Task 4.4: clicking a candidate row opens the RemitDrawer", async () => {
    // Candidates are remits (payer_claim_control_number-keyed) — the
    // row's `id` is the remit id, so clicking drills into the
    // RemitDrawer via `?remit=ID`.
    vi.stubGlobal(
      "fetch",
      vi.fn().mockResolvedValue({
        ok: true,
        json: async () => ({
          rejected: [],
          payer_rejected: [],
          candidates: [
            {
              id: "REM-7",
              kind: "remit",
              payer_claim_control_number: "REM-7",
              charge_amount: 200,
              payer_id: "P1",
              rendering_provider_npi: "1234567890",
              service_date: "2026-06-19",
              candidates: [],
            },
          ],
          unmatched: [],
          done_today: [],
        }),
      }),
    );

    const { container } = renderInbox();
    await waitFor(() => {
      expect(container.textContent).toContain("REM-7");
    });

    // No drawer yet — the URL is `/inbox` with no `?remit=`.
    expect(
      container.querySelector('[data-testid="remit-drawer"]')
    ).toBeNull();

    // Click the candidate row. The InboxRow renders as a <tr> with
    // the remit id in its first cell; clicking that row bubbles to
    // the Lane's onRowClick handler we wired in Task 4.4.
    const cell = Array.from(container.querySelectorAll("td")).find(
      (td) => td.textContent === "REM-7",
    );
    const row = cell?.closest("tr");
    expect(row).toBeTruthy();
    await act(async () => {
      fireEvent.click(row as HTMLElement);
    });

    // Drawer portals into document.body — check there, not container.
    await waitFor(() => {
      expect(
        document.body.querySelector('[data-testid="remit-drawer"]'),
      ).not.toBeNull();
    });
  });

  it("SP21 Task 4.4: deep-link ?remit=ID opens the drawer on mount", async () => {
    // Pre-set the URL so the hook reads REM-7 off `window.location.search`
    // during its `useState` initializer — no click needed. The inbox
    // already mounts the drawer, so a deep link to /inbox?remit=REM-7
    // should land with the drawer open.
    vi.stubGlobal(
      "fetch",
      vi.fn().mockResolvedValue({
        ok: true,
        json: async () => ({
          rejected: [],
          payer_rejected: [],
          candidates: [],
          unmatched: [],
          done_today: [],
        }),
      }),
    );
    (window as unknown as { happyDOM: { setURL: (u: string) => void } }).happyDOM.setURL(
      "http://localhost/inbox?remit=REM-7",
    );

    renderInbox();

    await waitFor(() => {
      expect(
        document.body.querySelector('[data-testid="remit-drawer"]'),
      ).not.toBeNull();
    });
  });
});
