/**
 * F3 — plugin menu contribution resolution (ticket #43, carnet §10 F3
 * acceptance criteria: "ordre déterministe, disabledReason, plugin disabled
 * absent, command handler appelé").
 */
import { describe, expect, it } from "vitest";

import type { PluginContributionDto } from "@/domain";
import { PluginRuntimeRegistry, type LoadedPlugin, type WhenContext } from "@/plugins/runtime";
import { PluginCommandRegistry, PluginLayoutRegistry, PluginMenuRegistry } from "@/plugins/runtime";
import { resolveMenuItems, resolveTopLevelMenus } from "./menus";

const NO_CONTEXT: WhenContext = {
  projectOpen: false,
  gitRepository: false,
  agentSelected: false,
  terminalFocused: false,
  layoutCellFocused: false,
};

function stubPlugin(pluginId: string, displayName: string, contributes: PluginContributionDto): LoadedPlugin {
  return {
    pluginId,
    displayName,
    contributes,
    commands: new PluginCommandRegistry(pluginId, new Set(contributes.menuItems.map((i) => i.command))),
    layouts: new PluginLayoutRegistry(pluginId, new Set(contributes.layouts.map((l) => l.type))),
    menu: new PluginMenuRegistry(pluginId),
    dispose: async () => {},
  };
}

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

describe("resolveTopLevelMenus", () => {
  it("sorts by order, then plugin display name, then label", () => {
    const registry = new PluginRuntimeRegistry();
    registry.add(
      stubPlugin("dev.b", "Beta", {
        ...empty(),
        menus: [{ id: "b.menu", label: "B Menu", topLevel: true, order: 0 }],
      }),
    );
    registry.add(
      stubPlugin("dev.a", "Alpha", {
        ...empty(),
        menus: [{ id: "a.menu", label: "A Menu", topLevel: true, order: 0 }],
      }),
    );
    registry.add(
      stubPlugin("dev.z", "Zulu", {
        ...empty(),
        menus: [{ id: "z.menu", label: "Z Menu", topLevel: true, order: -1 }],
      }),
    );

    const resolved = resolveTopLevelMenus(registry);
    expect(resolved.map((m) => m.label)).toEqual(["Z Menu", "A Menu", "B Menu"]);
  });
});

describe("resolveMenuItems", () => {
  it("only returns items targeting the requested menu, and only from loaded (i.e. enabled) plugins", () => {
    const registry = new PluginRuntimeRegistry();
    registry.add(
      stubPlugin("dev.acme", "Acme", {
        ...empty(),
        menuItems: [
          { id: "dev.acme.a", targetMenuId: "panels", label: "Open A", command: "dev.acme.a.cmd" },
          { id: "dev.acme.b", targetMenuId: "settings", label: "Open B", command: "dev.acme.b.cmd" },
        ],
      }),
    );
    // A disabled plugin never appears in the registry at all (carnet §1.3) —
    // simulated here by simply not adding it.

    const panelsItems = resolveMenuItems(registry, "panels", NO_CONTEXT);
    expect(panelsItems).toHaveLength(1);
    expect(panelsItems[0].label).toBe("Open A");

    const settingsItems = resolveMenuItems(registry, "settings", NO_CONTEXT);
    expect(settingsItems).toHaveLength(1);
    expect(settingsItems[0].label).toBe("Open B");
  });

  it("evaluates `when` and disables with a diagnostic reason on failure, enables on success", () => {
    const registry = new PluginRuntimeRegistry();
    registry.add(
      stubPlugin("dev.acme", "Acme", {
        ...empty(),
        menuItems: [
          {
            id: "dev.acme.needs-git",
            targetMenuId: "panels",
            label: "Needs git",
            command: "dev.acme.git.cmd",
            when: "gitRepository",
          },
          {
            id: "dev.acme.broken-when",
            targetMenuId: "panels",
            label: "Broken when",
            command: "dev.acme.broken.cmd",
            when: "notAVariable",
          },
        ],
      }),
    );

    const withoutGit = resolveMenuItems(registry, "panels", NO_CONTEXT);
    const needsGit = withoutGit.find((i) => i.label === "Needs git")!;
    expect(needsGit.enabled).toBe(false);
    expect(needsGit.disabledReason).toBeUndefined();

    const withGit = resolveMenuItems(registry, "panels", { ...NO_CONTEXT, gitRepository: true });
    expect(withGit.find((i) => i.label === "Needs git")!.enabled).toBe(true);

    const broken = withoutGit.find((i) => i.label === "Broken when")!;
    expect(broken.enabled).toBe(false);
    expect(broken.disabledReason).toMatch(/unknown variable/);
  });

  it("sorts resolved items deterministically", () => {
    const registry = new PluginRuntimeRegistry();
    registry.add(
      stubPlugin("dev.acme", "Acme", {
        ...empty(),
        menuItems: [
          { id: "dev.acme.z", targetMenuId: "panels", label: "Z item", command: "dev.acme.z.cmd", order: 1 },
          { id: "dev.acme.a", targetMenuId: "panels", label: "A item", command: "dev.acme.a.cmd", order: 0 },
        ],
      }),
    );
    const items = resolveMenuItems(registry, "panels", NO_CONTEXT);
    expect(items.map((i) => i.label)).toEqual(["A item", "Z item"]);
  });

  it("dispatches a command via the registry's runCommand", async () => {
    const registry = new PluginRuntimeRegistry();
    let ran = false;
    const plugin = stubPlugin("dev.acme", "Acme", {
      ...empty(),
      menuItems: [{ id: "dev.acme.a", targetMenuId: "panels", label: "Open A", command: "dev.acme.a.cmd" }],
    });
    plugin.commands.register("dev.acme.a.cmd", () => {
      ran = true;
    });
    registry.add(plugin);

    await registry.runCommand("dev.acme", "dev.acme.a.cmd");
    expect(ran).toBe(true);
  });
});
