/**
 * 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 { MockPluginGateway, MockSystemGateway } from "@/adapters/mock";
import type { PluginInstallResult, PluginRuntimeContributionCatalog } from "@/domain";
import type { Gateways } from "@/ports";
import { DIProvider } from "@/app/di";
import { PluginRuntimeRegistry } from "@/plugins/runtime";
import { PluginsPanel } from "./PluginsPanel";
import { PluginRuntimeProvider, type PluginRuntimeContextValue } from "./PluginRuntimeProvider";

function renderPanel(
  plugin?: MockPluginGateway,
  system?: MockSystemGateway,
  runtimeValue: PluginRuntimeContextValue = {
    registry: new PluginRuntimeRegistry(),
    failures: [],
    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 = { plugin, system } as unknown as Gateways;
  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" };
  }
}

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" }],
      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("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("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();
  });
});
