import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { ApiError, api } from "./api";

const originalFetch = global.fetch;
const mockFetch = vi.fn();

describe("api GET helpers", () => {
  beforeEach(() => {
    global.fetch = mockFetch as unknown as typeof fetch;
    mockFetch.mockReset();
  });
  afterEach(() => {
    global.fetch = originalFetch;
    vi.restoreAllMocks();
  });

  it("listBatches hits /api/batches and unwraps body.items", async () => {
    mockFetch.mockResolvedValueOnce(
      new Response(JSON.stringify({ items: [] }), {
        status: 200,
        headers: { "content-type": "application/json" },
      })
    );
    const out = await api.listBatches();
    expect(out).toEqual([]);
    expect(mockFetch).toHaveBeenCalledTimes(1);
    const called = mockFetch.mock.calls[0][0] as string;
    expect(called).toContain("/api/batches");
    expect(called).not.toContain("?");
  });

  it("listClaims builds the right URL with status + sort + order", async () => {
    mockFetch.mockResolvedValueOnce(
      new Response(
        JSON.stringify({ items: [], total: 0, returned: 0, has_more: false }),
        { status: 200, headers: { "content-type": "application/json" } }
      )
    );
    await api.listClaims({
      status: "submitted",
      sort: "billedAmount",
      order: "desc",
    });
    const called = mockFetch.mock.calls[0][0] as string;
    expect(called).toContain("/api/claims?");
    expect(called).toContain("status=submitted");
    expect(called).toContain("sort=billedAmount");
    expect(called).toContain("order=desc");
  });

  it("getBatch returns the result with the right id in the URL", async () => {
    mockFetch.mockResolvedValueOnce(
      new Response(
        JSON.stringify({
          envelope: {
            sender_id: "X",
            receiver_id: "Y",
            control_number: "1",
            transaction_date: "2026-01-01",
          },
          claims: [],
          summary: {},
          kind: "835",
        }),
        { status: 200, headers: { "content-type": "application/json" } }
      )
    );
    const out = await api.getBatch("abc");
    expect(out).toBeTruthy();
    const called = mockFetch.mock.calls[0][0] as string;
    expect(called).toContain("/api/batches/abc");
  });

  it("getClaimDetail hits /api/claims/{id} and returns the parsed body", async () => {
    // Minimal valid ClaimDetail shape — every spec-mandated key present,
    // including the optional `matchedRemittance: null` for an unpaired
    // claim (the dominant UI render path).
    const body = {
      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,
        },
      ],
    };
    mockFetch.mockResolvedValueOnce(
      new Response(JSON.stringify(body), {
        status: 200,
        headers: { "content-type": "application/json" },
      })
    );
    const out = await api.getClaimDetail("CLM-1");
    expect(out).toEqual(body);
    expect(mockFetch).toHaveBeenCalledTimes(1);
    const called = mockFetch.mock.calls[0][0] as string;
    // URL must include the encoded id, no trailing query string.
    expect(called).toContain("/api/claims/CLM-1");
    expect(called).not.toContain("?");
  });

  it("getClaimDetail throws an ApiError on 404", async () => {
    // The backend raises `{"error":"Not found","detail":"Claim X not found"}`
    // on missing ids (see api.py:get_claim_detail_endpoint). The client
    // must surface it as an ApiError so the drawer can distinguish
    // "doesn't exist" from a transient fetch failure (spec §3.4).
    mockFetch.mockResolvedValueOnce(
      new Response(
        JSON.stringify({ error: "Not found", detail: "Claim ghost not found" }),
        {
          status: 404,
          headers: { "content-type": "application/json" },
        }
      )
    );
    let caught: unknown;
    try {
      await api.getClaimDetail("ghost");
    } catch (err) {
      caught = err;
    }
    expect(caught).toBeInstanceOf(ApiError);
    expect((caught as ApiError).status).toBe(404);
  });

  it("serializeClaim837 returns the regenerated text + filename from Content-Disposition", async () => {
    const x12 = "ISA*00*          *00*          *ZZ*CYCLONE        *ZZ*RECEIVER       *260620*1200*^*00501*000000001*0*P*:~IEA*0*000000001~";
    mockFetch.mockResolvedValueOnce(
      new Response(x12, {
        status: 200,
        headers: {
          "content-type": "text/x12",
          // Backend emits: attachment; filename="claim-{id}.x12"
          "content-disposition": 'attachment; filename="claim-CLM-1.x12"',
        },
      })
    );
    const out = await api.serializeClaim837("CLM-1");
    expect(out.text).toBe(x12);
    expect(out.filename).toBe("claim-CLM-1.x12");
    // URL must hit the new SP8 endpoint with the encoded id.
    const called = mockFetch.mock.calls[0][0] as string;
    expect(called).toContain("/api/claims/CLM-1/serialize-837");
    // No Accept header negotiation needed — backend always returns text/x12.
    const init = mockFetch.mock.calls[0][1] as RequestInit | undefined;
    const headers = (init?.headers ?? {}) as Record<string, string>;
    expect(headers.Accept).toBeUndefined();
  });

  it("serializeClaim837 falls back to a default filename if Content-Disposition is missing", async () => {
    mockFetch.mockResolvedValueOnce(
      new Response("ISA*00*~", { status: 200 })
    );
    const out = await api.serializeClaim837("CLM-2");
    expect(out.filename).toBe("claim-CLM-2.x12");
  });

  it("serializeClaim837 throws ApiError on 404", async () => {
    mockFetch.mockResolvedValueOnce(
      new Response(
        JSON.stringify({ error: "Not found", detail: "Claim ghost not found" }),
        { status: 404, headers: { "content-type": "application/json" } }
      )
    );
    await expect(api.serializeClaim837("ghost")).rejects.toBeInstanceOf(ApiError);
  });
});
