/**
 * 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("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("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("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([]);
  });
});
