/**
 * Ticket #86, lot 2 — the web workspace's Tickets/Sprints tabs.
 *
 * Pins the carnet #86 UX contract exercised through the real `WebWorkspace`
 * (project list → tab navigation), driven entirely by the same
 * `MockTicketGateway`/`MockSystemGateway` pair the desktop ticket tests use, so
 * behaviour (event-driven refresh, optimistic concurrency, sprint semantics)
 * stays identical — only the presentation is web-specific.
 */
import { describe, it, expect } from "vitest";
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";

import { DIProvider } from "@/app/di";
import { createMockGateways, MockTicketGateway, MockWorkStateGateway } from "@/adapters/mock";
import type { Gateways } from "@/ports";
import type { ProjectWorkState, Ticket } from "@/domain";
import { WebWorkspace } from "../WebWorkspace";

function ticket(over: Partial<Ticket> = {}): Ticket {
  return {
    id: "t-1",
    ref: "#1",
    number: 1,
    title: "Ajouter tickets/sprints au web",
    description: "",
    status: "open",
    priority: "medium",
    sprintId: null,
    links: [],
    assignedAgentIds: [],
    attachments: [],
    createdBy: { kind: "user" },
    updatedBy: { kind: "user" },
    createdAt: 1,
    updatedAt: 1,
    version: 1,
    ...over,
  };
}

async function setup() {
  const gateways = createMockGateways();
  const project = await gateways.project.createProject("IdeA", "/srv/idea");
  return { gateways, projectId: project.id };
}

/**
 * Seeds a project's work-state with one idle agent. `WebTicketDetail`/
 * `WebTicketsView` resolve the assignable agent roster from
 * `get_project_work_state` (not `list_agents` — absent from the web-server
 * command allowlist, see `useWebProjectAgents`), so tests that need an
 * assignable agent must seed it here rather than via `MockAgentGateway`.
 */
function seedAgent(gateways: Gateways, projectId: string, agentId: string, name: string): void {
  const state: ProjectWorkState = {
    agents: [
      {
        agentId,
        name,
        profileId: "p1",
        busy: { state: "idle" },
        tickets: [],
      },
    ],
    conversations: [],
  };
  (gateways.workState as MockWorkStateGateway)._setProjectWorkState(projectId, state);
}

function renderWorkspace(gateways: Gateways) {
  return render(
    <DIProvider gateways={gateways}>
      <WebWorkspace />
    </DIProvider>,
  );
}

/** Opens the panel named `panelLabel` ("Tickets"/"Sprints"/…) via the "Panneaux" menu. */
async function openProjectAndTab(gateways: Gateways, panelLabel: string) {
  renderWorkspace(gateways);
  fireEvent.click(await screen.findByText("IdeA"));
  fireEvent.click(await screen.findByRole("button", { name: /^Panneaux :/ }));
  fireEvent.click(await screen.findByRole("button", { name: panelLabel }));
}

describe("WebWorkspace — project tabs (ticket #86)", () => {
  it("defaults to the Travail panel and lets the user switch to Tickets/Sprints via Panneaux", async () => {
    const { gateways } = await setup();
    renderWorkspace(gateways);
    fireEvent.click(await screen.findByText("IdeA"));

    expect(await screen.findByRole("button", { name: "Panneaux : Travail" })).toBeTruthy();

    fireEvent.click(screen.getByRole("button", { name: "Panneaux : Travail" }));
    fireEvent.click(await screen.findByRole("button", { name: "Tickets" }));
    expect(await screen.findByRole("heading", { name: "Tickets" })).toBeTruthy();

    fireEvent.click(screen.getByRole("button", { name: "Panneaux : Tickets" }));
    fireEvent.click(await screen.findByRole("button", { name: "Sprints" }));
    expect(await screen.findByRole("heading", { name: "Sprints" })).toBeTruthy();
  });

  it("renders the ticket list grouped by sprint, in French", async () => {
    const { gateways, projectId } = await setup();
    const tk = gateways.ticket as MockTicketGateway;
    tk._seedSprint(projectId, { id: "s1", order: 1, name: "Sprint courant" });
    tk._seedTicket(projectId, ticket({ ref: "#1", number: 1, title: "Ajouter tickets/sprints au web", sprintId: "s1" }));
    tk._seedTicket(projectId, ticket({ ref: "#2", number: 2, title: "Langue uniforme Settings", status: "closed", priority: "low" }));

    await openProjectAndTab(gateways, "Tickets");

    const list = await screen.findByTestId("web-ticket-list");
    expect(within(list).getByText("Sprint courant")).toBeTruthy();
    expect(within(list).getByText("Sans sprint")).toBeTruthy();
    expect(
      screen.getByRole("button", {
        name: "#1, Ajouter tickets/sprints au web, statut Ouvert, priorité Moyenne",
      }),
    ).toBeTruthy();
    expect(
      screen.getByRole("button", {
        name: "#2, Langue uniforme Settings, statut Fermé, priorité Faible",
      }),
    ).toBeTruthy();
  });

  it("shows the empty state with a create shortcut when the project has no tickets", async () => {
    const { gateways } = await setup();
    await openProjectAndTab(gateways, "Tickets");

    expect(await screen.findByText("Aucun ticket dans ce projet.")).toBeTruthy();
    expect(screen.getByRole("button", { name: "Créer un ticket" })).toBeTruthy();
  });

  it("creates a ticket and opens its detail", async () => {
    const { gateways } = await setup();
    await openProjectAndTab(gateways, "Tickets");
    await screen.findByText("Aucun ticket dans ce projet.");

    fireEvent.click(screen.getByRole("button", { name: "+ Ticket" }));
    expect(await screen.findByRole("heading", { name: "Nouveau ticket" })).toBeTruthy();

    fireEvent.change(screen.getByLabelText("Titre"), { target: { value: "Un nouveau ticket" } });
    fireEvent.click(screen.getByRole("button", { name: "Créer" }));

    await waitFor(() => expect(screen.getByText("Un nouveau ticket")).toBeTruthy());
    // Landed on the detail screen (back button visible), not the list.
    expect(screen.getByRole("button", { name: "← Tickets" })).toBeTruthy();
  });

  it("changes a ticket's status immediately from the detail view", async () => {
    const { gateways, projectId } = await setup();
    const tk = gateways.ticket as MockTicketGateway;
    tk._seedTicket(projectId, ticket());

    await openProjectAndTab(gateways, "Tickets");
    fireEvent.click(
      await screen.findByRole("button", {
        name: "#1, Ajouter tickets/sprints au web, statut Ouvert, priorité Moyenne",
      }),
    );

    const statusSelect = await screen.findByLabelText("Statut");
    fireEvent.change(statusSelect, { target: { value: "closed" } });

    await waitFor(async () => {
      const updated = await tk.read(projectId, "#1");
      expect(updated.status).toBe("closed");
    });
  });

  it("changes a ticket's sprint from the detail view, including clearing it back to Sans sprint", async () => {
    const { gateways, projectId } = await setup();
    const tk = gateways.ticket as MockTicketGateway;
    tk._seedSprint(projectId, { id: "s1", order: 1, name: "Sprint courant" });
    tk._seedSprint(projectId, { id: "s2", order: 2, name: "Sprint suivant" });
    tk._seedTicket(projectId, ticket());

    await openProjectAndTab(gateways, "Tickets");
    fireEvent.click(
      await screen.findByRole("button", {
        name: "#1, Ajouter tickets/sprints au web, statut Ouvert, priorité Moyenne",
      }),
    );

    const sprintSelect = await screen.findByLabelText("Sprint");
    fireEvent.change(sprintSelect, { target: { value: "s2" } });

    await waitFor(async () => {
      const updated = await tk.read(projectId, "#1");
      expect(updated.sprintId).toBe("s2");
    });

    // Clear it back to "Sans sprint" (ticket_unassign_sprint via setSprint(null)).
    fireEvent.change(await screen.findByLabelText("Sprint"), { target: { value: "" } });

    await waitFor(async () => {
      const updated = await tk.read(projectId, "#1");
      expect(updated.sprintId).toBeNull();
    });
  });

  it("assigns and unassigns an agent from the detail view", async () => {
    const { gateways, projectId } = await setup();
    const tk = gateways.ticket as MockTicketGateway;
    tk._seedTicket(projectId, ticket());
    seedAgent(gateways, projectId, "agent-1", "DevFrontend");

    await openProjectAndTab(gateways, "Tickets");
    fireEvent.click(
      await screen.findByRole("button", {
        name: "#1, Ajouter tickets/sprints au web, statut Ouvert, priorité Moyenne",
      }),
    );

    fireEvent.click(await screen.findByRole("button", { name: "Agents assignés" }));
    fireEvent.change(screen.getByLabelText("Assigner un agent"), {
      target: { value: "agent-1" },
    });
    fireEvent.click(screen.getByRole("button", { name: "Assigner" }));

    await waitFor(async () => {
      const updated = await tk.read(projectId, "#1");
      expect(updated.assignedAgentIds).toContain("agent-1");
    });
    await screen.findByText("DevFrontend");

    fireEvent.click(screen.getByRole("button", { name: "Désassigner DevFrontend" }));

    await waitFor(async () => {
      const updated = await tk.read(projectId, "#1");
      expect(updated.assignedAgentIds).not.toContain("agent-1");
    });
  });

  it("links and unlinks a ticket from the detail view", async () => {
    const { gateways, projectId } = await setup();
    const tk = gateways.ticket as MockTicketGateway;
    tk._seedTicket(projectId, ticket());
    tk._seedTicket(projectId, ticket({ ref: "#2", number: 2, title: "Autre ticket" }));

    await openProjectAndTab(gateways, "Tickets");
    fireEvent.click(
      await screen.findByRole("button", {
        name: "#1, Ajouter tickets/sprints au web, statut Ouvert, priorité Moyenne",
      }),
    );

    fireEvent.click(await screen.findByRole("button", { name: "Liens" }));
    fireEvent.click(screen.getByRole("button", { name: "+ Lier" }));

    const sheet = await screen.findByRole("dialog", { name: "Lier un ticket" });
    fireEvent.click(within(sheet).getByRole("button", { name: "#2, Autre ticket" }));

    await waitFor(async () => {
      const updated = await tk.read(projectId, "#1");
      expect(updated.links).toEqual([{ targetRef: "#2", kind: "relatesTo" }]);
    });
    await screen.findByText("#2");

    fireEvent.click(screen.getByRole("button", { name: "Retirer" }));

    await waitFor(async () => {
      const updated = await tk.read(projectId, "#1");
      expect(updated.links).toEqual([]);
    });
  });

  it("saves the carnet explicitly from the detail view", async () => {
    const { gateways, projectId } = await setup();
    const tk = gateways.ticket as MockTicketGateway;
    tk._seedTicket(projectId, ticket());

    await openProjectAndTab(gateways, "Tickets");
    fireEvent.click(
      await screen.findByRole("button", {
        name: "#1, Ajouter tickets/sprints au web, statut Ouvert, priorité Moyenne",
      }),
    );

    const carnetField = await screen.findByLabelText("Carnet");
    const saveButton = screen.getByRole("button", { name: "Enregistrer le carnet" });
    expect(saveButton).toHaveProperty("disabled", true);

    fireEvent.change(carnetField, { target: { value: "Notes de travail" } });
    expect(saveButton).toHaveProperty("disabled", false);
    fireEvent.click(saveButton);

    await waitFor(async () => {
      const updated = await tk.readCarnet(projectId, "#1");
      expect(updated.carnet).toBe("Notes de travail");
    });
  });

  it("filters the ticket list by free-text search", async () => {
    const { gateways, projectId } = await setup();
    const tk = gateways.ticket as MockTicketGateway;
    tk._seedTicket(projectId, ticket({ ref: "#1", number: 1, title: "Ajouter tickets/sprints au web" }));
    tk._seedTicket(projectId, ticket({ ref: "#2", number: 2, title: "Langue uniforme Settings" }));

    await openProjectAndTab(gateways, "Tickets");
    await screen.findByText("Langue uniforme Settings");

    fireEvent.change(screen.getByLabelText("Rechercher des tickets"), {
      target: { value: "Langue" },
    });

    await waitFor(() => {
      expect(screen.queryByText("Ajouter tickets/sprints au web")).toBeNull();
    });
    expect(screen.getByText("Langue uniforme Settings")).toBeTruthy();
  });

  it("deletes a ticket after confirmation and returns to the list", async () => {
    const { gateways, projectId } = await setup();
    const tk = gateways.ticket as MockTicketGateway;
    tk._seedTicket(projectId, ticket());

    await openProjectAndTab(gateways, "Tickets");
    fireEvent.click(
      await screen.findByRole("button", {
        name: "#1, Ajouter tickets/sprints au web, statut Ouvert, priorité Moyenne",
      }),
    );

    fireEvent.click(await screen.findByRole("button", { name: "Zone dangereuse" }));
    fireEvent.click(screen.getByRole("button", { name: "Supprimer" }));

    const dialog = await screen.findByRole("alertdialog", { name: "Supprimer ce ticket ?" });
    expect(within(dialog).getByText(/sera supprimé définitivement/)).toBeTruthy();
    fireEvent.click(within(dialog).getByRole("button", { name: "Supprimer" }));

    await waitFor(() => expect(screen.getByText("Aucun ticket dans ce projet.")).toBeTruthy());
  });
});

describe("WebWorkspace — Sprints tab (ticket #86)", () => {
  it("shows the empty state with a create shortcut", async () => {
    const { gateways } = await setup();
    await openProjectAndTab(gateways, "Sprints");
    expect(await screen.findByText("Aucun sprint.")).toBeTruthy();
    expect(screen.getByRole("button", { name: "Créer un sprint" })).toBeTruthy();
  });

  it("creates a sprint (disabled until a name is entered)", async () => {
    const { gateways, projectId } = await setup();
    const tk = gateways.ticket as MockTicketGateway;
    await openProjectAndTab(gateways, "Sprints");

    fireEvent.click(screen.getByRole("button", { name: "+ Sprint" }));
    const createButton = screen.getByRole("button", { name: "Créer" });
    expect(createButton).toHaveProperty("disabled", true);

    fireEvent.change(screen.getByLabelText("Nom du sprint"), {
      target: { value: "Backlog client web" },
    });
    expect(createButton).toHaveProperty("disabled", false);
    fireEvent.click(createButton);

    await waitFor(async () => {
      const sprints = await tk.listSprints(projectId);
      expect(sprints.map((s) => s.name)).toContain("Backlog client web");
    });
    expect(await screen.findByText("Backlog client web")).toBeTruthy();
  });

  it("renames, reorders and deletes a sprint with confirmation", async () => {
    const { gateways, projectId } = await setup();
    const tk = gateways.ticket as MockTicketGateway;
    tk._seedSprint(projectId, { id: "s1", order: 1, name: "Sprint A" });
    tk._seedSprint(projectId, { id: "s2", order: 2, name: "Sprint B" });

    await openProjectAndTab(gateways, "Sprints");
    await screen.findByText("Sprint A");

    // Rename.
    fireEvent.click(screen.getByRole("button", { name: "Renommer Sprint A" }));
    fireEvent.change(screen.getByLabelText("Renommer le sprint Sprint A"), {
      target: { value: "Sprint A renommé" },
    });
    fireEvent.click(screen.getByRole("button", { name: "Enregistrer" }));
    await waitFor(() => expect(screen.getByText("Sprint A renommé")).toBeTruthy());

    // Reorder: move Sprint B up.
    fireEvent.click(screen.getByRole("button", { name: "Monter Sprint B" }));
    await waitFor(async () => {
      const sprints = await tk.listSprints(projectId);
      expect(sprints.find((s) => s.id === "s2")?.order).toBe(1);
    });

    // Delete with confirmation.
    fireEvent.click(screen.getByRole("button", { name: "Supprimer Sprint B" }));
    const dialog = await screen.findByRole("alertdialog", {
      name: "Supprimer le sprint « Sprint B » ?",
    });
    expect(within(dialog).getByText(/passeront en « Sans sprint »/)).toBeTruthy();
    fireEvent.click(within(dialog).getByRole("button", { name: "Supprimer" }));

    await waitFor(async () => {
      const sprints = await tk.listSprints(projectId);
      expect(sprints.map((s) => s.id)).not.toContain("s2");
    });
  });

  it("adds tickets to a sprint via the mobile picker", async () => {
    const { gateways, projectId } = await setup();
    const tk = gateways.ticket as MockTicketGateway;
    tk._seedSprint(projectId, { id: "s1", order: 1, name: "Sprint A" });
    tk._seedTicket(projectId, ticket({ ref: "#1", number: 1, title: "Ticket un" }));
    tk._seedTicket(projectId, ticket({ ref: "#2", number: 2, title: "Ticket deux" }));

    await openProjectAndTab(gateways, "Sprints");
    await screen.findByText("Sprint A");

    fireEvent.click(screen.getByRole("button", { name: "Ajouter tickets" }));
    const sheet = await screen.findByRole("dialog", {
      name: "Ajouter des tickets au sprint « Sprint A »",
    });
    fireEvent.click(within(sheet).getByRole("checkbox", { name: "#1, Ticket un" }));
    fireEvent.click(
      within(sheet).getByRole("button", { name: /Ajouter au sprint/ }),
    );

    await waitFor(async () => {
      const t = await tk.read(projectId, "#1");
      expect(t.sprintId).toBe("s1");
    });
  });

  it("shows the sprint's tickets and removes one from the sprint", async () => {
    const { gateways, projectId } = await setup();
    const tk = gateways.ticket as MockTicketGateway;
    tk._seedSprint(projectId, { id: "s1", order: 1, name: "Sprint A" });
    tk._seedTicket(projectId, ticket({ ref: "#1", number: 1, title: "Ticket un", sprintId: "s1" }));

    await openProjectAndTab(gateways, "Sprints");
    await screen.findByText("Sprint A");

    expect(screen.getByText("Ticket un")).toBeTruthy();

    fireEvent.click(screen.getByRole("button", { name: "Retirer #1 du sprint Sprint A" }));

    await waitFor(async () => {
      const t = await tk.read(projectId, "#1");
      expect(t.sprintId).toBeNull();
    });
    await waitFor(() => expect(screen.queryByText("Ticket un")).toBeNull());
  });
});
