/**
 * LS7 — `ProjectsView` integration: clicking a conversation in the Work panel swaps
 * the terminal grid for the read-only `ConversationViewer`; the back button returns
 * to the grid; switching project resets the viewer; and with no conversation open
 * the terminal grid is shown unchanged (non-regression).
 */
import { describe, it, expect } from "vitest";
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";

import {
  MockAgentGateway,
  MockGitGateway,
  MockLayoutGateway,
  MockProfileGateway,
  MockProjectGateway,
  MockSystemGateway,
  MockTemplateGateway,
  MockTerminalGateway,
  MockWindowGateway,
} from "@/adapters/mock";
import type {
  ConversationGateway,
  Gateways,
  WorkStateGateway,
} from "@/ports";
import type { ProjectWorkState } from "@/domain";
import { DIProvider } from "@/app/di";
import { ProjectsView } from "./ProjectsView";

const CONV_ID = "conversation-open-me";

/** A work-state gateway that returns a fixed, clickable conversation for any project. */
function fixedWorkState(): WorkStateGateway {
  const state: ProjectWorkState = {
    agents: [
      {
        agentId: "agent-1",
        name: "Worker",
        profileId: "codex",
        busy: { state: "idle" },
        tickets: [
          {
            ticketId: "ticket-1",
            conversationId: CONV_ID,
            position: 0,
            status: "inProgress",
            source: { kind: "human" },
            requesterLabel: "Anthony",
            taskPreview: "do the thing",
            taskLen: 12,
          },
        ],
      },
    ],
    conversations: [
      {
        conversationId: CONV_ID,
        status: "ready",
        objectivePreview: "Objectif du fil",
        summaryPreview: "Résumé du fil",
        summaryLen: 13,
        upTo: "turn-1",
        recentTurns: [],
      },
    ],
  };
  return {
    getProjectWorkState: async () => structuredClone(state),
    attachBackgroundTask: async (taskId) => ({
      taskId,
      scrollback: new Uint8Array(),
      live: false,
      detach: () => {},
    }),
    cancelBackgroundTask: async () => {},
    retryBackgroundTask: async () => {},
  };
}

/** A conversation gateway that returns one identifiable turn for any thread. */
function fixedConversation(): ConversationGateway {
  return {
    readPage: async () => ({
      turns: [
        {
          id: "t1",
          atMs: 1_700_000_000_000,
          role: "prompt",
          source: { kind: "human" },
          text: "contenu du fil ouvert",
          textLen: 21,
        },
      ],
      hasMore: false,
    }),
  };
}

function renderView(
  project: MockProjectGateway,
  overrides: {
    agent?: MockAgentGateway;
    system?: MockSystemGateway;
  } = {},
) {
  const agent = overrides.agent ?? new MockAgentGateway();
  const system = overrides.system ?? new MockSystemGateway();
  const gateways = {
    system,
    project,
    agent,
    profile: new MockProfileGateway(),
    template: new MockTemplateGateway(agent),
    git: new MockGitGateway(),
    layout: new MockLayoutGateway(),
    terminal: new MockTerminalGateway(),
    workState: fixedWorkState(),
    conversation: fixedConversation(),
    window: new MockWindowGateway(),
  } as unknown as Gateways;
  return render(
    <DIProvider gateways={gateways}>
      <ProjectsView />
    </DIProvider>,
  );
}

/**
 * Opens a panel floating via the single « Panneaux » menu (#26): menu → panel
 * entry (opens its placement submenu) → « Flottant ».
 */
function openPanel(panelTitle: string) {
  fireEvent.click(screen.getByRole("button", { name: "Panneaux" }));
  fireEvent.click(screen.getByRole("button", { name: panelTitle }));
  fireEvent.click(screen.getByRole("button", { name: "Flottant" }));
}

async function openProjectAndWorkTab(label: string) {
  await screen.findByText(label);
  // Open the project whose root is `label` from the welcome projects list.
  const li = screen
    .getAllByRole("listitem")
    .find((node) => within(node).queryByText(label));
  fireEvent.click(within(li!).getByRole("button", { name: "Open" }));
  await screen.findByRole("tab");
  // Open the Work panel window from the View menu.
  openPanel("Work state");
}

describe("ProjectsView — LS7 conversation viewer integration", () => {
  it("opens the viewer on a conversation click and returns to the grid on back", async () => {
    const project = new MockProjectGateway();
    await project.createProject("alpha", "/p/a");
    renderView(project);

    await openProjectAndWorkTab("/p/a");

    // Before: the terminal grid owns the main area (non-regression baseline).
    expect(await screen.findByTestId("layout-grid")).toBeTruthy();
    expect(
      screen.queryByRole("button", { name: "← Retour aux terminaux" }),
    ).toBeNull();

    // Click the conversation row in the Work panel.
    const open = await screen.findByRole("button", {
      name: `open conversation ${CONV_ID}`,
    });
    fireEvent.click(open);

    // After: the viewer replaced the grid in the main area.
    expect(
      await screen.findByRole("button", { name: "← Retour aux terminaux" }),
    ).toBeTruthy();
    expect(await screen.findByText("contenu du fil ouvert")).toBeTruthy();
    expect(screen.queryByTestId("layout-grid")).toBeNull();

    // Back ⇒ the terminal grid is restored, the viewer is gone.
    fireEvent.click(
      screen.getByRole("button", { name: "← Retour aux terminaux" }),
    );
    expect(await screen.findByTestId("layout-grid")).toBeTruthy();
    expect(
      screen.queryByRole("button", { name: "← Retour aux terminaux" }),
    ).toBeNull();
  });

  it("resets the viewer when the active project changes", async () => {
    const project = new MockProjectGateway();
    await project.createProject("alpha", "/p/a");
    await project.createProject("beta", "/p/b");
    renderView(project);

    // Open BOTH projects as tabs. Use the projects manager window so the list
    // persists across activation (the inline welcome list is replaced by the
    // grid once a project is active).
    await screen.findByText("/p/a");
    openPanel("Projects");
    for (const root of ["/p/a", "/p/b"]) {
      const li = screen
        .getAllByRole("listitem")
        .find((node) => within(node).queryByText(root));
      fireEvent.click(within(li!).getByRole("button", { name: "Open" }));
    }
    await waitFor(() => expect(screen.getAllByRole("tab")).toHaveLength(2));

    // Make alpha the active tab, then open its conversation viewer.
    fireEvent.click(screen.getByRole("tab", { name: "alpha" }));
    openPanel("Work state");
    fireEvent.click(
      await screen.findByRole("button", { name: `open conversation ${CONV_ID}` }),
    );
    await screen.findByRole("button", { name: "← Retour aux terminaux" });

    // Switch the active project (top tab) ⇒ viewer resets to the grid.
    fireEvent.click(screen.getByRole("tab", { name: "beta" }));

    await waitFor(() =>
      expect(
        screen.queryByRole("button", { name: "← Retour aux terminaux" }),
      ).toBeNull(),
    );
    expect(await screen.findByTestId("layout-grid")).toBeTruthy();
  });

  it("shows the terminal grid unchanged when no conversation is open (non-regression)", async () => {
    const project = new MockProjectGateway();
    await project.createProject("alpha", "/p/a");
    renderView(project);

    await screen.findByText("/p/a");
    fireEvent.click(screen.getByRole("button", { name: "Open" }));
    await screen.findByRole("tab");

    // No conversation opened ⇒ the main area is the terminal grid, no viewer.
    expect(await screen.findByTestId("layout-grid")).toBeTruthy();
    expect(
      screen.queryByRole("button", { name: "← Retour aux terminaux" }),
    ).toBeNull();
  });

  it("labels rendezvous completion toasts with requester and target, then opens the conversation", async () => {
    const project = new MockProjectGateway();
    const created = await project.createProject("alpha", "/p/a");
    const agent = new MockAgentGateway();
    const system = new MockSystemGateway();
    const requester = await agent.createAgent(created.id, {
      name: "Main",
      profileId: "codex",
    });
    const target = await agent.createAgent(created.id, {
      name: "DevBackend",
      profileId: "codex",
    });
    renderView(project, { agent, system });

    await openProjectAndWorkTab("/p/a");
    system.emit({
      type: "backgroundTaskChanged",
      projectId: created.id,
      agentId: target.id,
      taskId: "task-rendezvous-91",
      state: "completed",
      requesterAgentId: requester.id,
      targetAgentId: target.id,
      conversationId: CONV_ID,
    });

    const toast = await screen.findByRole("button", {
      name: /Appel Main -> DevBackend terminé/i,
    });
    expect(within(toast).getByText("alpha · Task task-ren")).toBeTruthy();

    fireEvent.click(toast);

    expect(
      await screen.findByRole("button", { name: "← Retour aux terminaux" }),
    ).toBeTruthy();
    expect(await screen.findByText("contenu du fil ouvert")).toBeTruthy();
    expect(
      screen.queryByRole("button", { name: /Appel Main -> DevBackend terminé/i }),
    ).toBeNull();
  });

  it("labels rendezvous failure and cancellation toasts with explicit call attribution", async () => {
    const project = new MockProjectGateway();
    const created = await project.createProject("alpha", "/p/a");
    const agent = new MockAgentGateway();
    const system = new MockSystemGateway();
    const requester = await agent.createAgent(created.id, {
      name: "Main",
      profileId: "codex",
    });
    const target = await agent.createAgent(created.id, {
      name: "DevBackend",
      profileId: "codex",
    });
    renderView(project, { agent, system });

    await openProjectAndWorkTab("/p/a");
    system.emit({
      type: "backgroundTaskChanged",
      projectId: created.id,
      agentId: target.id,
      taskId: "task-rendezvous-failed",
      state: "failed",
      requesterAgentId: requester.id,
      targetAgentId: target.id,
    });
    system.emit({
      type: "backgroundTaskChanged",
      projectId: created.id,
      agentId: target.id,
      taskId: "task-rendezvous-cancelled",
      state: "cancelled",
      requesterAgentId: requester.id,
      targetAgentId: target.id,
    });

    expect(
      await screen.findByRole("button", {
        name: /Appel Main -> DevBackend en échec/i,
      }),
    ).toBeTruthy();
    expect(
      await screen.findByRole("button", {
        name: /Appel Main -> DevBackend annulé/i,
      }),
    ).toBeTruthy();
  });

  it("keeps the generic background-task toast when rendezvous agent ids are absent", async () => {
    const project = new MockProjectGateway();
    const created = await project.createProject("alpha", "/p/a");
    const system = new MockSystemGateway();
    renderView(project, { system });

    await openProjectAndWorkTab("/p/a");
    system.emit({
      type: "backgroundTaskChanged",
      projectId: created.id,
      agentId: "agent-generic-1",
      taskId: "task-generic-1",
      state: "failed",
    });

    const toast = await screen.findByRole("button", {
      name: /Background task failed/i,
    });
    expect(within(toast).getByText("alpha · agent-ge · task-gen")).toBeTruthy();

    fireEvent.click(toast);

    expect(
      screen.queryByRole("button", { name: "← Retour aux terminaux" }),
    ).toBeNull();
    expect(await screen.findByText("Work")).toBeTruthy();
  });

  it("prefixes task toast context with a truncatable project-name line and falls back when unresolved", async () => {
    const project = new MockProjectGateway();
    const created = await project.createProject(
      "alpha with a very long project name that must stay on one line",
      "/p/a",
    );
    const system = new MockSystemGateway();
    renderView(project, { system });

    await openProjectAndWorkTab("/p/a");
    system.emit({
      type: "backgroundTaskChanged",
      projectId: created.id,
      agentId: "agent-long-name-context",
      taskId: "task-long-name-context",
      state: "completed",
    });
    system.emit({
      type: "backgroundTaskChanged",
      projectId: "project-not-in-view",
      agentId: "agent-fallback-context",
      taskId: "task-fallback-context",
      state: "failed",
    });

    const longNameLine = await screen.findByText(
      "alpha with a very long project name that must stay on one line · agent-lo · task-lon",
    );
    expect(longNameLine.className).toContain("truncate");
    expect(
      await screen.findByText("Projet inconnu · agent-fa · task-fal"),
    ).toBeTruthy();
  });
});
