// @vitest-environment happy-dom
// React Query isn't used by this component (it receives a resolved
// ClaimDetail as a prop), but the surrounding test harness convention
// sets IS_REACT_ACT_ENVIRONMENT to suppress noisy act() warnings.
// Mirror the convention from ClaimDrawerSkeleton.test.tsx.
(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 { beforeEach, describe, expect, it, vi } from "vitest";
import { ClaimDrawerHeader } from "./ClaimDrawerHeader";
import * as apiModule from "@/lib/api";
import * as downloadModule from "@/lib/download";
import type { ClaimDetail } from "@/types";

// Mock the api + download helpers so we can assert the wiring without
// hitting the network or the real Blob/anchor machinery. The spies live
// at module scope but `beforeEach` resets their call history so the
// happy-path test doesn't leak into the error-path test.
const serializeSpy = vi.spyOn(apiModule.api, "serializeClaim837");
const downloadSpy = vi.spyOn(downloadModule, "downloadTextFile");

function renderIntoContainer(element: React.ReactElement): {
  container: HTMLDivElement;
  unmount: () => void;
} {
  const container = document.createElement("div");
  document.body.appendChild(container);
  const root: Root = createRoot(container);
  act(() => {
    root.render(element);
  });
  return {
    container,
    unmount: () => {
      act(() => root.unmount());
      container.remove();
    },
  };
}

/**
 * Minimal valid ClaimDetail fixture — every required key present so the
 * component typechecks. Mirrors the SAMPLE_DETAIL in
 * useClaimDetail.test.ts; only the fields the header actually reads
 * (id, state, stateLabel, billedAmount) vary between tests.
 */
function makeDetail(overrides: Partial<ClaimDetail> = {}): ClaimDetail {
  return {
    id: "CLM-1",
    batchId: "B-1",
    state: "submitted",
    stateLabel: "Submitted",
    billedAmount: 123.45,
    patientName: "Jane Doe",
    providerNpi: "1234567890",
    providerName: "Acme Clinic",
    payerName: "Medicaid",
    payerId: "MEDCO",
    submissionDate: "2026-01-15T00:00:00Z",
    serviceDateFrom: "2026-01-10",
    serviceDateTo: "2026-01-10",
    parsedAt: "2026-01-15T00:00:01Z",
    diagnoses: [{ code: "E11.9", qualifier: "ABK" }],
    serviceLines: [
      {
        lineNumber: 1,
        procedureQualifier: "HC",
        procedureCode: "99213",
        modifiers: [],
        charge: 100.0,
        units: 1,
        unitType: "UN",
        serviceDate: "2026-01-10",
      },
    ],
    parties: {
      billingProvider: {
        name: "Acme Clinic",
        npi: "1234567890",
        taxId: "12-3456789",
        address: {
          line1: "123 Main St",
          line2: null,
          city: "Denver",
          state: "CO",
          zip: "80202",
        },
      },
      subscriber: {
        firstName: "Jane",
        lastName: "Doe",
        memberId: "M-1",
        dob: null,
        gender: "F",
      },
      payer: { name: "Medicaid", id: "MEDCO" },
    },
    validation: { passed: true, errors: [], warnings: [] },
    rawSegments: [["ISA*00*"]],
    matchedRemittance: null,
    stateHistory: [
      {
        kind: "claim_submitted",
        ts: "2026-01-15T00:00:00Z",
        batchId: "B-1",
        remittanceId: null,
      },
    ],
    ...overrides,
  };
}

function renderHeader(
  claim: Partial<ClaimDetail>,
  onClose: () => void = vi.fn(),
  onError: (message: string) => void = vi.fn()
): { container: HTMLDivElement; unmount: () => void; onClose: () => void; onError: (m: string) => void } {
  const element = (
    <ClaimDrawerHeader claim={makeDetail(claim)} onClose={onClose} onError={onError} />
  );
  const { container, unmount } = renderIntoContainer(element);
  return { container, unmount, onClose, onError };
}

describe("ClaimDrawerHeader", () => {
  beforeEach(() => {
    serializeSpy.mockReset();
    downloadSpy.mockReset();
  });
  it("test_renders_claim_id_label_and_value", () => {
    const { container, unmount } = renderHeader({ id: "CLM-42" });

    // The eyebrow label "Claim" is rendered as uppercase text.
    const text = (container.textContent ?? "").toLowerCase();
    expect(text).toContain("claim");

    // The claim id sits in a node tagged with data-testid="header-id".
    const idEl = container.querySelector('[data-testid="header-id"]');
    expect(idEl).not.toBeNull();
    expect(idEl?.textContent).toBe("CLM-42");

    unmount();
  });

  it("test_renders_state_badge_with_label", () => {
    const { container, unmount } = renderHeader({
      state: "submitted",
      stateLabel: "Submitted",
    });

    const badge = container.querySelector('[data-testid="header-state"]');
    expect(badge).not.toBeNull();
    expect(badge?.textContent).toBe("Submitted");

    unmount();
  });

  it("test_state_badge_variant_matches_state", () => {
    // Each state must surface the right Badge variant. We sniff the
    // rendered className for the per-variant token (defined in
    // badge.tsx's cva config) so the assertion is independent of the
    // surrounding wrapper classes.
    const cases: Array<{
      state: string;
      stateLabel: string;
      expectedToken: string;
    }> = [
      // secondary → bg-muted/80 text-muted-foreground
      // (the polished muted treatment uses /80 for a slightly subtler tone)
      { state: "submitted", stateLabel: "Submitted", expectedToken: "bg-muted/80 text-muted-foreground" },
      // default → bg-primary/15 text-primary
      { state: "accepted", stateLabel: "Accepted", expectedToken: "bg-primary/15" },
      // destructive → bg-destructive/15
      { state: "denied", stateLabel: "Denied", expectedToken: "bg-destructive/15" },
      // success → bg-[hsl(var(--success)/0.14)] (covered by both matched and paid)
      { state: "matched", stateLabel: "Matched", expectedToken: "bg-[hsl(var(--success)/0.14)]" },
      { state: "paid", stateLabel: "Paid", expectedToken: "bg-[hsl(var(--success)/0.14)]" },
      // warning → bg-[hsl(var(--warning)/0.14)]
      { state: "pending", stateLabel: "Pending", expectedToken: "bg-[hsl(var(--warning)/0.14)]" },
    ];

    for (const c of cases) {
      const { container, unmount } = renderHeader({
        state: c.state,
        stateLabel: c.stateLabel,
      });
      const badge = container.querySelector('[data-testid="header-state"]');
      expect(badge, `badge missing for state=${c.state}`).not.toBeNull();
      const cls = badge?.className ?? "";
      expect(
        cls.includes(c.expectedToken),
        `expected badge className to include "${c.expectedToken}" for state=${c.state}, got "${cls}"`
      ).toBe(true);
      unmount();
    }
  });

  it("test_renders_total_amount_formatted", () => {
    // 123.45 → "$123.45" (the project's fmt.usdPrecise helper, which
    // uses minimumFractionDigits:2 / maximumFractionDigits:2).
    const { container, unmount } = renderHeader({ billedAmount: 123.45 });

    const amountEl = container.querySelector('[data-testid="header-amount"]');
    expect(amountEl).not.toBeNull();
    expect(amountEl?.textContent).toBe("$123.45");

    unmount();
  });

  it("test_close_button_calls_onClose", () => {
    const onClose = vi.fn();
    const { container, unmount } = renderHeader({}, onClose);

    const closeBtn = container.querySelector(
      '[data-testid="header-close"]'
    ) as HTMLButtonElement | null;
    expect(closeBtn).not.toBeNull();
    expect(onClose).not.toHaveBeenCalled();

    act(() => {
      closeBtn?.click();
    });
    expect(onClose).toHaveBeenCalledTimes(1);

    unmount();
  });

  it("test_uses_modern_palette_surface", () => {
    // The drawer header anchors itself on the light surface palette
    // token (matches ClaimDrawerSkeleton / ClaimDrawerError). The root
    // <header> element is tagged with data-testid="claim-drawer-header"
    // so we can sniff its className without coupling to the badge
    // or close-button wrappers.
    const { container, unmount } = renderHeader({});

    const root = container.querySelector('[data-testid="claim-drawer-header"]');
    expect(root).not.toBeNull();
    const cls = root?.className ?? "";
    expect(cls).toContain("bg-[color:var(--m-surface)]");

    unmount();
  });

  it("test_download_button_fetches_and_saves_file_for_claim_id", async () => {
    // Happy path: click → api.serializeClaim837(claim.id) → downloadTextFile
    // with the returned text + filename. We don't actually need to wait for
    // the download side-effect — we just verify the wiring (right ids,
    // right payload, right mime).
    serializeSpy.mockResolvedValueOnce({
      text: "ISA*00*~IEA*0*1~",
      filename: "claim-CLM-1.x12",
    });

    const { container, unmount } = renderHeader({ id: "CLM-1" });
    const btn = container.querySelector(
      '[data-testid="header-download-837"]'
    ) as HTMLButtonElement | null;
    expect(btn).not.toBeNull();

    await act(async () => {
      btn?.click();
      // Flush the microtask queue so the awaited handler resolves.
      await Promise.resolve();
    });

    expect(serializeSpy).toHaveBeenCalledTimes(1);
    expect(serializeSpy).toHaveBeenCalledWith("CLM-1");
    expect(downloadSpy).toHaveBeenCalledTimes(1);
    expect(downloadSpy).toHaveBeenCalledWith(
      "claim-CLM-1.x12",
      "text/x12",
      "ISA*00*~IEA*0*1~"
    );

    unmount();
  });

  it("test_download_button_routes_api_error_through_onError", async () => {
    // When serializeClaim837 throws, the header swallows the error and
    // hands the message to the optional onError callback. The download
    // helper must NOT be called on the failure path — that's the
    // difference between "the file was corrupted in the DB" and "we just
    // gave the user a broken empty file".
    serializeSpy.mockRejectedValueOnce(new Error("404 Not found"));
    const onError = vi.fn();

    const { container, unmount } = renderHeader({ id: "CLM-1" }, vi.fn(), onError);
    const btn = container.querySelector(
      '[data-testid="header-download-837"]'
    ) as HTMLButtonElement | null;

    await act(async () => {
      btn?.click();
      await Promise.resolve();
    });

    expect(onError).toHaveBeenCalledTimes(1);
    expect(onError).toHaveBeenCalledWith("404 Not found");
    expect(downloadSpy).not.toHaveBeenCalled();

    unmount();
  });
});
