// ACP CLI option collision tests cover ACP command flag registration boundaries.
import { Command } from "commander";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { runRegisteredCli } from "../test-utils/command-runner.js";
import { withTempSecretFiles } from "../test-utils/secret-file-fixture.js";
import { registerAcpCli } from "./acp-cli.js";

type AcpClientOptions = {
  verbose?: boolean;
};

type AcpGatewayOptions = {
  gatewayPassword?: string;
  gatewayToken?: string;
  prefixCwd?: boolean;
};

const mocks = vi.hoisted(() => ({
  runAcpClientInteractive: vi.fn(async (_opts: AcpClientOptions) => {}),
  serveAcpGateway: vi.fn(async (_opts: AcpGatewayOptions) => {}),
  defaultRuntime: {
    log: vi.fn(),
    error: vi.fn(),
    writeStdout: vi.fn(),
    writeJson: vi.fn(),
    exit: vi.fn(),
  },
}));

const { runAcpClientInteractive, serveAcpGateway, defaultRuntime } = mocks;

const passwordKey = () => ["pass", "word"].join("");

vi.mock("../acp/client.js", () => ({
  runAcpClientInteractive: (opts: AcpClientOptions) => mocks.runAcpClientInteractive(opts),
}));

vi.mock("../acp/server.js", () => ({
  serveAcpGateway: (opts: AcpGatewayOptions) => mocks.serveAcpGateway(opts),
}));

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

describe("acp cli option collisions", () => {
  function createAcpProgram() {
    const program = new Command();
    registerAcpCli(program);
    return program;
  }

  async function parseAcp(args: string[]) {
    const program = createAcpProgram();
    await program.parseAsync(["acp", ...args], { from: "user" });
  }

  function expectCliError(pattern: RegExp) {
    expect(serveAcpGateway).not.toHaveBeenCalled();
    const errors = defaultRuntime.error.mock.calls.map(([message]) => String(message));
    expect(errors.some((message) => pattern.test(message))).toBe(true);
    expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
  }

  function requireFirstMockArg(mock: { mock: { calls: ReadonlyArray<ReadonlyArray<unknown>> } }) {
    const call = mock.mock.calls[0];
    if (!call) {
      throw new Error("expected mock to have at least one call");
    }
    return call[0];
  }

  beforeEach(() => {
    runAcpClientInteractive.mockClear();
    serveAcpGateway.mockClear();
    defaultRuntime.log.mockClear();
    defaultRuntime.error.mockClear();
    defaultRuntime.writeStdout.mockClear();
    defaultRuntime.writeJson.mockClear();
    defaultRuntime.exit.mockClear();
  });

  it("forwards --verbose to `acp client` when parent and child option names collide", async () => {
    await runRegisteredCli({
      register: registerAcpCli as (program: Command) => void,
      argv: ["acp", "client", "--verbose"],
    });

    expect(runAcpClientInteractive).toHaveBeenCalledTimes(1);
    const clientOptions = requireFirstMockArg(runAcpClientInteractive) as { verbose?: boolean };
    expect(clientOptions?.verbose).toBe(true);
  });

  it("forwards --no-prefix-cwd to the ACP bridge", async () => {
    await parseAcp(["--no-prefix-cwd"]);

    expect(serveAcpGateway).toHaveBeenCalledTimes(1);
    const gatewayOptions = requireFirstMockArg(serveAcpGateway) as {
      prefixCwd?: boolean;
    };
    expect(gatewayOptions?.prefixCwd).toBe(false);
  });

  it("defaults to prefixing the working directory", async () => {
    await parseAcp([]);

    expect(serveAcpGateway).toHaveBeenCalledTimes(1);
    const gatewayOptions = requireFirstMockArg(serveAcpGateway) as {
      prefixCwd?: boolean;
    };
    expect(gatewayOptions?.prefixCwd).toBe(true);
  });

  it("loads gateway token/password from files", async () => {
    await withTempSecretFiles(
      "openclaw-acp-cli-",
      { token: "tok_file\n", [passwordKey()]: "pw_file\n" },
      async (files) => {
        // pragma: allowlist secret
        await parseAcp([
          "--token-file",
          files.tokenFile ?? "",
          "--password-file",
          files.passwordFile ?? "",
        ]);
      },
    );

    expect(serveAcpGateway).toHaveBeenCalledTimes(1);
    const gatewayOptions = requireFirstMockArg(serveAcpGateway) as {
      gatewayPassword?: string;
      gatewayToken?: string;
    };
    expect(gatewayOptions?.gatewayToken).toBe("tok_file");
    expect(gatewayOptions?.gatewayPassword).toBe("pw_file"); // pragma: allowlist secret
  });

  it.each([
    {
      name: "rejects mixed secret flags and file flags",
      files: { token: "tok_file\n" },
      args: (tokenFile: string) => ["--token", "tok_inline", "--token-file", tokenFile],
      expected: /Use either --token .*--token-file for Gateway token\./,
    },
    {
      name: "rejects mixed password flags and file flags",
      files: { password: "pw_file\n" }, // pragma: allowlist secret
      args: (_tokenFile: string, passwordFile: string) => [
        "--password",
        "pw_inline",
        "--password-file",
        passwordFile,
      ],
      expected: /Use either --passw.*d .*--password-file for Gateway password\./,
    },
  ])("$name", async ({ files, args, expected }) => {
    await withTempSecretFiles("openclaw-acp-cli-", files, async ({ tokenFile, passwordFile }) => {
      await parseAcp(args(tokenFile ?? "", passwordFile ?? ""));
    });

    expectCliError(expected);
  });

  it("warns when inline secret flags are used", async () => {
    await parseAcp(["--token", "tok_inline", "--password", "pw_inline"]);

    const errors = defaultRuntime.error.mock.calls.map(([message]) => String(message));
    expect(errors).toContain(
      "Warning: --token can be exposed via process listings. Prefer --token-file or environment variables.",
    );
    expect(errors).toContain(
      "Warning: --password can be exposed via process listings. Prefer --password-file or environment variables.",
    );
  });

  it("trims token file path before reading", async () => {
    await withTempSecretFiles("openclaw-acp-cli-", { token: "tok_file\n" }, async (files) => {
      await parseAcp(["--token-file", `  ${files.tokenFile ?? ""}  `]);
    });

    expect(serveAcpGateway).toHaveBeenCalledTimes(1);
    const gatewayOptions = requireFirstMockArg(serveAcpGateway) as { gatewayToken?: string };
    expect(gatewayOptions?.gatewayToken).toBe("tok_file");
  });

  it("reports missing token-file read errors", async () => {
    await parseAcp(["--token-file", "/tmp/openclaw-acp-missing-token.txt"]);
    expectCliError(/Failed to (inspect|read) Gateway token file/);
  });

  it("formats client errors with formatErrorMessage instead of String(err) (#83904)", async () => {
    runAcpClientInteractive.mockImplementationOnce(async () => {
      throw { code: 42, why: "boom" } as unknown as Error;
    });
    const program = createAcpProgram();
    await program.parseAsync(["acp", "client"], { from: "user" });

    const errors = defaultRuntime.error.mock.calls.map(([message]) => String(message));
    expect(errors).toContain('{"code":42,"why":"boom"}');
    expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
  });
});
