/**
 * L2 — the projects feature wired to the stateful `MockProjectGateway` via the
 * real `DIProvider`. Covers create → list/tab, open → tab, close → remove,
 * duplicate root (INVALID) and unknown id (NOT_FOUND) error handling.
 */
import { describe, it, expect } from "vitest";
import {
  render,
  screen,
  within,
  waitFor,
  fireEvent,
} from "@testing-library/react";

import { MockAgentGateway, MockDesktopServerGateway, MockGitGateway, MockProfileGateway, MockProjectGateway, MockSystemGateway, MockTemplateGateway, MockWindowGateway, MockWorkStateGateway } from "@/adapters/mock";
import type { Gateways } from "@/ports";
import { DIProvider } from "@/app/di";
import { ProjectsView } from "./ProjectsView";

/** Renders the view behind a DI provider seeded with a fresh project gateway. */
function renderView(
  project: MockProjectGateway = new MockProjectGateway(),
  system: MockSystemGateway = new MockSystemGateway(),
) {
  // The project gateway drives the primary assertions; agent + profile stubs are
  // required because ProjectsView now renders AgentsPanel for the active tab.
  // template gateway is required because ProjectsView now renders TemplatesPanel.
  // desktopServer is required by Settings → Deployment (#68).
  const agentGateway = new MockAgentGateway();
  const gateways = {
    system,
    project,
    agent: agentGateway,
    profile: new MockProfileGateway(),
    template: new MockTemplateGateway(agentGateway),
    git: new MockGitGateway(),
    workState: new MockWorkStateGateway(),
    window: new MockWindowGateway(),
    desktopServer: new MockDesktopServerGateway(),
  } as unknown as Gateways;
  return {
    project,
    system,
    ...render(
      <DIProvider gateways={gateways}>
        <ProjectsView />
      </DIProvider>,
    ),
  };
}

/** Waits for the initial async refresh to settle (Refresh button re-enabled). */
async function waitForIdle() {
  await waitFor(() =>
    expect(
      (
        screen.getByRole("button", { name: "Refresh" }) as HTMLButtonElement
      ).disabled,
    ).toBe(false),
  );
}

/** Fills the create form and submits it (assumes the form is idle/enabled). */
async function createProject(name: string, root: string) {
  await waitForIdle();
  fireEvent.change(screen.getByLabelText("project name"), {
    target: { value: name },
  });
  fireEvent.change(screen.getByLabelText("project root"), {
    target: { value: root },
  });
  fireEvent.click(screen.getByRole("button", { name: "Create project" }));
}

function tablist() {
  return screen.getByRole("tablist");
}

/**
 * Opens a panel floating via the single « Panneaux » menu (#26): click the menu,
 * click the panel entry (opens its placement submenu), then pick « Flottant ».
 * `panelTitle` is the panel's human title ("Work state", "Project context",
 * "Agents", "Git", "Projects", …).
 */
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" }));
}

/** Clicks an item in a flat top-level menu (e.g. Settings). */
function openMenuItem(menu: string, item: string) {
  fireEvent.click(screen.getByRole("button", { name: menu }));
  fireEvent.click(screen.getByRole("button", { name: item }));
}

describe("ProjectsView (with MockProjectGateway)", () => {
  it("creating a project adds it to the known list and opens a tab", async () => {
    renderView();

    expect(screen.getByText("No projects yet.")).toBeTruthy();

    await createProject("alpha", "/home/me/alpha");

    // The created project shows as an active tab once the request settles.
    const tab = await screen.findByRole("tab");
    expect(tab.getAttribute("aria-selected")).toBe("true");
    expect(within(tab).getByText("alpha")).toBeTruthy();
    // And it is listed in the projects manager (File → Projects…) with its root.
    openPanel("Projects");
    expect(
      (await screen.findAllByText("/home/me/alpha")).length,
    ).toBeGreaterThan(0);
  });

  it("opening a project creates a tab and makes it the active one", async () => {
    // Seed two projects directly through the stateful gateway.
    const project = new MockProjectGateway();
    const a = await project.createProject("alpha", "/p/a");
    const b = await project.createProject("beta", "/p/b");
    renderView(project);

    // Wait for the initial refresh to list both.
    await screen.findByText("/p/a");
    await screen.findByText("/p/b");

    // Open the projects manager window so the list persists across activation
    // (the inline welcome list is replaced by the terminal grid once active).
    openPanel("Projects");

    // Open alpha, then beta — re-querying the list after each activation.
    const openInList = (name: string) =>
      within(
        screen.getAllByRole("listitem").find((li) => within(li).queryByText(name))!,
      ).getByRole("button", { name: "Open" });

    fireEvent.click(openInList("alpha"));
    await waitFor(() =>
      expect(within(tablist()).getAllByRole("tab")).toHaveLength(1),
    );

    fireEvent.click(openInList("beta"));

    await waitFor(() =>
      expect(within(tablist()).getAllByRole("tab")).toHaveLength(2),
    );

    // The last-opened tab (beta) is active.
    const tabs = within(tablist()).getAllByRole("tab");
    const betaTab = tabs.find((t) => within(t).queryByText("beta"))!;
    const alphaTab = tabs.find((t) => within(t).queryByText("alpha"))!;
    expect(betaTab.getAttribute("aria-selected")).toBe("true");
    expect(alphaTab.getAttribute("aria-selected")).toBe("false");

    // Ignore unused binding lint for `a`/`b`.
    expect([a.id, b.id]).toHaveLength(2);
  });

  it("shows and renders the Work sidebar tab for an active project", 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", { name: "alpha" });

    openPanel("Work state");

    expect(await screen.findByText("No agent work state.")).toBeTruthy();
  });

  it("switches the active project via the tab-bar + button → Projects panel (#26)", async () => {
    const project = new MockProjectGateway();
    await project.createProject("alpha", "/p/a");
    await project.createProject("beta", "/p/b");
    renderView(project);

    // Open alpha from the welcome list ⇒ it becomes the active tab.
    await screen.findByText("/p/a");
    const alphaLi = screen
      .getAllByRole("listitem")
      .find((n) => within(n).queryByText("alpha"))!;
    fireEvent.click(within(alphaLi).getByRole("button", { name: "Open" }));
    await screen.findByRole("tab", { name: "alpha" });

    // The "+" button opens the Projects panel (floating); pick beta from its
    // known-projects list — no verbose "Projet : …" selector anymore.
    fireEvent.click(screen.getByRole("button", { name: "open projects panel" }));
    const betaLi = await waitFor(() =>
      screen.getAllByRole("listitem").find((n) => within(n).queryByText("beta"))!,
    );
    fireEvent.click(within(betaLi).getByRole("button", { name: "Open" }));

    await waitFor(() => expect(screen.getAllByRole("tab")).toHaveLength(2));
    const betaTab = screen
      .getAllByRole("tab")
      .find((t) => within(t).queryByText("beta"))!;
    expect(betaTab.getAttribute("aria-selected")).toBe("true");
  });

  it("opens the Settings surface from the Settings menu and closes it from the view (#16/#68)", async () => {
    renderView();
    await waitForIdle();

    // The project surface (create form) is visible; profiles settings are not.
    expect(screen.getByLabelText("project name")).toBeTruthy();
    expect(screen.queryByLabelText("ai profiles settings")).toBeNull();

    // Settings → AI Profiles swaps the main area to the profiles settings.
    openMenuItem("Paramètres", "Profils IA");
    expect(await screen.findByLabelText("ai profiles settings")).toBeTruthy();
    expect(screen.queryByLabelText("project name")).toBeNull();

    // #68: closing is an explicit action in the view, not an alternating menu
    // label — the label never scaled past one section.
    fireEvent.click(screen.getByRole("button", { name: "Fermer les paramètres" }));
    await waitFor(() =>
      expect(screen.getByLabelText("project name")).toBeTruthy(),
    );
    expect(screen.queryByLabelText("ai profiles settings")).toBeNull();
  });

  it("navigates between Settings sections from the menu and the nav column (#68)", async () => {
    renderView();
    await waitForIdle();

    // The Settings menu now names sections; Deployment opens directly.
    openMenuItem("Paramètres", "Déploiement");
    expect(await screen.findByLabelText("deployment settings")).toBeTruthy();
    expect(screen.queryByLabelText("ai profiles settings")).toBeNull();

    // The internal nav column switches sections without leaving Settings.
    const nav = screen.getByRole("navigation", { name: "settings sections" });
    fireEvent.click(within(nav).getByRole("button", { name: "Profils IA" }));
    expect(await screen.findByLabelText("ai profiles settings")).toBeTruthy();
    expect(screen.queryByLabelText("deployment settings")).toBeNull();
  });

  it("closing a tab removes it from the tab bar", async () => {
    renderView();
    await createProject("alpha", "/home/me/alpha");

    await screen.findByRole("tab");

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

    await waitFor(() => {
      expect(screen.getByText("No open tabs.")).toBeTruthy();
    });
  });

  it("creating a project with a duplicate root surfaces an INVALID error", async () => {
    renderView();

    await createProject("alpha", "/dup/root");
    await screen.findByRole("tab"); // first project opened as a tab

    // Reopen the projects manager (the inline form is replaced by the grid once
    // a project is active), then create a second project at the same root.
    openPanel("Projects");
    await createProject("beta", "/dup/root");

    const alert = await screen.findByRole("alert");
    expect(alert.textContent).toMatch(/already exists/i);
  });

  it("menu panels show the selected panel and hide others (#16)", async () => {
    // Set up a project so that agent/template/git panels are reachable.
    const project = new MockProjectGateway();
    await project.createProject("alpha", "/p/a");
    renderView(project);

    // Open alpha so the panels become active. With a project open the main area
    // is the terminal grid — panels now live behind the menu bar.
    await screen.findByText("/p/a");
    fireEvent.click(screen.getByRole("button", { name: "Open" }));
    await waitFor(() =>
      expect(within(tablist()).getAllByRole("tab")).toHaveLength(1),
    );

    // File → Projects… opens the projects manager window — form inputs visible.
    openPanel("Projects");
    await waitFor(() =>
      expect(screen.getByLabelText("project name")).toBeTruthy(),
    );

    // View → Agents — agent form visible; project form window is replaced.
    openPanel("Agents");
    await waitFor(() =>
      expect(screen.getByLabelText("agent name")).toBeTruthy(),
    );
    expect(screen.queryByLabelText("project name")).toBeNull();

    // View → Context — shared project context editor visible.
    openPanel("Project context");
    await waitFor(() =>
      expect(screen.getByLabelText("project context")).toBeTruthy(),
    );
    expect(screen.queryByLabelText("agent name")).toBeNull();

    // View → Templates — the "New template" button visible.
    openPanel("Templates");
    await waitFor(() =>
      expect(screen.getByRole("button", { name: "create template" })).toBeTruthy(),
    );
    expect(screen.queryByLabelText("agent name")).toBeNull();

    // View → Git — commit message input visible.
    openPanel("Git");
    await waitFor(() =>
      expect(screen.getByLabelText("commit message")).toBeTruthy(),
    );

    // File → Projects… again — form accessible once more.
    openPanel("Projects");
    await waitFor(() =>
      expect(screen.getByLabelText("project name")).toBeTruthy(),
    );
  });

  it("edits the shared project context stored under .ideai", async () => {
    const project = new MockProjectGateway();
    const created = await project.createProject("alpha", "/p/a");
    await project.updateProjectContext(created.id, "# Existing context");
    renderView(project);

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

    const editor = (await screen.findByLabelText(
      "project context",
    )) as HTMLTextAreaElement;
    expect(editor.value).toBe("# Existing context");

    fireEvent.change(editor, { target: { value: "# Shared\n\nUse pnpm." } });
    fireEvent.click(screen.getByRole("button", { name: "Save" }));

    await waitFor(async () => {
      await expect(project.readProjectContext(created.id)).resolves.toBe(
        "# Shared\n\nUse pnpm.",
      );
    });
  });

  it("Browse… button calls pickFolder and fills the root field with the returned path", async () => {
    const system = new MockSystemGateway();
    renderView(new MockProjectGateway(), system);
    await waitForIdle();

    // Root field starts empty.
    const rootInput = screen.getByLabelText("project root") as HTMLInputElement;
    expect(rootInput.value).toBe("");

    // Click Browse… — the mock returns "/home/user/mock-project".
    fireEvent.click(screen.getByRole("button", { name: "browse project folder" }));

    // Wait for the async pickFolder to fill the field.
    await waitFor(() => {
      expect((screen.getByLabelText("project root") as HTMLInputElement).value).toBe(
        "/home/user/mock-project",
      );
    });
  });

  it("opening an unknown project id surfaces a NOT_FOUND error", async () => {
    // Stub a stale list entry whose id the gateway won't resolve.
    const project = new MockProjectGateway();
    project.listProjects = async () => [
      {
        id: "ghost-id",
        name: "ghost",
        root: "/p/ghost",
        remote: { kind: "local" },
        createdAt: 0,
      },
    ];
    renderView(project);

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

    const alert = await screen.findByRole("alert");
    expect(alert.textContent).toMatch(/not found/i);
    expect(screen.getByText("No open tabs.")).toBeTruthy();
  });
});
