import { afterEach, describe, expect, it, vi } from "vitest";
import { registerPluginHttpRoute } from "./http-registry.js";
import { createEmptyPluginRegistry } from "./registry-empty.js";
import {
  pinActivePluginHttpRouteRegistry,
  releasePinnedPluginHttpRouteRegistry,
  resetPluginRuntimeStateForTest,
  setActivePluginRegistry,
} from "./runtime.js";

function expectRouteRegistrationDenied(params: {
  replaceExisting: boolean;
  expectedLogFragment: string;
}) {
  const { registry, logs, register } = createLoggedRouteHarness();

  register({
    path: "/plugins/demo",
    auth: "plugin",
    pluginId: "demo-a",
    source: "demo-a-src",
  });

  const unregister = register({
    path: "/plugins/demo",
    auth: "plugin",
    ...(params.replaceExisting ? { replaceExisting: true } : {}),
    pluginId: "demo-b",
    source: "demo-b-src",
  });

  expect(registry.httpRoutes).toHaveLength(1);
  expect(logs.at(-1)).toContain(params.expectedLogFragment);

  unregister();
  expect(registry.httpRoutes).toHaveLength(1);
}

function expectRegisteredRouteShape(
  registry: ReturnType<typeof createEmptyPluginRegistry>,
  params: {
    path: string;
    handler?: unknown;
    auth: "plugin" | "gateway";
    match?: "exact" | "prefix";
  },
) {
  expect(registry.httpRoutes).toHaveLength(1);
  expect(registry.httpRoutes[0]).toEqual(
    expect.objectContaining({
      path: params.path,
      auth: params.auth,
      ...(params.match ? { match: params.match } : {}),
      ...(params.handler ? { handler: params.handler } : {}),
    }),
  );
}

function createLoggedRouteHarness() {
  const registry = createEmptyPluginRegistry();
  const logs: string[] = [];
  return {
    registry,
    logs,
    register: (
      params: Omit<
        Parameters<typeof registerPluginHttpRoute>[0],
        "registry" | "handler" | "log"
      > & {
        handler?: Parameters<typeof registerPluginHttpRoute>[0]["handler"];
      },
    ) =>
      registerPluginHttpRoute({
        ...params,
        handler: params.handler ?? vi.fn(),
        registry,
        log: (msg) => logs.push(msg),
      }),
  };
}

describe("registerPluginHttpRoute", () => {
  afterEach(() => {
    releasePinnedPluginHttpRouteRegistry();
    resetPluginRuntimeStateForTest();
  });

  it("registers route and unregisters it", () => {
    const registry = createEmptyPluginRegistry();
    const handler = vi.fn();

    const unregister = registerPluginHttpRoute({
      path: "/plugins/demo",
      auth: "plugin",
      handler,
      registry,
    });

    expectRegisteredRouteShape(registry, {
      path: "/plugins/demo",
      handler,
      auth: "plugin",
      match: "exact",
    });

    unregister();
    expect(registry.httpRoutes).toHaveLength(0);
  });

  it("returns noop unregister when path is missing", () => {
    const registry = createEmptyPluginRegistry();
    const logs: string[] = [];
    const unregister = registerPluginHttpRoute({
      path: "",
      auth: "plugin",
      handler: vi.fn(),
      registry,
      accountId: "default",
      log: (msg) => logs.push(msg),
    });

    expect(registry.httpRoutes).toHaveLength(0);
    expect(logs).toEqual(['plugin: webhook path missing for account "default"']);
    expect(() => unregister()).not.toThrow();
  });

  it("replaces stale route on same path when replaceExisting=true", () => {
    const { registry, logs, register } = createLoggedRouteHarness();
    const firstHandler = vi.fn();
    const secondHandler = vi.fn();

    const unregisterFirst = register({
      path: "/plugins/synology",
      auth: "plugin",
      handler: firstHandler,
      accountId: "default",
      pluginId: "synology-chat",
    });

    const unregisterSecond = register({
      path: "/plugins/synology",
      auth: "plugin",
      replaceExisting: true,
      handler: secondHandler,
      accountId: "default",
      pluginId: "synology-chat",
    });

    expect(registry.httpRoutes).toHaveLength(1);
    expect(registry.httpRoutes[0]?.handler).toBe(secondHandler);
    expect(logs).toContain(
      'plugin: replacing stale webhook path /plugins/synology (exact) for account "default" (synology-chat)',
    );

    // Old unregister must not remove the replacement route.
    unregisterFirst();
    expect(registry.httpRoutes).toHaveLength(1);
    expect(registry.httpRoutes[0]?.handler).toBe(secondHandler);

    unregisterSecond();
    expect(registry.httpRoutes).toHaveLength(0);
  });

  it.each([
    {
      name: "rejects conflicting route registrations without replaceExisting",
      replaceExisting: false,
      expectedLogFragment: "route conflict",
    },
    {
      name: "rejects route replacement when a different plugin owns the route",
      replaceExisting: true,
      expectedLogFragment: "route replacement denied",
    },
  ] as const)("$name", ({ replaceExisting, expectedLogFragment }) => {
    expectRouteRegistrationDenied({
      replaceExisting,
      expectedLogFragment,
    });
  });

  it("rejects mixed-auth overlapping routes", () => {
    const { registry, logs, register } = createLoggedRouteHarness();

    register({
      path: "/plugin/secure",
      auth: "gateway",
      match: "prefix",
      pluginId: "demo-gateway",
      source: "demo-gateway-src",
    });

    const unregister = register({
      path: "/plugin/secure/report",
      auth: "plugin",
      match: "exact",
      pluginId: "demo-plugin",
      source: "demo-plugin-src",
    });

    expect(registry.httpRoutes).toHaveLength(1);
    expect(logs.at(-1)).toContain("route overlap denied");

    unregister();
    expect(registry.httpRoutes).toHaveLength(1);
  });

  it("uses the pinned route registry when the active registry changes later", () => {
    const startupRegistry = createEmptyPluginRegistry();
    const laterActiveRegistry = createEmptyPluginRegistry();

    setActivePluginRegistry(startupRegistry);
    pinActivePluginHttpRouteRegistry(startupRegistry);
    setActivePluginRegistry(laterActiveRegistry);

    const unregister = registerPluginHttpRoute({
      path: "/bluebubbles-webhook",
      auth: "plugin",
      handler: vi.fn(),
    });

    expectRegisteredRouteShape(startupRegistry, {
      path: "/bluebubbles-webhook",
      auth: "plugin",
    });
    expect(laterActiveRegistry.httpRoutes).toHaveLength(0);

    unregister();
    expect(startupRegistry.httpRoutes).toHaveLength(0);
  });
});
