import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js";
import { createPluginActivationSource, normalizePluginsConfig } from "../plugins/config-state.js";
import { clearPluginDiscoveryCache } from "../plugins/discovery.js";
import { clearPluginManifestRegistryCache } from "../plugins/manifest-registry.js";
import {
  __testing,
  canLoadActivatedBundledPluginPublicSurface,
  listImportedBundledPluginFacadeIds,
  loadBundledPluginPublicSurfaceModuleSync,
  resetFacadeRuntimeStateForTest,
} from "./facade-runtime.js";
import { createPluginSdkTestHarness } from "./test-helpers.js";

const { createTempDirSync } = createPluginSdkTestHarness();
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
const originalStateDir = process.env.OPENCLAW_STATE_DIR;

function createBundledPluginDir(prefix: string, marker: string): string {
  const rootDir = createTempDirSync(prefix);
  fs.mkdirSync(path.join(rootDir, "demo"), { recursive: true });
  fs.writeFileSync(
    path.join(rootDir, "demo", "api.js"),
    `export const marker = ${JSON.stringify(marker)};\n`,
    "utf8",
  );
  return rootDir;
}

function createThrowingPluginDir(prefix: string): string {
  const rootDir = createTempDirSync(prefix);
  fs.mkdirSync(path.join(rootDir, "bad"), { recursive: true });
  fs.writeFileSync(
    path.join(rootDir, "bad", "api.js"),
    `throw new Error("plugin load failure");\n`,
    "utf8",
  );
  return rootDir;
}

afterEach(() => {
  vi.restoreAllMocks();
  clearRuntimeConfigSnapshot();
  resetFacadeRuntimeStateForTest();
  clearPluginDiscoveryCache();
  clearPluginManifestRegistryCache();
  vi.doUnmock("../plugins/manifest-registry.js");
  if (originalBundledPluginsDir === undefined) {
    delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
  } else {
    process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
  }
  if (originalStateDir === undefined) {
    delete process.env.OPENCLAW_STATE_DIR;
  } else {
    process.env.OPENCLAW_STATE_DIR = originalStateDir;
  }
});

describe("plugin-sdk facade runtime", () => {
  it("honors bundled plugin dir overrides outside the package root", () => {
    const overrideA = createBundledPluginDir("openclaw-facade-runtime-a-", "override-a");
    const overrideB = createBundledPluginDir("openclaw-facade-runtime-b-", "override-b");

    process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = overrideA;
    const fromA = __testing.resolveFacadeModuleLocation({
      dirName: "demo",
      artifactBasename: "api.js",
    });
    expect(fromA).toEqual({
      modulePath: path.join(overrideA, "demo", "api.js"),
      boundaryRoot: overrideA,
    });

    process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = overrideB;
    const fromB = __testing.resolveFacadeModuleLocation({
      dirName: "demo",
      artifactBasename: "api.js",
    });
    expect(fromB).toEqual({
      modulePath: path.join(overrideB, "demo", "api.js"),
      boundaryRoot: overrideB,
    });
  });

  it("returns the same object identity on repeated calls (sentinel consistency)", () => {
    const dir = createBundledPluginDir("openclaw-facade-identity-", "identity-check");
    process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
    const location = {
      modulePath: path.join(dir, "demo", "api.js"),
      boundaryRoot: dir,
    };
    const loader = vi.fn(() => ({ marker: "identity-check" }));

    const first = __testing.loadFacadeModuleAtLocationSync<{ marker: string }>({
      location,
      trackedPluginId: "demo",
      loadModule: loader,
    });
    const second = __testing.loadFacadeModuleAtLocationSync<{ marker: string }>({
      location,
      trackedPluginId: "demo",
      loadModule: loader,
    });
    expect(first).toBe(second);
    expect(first.marker).toBe("identity-check");
    expect(listImportedBundledPluginFacadeIds()).toEqual(["demo"]);
    expect(loader).toHaveBeenCalledTimes(1);
  });

  it("breaks circular facade re-entry during module evaluation", () => {
    const dir = createBundledPluginDir("openclaw-facade-circular-", "circular-ok");
    const location = {
      modulePath: path.join(dir, "demo", "api.js"),
      boundaryRoot: dir,
    };
    let reentered: { marker?: string } | undefined;
    const loader = vi.fn(() => {
      reentered = __testing.loadFacadeModuleAtLocationSync<{ marker?: string }>({
        location,
        trackedPluginId: "demo",
        loadModule: loader,
      });
      return { marker: "circular-ok" };
    });

    const loaded = __testing.loadFacadeModuleAtLocationSync<{ marker: string }>({
      location,
      trackedPluginId: "demo",
      loadModule: loader,
    });

    expect(loaded.marker).toBe("circular-ok");
    expect(reentered).toBe(loaded);
    expect(reentered?.marker).toBe("circular-ok");
    expect(loader).toHaveBeenCalledTimes(1);
  });

  it("back-fills the sentinel before post-load facade tracking re-enters", () => {
    const dir = createBundledPluginDir("openclaw-facade-post-load-", "post-load-ok");
    const location = {
      modulePath: path.join(dir, "demo", "api.js"),
      boundaryRoot: dir,
    };
    const reentryMarkers: Array<string | undefined> = [];
    const loader = vi.fn(() => ({ marker: "post-load-ok" }));

    const loaded = __testing.loadFacadeModuleAtLocationSync<{ marker: string }>({
      location,
      trackedPluginId: () => {
        const reentered = __testing.loadFacadeModuleAtLocationSync<{ marker?: string }>({
          location,
          trackedPluginId: "demo",
          loadModule: loader,
        });
        reentryMarkers.push(reentered.marker);
        return "demo";
      },
      loadModule: loader,
    });

    expect(loaded.marker).toBe("post-load-ok");
    expect(reentryMarkers.length).toBeGreaterThan(0);
    expect(reentryMarkers.every((marker) => marker === "post-load-ok")).toBe(true);
    expect(listImportedBundledPluginFacadeIds()).toEqual(["demo"]);
    expect(loader).toHaveBeenCalledTimes(1);
  });
  it("clears the cache on load failure so retries re-execute", () => {
    const dir = createThrowingPluginDir("openclaw-facade-throw-");
    process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;

    expect(() =>
      loadBundledPluginPublicSurfaceModuleSync<{ marker: string }>({
        dirName: "bad",
        artifactBasename: "api.js",
      }),
    ).toThrow("plugin load failure");

    expect(listImportedBundledPluginFacadeIds()).toEqual([]);

    // A second call must also throw (not return a stale empty sentinel).
    expect(() =>
      loadBundledPluginPublicSurfaceModuleSync<{ marker: string }>({
        dirName: "bad",
        artifactBasename: "api.js",
      }),
    ).toThrow("plugin load failure");
  });

  it("blocks runtime-api facade loads for bundled plugins that are not activated", () => {
    const access = __testing.evaluateBundledPluginPublicSurfaceAccess({
      params: {
        dirName: "discord",
        artifactBasename: "runtime-api.js",
      },
      manifestRecord: {
        id: "discord",
        origin: "bundled",
        enabledByDefault: false,
        rootDir: "/tmp/discord",
        channels: ["discord"],
      },
      config: {},
      normalizedPluginsConfig: normalizePluginsConfig(),
      activationSource: createPluginActivationSource({ config: {} }),
      autoEnabledReasons: {},
    });

    expect(access.allowed).toBe(false);
    expect(access.pluginId).toBe("discord");
    expect(access.reason).toBeTruthy();
    expect(() =>
      __testing.throwForBundledPluginPublicSurfaceAccess({
        access,
        request: {
          dirName: "discord",
          artifactBasename: "runtime-api.js",
        },
      }),
    ).toThrow(/Bundled plugin public surface access blocked/);
    expect(access.allowed).toBe(false);
  });

  it("allows runtime-api facade loads when the bundled plugin is explicitly enabled", () => {
    const dir = createTempDirSync("openclaw-facade-runtime-enabled-");
    fs.mkdirSync(path.join(dir, "discord"), { recursive: true });
    fs.writeFileSync(
      path.join(dir, "discord", "runtime-api.js"),
      'export const marker = "runtime-api-enabled";\n',
      "utf8",
    );
    const config = {
      plugins: {
        entries: {
          discord: {
            enabled: true,
          },
        },
      },
    } as const;
    const access = __testing.evaluateBundledPluginPublicSurfaceAccess({
      params: {
        dirName: "discord",
        artifactBasename: "runtime-api.js",
      },
      manifestRecord: {
        id: "discord",
        origin: "bundled",
        enabledByDefault: false,
        rootDir: "/tmp/discord",
        channels: ["discord"],
      },
      config,
      normalizedPluginsConfig: normalizePluginsConfig(config.plugins),
      activationSource: createPluginActivationSource({ config }),
      autoEnabledReasons: {},
    });
    const loader = vi.fn(() => ({ marker: "runtime-api-enabled" }));
    const location = {
      modulePath: path.join(dir, "discord", "runtime-api.js"),
      boundaryRoot: dir,
    };

    expect(access.allowed).toBe(true);
    const loaded = __testing.loadFacadeModuleAtLocationSync<{ marker: string }>({
      location,
      trackedPluginId: "discord",
      loadModule: loader,
    });
    expect(loaded.marker).toBe("runtime-api-enabled");
    expect(loader).toHaveBeenCalledTimes(1);
  });

  it("resolves a globally-installed plugin whose rootDir basename matches the dirName", () => {
    const lineDir = createTempDirSync("openclaw-facade-global-line-");
    fs.mkdirSync(lineDir, { recursive: true });
    fs.writeFileSync(
      path.join(lineDir, "runtime-api.js"),
      'export const marker = "global-line";\n',
      "utf8",
    );
    fs.writeFileSync(
      path.join(lineDir, "package.json"),
      JSON.stringify({
        name: "@openclaw/line",
        version: "0.0.0",
        openclaw: {
          extensions: ["./runtime-api.js"],
          channel: { id: "line" },
        },
      }),
      "utf8",
    );
    fs.writeFileSync(
      path.join(lineDir, "openclaw.plugin.json"),
      JSON.stringify({
        id: "line",
        channels: ["line"],
        configSchema: { type: "object", additionalProperties: false, properties: {} },
      }),
      "utf8",
    );

    expect(
      __testing.resolveRegistryPluginModuleLocationFromRegistry({
        registry: [
          {
            id: "line",
            rootDir: lineDir,
            channels: ["line"],
          },
        ],
        dirName: "line",
        artifactBasename: "runtime-api.js",
      }),
    ).toEqual({
      modulePath: path.join(lineDir, "runtime-api.js"),
      boundaryRoot: lineDir,
    });
  });

  it("resolves a globally-installed plugin with an encoded scoped rootDir basename", () => {
    const encodedDir = createTempDirSync("openclaw-facade-encoded-line-");
    fs.mkdirSync(encodedDir, { recursive: true });
    fs.writeFileSync(
      path.join(encodedDir, "runtime-api.js"),
      'export const marker = "encoded-global-line";\n',
      "utf8",
    );
    fs.writeFileSync(
      path.join(encodedDir, "package.json"),
      JSON.stringify({
        name: "@openclaw/line",
        version: "0.0.0",
        openclaw: {
          extensions: ["./runtime-api.js"],
          channel: { id: "line" },
        },
      }),
      "utf8",
    );
    fs.writeFileSync(
      path.join(encodedDir, "openclaw.plugin.json"),
      JSON.stringify({
        id: "line",
        channels: ["line"],
        configSchema: { type: "object", additionalProperties: false, properties: {} },
      }),
      "utf8",
    );

    expect(
      __testing.resolveRegistryPluginModuleLocationFromRegistry({
        registry: [
          {
            id: "line",
            rootDir: encodedDir,
            channels: ["line"],
          },
        ],
        dirName: "line",
        artifactBasename: "runtime-api.js",
      }),
    ).toEqual({
      modulePath: path.join(encodedDir, "runtime-api.js"),
      boundaryRoot: encodedDir,
    });
  });

  it("keeps shared runtime-core facades available without plugin activation", () => {
    setRuntimeConfigSnapshot({});

    expect(
      canLoadActivatedBundledPluginPublicSurface({
        dirName: "speech-core",
        artifactBasename: "runtime-api.js",
      }),
    ).toBe(true);
    expect(
      canLoadActivatedBundledPluginPublicSurface({
        dirName: "image-generation-core",
        artifactBasename: "runtime-api.js",
      }),
    ).toBe(true);
    expect(
      canLoadActivatedBundledPluginPublicSurface({
        dirName: "media-understanding-core",
        artifactBasename: "runtime-api.js",
      }),
    ).toBe(true);
  });
});
