/**
 * LS7 — `useConversationThread`: initial load = latest page, `loadOlder` prepends
 * (no dup, chronological), `hasMore=false` stops, `refreshTail` appends new turns
 * (no dup), and `busy`/`error` states.
 */
import { describe, it, expect } from "vitest";
import { renderHook, waitFor, act } from "@testing-library/react";
import type { ReactNode } from "react";

import { DIProvider } from "@/app/di";
import { MockConversationGateway } from "@/adapters/mock";
import type { ConversationGateway, Gateways } from "@/ports";
import type { TurnView } from "@/domain";
import { useConversationThread } from "./useConversationThread";

const P = "proj";
const C = "conv";

function turn(n: number): TurnView {
  return {
    id: `t${n}`,
    atMs: 1_700_000_000_000 + n,
    role: n % 2 === 0 ? "response" : "prompt",
    source: { kind: "human" },
    text: `texte ${n}`,
    textLen: 8,
  };
}

function wrapperFor(conversation: ConversationGateway) {
  const gateways = { conversation } as unknown as Gateways;
  return function Wrapper({ children }: { children: ReactNode }) {
    return <DIProvider gateways={gateways}>{children}</DIProvider>;
  };
}

function seededGateway(count: number): MockConversationGateway {
  const gw = new MockConversationGateway();
  gw._setTurns(
    P,
    C,
    Array.from({ length: count }, (_, i) => turn(i + 1)),
  );
  return gw;
}

describe("useConversationThread", () => {
  it("loads the latest page on open (backward, most recent)", async () => {
    const gw = seededGateway(120);
    const { result } = renderHook(() => useConversationThread(P, C), {
      wrapper: wrapperFor(gw),
    });

    await waitFor(() => expect(result.current.turns.length).toBe(50));
    // Most recent 50 (t71..t120), ascending, ending on the newest.
    expect(result.current.turns[0].id).toBe("t71");
    expect(result.current.turns.at(-1)?.id).toBe("t120");
    expect(result.current.hasMore).toBe(true);
    expect(result.current.busy).toBe(false);
    expect(result.current.error).toBeNull();
  });

  it("loadOlder prepends older turns without duplicates, preserving order", async () => {
    const gw = seededGateway(120);
    const { result } = renderHook(() => useConversationThread(P, C), {
      wrapper: wrapperFor(gw),
    });
    await waitFor(() => expect(result.current.turns.length).toBe(50));

    await act(async () => {
      await result.current.loadOlder();
    });

    // t21..t70 prepended before t71..t120 ⇒ t21..t120 (100), strictly ascending.
    expect(result.current.turns.length).toBe(100);
    expect(result.current.turns[0].id).toBe("t21");
    expect(result.current.turns.at(-1)?.id).toBe("t120");
    // No duplicate ids, order is the numeric order.
    const ids = result.current.turns.map((t) => t.id);
    expect(new Set(ids).size).toBe(100);
    const nums = ids.map((id) => Number(id.slice(1)));
    expect(nums).toEqual([...nums].sort((a, b) => a - b));
  });

  it("loadOlder is a no-op (no growth) when there is nothing older", async () => {
    const gw = seededGateway(30); // fits in one default page (50)
    const { result } = renderHook(() => useConversationThread(P, C), {
      wrapper: wrapperFor(gw),
    });
    await waitFor(() => expect(result.current.turns.length).toBe(30));
    expect(result.current.hasMore).toBe(false);

    await act(async () => {
      await result.current.loadOlder();
    });

    // Nothing strictly before the oldest ⇒ buffer unchanged, hasMore stays false.
    expect(result.current.turns.length).toBe(30);
    expect(result.current.hasMore).toBe(false);
  });

  it("refreshTail appends newly-arrived turns without duplicates", async () => {
    const gw = seededGateway(10);
    const { result } = renderHook(() => useConversationThread(P, C), {
      wrapper: wrapperFor(gw),
    });
    await waitFor(() => expect(result.current.turns.length).toBe(10));

    // Five new turns arrive at the tail of the thread.
    gw._setTurns(
      P,
      C,
      Array.from({ length: 15 }, (_, i) => turn(i + 1)),
    );

    await act(async () => {
      await result.current.refreshTail();
    });

    // t11..t15 appended after t1..t10 ⇒ t1..t15, no duplicate.
    expect(result.current.turns.map((t) => t.id)).toEqual(
      Array.from({ length: 15 }, (_, i) => `t${i + 1}`),
    );
  });

  it("surfaces an error and clears busy when the gateway rejects", async () => {
    const failing: ConversationGateway = {
      readPage: () => Promise.reject(new Error("boom-store")),
    };
    const { result } = renderHook(() => useConversationThread(P, C), {
      wrapper: wrapperFor(failing),
    });

    await waitFor(() => expect(result.current.error).toContain("boom-store"));
    expect(result.current.busy).toBe(false);
    expect(result.current.turns).toEqual([]);
  });

  it("toggles busy while a page request is in flight", async () => {
    let release!: (page: { turns: TurnView[]; hasMore: boolean }) => void;
    const gated: ConversationGateway = {
      readPage: () =>
        new Promise((resolve) => {
          release = resolve;
        }),
    };
    const { result } = renderHook(() => useConversationThread(P, C), {
      wrapper: wrapperFor(gated),
    });

    // The open() call is pending ⇒ busy is true.
    await waitFor(() => expect(result.current.busy).toBe(true));

    await act(async () => {
      release({ turns: [turn(1)], hasMore: false });
    });
    await waitFor(() => expect(result.current.busy).toBe(false));
    expect(result.current.turns.map((t) => t.id)).toEqual(["t1"]);
  });
});
