// @vitest-environment node
// `useTailStore` is a pure zustand store with no DOM / React dependencies,
// so a node environment is fine. We import `getState()` and `setState`
// from the store directly (no React render) and assert on the resulting
// state shape.

import { beforeEach, describe, expect, it } from "vitest";
import type {
  Ack,
  Activity,
  Claim,
  ClaimAck,
  Remittance,
  Ta1Ack,
} from "@/types";
import { TAIL_CAP, useTailStore } from "./tail-store";

function makeClaim(id: string, overrides: Partial<Claim> = {}): Claim {
  return {
    id,
    patientName: `Patient ${id}`,
    providerNpi: "1234567890",
    payerName: "Medicaid",
    cptCode: "99213",
    billedAmount: 100,
    receivedAmount: 0,
    status: "submitted",
    submissionDate: "2026-06-20T00:00:00Z",
    ...overrides,
  };
}

function makeRemittance(id: string, overrides: Partial<Remittance> = {}): Remittance {
  return {
    id,
    claimId: `CLM-${id}`,
    payerName: "Medicaid",
    paidAmount: 100,
    adjustmentAmount: 0,
    receivedDate: "2026-06-20",
    checkNumber: id,
    status: "received",
    ...overrides,
  };
}

function makeActivity(id: string, overrides: Partial<Activity> = {}): Activity {
  return {
    id,
    kind: "claim_submitted",
    message: `Event ${id}`,
    timestamp: "2026-06-20T00:00:00Z",
    ...overrides,
  };
}

function makeAck(id: number, overrides: Partial<Ack> = {}): Ack {
  return {
    id,
    sourceBatchId: `BATCH-${id}`,
    acceptedCount: 1,
    rejectedCount: 0,
    receivedCount: 1,
    ackCode: "A",
    parsedAt: "2026-07-02T12:00:00Z",
    patientControlNumber: `PCN-${id}`,
    ...overrides,
  };
}

function makeTa1Ack(id: number, overrides: Partial<Ta1Ack> = {}): Ta1Ack {
  return {
    id,
    controlNumber: `0000000${id}`,
    ackCode: "A",
    noteCode: "000",
    interchangeDate: "2026-07-02",
    interchangeTime: "1200",
    senderId: "SENDER",
    receiverId: "RECEIVER",
    sourceBatchId: `TA1-BATCH-${id}`,
    parsedAt: "2026-07-02T12:00:00Z",
    ...overrides,
  };
}

function makeClaimAck(id: number, overrides: Partial<ClaimAck> = {}): ClaimAck {
  return {
    id,
    claimId: "CLM-1",
    batchId: null,
    ackId: 99,
    ackKind: "999",
    ak2Index: 0,
    setControlNumber: "991102989",
    setAcceptRejectCode: "A",
    linkedAt: "2026-07-02T12:00:00Z",
    linkedBy: "auto",
    claimState: "received",
    ...overrides,
  };
}

describe("useTailStore", () => {
  // Each test starts from a known-empty state. The store is module-level
  // (singleton), so we use `reset()` to clear between cases — that's also
  // what production code calls on stream open.
  beforeEach(() => {
    useTailStore.getState().reset("claims");
    useTailStore.getState().reset("remittances");
    useTailStore.getState().reset("activity");
    useTailStore.getState().reset("acks");
    useTailStore.getState().reset("ta1_acks");
    useTailStore.getState().reset("claim-acks");
  });

  it("test_add_claim_adds_new_claim_keyed_by_id", () => {
    const { addClaim } = useTailStore.getState();
    addClaim(makeClaim("CLM-1"));
    addClaim(makeClaim("CLM-2"));
    const { claims } = useTailStore.getState();
    expect(Object.keys(claims)).toHaveLength(2);
    expect(claims["CLM-1"]?.id).toBe("CLM-1");
    expect(claims["CLM-2"]?.id).toBe("CLM-2");
  });

  it("test_add_claim_with_duplicate_id_is_noop", () => {
    const { addClaim } = useTailStore.getState();
    addClaim(makeClaim("CLM-1", { patientName: "Original" }));
    addClaim(makeClaim("CLM-1", { patientName: "Updated" }));
    const { claims } = useTailStore.getState();
    expect(Object.keys(claims)).toHaveLength(1);
    // First write wins; duplicates are silently dropped so the snapshot
    // replay on reconnect doesn't trample the canonical row.
    expect(claims["CLM-1"]?.patientName).toBe("Original");
  });

  it("test_reset_claims_clears_only_claims_slice", () => {
    const { addClaim, addRemittance, addActivity, reset } = useTailStore.getState();
    addClaim(makeClaim("CLM-1"));
    addRemittance(makeRemittance("RMT-1"));
    addActivity(makeActivity("ACT-1"));

    reset("claims");
    const s = useTailStore.getState();
    expect(Object.keys(s.claims)).toHaveLength(0);
    expect(Object.keys(s.remittances)).toHaveLength(1);
    expect(s.activity).toHaveLength(1);
  });

  it("test_add_activity_appends", () => {
    const { addActivity } = useTailStore.getState();
    addActivity(makeActivity("ACT-1", { message: "first" }));
    addActivity(makeActivity("ACT-2", { message: "second" }));
    addActivity(makeActivity("ACT-3", { message: "third" }));
    const { activity } = useTailStore.getState();
    expect(activity).toHaveLength(3);
    // Activity has no stable id; it lives in an array. Insertion order is
    // the only ordering signal.
    expect(activity.map((a) => a.message)).toEqual(["first", "second", "third"]);
  });

  it("test_fifo_cap_evicts_oldest_when_over_10000", () => {
    // Insert 5 over the cap so the eviction policy must actually run.
    // The plan calls for evicting the oldest 100 (or enough to be back at
    // the cap) — we just assert: size == TAIL_CAP and the very-first
    // items are gone.
    //
    // 10 005 individual `addClaim` calls is intentionally a stress test
    // of the eviction path: each call rebuilds the dict + order array
    // so the wall-clock cost is O(N^2) in the number of items ever
    // inserted. Bump the per-test timeout from the default 5s to 60s
    // so this case can complete in CI on slow runners.
    const N = TAIL_CAP + 5;
    const { addClaim } = useTailStore.getState();
    for (let i = 0; i < N; i++) {
      addClaim(makeClaim(`CLM-${i.toString().padStart(6, "0")}`));
    }
    const { claims } = useTailStore.getState();
    expect(Object.keys(claims)).toHaveLength(TAIL_CAP);
    // The five oldest (CLM-000000 .. CLM-000004) must be gone.
    expect(claims["CLM-000000"]).toBeUndefined();
    expect(claims["CLM-000004"]).toBeUndefined();
    // The most recent (CLM-0010004) must be present.
    const lastKey = `CLM-${(N - 1).toString().padStart(6, "0")}`;
    expect(claims[lastKey]).toBeDefined();
  }, 60_000);

  // -------------------------------------------------------------------------
  // SP25: acks + ta1_acks slices mirror the claims/remittances key-by-id
  // pattern (both have a stable `id` from the database row).
  // -------------------------------------------------------------------------

  it("test_add_ack_adds_new_ack_keyed_by_id", () => {
    const { addAck } = useTailStore.getState();
    addAck(makeAck(1));
    addAck(makeAck(2));
    const { acks } = useTailStore.getState();
    expect(Object.keys(acks)).toHaveLength(2);
    expect(acks["1"]?.sourceBatchId).toBe("BATCH-1");
    expect(acks["2"]?.sourceBatchId).toBe("BATCH-2");
  });

  it("test_add_ack_with_duplicate_id_is_noop", () => {
    const { addAck } = useTailStore.getState();
    addAck(makeAck(1, { sourceBatchId: "BATCH-original" }));
    addAck(makeAck(1, { sourceBatchId: "BATCH-replay" }));
    const { acks } = useTailStore.getState();
    expect(Object.keys(acks)).toHaveLength(1);
    // First write wins; snapshot replay must not trample the canonical row.
    expect(acks["1"]?.sourceBatchId).toBe("BATCH-original");
  });

  it("test_reset_acks_clears_only_acks_slice", () => {
    const { addAck, addClaim, reset } = useTailStore.getState();
    addAck(makeAck(1));
    addClaim(makeClaim("CLM-1"));

    reset("acks");
    const s = useTailStore.getState();
    expect(Object.keys(s.acks)).toHaveLength(0);
    // The other slices are untouched.
    expect(Object.keys(s.claims)).toHaveLength(1);
  });

  it("test_add_ta1_ack_adds_new_ta1_ack_keyed_by_id", () => {
    const { addTa1Ack } = useTailStore.getState();
    addTa1Ack(makeTa1Ack(1));
    addTa1Ack(makeTa1Ack(2));
    const { ta1Acks } = useTailStore.getState();
    expect(Object.keys(ta1Acks)).toHaveLength(2);
    expect(ta1Acks["1"]?.controlNumber).toBe("00000001");
    expect(ta1Acks["2"]?.controlNumber).toBe("00000002");
  });

  it("test_add_ta1_ack_with_duplicate_id_is_noop", () => {
    const { addTa1Ack } = useTailStore.getState();
    addTa1Ack(makeTa1Ack(7, { senderId: "ORIGINAL" }));
    addTa1Ack(makeTa1Ack(7, { senderId: "REPLAY" }));
    const { ta1Acks } = useTailStore.getState();
    expect(Object.keys(ta1Acks)).toHaveLength(1);
    expect(ta1Acks["7"]?.senderId).toBe("ORIGINAL");
  });

  it("test_reset_ta1_acks_clears_only_ta1_acks_slice", () => {
    const { addTa1Ack, addAck, reset } = useTailStore.getState();
    addTa1Ack(makeTa1Ack(1));
    addAck(makeAck(1));

    reset("ta1_acks");
    const s = useTailStore.getState();
    expect(Object.keys(s.ta1Acks)).toHaveLength(0);
    expect(Object.keys(s.acks)).toHaveLength(1);
  });

  // -------------------------------------------------------------------------
  // SP28: the claim↔ack link slice mirrors the SP25 acks/ta1_acks slice
  // (numeric id from the `claim_acks` table, first write wins, FIFO-capped
  // at TAIL_CAP). The slice drives both the ClaimDrawer's
  // <Acknowledgments /> panel and the AckDrawer's <MatchedClaim /> panel
  // via `useTailStream("claim-acks")` + `useMergedTail("claim-acks", …)`.
  // -------------------------------------------------------------------------

  it("test_add_claim_ack_adds_new_claim_ack_keyed_by_id", () => {
    const { addClaimAck } = useTailStore.getState();
    addClaimAck(makeClaimAck(1));
    addClaimAck(makeClaimAck(2));
    const { claimAcks } = useTailStore.getState();
    expect(Object.keys(claimAcks)).toHaveLength(2);
    expect(claimAcks["1"]?.claimId).toBe("CLM-1");
    expect(claimAcks["2"]?.claimId).toBe("CLM-1");
  });

  it("test_add_claim_ack_with_duplicate_id_is_noop", () => {
    const { addClaimAck } = useTailStore.getState();
    addClaimAck(makeClaimAck(1, { setAcceptRejectCode: "A" }));
    addClaimAck(makeClaimAck(1, { setAcceptRejectCode: "R" }));
    const { claimAcks } = useTailStore.getState();
    expect(Object.keys(claimAcks)).toHaveLength(1);
    // First write wins; snapshot replay must not trample the canonical row.
    expect(claimAcks["1"]?.setAcceptRejectCode).toBe("A");
  });

  it("test_reset_claim_acks_clears_only_claim_acks_slice", () => {
    const { addClaimAck, addAck, reset } = useTailStore.getState();
    addClaimAck(makeClaimAck(1));
    addAck(makeAck(1));

    reset("claim-acks");
    const s = useTailStore.getState();
    expect(Object.keys(s.claimAcks)).toHaveLength(0);
    // The other slices are untouched.
    expect(Object.keys(s.acks)).toHaveLength(1);
  });

  it("test_claim_acks_slice_evicts_oldest_when_over_10000", () => {
    // Cap-eviction smoke test — same shape as the claims FIFO test
    // above. 5 over the cap to exercise the eviction path without
    // pinning down the wall-clock cost of a full 10 005-insert run.
    const N = TAIL_CAP + 5;
    const { addClaimAck } = useTailStore.getState();
    for (let i = 0; i < N; i++) {
      addClaimAck(makeClaimAck(i));
    }
    const { claimAcks } = useTailStore.getState();
    expect(Object.keys(claimAcks)).toHaveLength(TAIL_CAP);
    // The five oldest ids must be gone.
    expect(claimAcks["0"]).toBeUndefined();
    expect(claimAcks["4"]).toBeUndefined();
    // The most recent id must be present.
    expect(claimAcks[String(N - 1)]).toBeDefined();
  }, 60_000);
});
