import { Command } from "commander";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const callGateway = vi.fn(async () => ({ ok: true }));
const resolveGatewayProgramArguments = vi.fn(async () => ({
  programArguments: ["/bin/node", "cli", "gateway", "--port", "18789"],
}));
const serviceInstall = vi.fn().mockResolvedValue(undefined);
const serviceUninstall = vi.fn().mockResolvedValue(undefined);
const serviceStop = vi.fn().mockResolvedValue(undefined);
const serviceRestart = vi.fn().mockResolvedValue(undefined);
const serviceIsLoaded = vi.fn().mockResolvedValue(false);
const serviceReadCommand = vi.fn().mockResolvedValue(null);
const serviceReadRuntime = vi.fn().mockResolvedValue({ status: "running" });
const findExtraGatewayServices = vi.fn(async () => []);
const inspectPortUsage = vi.fn(async (port: number) => ({
  port,
  status: "free",
  listeners: [],
  hints: [],
}));

const runtimeLogs: string[] = [];
const runtimeErrors: string[] = [];
const defaultRuntime = {
  log: (msg: string) => runtimeLogs.push(msg),
  error: (msg: string) => runtimeErrors.push(msg),
  exit: (code: number) => {
    throw new Error(`__exit__:${code}`);
  },
};

vi.mock("../gateway/call.js", () => ({
  callGateway: (opts: unknown) => callGateway(opts),
}));

vi.mock("../daemon/program-args.js", () => ({
  resolveGatewayProgramArguments: (opts: unknown) => resolveGatewayProgramArguments(opts),
}));

vi.mock("../daemon/service.js", () => ({
  resolveGatewayService: () => ({
    label: "LaunchAgent",
    loadedText: "loaded",
    notLoadedText: "not loaded",
    install: serviceInstall,
    uninstall: serviceUninstall,
    stop: serviceStop,
    restart: serviceRestart,
    isLoaded: serviceIsLoaded,
    readCommand: serviceReadCommand,
    readRuntime: serviceReadRuntime,
  }),
}));

vi.mock("../daemon/legacy.js", () => ({
  findLegacyGatewayServices: async () => [],
}));

vi.mock("../daemon/inspect.js", () => ({
  findExtraGatewayServices: (env: unknown, opts?: unknown) => findExtraGatewayServices(env, opts),
  renderGatewayServiceCleanupHints: () => [],
}));

vi.mock("../infra/ports.js", () => ({
  inspectPortUsage: (port: number) => inspectPortUsage(port),
  formatPortDiagnostics: () => ["Port 18789 is already in use."],
}));

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

vi.mock("./deps.js", () => ({
  createDefaultDeps: () => {},
}));

vi.mock("./progress.js", () => ({
  withProgress: async (_opts: unknown, fn: () => Promise<unknown>) => await fn(),
}));

describe("daemon-cli coverage", () => {
  const originalEnv = {
    OPENCLAW_STATE_DIR: process.env.OPENCLAW_STATE_DIR,
    OPENCLAW_CONFIG_PATH: process.env.OPENCLAW_CONFIG_PATH,
    OPENCLAW_GATEWAY_PORT: process.env.OPENCLAW_GATEWAY_PORT,
    OPENCLAW_PROFILE: process.env.OPENCLAW_PROFILE,
  };

  beforeEach(() => {
    process.env.OPENCLAW_STATE_DIR = "/tmp/clawdbot-cli-state";
    process.env.OPENCLAW_CONFIG_PATH = "/tmp/clawdbot-cli-state/openclaw.json";
    delete process.env.OPENCLAW_GATEWAY_PORT;
    delete process.env.OPENCLAW_PROFILE;
    serviceReadCommand.mockResolvedValue(null);
  });

  afterEach(() => {
    if (originalEnv.OPENCLAW_STATE_DIR !== undefined)
      process.env.OPENCLAW_STATE_DIR = originalEnv.OPENCLAW_STATE_DIR;
    else delete process.env.OPENCLAW_STATE_DIR;

    if (originalEnv.OPENCLAW_CONFIG_PATH !== undefined)
      process.env.OPENCLAW_CONFIG_PATH = originalEnv.OPENCLAW_CONFIG_PATH;
    else delete process.env.OPENCLAW_CONFIG_PATH;

    if (originalEnv.OPENCLAW_GATEWAY_PORT !== undefined)
      process.env.OPENCLAW_GATEWAY_PORT = originalEnv.OPENCLAW_GATEWAY_PORT;
    else delete process.env.OPENCLAW_GATEWAY_PORT;

    if (originalEnv.OPENCLAW_PROFILE !== undefined)
      process.env.OPENCLAW_PROFILE = originalEnv.OPENCLAW_PROFILE;
    else delete process.env.OPENCLAW_PROFILE;
  });

  it("probes gateway status by default", async () => {
    runtimeLogs.length = 0;
    runtimeErrors.length = 0;
    callGateway.mockClear();

    const { registerDaemonCli } = await import("./daemon-cli.js");
    const program = new Command();
    program.exitOverride();
    registerDaemonCli(program);

    await program.parseAsync(["daemon", "status"], { from: "user" });

    expect(callGateway).toHaveBeenCalledTimes(1);
    expect(callGateway).toHaveBeenCalledWith(expect.objectContaining({ method: "status" }));
    expect(findExtraGatewayServices).toHaveBeenCalled();
    expect(inspectPortUsage).toHaveBeenCalled();
  }, 20_000);

  it("derives probe URL from service args + env (json)", async () => {
    runtimeLogs.length = 0;
    runtimeErrors.length = 0;
    callGateway.mockClear();
    inspectPortUsage.mockClear();

    serviceReadCommand.mockResolvedValueOnce({
      programArguments: ["/bin/node", "cli", "gateway", "--port", "19001"],
      environment: {
        OPENCLAW_PROFILE: "dev",
        OPENCLAW_STATE_DIR: "/tmp/clawdbot-daemon-state",
        OPENCLAW_CONFIG_PATH: "/tmp/clawdbot-daemon-state/openclaw.json",
        OPENCLAW_GATEWAY_PORT: "19001",
      },
      sourcePath: "/tmp/com.clawdbot.gateway.plist",
    });

    const { registerDaemonCli } = await import("./daemon-cli.js");
    const program = new Command();
    program.exitOverride();
    registerDaemonCli(program);

    await program.parseAsync(["daemon", "status", "--json"], { from: "user" });

    expect(callGateway).toHaveBeenCalledWith(
      expect.objectContaining({
        url: "ws://127.0.0.1:19001",
        method: "status",
      }),
    );
    expect(inspectPortUsage).toHaveBeenCalledWith(19001);

    const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
    const parsed = JSON.parse(jsonLine ?? "{}") as {
      gateway?: { port?: number; portSource?: string; probeUrl?: string };
      config?: { mismatch?: boolean };
      rpc?: { url?: string; ok?: boolean };
    };
    expect(parsed.gateway?.port).toBe(19001);
    expect(parsed.gateway?.portSource).toBe("service args");
    expect(parsed.gateway?.probeUrl).toBe("ws://127.0.0.1:19001");
    expect(parsed.config?.mismatch).toBe(true);
    expect(parsed.rpc?.url).toBe("ws://127.0.0.1:19001");
    expect(parsed.rpc?.ok).toBe(true);
  }, 20_000);

  it("passes deep scan flag for daemon status", async () => {
    findExtraGatewayServices.mockClear();

    const { registerDaemonCli } = await import("./daemon-cli.js");
    const program = new Command();
    program.exitOverride();
    registerDaemonCli(program);

    await program.parseAsync(["daemon", "status", "--deep"], { from: "user" });

    expect(findExtraGatewayServices).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({ deep: true }),
    );
  });

  it("installs the daemon when requested", async () => {
    serviceIsLoaded.mockResolvedValueOnce(false);
    serviceInstall.mockClear();

    const { registerDaemonCli } = await import("./daemon-cli.js");
    const program = new Command();
    program.exitOverride();
    registerDaemonCli(program);

    await program.parseAsync(["daemon", "install", "--port", "18789"], {
      from: "user",
    });

    expect(serviceInstall).toHaveBeenCalledTimes(1);
  });

  it("installs the daemon with json output", async () => {
    runtimeLogs.length = 0;
    runtimeErrors.length = 0;
    serviceIsLoaded.mockResolvedValueOnce(false);
    serviceInstall.mockClear();

    const { registerDaemonCli } = await import("./daemon-cli.js");
    const program = new Command();
    program.exitOverride();
    registerDaemonCli(program);

    await program.parseAsync(["daemon", "install", "--port", "18789", "--json"], {
      from: "user",
    });

    const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
    const parsed = JSON.parse(jsonLine ?? "{}") as {
      ok?: boolean;
      action?: string;
      result?: string;
    };
    expect(parsed.ok).toBe(true);
    expect(parsed.action).toBe("install");
    expect(parsed.result).toBe("installed");
  });

  it("starts and stops the daemon via service helpers", async () => {
    serviceRestart.mockClear();
    serviceStop.mockClear();
    serviceIsLoaded.mockResolvedValue(true);

    const { registerDaemonCli } = await import("./daemon-cli.js");
    const program = new Command();
    program.exitOverride();
    registerDaemonCli(program);

    await program.parseAsync(["daemon", "start"], { from: "user" });
    await program.parseAsync(["daemon", "stop"], { from: "user" });

    expect(serviceRestart).toHaveBeenCalledTimes(1);
    expect(serviceStop).toHaveBeenCalledTimes(1);
  });

  it("emits json for daemon start/stop", async () => {
    runtimeLogs.length = 0;
    runtimeErrors.length = 0;
    serviceRestart.mockClear();
    serviceStop.mockClear();
    serviceIsLoaded.mockResolvedValue(true);

    const { registerDaemonCli } = await import("./daemon-cli.js");
    const program = new Command();
    program.exitOverride();
    registerDaemonCli(program);

    await program.parseAsync(["daemon", "start", "--json"], { from: "user" });
    await program.parseAsync(["daemon", "stop", "--json"], { from: "user" });

    const jsonLines = runtimeLogs.filter((line) => line.trim().startsWith("{"));
    const parsed = jsonLines.map((line) => JSON.parse(line) as { action?: string; ok?: boolean });
    expect(parsed.some((entry) => entry.action === "start" && entry.ok === true)).toBe(true);
    expect(parsed.some((entry) => entry.action === "stop" && entry.ok === true)).toBe(true);
  });
});
