// @vitest-environment happy-dom
// React's act-aware env — mirror the other hook tests.
(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 } from "vitest";
import { useMergedTail } from "./useMergedTail";
import { useTailStore } from "@/store/tail-store";
import type { Claim, Remittance, Activity } from "@/types";

/**
 * Same `renderHook` shim used in `useClaimDetail`, `useDrawerUrlState`,
 * etc. — the project doesn't ship `@testing-library/react`, so we wire a
 * Probe component into a real `createRoot` and read the hook's return
 * value through a shared `result` object. `act` flushes React's state
 * updates between micro-tasks.
 */
function renderHook<TResult>(setup: () => TResult): {
  result: { current: TResult | undefined };
  unmount: () => void;
} {
  const result: { current: TResult | undefined } = { current: undefined };
  const container = document.createElement("div");
  document.body.appendChild(container);

  function Probe() {
    result.current = setup();
    return null;
  }

  const root: Root = createRoot(container);
  act(() => {
    root.render(React.createElement(Probe));
  });

  return {
    result,
    unmount: () => {
      act(() => root.unmount());
      container.remove();
    },
  };
}

// ---------------------------------------------------------------------------
// Sample factories. The hook is generic over `T extends { id: string }`,
// so for `claims` we use Claim, for `remittances` we use Remittance, etc.
// Tests populate the store via `useTailStore.getState().addX(...)` — the
// same path the live-tail hook uses — so the merge is exercised against
// the real store shape (including the `claimOrder`/`remitOrder` indexing
// the store maintains for the keyed-by-id slices).
// ---------------------------------------------------------------------------

function claim(id: string, patientName: string): Claim {
  return {
    id,
    patientName,
    providerNpi: "1234567890",
    payerName: "Medicaid",
    cptCode: "99213",
    billedAmount: 100,
    receivedAmount: 0,
    status: "submitted",
    submissionDate: "2026-06-20T00:00:00Z",
  };
}

function remit(id: string, claimId: string): Remittance {
  return {
    id,
    claimId,
    payerName: "Medicaid",
    paidAmount: 100,
    adjustmentAmount: 0,
    receivedDate: "2026-06-20",
    checkNumber: id,
    status: "received",
  };
}

function activity(id: string, message: string): Activity {
  return {
    id,
    kind: "claim_submitted",
    message,
    timestamp: "2026-06-20T00:00:00Z",
  };
}

describe("useMergedTail", () => {
  beforeEach(() => {
    // Singleton store — clear each slice between tests so cases are
    // independent. Mirrors the pattern in `tail-store.test.ts`.
    useTailStore.getState().reset("claims");
    useTailStore.getState().reset("remittances");
    useTailStore.getState().reset("activity");
  });

  it("test_merges_base_and_tail_by_id_base_first", () => {
    // Base: [A, B]. Tail store adds [B, C, D] — note B is a duplicate
    // of a base item (mirrors what a snapshot replay on reconnect would
    // produce). After dedup, tail should contribute only [C, D].
    const base = [claim("A", "Alice"), claim("B", "Bob")];
    const { addClaim } = useTailStore.getState();
    addClaim(claim("B", "Bob-Updated")); // deduped — first write wins
    addClaim(claim("C", "Carol"));
    addClaim(claim("D", "Dave"));

    const { result, unmount } = renderHook(() =>
      useMergedTail("claims", base),
    );

    const merged = result.current ?? [];
    // Base items first, in their original order; then tail in arrival
    // order, with duplicates of base ids dropped.
    expect(merged.map((c) => c.id)).toEqual(["A", "B", "C", "D"]);
    // The base version of B must be preserved (the store's first-write-
    // wins rule keeps "Bob", not "Bob-Updated").
    expect(merged[1]?.patientName).toBe("Bob");

    unmount();
  });

  it("test_filter_predicate_drops_tail_items_that_dont_match", () => {
    // Base: [A]. Tail store: [B, C, D]. Predicate `id !== "D"` drops
    // the trailing item — the filter must run AFTER dedup and only
    // affect the tail contribution.
    const base = [claim("A", "Alice")];
    const { addClaim } = useTailStore.getState();
    addClaim(claim("B", "Bob"));
    addClaim(claim("C", "Carol"));
    addClaim(claim("D", "Dave"));

    const { result, unmount } = renderHook(() =>
      useMergedTail("claims", base, (c) => c.id !== "D"),
    );

    const merged = result.current ?? [];
    expect(merged.map((c) => c.id)).toEqual(["A", "B", "C"]);

    unmount();
  });

  it("test_empty_tail_store_returns_base_unchanged", () => {
    const base = [claim("A", "Alice"), claim("B", "Bob")];

    const { result, unmount } = renderHook(() =>
      useMergedTail("claims", base),
    );

    const merged = result.current ?? [];
    expect(merged).toHaveLength(2);
    expect(merged.map((c) => c.id)).toEqual(["A", "B"]);
    // The hook returns base verbatim when the tail slice is empty —
    // no copy churn, so `toBe` reference equality holds.
    expect(merged[0]).toBe(base[0]);
    expect(merged[1]).toBe(base[1]);

    unmount();
  });

  it("test_new_tail_item_appears_after_base", async () => {
    // Base: [A, B]. Tail store empty initially. After the component
    // mounts, we push a new claim to the store; the merged list must
    // include the new tail item, surfaced via re-render. (Plan calls
    // this "appears_at_top" — in this hook's contract the new item
    // comes AFTER base items, not before, so the assertion is that it
    // appears at all and is positioned correctly.)
    const base = [claim("A", "Alice"), claim("B", "Bob")];

    const { result, unmount } = renderHook(() =>
      useMergedTail("claims", base),
    );

    expect(result.current?.map((c) => c.id)).toEqual(["A", "B"]);

    // New item lands in the store → zustand notifies subscribers → the
    // hook re-renders with the merged result.
    await act(async () => {
      useTailStore.getState().addClaim(claim("C", "Carol"));
    });

    const merged = result.current ?? [];
    // Base first, then tail in arrival order.
    expect(merged.map((c) => c.id)).toEqual(["A", "B", "C"]);
    expect(merged[2]?.patientName).toBe("Carol");

    unmount();
  });

  it("test_activity_resource_uses_array_slice_and_dedupes_by_id", () => {
    // Activity doesn't have a keyed-by-id map — it's a plain array.
    // The hook must still dedup against baseItems by id (Activity has
    // an id, even though the store's addActivity doesn't dedup).
    const base = [activity("A", "alpha"), activity("B", "bravo")];
    const { addActivity } = useTailStore.getState();
    addActivity(activity("B", "bravo-dup")); // duplicate id
    addActivity(activity("C", "charlie"));
    addActivity(activity("D", "delta"));

    const { result, unmount } = renderHook(() =>
      useMergedTail("activity", base),
    );

    const merged = result.current ?? [];
    // Base first, then tail in arrival order. Tail's "B" is dropped
    // because it's a duplicate of base's "B".
    expect(merged.map((a) => a.id)).toEqual(["A", "B", "C", "D"]);

    unmount();
  });

  it("test_remittance_resource_orders_by_remitOrder", () => {
    // Smoke test for the remittances slice — exercise the second keyed
    // slice so we know the claim/remit branches are symmetric.
    const base = [remit("R-1", "CLM-1")];
    const { addRemittance } = useTailStore.getState();
    addRemittance(remit("R-2", "CLM-2"));
    addRemittance(remit("R-3", "CLM-3"));

    const { result, unmount } = renderHook(() =>
      useMergedTail("remittances", base),
    );

    const merged = result.current ?? [];
    expect(merged.map((r) => r.id)).toEqual(["R-1", "R-2", "R-3"]);

    unmount();
  });
});
