/**
 * F1 — plugin bootstrap loader tests (ticket #43, carnet §6/§10 F1 acceptance
 * criteria: "plugins mock chargés, registration refusée si non déclarée,
 * dispose appelé, disabled absent du bootstrap").
 *
 * Bundles are loaded via a real dynamic `import()` of `data:` URLs (supported
 * by Node's ESM loader, which Vitest runs on) so the loader is exercised
 * exactly as it runs against the `idea-plugin://...` protocol in production —
 * no mocking of `import()` itself.
 */
import { describe, expect, it } from "vitest";

import type { PluginContributionDto, PluginRuntimePlugin } from "@/domain";
import { loadPlugins } from "./loader";
import type { PluginGatewaySet } from "./loader";

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

function emptyContributes(): PluginContributionDto {
  return { menus: [], menuItems: [], layouts: [], mcpServers: [] };
}

const gateways = {} as PluginGatewaySet;

function entry(overrides: Partial<PluginRuntimePlugin> & { bundleUrl: string }): PluginRuntimePlugin {
  return {
    id: "mock.plugin",
    displayName: "Mock Plugin",
    version: "1.0.0",
    contentHash: "abc123",
    contributes: emptyContributes(),
    ...overrides,
  };
}

describe("loadPlugins", () => {
  it("loads a well-formed plugin bundle and calls activate(ctx)", async () => {
    const bundle = dataUrl(`
      export function activate(ctx) {
        globalThis.__activatedWith = ctx.pluginId;
        return {};
      }
    `);
    const { registry, failures } = await loadPlugins(
      [entry({ id: "dev.acme.one", displayName: "One", bundleUrl: bundle })],
      gateways,
    );
    expect(failures).toEqual([]);
    expect(registry.list().map((p) => p.pluginId)).toEqual(["dev.acme.one"]);
    expect((globalThis as Record<string, unknown>).__activatedWith).toBe("dev.acme.one");
  });

  it("loads an SDK-style default export containing activate(ctx)", async () => {
    const bundle = dataUrl(`
      export default {
        activate(ctx) {
          globalThis.__defaultExportActivatedWith = ctx.pluginId;
        },
      };
    `);
    const { registry, failures } = await loadPlugins(
      [entry({ id: "com.example.hello-plugin", displayName: "Hello Plugin", bundleUrl: bundle })],
      gateways,
    );

    expect(failures).toEqual([]);
    expect(registry.list().map((p) => p.pluginId)).toEqual(["com.example.hello-plugin"]);
    expect((globalThis as Record<string, unknown>).__defaultExportActivatedWith).toBe(
      "com.example.hello-plugin",
    );
  });

  it("refuses to register a command not declared in the manifest", async () => {
    const bundle = dataUrl(`
      export function activate(ctx) {
        let caught = null;
        try {
          ctx.commands.register("dev.acme.undeclared.cmd", () => {});
        } catch (e) {
          caught = String(e);
        }
        globalThis.__registerError = caught;
        return {};
      }
    `);
    const { failures } = await loadPlugins(
      [
        entry({
          id: "dev.acme.two",
          displayName: "Two",
          bundleUrl: bundle,
          contributes: emptyContributes(),
        }),
      ],
      gateways,
    );
    expect(failures).toEqual([]);
    expect((globalThis as Record<string, unknown>).__registerError).toMatch(
      /not declared by any menu item/,
    );
  });

  it("accepts registering a command declared via a menu item contribution", async () => {
    const bundle = dataUrl(`
      export function activate(ctx) {
        ctx.commands.register("dev.acme.three.open", () => {
          globalThis.__ranCommand = true;
        });
        return {};
      }
    `);
    const { registry, failures } = await loadPlugins(
      [
        entry({
          id: "dev.acme.three",
          displayName: "Three",
          bundleUrl: bundle,
          contributes: {
            ...emptyContributes(),
            menuItems: [
              {
                id: "dev.acme.three.item",
                targetMenuId: "panels",
                label: "Open",
                command: "dev.acme.three.open",
              },
            ],
          },
        }),
      ],
      gateways,
    );
    expect(failures).toEqual([]);
    await registry.runCommand("dev.acme.three", "dev.acme.three.open");
    expect((globalThis as Record<string, unknown>).__ranCommand).toBe(true);
  });

  it("loads plugins built against the public SDK context shape", async () => {
    const bundle = dataUrl(`
      export function activate(ctx) {
        ctx.logger.info("hello");
        const disposable = ctx.commands.registerCommand("hello-plugin.sayHello", () => {
          globalThis.__helloCommandRan = true;
          return "Hello from IdeA";
        });
        ctx.subscriptions.push(disposable);
      }
    `);
    const { registry, failures } = await loadPlugins(
      [
        entry({
          id: "com.example.hello-plugin",
          displayName: "Hello Plugin",
          bundleUrl: bundle,
          contributes: {
            ...emptyContributes(),
            menuItems: [
              {
                id: "hello-plugin.sayHello.item",
                targetMenuId: "plugin:hello-plugin.menu",
                label: "Say Hello",
                command: "hello-plugin.sayHello",
              },
            ],
          },
        }),
      ],
      gateways,
    );

    expect(failures).toEqual([]);
    await registry.runCommand("com.example.hello-plugin", "hello-plugin.sayHello");
    expect((globalThis as Record<string, unknown>).__helloCommandRan).toBe(true);

    await registry.remove("com.example.hello-plugin");
    expect(registry.get("com.example.hello-plugin")).toBeUndefined();
  });

  it("does not inject the public plugin services facade without the tooling capability", async () => {
    const bundle = dataUrl(`
      export function activate(ctx) {
        globalThis.__servicesWithoutTooling = ctx.services;
      }
    `);
    const { failures } = await loadPlugins(
      [entry({ id: "dev.acme.no-services", displayName: "No Services", bundleUrl: bundle })],
      gateways,
    );

    expect(failures).toEqual([]);
    expect((globalThis as Record<string, unknown>).__servicesWithoutTooling).toBeUndefined();
  });

  it("injects the public plugin services facade for plugins declaring tooling", async () => {
    const bundle = dataUrl(`
      export function activate(ctx) {
        globalThis.__activationContextKeys = Object.keys(ctx).sort();
        globalThis.__hasPrivateGateway = [
          "project",
          "git",
          "terminal",
          "agents",
          "system",
          "workState",
          "focusedProject",
          "pluginWorkspace",
          "pluginTask",
          "pluginToolchain",
          "pluginEvents",
          "pluginConfig",
          "pluginStorage",
        ].some((key) => key in ctx);
        globalThis.__serviceKeys = Object.keys(ctx.services).sort();
        globalThis.__workspaceServiceKeys = Object.keys(ctx.services.workspace).sort();
        globalThis.__taskServiceKeys = Object.keys(ctx.services.tasks).sort();
        globalThis.__toolingServiceKeys = Object.keys(ctx.services.tooling).sort();
        globalThis.__eventServiceKeys = Object.keys(ctx.services.events).sort();
        globalThis.__configServiceKeys = Object.keys(ctx.services.config).sort();
        globalThis.__terminalServiceKeys = Object.keys(ctx.services.terminal).sort();
        globalThis.__windowServiceKeys = Object.keys(ctx.services.windows).sort();
      }
    `);
    const { failures } = await loadPlugins(
      [
        entry({
          id: "dev.acme.services",
          displayName: "Services",
          capabilities: ["tooling"],
          bundleUrl: bundle,
        }),
      ],
      gateways,
    );

    expect(failures).toEqual([]);
    expect((globalThis as Record<string, unknown>).__activationContextKeys).toEqual([
      "commands",
      "layouts",
      "logger",
      "menu",
      "pluginDisplayName",
      "pluginId",
      "services",
      "storage",
      "subscriptions",
      "version",
    ]);
    expect((globalThis as Record<string, unknown>).__hasPrivateGateway).toBe(false);
    expect((globalThis as Record<string, unknown>).__serviceKeys).toEqual([
      "config",
      "events",
      "tasks",
      "terminal",
      "tooling",
      "windows",
      "workspace",
    ]);
    expect((globalThis as Record<string, unknown>).__workspaceServiceKeys).toEqual([
      "getCurrentProject",
      "getProjectRoot",
      "listDirectory",
      "queryStructure",
      "readBinaryFile",
      "readProjectContext",
      "readTextFile",
      "resolvePath",
      "stat",
      "updateProjectContext",
      "watch",
      "writeBinaryFile",
      "writeTextFile",
    ]);
    expect((globalThis as Record<string, unknown>).__taskServiceKeys).toEqual([
      "attachOutput",
      "cancel",
      "getCommandStatus",
      "getStatus",
      "list",
      "retry",
      "runCommand",
    ]);
    expect((globalThis as Record<string, unknown>).__toolingServiceKeys).toEqual([
      "diagnose",
    ]);
    expect((globalThis as Record<string, unknown>).__eventServiceKeys).toEqual([
      "subscribe",
    ]);
    expect((globalThis as Record<string, unknown>).__configServiceKeys).toEqual([
      "readDocument",
      "updateDocument",
    ]);
    expect((globalThis as Record<string, unknown>).__terminalServiceKeys).toEqual([
      "close",
      "open",
      "reattach",
    ]);
    expect((globalThis as Record<string, unknown>).__windowServiceKeys).toEqual([
      "open",
    ]);
  });

  it("injects services for UI plugins and validates plugin layout windows against declared layouts", async () => {
    const calls: unknown[] = [];
    const windowGateways = {
      ...gateways,
      window: {
        async openViewWindow() {},
        async openPluginLayoutWindow(input: unknown) {
          calls.push(input);
          return {
            label: "view-plugin-layout",
            url: "index.html?pluginLayout=1",
            alreadyOpen: false,
            providerPluginDisplayName: "React Plugin",
            layoutLabel: "Dashboard",
            surface: {
              pluginId: "dev.acme.react",
              layoutType: "dev.acme.react.dashboard",
              state: { tab: "overview" },
            },
          };
        },
        async closeViewWindow() {},
        async listOpenViewWindows() {
          return [];
        },
        async onViewWindowClosed() {
          return () => {};
        },
      },
    } as PluginGatewaySet;
    const bundle = dataUrl(`
      export async function activate(ctx) {
        globalThis.__uiServicesAvailable = Boolean(ctx.services);
        globalThis.__pluginWindowResult = await ctx.services.windows.open({
          layoutType: "dev.acme.react.dashboard",
          state: { tab: "overview" },
        });
        try {
          await ctx.services.windows.open({ layoutType: "dev.acme.react.missing" });
        } catch (e) {
          globalThis.__pluginWindowValidationError = String(e);
        }
      }
    `);

    const { failures } = await loadPlugins(
      [
        entry({
          id: "dev.acme.react",
          displayName: "React Plugin",
          capabilities: ["ui"],
          bundleUrl: bundle,
          contributes: {
            ...emptyContributes(),
            layouts: [
              {
                type: "dev.acme.react.dashboard",
                label: "Dashboard",
                component: "Dashboard",
              },
            ],
          },
        }),
      ],
      windowGateways,
    );

    expect(failures).toEqual([]);
    expect((globalThis as Record<string, unknown>).__uiServicesAvailable).toBe(true);
    expect((globalThis as Record<string, unknown>).__pluginWindowResult).toMatchObject({
      label: "view-plugin-layout",
      surface: { layoutType: "dev.acme.react.dashboard" },
    });
    expect((globalThis as Record<string, unknown>).__pluginWindowValidationError).toMatch(
      /undeclared layout/,
    );
    expect(calls).toEqual([
      {
        pluginId: "dev.acme.react",
        layoutType: "dev.acme.react.dashboard",
        state: { tab: "overview" },
      },
    ]);
  });

  it("injects plugin-owned storage scoped to the activating plugin id", async () => {
    const calls: unknown[] = [];
    const storageGateways = {
      ...gateways,
      pluginStorage: {
        async get(input: unknown) {
          calls.push(["get", input]);
          return { launches: 1 };
        },
        async set(input: unknown) {
          calls.push(["set", input]);
        },
        async delete(input: unknown) {
          calls.push(["delete", input]);
          return true;
        },
      },
    } as PluginGatewaySet;
    const bundle = dataUrl(`
      export async function activate(ctx) {
        globalThis.__storageKeys = Object.keys(ctx.storage).sort();
        globalThis.__storedValue = await ctx.storage.get("helloPlugin.state");
        await ctx.storage.set("helloPlugin.state", { launches: 2 });
        await ctx.storage.delete("helloPlugin.state");
      }
    `);

    const { failures } = await loadPlugins(
      [entry({ id: "dev.acme.storage", displayName: "Storage", bundleUrl: bundle })],
      storageGateways,
    );

    expect(failures).toEqual([]);
    expect((globalThis as Record<string, unknown>).__storageKeys).toEqual([
      "delete",
      "get",
      "set",
    ]);
    expect((globalThis as Record<string, unknown>).__storedValue).toEqual({ launches: 1 });
    expect(calls).toEqual([
      ["get", { pluginId: "dev.acme.storage", key: "helloPlugin.state" }],
      [
        "set",
        {
          pluginId: "dev.acme.storage",
          key: "helloPlugin.state",
          value: { launches: 2 },
        },
      ],
      ["delete", { pluginId: "dev.acme.storage", key: "helloPlugin.state" }],
    ]);
  });

  it("leaves project-scoped plugins pending without a focused project", async () => {
    const bundle = dataUrl(`
      export function activate(ctx) {
        globalThis.__pendingProjectPluginActivated = ctx.pluginId;
      }
    `);
    const noFocusedProjectGateways = {
      ...gateways,
      focusedProject: {
        async getFocusedProject() {
          return null;
        },
      },
    } as PluginGatewaySet;

    const { registry, failures, pending } = await loadPlugins(
      [
        entry({
          id: "dev.acme.project-only",
          displayName: "Project Only",
          activationScope: "project",
          bundleUrl: bundle,
        }),
      ],
      noFocusedProjectGateways,
    );

    expect(registry.list()).toEqual([]);
    expect(failures).toEqual([]);
    expect(pending).toEqual([
      {
        pluginId: "dev.acme.project-only",
        displayName: "Project Only",
        reason: "En attente d'un projet actif.",
      },
    ]);
    expect((globalThis as Record<string, unknown>).__pendingProjectPluginActivated).toBeUndefined();
  });

  it("activates project-scoped plugins when a project is already focused", async () => {
    const bundle = dataUrl(`
      export function activate(ctx) {
        globalThis.__focusedProjectPluginActivated = ctx.pluginId;
      }
    `);
    const focusedProjectGateways = {
      ...gateways,
      focusedProject: {
        async getFocusedProject() {
          return { id: "p1", name: "Alpha", root: "/tmp/alpha" };
        },
      },
    } as PluginGatewaySet;

    const { registry, failures, pending } = await loadPlugins(
      [
        entry({
          id: "dev.acme.project-focused",
          displayName: "Project Focused",
          activationScope: "project",
          bundleUrl: bundle,
        }),
      ],
      focusedProjectGateways,
    );

    expect(failures).toEqual([]);
    expect(pending).toEqual([]);
    expect(registry.list().map((p) => p.pluginId)).toEqual(["dev.acme.project-focused"]);
    expect((globalThis as Record<string, unknown>).__focusedProjectPluginActivated).toBe(
      "dev.acme.project-focused",
    );
  });

  it("treats omitted activationScope as app and isolates a project-required activation failure", async () => {
    const projectDependentBundle = dataUrl(`
      export async function activate(ctx) {
        await ctx.services.workspace.getProjectRoot();
      }
    `);
    const healthyBundle = dataUrl(`
      export function activate(ctx) {
        globalThis.__healthyAppPluginActivated = ctx.pluginId;
      }
    `);
    const noFocusedProjectGateways = {
      ...gateways,
      focusedProject: {
        async getFocusedProject() {
          return null;
        },
      },
    } as PluginGatewaySet;

    const { registry, failures, pending } = await loadPlugins(
      [
        entry({
          id: "dev.acme.default-app-scope",
          displayName: "Default App Scope",
          capabilities: ["tooling"],
          bundleUrl: projectDependentBundle,
        }),
        entry({
          id: "dev.acme.healthy-app",
          displayName: "Healthy App",
          bundleUrl: healthyBundle,
        }),
      ],
      noFocusedProjectGateways,
    );

    expect(registry.list().map((p) => p.pluginId)).toEqual(["dev.acme.healthy-app"]);
    expect(failures).toEqual([
      {
        pluginId: "dev.acme.default-app-scope",
        reason: "no current project is focused",
      },
    ]);
    expect(pending).toEqual([]);
    expect((globalThis as Record<string, unknown>).__healthyAppPluginActivated).toBe(
      "dev.acme.healthy-app",
    );
  });

  it("loads the hello-plugin command and layout contribution shape", async () => {
    const bundle = dataUrl(`
      export function activate(ctx) {
        ctx.commands.registerCommand("hello-plugin", () => {
          globalThis.__helloArchiveCommandRan = true;
        });
        ctx.layouts.register({
          type: "hello-plugin.hello-world",
          component: () => "hello-world",
        });
      }
    `);
    const { registry, failures } = await loadPlugins(
      [
        entry({
          id: "com.example.hello-plugin",
          displayName: "Hello Plugin",
          bundleUrl: bundle,
          contributes: {
            menus: [{ id: "hello-plugin.menu", label: "Hello Plugin", topLevel: true }],
            menuItems: [
              {
                id: "hello-plugin.command.item",
                targetMenuId: "hello-plugin.menu",
                label: "hello-plugin",
                command: "hello-plugin",
              },
            ],
            layouts: [
              {
                type: "hello-plugin.hello-world",
                label: "hello-world",
                component: "hello-world",
              },
            ],
          } as unknown as PluginContributionDto,
        }),
      ],
      gateways,
    );

    expect(failures).toEqual([]);
    expect(registry.get("com.example.hello-plugin")?.contributes.layouts).toEqual([
      {
        type: "hello-plugin.hello-world",
        label: "hello-world",
        component: "hello-world",
      },
    ]);
    expect(registry.get("com.example.hello-plugin")?.contributes.mcpServers).toEqual([]);
    await registry.runCommand("com.example.hello-plugin", "hello-plugin");
    expect((globalThis as Record<string, unknown>).__helloArchiveCommandRan).toBe(true);
    const Layout = registry.layoutComponent(
      "com.example.hello-plugin",
      "hello-plugin.hello-world",
    );
    expect(Layout).toBeDefined();
    expect((Layout as unknown as () => string)()).toBe("hello-world");
  });

  it("confines a malformed runtime catalog entry and still loads healthy plugins", async () => {
    const bundle = dataUrl(`
      export function activate(ctx) {
        globalThis.__healthyPluginActivated = ctx.pluginId;
      }
    `);

    const { registry, failures } = await loadPlugins(
      [
        null,
        entry({
          id: "dev.acme.healthy",
          displayName: "Healthy",
          bundleUrl: bundle,
          contributes: {
            menus: [null],
            menuItems: [null, { id: "x", targetMenuId: "panels", label: "X" }],
            layouts: [null, { label: "Missing type" }],
          } as unknown as PluginContributionDto,
        }),
      ] as unknown as PluginRuntimePlugin[],
      gateways,
    );

    expect(registry.list().map((p) => p.pluginId)).toEqual(["dev.acme.healthy"]);
    expect(failures).toEqual([
      { pluginId: "<unknown-plugin>", reason: expect.stringContaining("bundle URL") },
    ]);
    expect((globalThis as Record<string, unknown>).__healthyPluginActivated).toBe(
      "dev.acme.healthy",
    );
  });

  it("calls dispose() on removal (best-effort)", async () => {
    const bundle = dataUrl(`
      export function activate(ctx) {
        return {
          dispose: () => {
            globalThis.__disposed = ctx.pluginId;
          },
        };
      }
    `);
    const { registry } = await loadPlugins(
      [entry({ id: "dev.acme.four", displayName: "Four", bundleUrl: bundle })],
      gateways,
    );
    await registry.remove("dev.acme.four");
    expect((globalThis as Record<string, unknown>).__disposed).toBe("dev.acme.four");
    expect(registry.get("dev.acme.four")).toBeUndefined();
  });

  it("collects a failure instead of throwing when a bundle has no activate()", async () => {
    const bundle = dataUrl(`export const notAPlugin = true;`);
    const { registry, failures } = await loadPlugins(
      [entry({ id: "dev.acme.five", displayName: "Five", bundleUrl: bundle })],
      gateways,
    );
    expect(registry.list()).toEqual([]);
    expect(failures).toEqual([
      { pluginId: "dev.acme.five", reason: expect.stringContaining("activate") },
    ]);
  });

  it("disposes partial subscriptions when activation fails", async () => {
    const bundle = dataUrl(`
      export function activate(ctx) {
        ctx.subscriptions.push({
          dispose() {
            globalThis.__partialActivationDisposed = ctx.pluginId;
          },
        });
        throw new Error("activation failed");
      }
    `);

    const { registry, failures } = await loadPlugins(
      [entry({ id: "dev.acme.partial", displayName: "Partial", bundleUrl: bundle })],
      gateways,
    );

    expect(registry.list()).toEqual([]);
    expect(failures).toEqual([
      { pluginId: "dev.acme.partial", reason: "activation failed" },
    ]);
    expect((globalThis as Record<string, unknown>).__partialActivationDisposed).toBe(
      "dev.acme.partial",
    );
  });

  it("collects a failure when plugin activation does not settle", async () => {
    const bundle = dataUrl(`
      export function activate() {
        return new Promise(() => {});
      }
    `);

    const { registry, failures } = await loadPlugins(
      [entry({ id: "dev.acme.hung", displayName: "Hung", bundleUrl: bundle })],
      gateways,
      { timeoutMs: 10 },
    );

    expect(registry.list()).toEqual([]);
    expect(failures).toEqual([
      {
        pluginId: "dev.acme.hung",
        reason: "activating plugin timed out after 10 ms",
      },
    ]);
  });

  it("only loads what the catalog contains — a disabled/absent plugin is simply never in it", async () => {
    // The backend contract (carnet §1.3) filters the catalog to
    // `enabled && !pendingUninstall` before the loader ever sees it; the
    // loader itself has nothing more to filter — an empty catalog loads
    // nothing.
    const { registry, failures } = await loadPlugins([], gateways);
    expect(registry.list()).toEqual([]);
    expect(failures).toEqual([]);
  });
});
