/**
 * 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, MockGitGateway, MockProfileGateway, MockProjectGateway, MockSystemGateway, MockTemplateGateway } 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.
  const agentGateway = new MockAgentGateway();
  const gateways = {
    system,
    project,
    agent: agentGateway,
    profile: new MockProfileGateway(),
    template: new MockTemplateGateway(agentGateway),
    git: new MockGitGateway(),
  } 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");
}

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 the root is rendered (known-projects list + active-tab panel).
    expect(screen.getAllByText("/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 alpha, then beta.
    const items = screen.getAllByRole("listitem");
    const openA = within(
      items.find((li) => within(li).queryByText("alpha"))!,
    ).getByRole("button", { name: "Open" });
    fireEvent.click(openA);

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

    const openB = within(
      items.find((li) => within(li).queryByText("beta"))!,
    ).getByRole("button", { name: "Open" });
    fireEvent.click(openB);

    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("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

    // Second project at the same root.
    await createProject("beta", "/dup/root");

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

  it("sidebar tab switch shows the selected panel and hides others", 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 sidebar panels become active.
    await screen.findByText("/p/a");
    fireEvent.click(screen.getByRole("button", { name: "Open" }));
    await waitFor(() =>
      expect(within(tablist()).getAllByRole("tab")).toHaveLength(1),
    );

    // Default sidebar tab is "Projects" — the form inputs must be accessible.
    expect(screen.getByLabelText("project name")).toBeTruthy();
    expect(screen.getByLabelText("project root")).toBeTruthy();

    // Switch to Agents tab — agent form should be visible.
    fireEvent.click(screen.getByRole("button", { name: "Agents" }));
    // Agent panel renders an "agent name" input; project form is now hidden.
    await waitFor(() =>
      expect(screen.getByLabelText("agent name")).toBeTruthy(),
    );
    expect(screen.queryByLabelText("project name")).toBeNull();

    // Switch to Context tab — shared project context editor should be visible.
    fireEvent.click(screen.getByRole("button", { name: "Context" }));
    await waitFor(() =>
      expect(screen.getByLabelText("project context")).toBeTruthy(),
    );
    expect(screen.queryByLabelText("agent name")).toBeNull();

    // Switch to Templates tab — the "New template" button should be visible.
    fireEvent.click(screen.getByRole("button", { name: "Templates" }));
    await waitFor(() =>
      expect(screen.getByRole("button", { name: "create template" })).toBeTruthy(),
    );
    expect(screen.queryByLabelText("agent name")).toBeNull();

    // Switch to Git tab — commit message input should be visible.
    fireEvent.click(screen.getByRole("button", { name: "Git" }));
    await waitFor(() =>
      expect(screen.getByLabelText("commit message")).toBeTruthy(),
    );

    // Switch back to Projects tab — form is accessible again.
    fireEvent.click(screen.getByRole("button", { name: "Projects" }));
    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" }));
    fireEvent.click(screen.getByRole("button", { name: "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();
  });
});
