/**
 * F2 — `PluginsPanel` (ticket #43, carnet §10 F2 acceptance criteria): install
 * from archive/directory with pre-install review, full-trust mention visible,
 * enable/disable/uninstall flows, and `restartRequired` rendered.
 */
import { describe, it, expect } from "vitest";
import { render, screen, waitFor, fireEvent, within } from "@testing-library/react";

import {
  createMockGateways,
  MockFocusedProjectGateway,
  MockPluginGateway,
  MockSystemGateway,
} from "@/adapters/mock";
import type { PluginInstallResult, PluginReview, PluginRuntimeContributionCatalog } from "@/domain";
import type { Gateways, ReviewPluginPackageInput } from "@/ports";
import { DIProvider } from "@/app/di";
import { PluginRuntimeRegistry } from "@/plugins/runtime";
import { PluginsPanel } from "./PluginsPanel";
import { PluginRuntimeProvider, type PluginRuntimeContextValue } from "./PluginRuntimeProvider";

function dataUrl(source: string): string {
  return `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
}

function renderPanel(
  plugin?: MockPluginGateway,
  system?: MockSystemGateway,
  runtimeValue: PluginRuntimeContextValue = {
    registry: new PluginRuntimeRegistry(),
    failures: [],
    pending: [],
    loading: false,
  },
) {
  const p = plugin ?? new MockPluginGateway();
  const s = system ?? new MockSystemGateway();
  const gateways = { plugin: p, system: s } as unknown as Gateways;
  return {
    plugin: p,
    system: s,
    ...render(
      <DIProvider gateways={gateways}>
        <PluginRuntimeProvider value={runtimeValue}>
          <PluginsPanel />
        </PluginRuntimeProvider>
      </DIProvider>,
    ),
  };
}

function renderPanelWithLiveRuntime(plugin: MockPluginGateway, system = new MockSystemGateway()) {
  const gateways = { ...createMockGateways(), plugin, system };
  return render(
    <DIProvider gateways={gateways}>
      <PluginRuntimeProvider>
        <PluginsPanel />
      </PluginRuntimeProvider>
    </DIProvider>,
  );
}

class InvalidInstallPluginGateway extends MockPluginGateway {
  async installFromDirectory(path: string): Promise<PluginInstallResult> {
    const plugin = {
      id: "dev.idea.fixtures.missing-main",
      displayName: "Missing Main Plugin",
      version: "0.1.0",
      sourceKind: "directory" as const,
      sourceLabel: path,
      lifecycleState: "invalid" as const,
      enabled: false,
      pendingUninstall: false,
      restartRequired: false,
      trustLevel: "full" as const,
      contributionSummary: { topLevelMenus: 1, menuItems: 1, layouts: 0, mcpServers: 0 },
      error: "plugin main asset is missing: dist/index.js",
    };
    this._seedPlugin(plugin);
    return { plugin, restartRequired: false };
  }
}

class FailingRuntimeCatalogPluginGateway extends MockPluginGateway {
  async listRuntimeContributions(): Promise<PluginRuntimeContributionCatalog> {
    throw { code: "INVALID", message: "runtime catalog failed" };
  }
}

class ProjectScopedRuntimeCatalogPluginGateway extends MockPluginGateway {
  constructor(private readonly bundleUrl: string) {
    super();
  }

  async listRuntimeContributions(): Promise<PluginRuntimeContributionCatalog> {
    return {
      plugins: [
        {
          id: "dev.acme.project-plugin",
          displayName: "Project Plugin",
          version: "1.0.0",
          activationScope: "project",
          bundleUrl: this.bundleUrl,
          contentHash: "project-plugin",
          contributes: { menus: [], menuItems: [], layouts: [], mcpServers: [] },
        },
      ],
    };
  }
}

class MixedActivationScopeRuntimeCatalogPluginGateway extends MockPluginGateway {
  constructor(
    private readonly failingAppBundleUrl: string,
    private readonly projectBundleUrl: string,
  ) {
    super();
  }

  async listRuntimeContributions(): Promise<PluginRuntimeContributionCatalog> {
    return {
      plugins: [
        {
          id: "dev.acme.app-needs-project",
          displayName: "App Needs Project",
          version: "1.0.0",
          capabilities: ["tooling"],
          activationScope: "app",
          bundleUrl: this.failingAppBundleUrl,
          contentHash: "app-needs-project",
          contributes: { menus: [], menuItems: [], layouts: [], mcpServers: [] },
        },
        {
          id: "dev.acme.project-plugin",
          displayName: "Project Plugin",
          version: "1.0.0",
          activationScope: "project",
          bundleUrl: this.projectBundleUrl,
          contentHash: "project-plugin",
          contributes: { menus: [], menuItems: [], layouts: [], mcpServers: [] },
        },
      ],
    };
  }
}

class BackendShapedReviewPluginGateway extends MockPluginGateway {
  async reviewPackage(input: ReviewPluginPackageInput): Promise<PluginReview> {
    const label = input.path.split("/").pop() ?? input.path;
    return {
      id: "dev.acme.gitgraph",
      displayName: label.replace(/\.(ideaplug|zip|vsix)$/i, ""),
      publisher: "Acme",
      version: "1.2.3",
      description: "Graph",
      sourceKind: input.sourceKind,
      sourceLabel: input.path,
      contentHash: "abc",
      trustLevel: "full",
      contributionSummary: { topLevelMenus: 1, menuItems: 2, layouts: 3, mcpServers: 4 },
      contributes: { menus: [], menuItems: [], layouts: [], mcpServers: [] },
    } as unknown as PluginReview;
  }
}

describe("PluginsPanel", () => {
  it("shows an empty state with no plugins installed", async () => {
    renderPanel();
    expect(await screen.findByText("Aucun plugin installé.")).toBeTruthy();
  });

  it("keeps the Plugins panel rendered when a runtime bundle fails to load", async () => {
    renderPanel(undefined, undefined, {
      registry: new PluginRuntimeRegistry(),
      failures: [{ pluginId: "com.example.hello-plugin", reason: "Cannot use import statement outside a module" }],
      pending: [],
      loading: false,
    });

    expect(await screen.findByText("Aucun plugin installé.")).toBeTruthy();
    expect(screen.getByText("Certains plugins installés n'ont pas pu être chargés.")).toBeTruthy();
    expect(screen.getByText("com.example.hello-plugin")).toBeTruthy();
    expect(screen.getByText(/Cannot use import statement outside a module/)).toBeTruthy();
  });

  it("keeps the Plugins panel rendered when the runtime catalog fetch fails", async () => {
    renderPanelWithLiveRuntime(new FailingRuntimeCatalogPluginGateway());

    expect(await screen.findByText("Aucun plugin installé.")).toBeTruthy();
    expect(screen.getByText("Certains plugins installés n'ont pas pu être chargés.")).toBeTruthy();
    expect(screen.getByText("<runtime-catalog>")).toBeTruthy();
    expect(screen.getByText(/runtime catalog failed/)).toBeTruthy();
  });

  it("renders runtime failures before pending plugins without repeating invariant pending reasons", async () => {
    renderPanel(undefined, undefined, {
      registry: new PluginRuntimeRegistry(),
      failures: [{ pluginId: "dev.acme.failed", reason: "activation failed" }],
      pending: [
        {
          pluginId: "dev.acme.pending",
          displayName: "Pending Plugin",
          reason: "En attente d'un projet actif.",
        },
      ],
      loading: false,
    });

    expect(await screen.findByText("Aucun plugin installé.")).toBeTruthy();
    const failureTitle = screen.getByText("Certains plugins installés n'ont pas pu être chargés.");
    const pendingTitle = screen.getByText("Certains plugins attendent un projet actif.");
    expect(
      failureTitle.compareDocumentPosition(pendingTitle) & Node.DOCUMENT_POSITION_FOLLOWING,
    ).toBeTruthy();
    expect(screen.getByText("Pending Plugin")).toBeTruthy();
    expect(screen.queryByText("En attente d'un projet actif.")).toBeNull();
  });

  it("shows project-scoped runtime plugins as pending until a project is focused", async () => {
    delete (globalThis as Record<string, unknown>).__projectPluginActivations;
    delete (globalThis as Record<string, unknown>).__projectPluginActivatedWith;
    const bundle = dataUrl(`
      export function activate(ctx) {
        globalThis.__projectPluginActivations = (globalThis.__projectPluginActivations ?? 0) + 1;
        globalThis.__projectPluginActivatedWith = ctx.pluginId;
      }
    `);
    const focusedProject = new MockFocusedProjectGateway();
    const gateways = {
      ...createMockGateways(),
      focusedProject,
      plugin: new ProjectScopedRuntimeCatalogPluginGateway(bundle),
    };

    render(
      <DIProvider gateways={gateways}>
        <PluginRuntimeProvider>
          <PluginsPanel />
        </PluginRuntimeProvider>
      </DIProvider>,
    );

    expect(await screen.findByText("Certains plugins attendent un projet actif.")).toBeTruthy();
    expect(screen.getByText("Project Plugin")).toBeTruthy();
    expect(screen.queryByText("Certains plugins installés n'ont pas pu être chargés.")).toBeNull();

    await focusedProject.setFocusedProject({ id: "p1", name: "Alpha", root: "/tmp/alpha" });

    await waitFor(() => {
      expect(screen.queryByText("Certains plugins attendent un projet actif.")).toBeNull();
      expect((globalThis as Record<string, unknown>).__projectPluginActivatedWith).toBe(
        "dev.acme.project-plugin",
      );
    });

    await focusedProject.setFocusedProject({ id: "p2", name: "Beta", root: "/tmp/beta" });
    expect((globalThis as Record<string, unknown>).__projectPluginActivations).toBe(1);
  });

  it("keeps app-scope failures distinct from project-scope pending plugins, without cross-blocking", async () => {
    delete (globalThis as Record<string, unknown>).__mixedProjectPluginActivations;
    delete (globalThis as Record<string, unknown>).__mixedProjectPluginActivatedWith;
    const failingAppBundle = dataUrl(`
      export async function activate(ctx) {
        await ctx.services.workspace.getProjectRoot();
      }
    `);
    const projectBundle = dataUrl(`
      export function activate(ctx) {
        globalThis.__mixedProjectPluginActivations =
          (globalThis.__mixedProjectPluginActivations ?? 0) + 1;
        globalThis.__mixedProjectPluginActivatedWith = ctx.pluginId;
      }
    `);
    const focusedProject = new MockFocusedProjectGateway();
    const gateways = {
      ...createMockGateways(),
      focusedProject,
      plugin: new MixedActivationScopeRuntimeCatalogPluginGateway(
        failingAppBundle,
        projectBundle,
      ),
    };

    render(
      <DIProvider gateways={gateways}>
        <PluginRuntimeProvider>
          <PluginsPanel />
        </PluginRuntimeProvider>
      </DIProvider>,
    );

    const failureTitle = await screen.findByText("Certains plugins installés n'ont pas pu être chargés.");
    const failureSection = failureTitle.closest("section");
    expect(failureSection).not.toBeNull();
    expect(within(failureSection as HTMLElement).getByText("dev.acme.app-needs-project")).toBeTruthy();
    expect(
      within(failureSection as HTMLElement).getByText((_, element) =>
        element?.tagName === "LI" &&
        (element.textContent?.includes("no current project is focused") ?? false),
      ),
    ).toBeTruthy();
    expect(screen.getByText("Certains plugins attendent un projet actif.")).toBeTruthy();
    expect(screen.getByText("Project Plugin")).toBeTruthy();

    await focusedProject.setFocusedProject({ id: "p1", name: "Alpha", root: "/tmp/alpha" });

    await waitFor(() => {
      expect(screen.queryByText("Certains plugins attendent un projet actif.")).toBeNull();
      expect((globalThis as Record<string, unknown>).__mixedProjectPluginActivatedWith).toBe(
        "dev.acme.project-plugin",
      );
    });

    await focusedProject.setFocusedProject({ id: "p2", name: "Beta", root: "/tmp/beta" });
    expect((globalThis as Record<string, unknown>).__mixedProjectPluginActivations).toBe(1);
  });

  it("installs from an archive via the review dialog, mentioning full-trust", async () => {
    renderPanel();
    await screen.findByText("Aucun plugin installé.");

    fireEvent.click(screen.getByRole("button", { name: "Installer depuis une archive…" }));

    const dialog = await screen.findByRole("dialog");
    expect(within(dialog).getByText(/full-trust/)).toBeTruthy();

    fireEvent.click(within(dialog).getByRole("button", { name: "Installer" }));

    // The installed plugin now shows in the list, restart required (carnet §1.4).
    expect(await screen.findByText("mock-plugin")).toBeTruthy();
    expect(screen.getByText("Redémarrage requis")).toBeTruthy();
    expect(screen.getByText("Activé")).toBeTruthy();
  });

  it("keeps install, uninstall and reinstall stable with backend-shaped reviews", async () => {
    renderPanel(new BackendShapedReviewPluginGateway());
    await screen.findByText("Aucun plugin installé.");

    fireEvent.click(screen.getByRole("button", { name: "Installer depuis une archive…" }));
    let dialog = await screen.findByRole("dialog");
    expect(within(dialog).getByText(/full-trust/)).toBeTruthy();
    fireEvent.click(within(dialog).getByRole("button", { name: "Installer" }));
    expect(await screen.findByText("mock-plugin")).toBeTruthy();

    fireEvent.click(screen.getByRole("button", { name: "Désinstaller" }));
    dialog = await screen.findByRole("dialog");
    fireEvent.click(within(dialog).getByRole("button", { name: "Désinstaller" }));
    expect(await screen.findByText("Aucun plugin installé.")).toBeTruthy();

    fireEvent.click(screen.getByRole("button", { name: "Installer depuis une archive…" }));
    dialog = await screen.findByRole("dialog");
    expect(within(dialog).getByRole("button", { name: "Installer" })).toBeTruthy();
    fireEvent.click(within(dialog).getByRole("button", { name: "Installer" }));

    expect(await screen.findByText("mock-plugin")).toBeTruthy();
    expect(screen.queryByText("INTERFACE INTERROMPUE")).toBeNull();
  });

  it("renders an invalid plugin returned after install as isolated, without replacing the panel", async () => {
    renderPanel(new InvalidInstallPluginGateway());
    await screen.findByText("Aucun plugin installé.");

    fireEvent.click(screen.getByRole("button", { name: "Installer depuis un dossier…" }));
    const dialog = await screen.findByRole("dialog");
    fireEvent.click(within(dialog).getByRole("button", { name: "Installer" }));

    expect(await screen.findByText("Missing Main Plugin")).toBeTruthy();
    expect(screen.getByText("Invalide")).toBeTruthy();
    expect(screen.getByText("plugin main asset is missing: dist/index.js")).toBeTruthy();
    expect(screen.getByRole("button", { name: "Isolé" }).hasAttribute("disabled")).toBe(true);
    expect(screen.queryByRole("button", { name: "Activer" })).toBeNull();
  });

  it("disables an installed plugin after confirmation", async () => {
    const plugin = new MockPluginGateway();
    plugin._seedPlugin({
      id: "dev.acme.one",
      displayName: "Acme One",
      version: "1.0.0",
      sourceKind: "archive",
      lifecycleState: "enabled",
      enabled: true,
      pendingUninstall: false,
      restartRequired: false,
      trustLevel: "full",
      contributionSummary: { topLevelMenus: 0, menuItems: 0, layouts: 0, mcpServers: 0 },
    });
    renderPanel(plugin);

    await screen.findByText("Acme One");
    fireEvent.click(screen.getByRole("button", { name: "Désactiver" }));

    const dialog = await screen.findByRole("dialog");
    fireEvent.click(within(dialog).getByRole("button", { name: "Désactiver" }));

    await waitFor(() => expect(screen.getByText("Désactivé")).toBeTruthy());
  });

  it("uninstalls an installed plugin after confirmation", async () => {
    const plugin = new MockPluginGateway();
    plugin._seedPlugin({
      id: "dev.acme.two",
      displayName: "Acme Two",
      version: "1.0.0",
      sourceKind: "directory",
      lifecycleState: "enabled",
      enabled: true,
      pendingUninstall: false,
      restartRequired: false,
      trustLevel: "full",
      contributionSummary: { topLevelMenus: 0, menuItems: 0, layouts: 0, mcpServers: 0 },
    });
    renderPanel(plugin);

    await screen.findByText("Acme Two");
    fireEvent.click(screen.getByRole("button", { name: "Désinstaller" }));

    const dialog = await screen.findByRole("dialog");
    fireEvent.click(within(dialog).getByRole("button", { name: "Désinstaller" }));

    await waitFor(() => expect(screen.queryByText("Acme Two")).toBeNull());
    expect(await screen.findByText("Aucun plugin installé.")).toBeTruthy();
  });
});
