/**
 * LS7 — `ConversationViewer`: chronological turns, role/source labels, header
 * User↔Agent vs Agent↔Agent, collapsible tool activity, empty/error/loading
 * states, scroll-up "load older" trigger, and the back button (`onClose`).
 */
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import type { ReactNode } from "react";

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

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

function human(n: number, text = `humain ${n}`): TurnView {
  return {
    id: `t${n}`,
    atMs: 1_700_000_000_000 + n,
    role: "prompt",
    source: { kind: "human" },
    text,
    textLen: text.length,
  };
}
function agent(n: number, agentId: string, text = `agent ${n}`): TurnView {
  return {
    id: `t${n}`,
    atMs: 1_700_000_000_000 + n,
    role: "response",
    source: { kind: "agent", agentId },
    text,
    textLen: text.length,
  };
}

function renderViewer(opts: {
  conversation: ConversationGateway;
  workState?: MockWorkStateGateway;
  onClose?: () => void;
}) {
  const workState = opts.workState ?? new MockWorkStateGateway();
  const gateways = {
    conversation: opts.conversation,
    workState,
  } as unknown as Gateways;
  const onClose = opts.onClose ?? vi.fn();
  const ui = (children: ReactNode) => (
    <DIProvider gateways={gateways}>{children}</DIProvider>
  );
  return {
    onClose,
    ...render(
      ui(
        <ConversationViewer
          projectId={P}
          conversationId={C}
          onClose={onClose}
        />,
      ),
    ),
  };
}

function seeded(turns: TurnView[]): MockConversationGateway {
  const gw = new MockConversationGateway();
  gw._setTurns(P, C, turns);
  return gw;
}

describe("ConversationViewer", () => {
  it("renders turns chronologically with their full text", async () => {
    const gw = seeded([human(1, "première"), agent(2, "a7", "deuxième")]);
    renderViewer({ conversation: gw });

    expect(await screen.findByText("première")).toBeTruthy();
    expect(screen.getByText("deuxième")).toBeTruthy();
    // Role labels are rendered.
    expect(screen.getAllByText(/· Prompt/).length).toBeGreaterThan(0);
    expect(screen.getAllByText(/· Response/).length).toBeGreaterThan(0);
  });

  it("titles the header User ↔ Agent and resolves the agent name from work-state", async () => {
    const gw = seeded([human(1), agent(2, "agent-7")]);
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(P, {
      agents: [
        { agentId: "agent-7", name: "Architecte", profileId: "x", busy: { state: "idle" } },
      ],
    });
    renderViewer({ conversation: gw, workState });

    expect(await screen.findByText("User ↔ Architecte")).toBeTruthy();
  });

  it("titles the header Agent ↔ Agent for a delegation thread", async () => {
    const gw = seeded([agent(1, "agent-a"), agent(2, "agent-b")]);
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(P, {
      agents: [
        { agentId: "agent-a", name: "Alpha", profileId: "x", busy: { state: "idle" } },
        { agentId: "agent-b", name: "Bravo", profileId: "x", busy: { state: "idle" } },
      ],
    });
    renderViewer({ conversation: gw, workState });

    expect(await screen.findByText("Alpha ↔ Bravo")).toBeTruthy();
  });

  it("dims and collapses tool activity, expanding on click", async () => {
    const gw = seeded([
      human(1),
      { ...agent(2, "a7"), role: "toolActivity", text: "détails outillés secrets" },
    ]);
    renderViewer({ conversation: gw });

    const toggle = await screen.findByRole("button", { name: /Tool activity/ });
    // Collapsed by default ⇒ the tool text is hidden.
    expect(toggle.getAttribute("aria-expanded")).toBe("false");
    expect(screen.queryByText("détails outillés secrets")).toBeNull();

    fireEvent.click(toggle);
    expect(toggle.getAttribute("aria-expanded")).toBe("true");
    expect(screen.getByText("détails outillés secrets")).toBeTruthy();
  });

  it("shows the empty state for a conversation with no turns", async () => {
    renderViewer({ conversation: seeded([]) });
    expect(await screen.findByText("Aucun tour pour le moment.")).toBeTruthy();
  });

  it("shows the loading state while the first page is in flight", async () => {
    const gated: ConversationGateway = { readPage: () => new Promise(() => {}) };
    renderViewer({ conversation: gated });
    expect(await screen.findByText("Chargement du fil…")).toBeTruthy();
  });

  it("shows an error alert when the gateway rejects", async () => {
    const failing: ConversationGateway = {
      readPage: () => Promise.reject(new Error("lecture impossible")),
    };
    renderViewer({ conversation: failing });
    const alert = await screen.findByRole("alert");
    expect(alert.textContent).toContain("lecture impossible");
  });

  it("offers a 'load older' control when more turns exist and triggers loadOlder", async () => {
    // 120 turns ⇒ the first page (latest 50) has older turns ⇒ the button appears.
    const gw = seeded(
      Array.from({ length: 120 }, (_, i) => human(i + 1, `tour ${i + 1}`)),
    );
    renderViewer({ conversation: gw });

    const older = await screen.findByRole("button", {
      name: "Charger les tours précédents",
    });
    // Before: the oldest visible is t71.
    expect(screen.queryByText("tour 21")).toBeNull();
    fireEvent.click(older);
    // After loadOlder: older turns (e.g. tour 21) are prepended into the view.
    expect(await screen.findByText("tour 21")).toBeTruthy();
  });

  it("calls onClose when the back button is clicked", async () => {
    const gw = seeded([human(1)]);
    const { onClose } = renderViewer({ conversation: gw });
    fireEvent.click(
      await screen.findByRole("button", { name: "← Retour aux terminaux" }),
    );
    expect(onClose).toHaveBeenCalledTimes(1);
  });
});
