// Skills workshop CLI tests cover workshop skill commands and filesystem setup.
import fs from "node:fs/promises";
import path from "node:path";
import { Command } from "commander";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
  createOpenClawTestState,
  type OpenClawTestState,
} from "../test-utils/openclaw-test-state.js";
import { createTrackedTempDirs } from "../test-utils/tracked-temp-dirs.js";
import { registerSkillsCli } from "./skills-cli.js";

const tempDirs = createTrackedTempDirs();
let testState: OpenClawTestState;
let stateDir = "";

const mocks = vi.hoisted(() => {
  const runtimeStdout: string[] = [];
  const runtimeErrors: string[] = [];
  const defaultRuntime = {
    log: vi.fn((message: string) => {
      runtimeStdout.push(message);
    }),
    error: vi.fn((message: string) => {
      runtimeErrors.push(message);
    }),
    writeStdout: vi.fn((value: string) => {
      runtimeStdout.push(value.endsWith("\n") ? value.slice(0, -1) : value);
    }),
    writeJson: vi.fn((value: unknown, space = 2) => {
      runtimeStdout.push(JSON.stringify(value, null, space > 0 ? space : undefined));
    }),
    exit: vi.fn((code: number) => {
      throw new Error(`__exit__:${code}`);
    }),
  };
  return {
    defaultRuntime,
    runtimeStdout,
    runtimeErrors,
    workspaceDir: "",
  };
});

vi.mock("../runtime.js", () => ({
  defaultRuntime: mocks.defaultRuntime,
}));

vi.mock("../terminal/links.js", () => ({
  formatDocsLink: () => "docs.openclaw.ai/cli/skills",
}));

vi.mock("../terminal/theme.js", () => ({
  theme: {
    command: (value: string) => value,
    error: (value: string) => value,
    heading: (value: string) => value,
    muted: (value: string) => value,
    success: (value: string) => value,
    warn: (value: string) => value,
  },
}));

vi.mock("../config/config.js", () => ({
  getRuntimeConfig: () => ({}),
  resetConfigRuntimeState: () => undefined,
}));

vi.mock("../agents/agent-scope.js", () => ({
  resolveAgentIdByWorkspacePath: () => undefined,
  resolveDefaultAgentId: () => "main",
  resolveAgentWorkspaceDir: () => mocks.workspaceDir,
}));

describe("skills workshop cli", () => {
  const createProgram = () => {
    const program = new Command();
    program.exitOverride();
    registerSkillsCli(program);
    return program;
  };

  const runCommand = async (argv: string[]) => {
    try {
      await createProgram().parseAsync(argv, { from: "user" });
    } catch (error) {
      if (error instanceof Error && error.message === "__exit__:0") {
        return;
      }
      throw error;
    }
  };

  beforeEach(async () => {
    testState = await createOpenClawTestState({
      layout: "state-only",
      prefix: "openclaw-skills-cli-workshop-state-",
    });
    mocks.workspaceDir = await tempDirs.make("openclaw-skills-cli-workshop-");
    stateDir = testState.stateDir;
    mocks.runtimeStdout.length = 0;
    mocks.runtimeErrors.length = 0;
    mocks.defaultRuntime.log.mockClear();
    mocks.defaultRuntime.error.mockClear();
    mocks.defaultRuntime.writeStdout.mockClear();
    mocks.defaultRuntime.writeJson.mockClear();
    mocks.defaultRuntime.exit.mockClear();
  });

  afterEach(async () => {
    await testState.cleanup();
    await tempDirs.cleanup();
  });

  it("creates, lists, inspects, and applies a skill proposal", async () => {
    const draftPath = path.join(mocks.workspaceDir, "proposal-draft");
    await fs.mkdir(path.join(draftPath, "references"), { recursive: true });
    await fs.writeFile(
      path.join(draftPath, "PROPOSAL.md"),
      "# Paris Weather\n\nCheck current weather before advice.\n",
      "utf8",
    );
    await fs.writeFile(
      path.join(draftPath, "references", "weather.md"),
      "Use current conditions before recommendations.\n",
      "utf8",
    );

    await runCommand([
      "skills",
      "workshop",
      "propose-create",
      "--name",
      "Paris Weather",
      "--description",
      "Weather lookup workflow",
      "--proposal-dir",
      draftPath,
    ]);

    const proposalId = mocks.runtimeStdout.at(-1);
    expect(proposalId).toMatch(/^paris-weather-/);

    await runCommand(["skills", "workshop", "list"]);
    expect(mocks.runtimeStdout.at(-1)).toContain(`${proposalId}  pending  create`);

    await runCommand(["skills", "workshop", "inspect", proposalId!]);
    expect(mocks.runtimeStdout.at(-1)).toContain("status: proposal");
    expect(mocks.runtimeStdout.at(-1)).toContain("--- references/weather.md ---");
    expect(mocks.runtimeStdout.at(-1)).toContain("Use current conditions before recommendations.");

    const revisedPath = path.join(mocks.workspaceDir, "revised-proposal.md");
    await fs.writeFile(
      revisedPath,
      "# Paris Weather\n\nCheck current weather and alerts before advice.\n",
      "utf8",
    );
    await runCommand([
      "skills",
      "workshop",
      "revise",
      proposalId!,
      "--description",
      "Revised weather lookup workflow",
      "--proposal",
      revisedPath,
    ]);
    expect(mocks.runtimeStdout.at(-1)).toContain(`Revised ${proposalId} v2`);

    await runCommand(["skills", "workshop", "apply", proposalId!]);
    expect(mocks.runtimeStdout.at(-1)).toContain("Applied");
    await expect(
      fs.readFile(path.join(mocks.workspaceDir, "skills", "paris-weather", "SKILL.md"), "utf8"),
    ).resolves.toContain("Check current weather and alerts");
    await expect(
      fs.readFile(
        path.join(mocks.workspaceDir, "skills", "paris-weather", "references", "weather.md"),
        "utf8",
      ),
    ).resolves.toContain("Use current conditions");
  });

  it("scopes list and inspect to the selected workspace", async () => {
    const firstWorkspaceDir = mocks.workspaceDir;
    const draftPath = path.join(firstWorkspaceDir, "proposal-draft.md");
    await fs.writeFile(draftPath, "# First CLI Skill\n", "utf8");

    await runCommand([
      "skills",
      "workshop",
      "propose-create",
      "--name",
      "First CLI Skill",
      "--description",
      "First workspace proposal",
      "--proposal",
      draftPath,
    ]);
    const proposalId = mocks.runtimeStdout.at(-1);
    expect(proposalId).toMatch(/^first-cli-skill-/);

    mocks.workspaceDir = await tempDirs.make("openclaw-skills-cli-workshop-second-");
    await runCommand(["skills", "workshop", "list"]);
    expect(mocks.runtimeStdout.at(-1)).toBe("No skill proposals.");
    await expect(runCommand(["skills", "workshop", "inspect", proposalId!])).rejects.toThrow(
      "__exit__:1",
    );
    expect(mocks.runtimeErrors).toContain(`Skill proposal not found: ${proposalId}`);

    mocks.workspaceDir = firstWorkspaceDir;
    await runCommand(["skills", "workshop", "inspect", proposalId!]);
    expect(mocks.runtimeStdout.at(-1)).toContain("status: proposal");
  });

  it("rejects missing proposal drafts before creating workshop state", async () => {
    await expect(
      runCommand([
        "skills",
        "workshop",
        "propose-create",
        "--name",
        "Missing Draft",
        "--description",
        "Missing draft",
        "--proposal",
        path.join(mocks.workspaceDir, "missing.md"),
      ]),
    ).rejects.toThrow("__exit__:1");

    expect(mocks.runtimeErrors[0]).toContain("file not found");
    await expect(fs.access(path.join(stateDir, "skill-workshop"))).rejects.toThrow();
  });
});
