/**
 * #23/#47 — the panel-only entry. `parseViewWindowParams` validates the
 * `?panel=` query (no project id anymore), and `ViewWindow` follows the main
 * window's focused project: it shows an "open a project" shell while no project
 * is focused and mounts the requested view once one is, without ever opening a
 * project itself.
 */
import { describe, it, expect } from "vitest";
import { render, screen, waitFor, act } from "@testing-library/react";

import { DIProvider } from "@/app/di";
import {
  MockAgentGateway,
  MockFocusedProjectGateway,
  MockProjectGateway,
  MockSystemGateway,
  MockTicketGateway,
} from "@/adapters/mock";
import type { Gateways } from "@/ports";
import {
  PluginCommandRegistry,
  PluginLayoutRegistry,
  PluginMenuRegistry,
  PluginRuntimeRegistry,
  type LoadedPlugin,
  type PluginLayoutProps,
} from "@/plugins/runtime";
import { PluginLayoutWindow, ViewWindow, parseViewWindowParams } from "./ViewWindow";

function b64url(value: string): string {
  return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}

describe("parseViewWindowParams (#47)", () => {
  it("parses a valid panel-only query (ignores any legacy project param)", () => {
    expect(parseViewWindowParams("?panel=tickets")).toEqual({
      kind: "panel",
      panel: "tickets",
    });
    // A restored legacy URL that still carries `project` parses to panel only.
    expect(parseViewWindowParams("?panel=tickets&project=p-1")).toEqual({
      kind: "panel",
      panel: "tickets",
    });
  });

  it("parses a plugin layout window query from the backend base64url contract", () => {
    expect(
      parseViewWindowParams(
        `?pluginLayout=1&pluginId=${b64url("dev.acme.plugin")}&layoutType=${b64url(
          "dev.acme.plugin.dashboard",
        )}&state=${b64url(JSON.stringify({ tab: "overview" }))}`,
      ),
    ).toEqual({
      kind: "pluginLayout",
      plugin: {
        pluginId: "dev.acme.plugin",
        layoutType: "dev.acme.plugin.dashboard",
        state: { tab: "overview" },
      },
    });
  });

  it("rejects a missing panel or the non-detachable projects panel", () => {
    expect(parseViewWindowParams("?project=p-1")).toBeNull();
    expect(parseViewWindowParams("?panel=projects")).toBeNull();
    expect(parseViewWindowParams("?panel=bogus")).toBeNull();
    expect(parseViewWindowParams("")).toBeNull();
  });
});

function StubPluginLayout(props: PluginLayoutProps) {
  return <div data-testid="plugin-window-state">{JSON.stringify(props.state)}</div>;
}

function pluginRegistry(): PluginRuntimeRegistry {
  const registry = new PluginRuntimeRegistry();
  const layouts = new PluginLayoutRegistry(
    "dev.acme.plugin",
    new Set(["dev.acme.plugin.dashboard"]),
  );
  layouts.register({
    type: "dev.acme.plugin.dashboard",
    component: StubPluginLayout,
  });
  const loaded: LoadedPlugin = {
    pluginId: "dev.acme.plugin",
    displayName: "Acme Plugin",
    contributes: {
      menus: [],
      menuItems: [],
      layouts: [
        {
          type: "dev.acme.plugin.dashboard",
          label: "Dashboard",
          component: "Dashboard",
        },
      ],
      mcpServers: [],
    },
    commands: new PluginCommandRegistry("dev.acme.plugin", new Set()),
    layouts,
    menu: new PluginMenuRegistry("dev.acme.plugin"),
    dispose: async () => {},
  };
  registry.add(loaded);
  return registry;
}

describe("PluginLayoutWindow (#143)", () => {
  it("mounts the plugin layout for the focused project with URL state", async () => {
    const system = new MockSystemGateway();
    const project = new MockProjectGateway();
    const created = await project.createProject("gamma", "/p/g");
    const focusedProject = new MockFocusedProjectGateway();
    await focusedProject.setFocusedProject({
      id: created.id,
      name: "gamma",
      root: "/p/g",
    });
    const gateways = {
      system,
      project,
      agent: new MockAgentGateway(),
      ticket: new MockTicketGateway(system),
      focusedProject,
    } as unknown as Gateways;

    render(
      <DIProvider gateways={gateways}>
        <PluginLayoutWindow
          plugin={{
            pluginId: "dev.acme.plugin",
            layoutType: "dev.acme.plugin.dashboard",
            state: { tab: "overview" },
          }}
          runtimeValue={{ registry: pluginRegistry(), failures: [], pending: [], loading: false }}
        />
      </DIProvider>,
    );

    await waitFor(() => expect(screen.getByText("Dashboard")).toBeTruthy());
    await waitFor(() => expect(screen.getByText("· gamma")).toBeTruthy());
    expect(screen.getByTestId("plugin-window-state").textContent).toBe(
      JSON.stringify({ tab: "overview" }),
    );
  });
});

describe("ViewWindow (#47)", () => {
  it("shows the 'open a project' shell when no project is focused", async () => {
    const system = new MockSystemGateway();
    const gateways = {
      system,
      project: new MockProjectGateway(),
      agent: new MockAgentGateway(),
      ticket: new MockTicketGateway(system),
      focusedProject: new MockFocusedProjectGateway(),
    } as unknown as Gateways;

    render(
      <DIProvider gateways={gateways}>
        <ViewWindow panel="tickets" />
      </DIProvider>,
    );

    // The panel title chrome is present, but no project-scoped view mounts…
    expect(screen.getByRole("heading", { name: "Tickets" })).toBeTruthy();
    await waitFor(() =>
      expect(screen.getByText(/Ouvrez un projet/i)).toBeTruthy(),
    );
    expect(screen.queryByLabelText("search tickets")).toBeNull();
  });

  it("mounts the view for the focused project and follows focus changes", async () => {
    const system = new MockSystemGateway();
    const project = new MockProjectGateway();
    const created = await project.createProject("alpha", "/p/a");
    const focusedProject = new MockFocusedProjectGateway();
    const gateways = {
      system,
      project,
      agent: new MockAgentGateway(),
      ticket: new MockTicketGateway(system),
      focusedProject,
    } as unknown as Gateways;

    render(
      <DIProvider gateways={gateways}>
        <ViewWindow panel="tickets" />
      </DIProvider>,
    );

    // No focus yet → shell.
    await waitFor(() =>
      expect(screen.getByText(/Ouvrez un projet/i)).toBeTruthy(),
    );

    // The main window publishes a focused project → the window follows it.
    await act(async () => {
      await focusedProject.setFocusedProject({
        id: created.id,
        name: "alpha",
        root: "/p/a",
      });
    });

    await waitFor(() => expect(screen.getByText("· alpha")).toBeTruthy());
    await waitFor(() =>
      expect(screen.getByLabelText("search tickets")).toBeTruthy(),
    );

    // Focus cleared (project closed) → back to the shell, view unmounts.
    await act(async () => {
      await focusedProject.setFocusedProject(null);
    });
    await waitFor(() =>
      expect(screen.getByText(/Ouvrez un projet/i)).toBeTruthy(),
    );
    expect(screen.queryByLabelText("search tickets")).toBeNull();
  });

  it("reads the current focus at mount (restored window with a live focus)", async () => {
    const system = new MockSystemGateway();
    const project = new MockProjectGateway();
    const created = await project.createProject("beta", "/p/b");
    const focusedProject = new MockFocusedProjectGateway();
    // Focus already set before the window mounts (main window opened first).
    await focusedProject.setFocusedProject({
      id: created.id,
      name: "beta",
      root: "/p/b",
    });
    const gateways = {
      system,
      project,
      agent: new MockAgentGateway(),
      ticket: new MockTicketGateway(system),
      focusedProject,
    } as unknown as Gateways;

    render(
      <DIProvider gateways={gateways}>
        <ViewWindow panel="tickets" />
      </DIProvider>,
    );

    await waitFor(() => expect(screen.getByText("· beta")).toBeTruthy());
    await waitFor(() =>
      expect(screen.getByLabelText("search tickets")).toBeTruthy(),
    );
  });
});
