import { describe, expect, it } from "vitest";
import {
  applyImportedMcpServersToConfig,
  applyMcpSpecUpdateToConfig,
  classifyMcpStatusReason,
  getAllMcpSpecs,
  normalizeImportedMcpServer,
  stripLegacyMcpConfigForRaw,
} from "../src/cli/commands/desktop.js";
import type { ReasonixConfig } from "../src/config.js";

describe("getAllMcpSpecs", () => {
  it("returns legacy cfg.mcp specs", () => {
    const cfg: ReasonixConfig = {
      mcp: ["fs=npx -y @scope/fs /tmp", "git=uvx mcp-server-git"],
    };
    const specs = getAllMcpSpecs(cfg);
    expect(specs).toHaveLength(2);
    expect(specs).toContain("fs=npx -y @scope/fs /tmp");
    expect(specs).toContain("git=uvx mcp-server-git");
  });

  it("returns mcpServers specs when legacy mcp is absent", () => {
    const cfg: ReasonixConfig = {
      mcpServers: {
        github: {
          command: "npx",
          args: ["-y", "@modelcontextprotocol/server-github"],
        },
      },
    };
    const specs = getAllMcpSpecs(cfg);
    expect(specs.length).toBeGreaterThan(0);
    expect(specs.some((s) => s.startsWith("github="))).toBe(true);
  });

  it("merges both legacy mcp and mcpServers", () => {
    const cfg: ReasonixConfig = {
      mcp: ["fs=npx -y @scope/fs /tmp"],
      mcpServers: {
        github: {
          command: "npx",
          args: ["-y", "@modelcontextprotocol/server-github"],
        },
      },
    };
    const specs = getAllMcpSpecs(cfg);
    expect(specs.length).toBeGreaterThanOrEqual(2);
    expect(specs.some((s) => s.startsWith("fs="))).toBe(true);
    expect(specs.some((s) => s.startsWith("github="))).toBe(true);
  });

  it("mcpServers wins on name conflict", () => {
    const cfg: ReasonixConfig = {
      mcp: ["fs=npx -y @scope/fs /tmp"],
      mcpServers: {
        fs: {
          command: "node",
          args: ["server.js"],
        },
      },
    };
    const specs = getAllMcpSpecs(cfg);
    const fsSpec = specs.find((s) => s.startsWith("fs="));
    expect(fsSpec).toContain("node");
    expect(fsSpec).not.toContain("npx");
  });

  it("returns empty array when neither mcp nor mcpServers present", () => {
    const cfg: ReasonixConfig = {};
    const specs = getAllMcpSpecs(cfg);
    expect(specs).toEqual([]);
  });

  it("removes the edited anonymous legacy spec by normalized raw form", () => {
    const cfg: ReasonixConfig = {
      mcp: ["  npx   -y   @scope/fs   /tmp  ", "git=uvx mcp-server-git"],
    };

    stripLegacyMcpConfigForRaw(cfg, "npx -y @scope/fs /tmp");

    expect(cfg.mcp).toEqual(["git=uvx mcp-server-git"]);
  });
});

describe("normalizeImportedMcpServer", () => {
  it("normalizes stdio servers and drops invalid optional fields", () => {
    const normalized = normalizeImportedMcpServer({
      name: " fs ",
      transport: "stdio",
      command: " npx ",
      args: ["-y", 42, "@scope/fs"],
      env: { TOKEN: "abc", EMPTY: "", BAD: 1 },
      cwd: " /workspace ",
      disabled: true,
      requestTimeoutMs: 12_000,
    });

    expect(normalized?.name).toBe("fs");
    expect(normalized?.config).toMatchObject({
      transport: "stdio",
      command: "npx",
      args: ["-y", "@scope/fs"],
      env: { TOKEN: "abc" },
      cwd: "/workspace",
      disabled: true,
      requestTimeoutMs: 12_000,
    });
  });

  it("normalizes URL transports and headers", () => {
    const normalized = normalizeImportedMcpServer({
      name: "remote",
      transport: "streamable-http",
      url: " https://mcp.example.test/api ",
      headers: { Authorization: "Bearer token", Empty: "", Bad: 1 },
      requestTimeoutMs: Number.POSITIVE_INFINITY,
    });

    expect(normalized?.config).toMatchObject({
      transport: "streamable-http",
      url: "https://mcp.example.test/api",
      headers: { Authorization: "Bearer token" },
    });
    expect(normalized?.config.requestTimeoutMs).toBeUndefined();
  });

  it("rejects unnamed servers and missing transport payloads", () => {
    expect(
      normalizeImportedMcpServer({ name: " ", transport: "stdio", command: "npx" }),
    ).toBeNull();
    expect(normalizeImportedMcpServer({ name: "fs", transport: "stdio" })).toBeNull();
    expect(normalizeImportedMcpServer({ name: "remote", transport: "sse" })).toBeNull();
  });
});

describe("desktop MCP config mutations", () => {
  it("imports valid servers into mcpServers and removes matching legacy entries", () => {
    const cfg: ReasonixConfig = {
      mcp: ["fs=npx -y @scope/fs /tmp", " npx   -y   @scope/fs   /tmp ", "keep=uvx keep"],
      mcpDisabled: ["fs", "keep"],
      mcpEnv: { fs: { TOKEN: "old" }, keep: { TOKEN: "keep" } },
      mcpServers: {
        existing: { command: "uvx", args: ["existing"] },
      },
    };

    const result = applyImportedMcpServersToConfig(cfg, [
      { name: "fs", transport: "stdio", command: "npx", args: ["-y", "@scope/fs", "/tmp"] },
    ]);

    expect(cfg.mcpServers?.existing).toEqual({ command: "uvx", args: ["existing"] });
    expect(cfg.mcpServers?.fs).toMatchObject({
      transport: "stdio",
      command: "npx",
      args: ["-y", "@scope/fs", "/tmp"],
    });
    expect(cfg.mcp).toEqual(["keep=uvx keep"]);
    expect(cfg.mcpDisabled).toEqual(["keep"]);
    expect(cfg.mcpEnv).toEqual({ keep: { TOKEN: "keep" } });
    expect(result.forceSpecs).toEqual(["fs=npx -y @scope/fs /tmp"]);
  });

  it("rejects imports with no valid servers", () => {
    const cfg: ReasonixConfig = {};

    expect(() =>
      applyImportedMcpServersToConfig(cfg, [{ name: "fs", transport: "stdio" }]),
    ).toThrow("no valid servers received");
    expect(cfg).toEqual({});
  });

  it("updates renamed servers by deleting the old canonical name and legacy duplicates", () => {
    const cfg: ReasonixConfig = {
      mcp: ["old=npx old-server", "new=node server.js", " node   server.js ", "keep=uvx keep"],
      mcpDisabled: ["old", "new", "keep"],
      mcpEnv: { old: { TOKEN: "old" }, new: { TOKEN: "new" }, keep: { TOKEN: "keep" } },
      mcpServers: {
        old: { command: "npx", args: ["old-server"] },
        keep: { command: "uvx", args: ["keep"] },
      },
    };

    const result = applyMcpSpecUpdateToConfig(cfg, "old=npx old-server", {
      name: "new",
      transport: "stdio",
      command: "node",
      args: ["server.js"],
    });

    expect(cfg.mcpServers?.old).toBeUndefined();
    expect(cfg.mcpServers?.new).toMatchObject({
      transport: "stdio",
      command: "node",
      args: ["server.js"],
    });
    expect(cfg.mcpServers?.keep).toEqual({ command: "uvx", args: ["keep"] });
    expect(cfg.mcp).toEqual(["keep=uvx keep"]);
    expect(cfg.mcpDisabled).toEqual(["keep"]);
    expect(cfg.mcpEnv).toEqual({ keep: { TOKEN: "keep" } });
    expect(result).toEqual({
      updatedRaw: "new=node server.js",
      forceSpecs: ["old=npx old-server", "new=node server.js"],
    });
  });

  it("rejects invalid spec updates without mutating config", () => {
    const cfg: ReasonixConfig = {
      mcpServers: { fs: { command: "npx", args: ["old"] } },
    };

    expect(() => applyMcpSpecUpdateToConfig(cfg, "fs=npx old", { name: "fs" })).toThrow(
      "invalid server config",
    );
    expect(cfg).toEqual({ mcpServers: { fs: { command: "npx", args: ["old"] } } });
  });
});

describe("classifyMcpStatusReason", () => {
  it("maps common failure strings to stable UI hints", () => {
    expect(classifyMcpStatusReason(undefined)).toBeUndefined();
    expect(classifyMcpStatusReason("No bearer token configured")).toBe("missing-token");
    expect(classifyMcpStatusReason("HTTP 401 unauthorized")).toBe("auth");
    expect(classifyMcpStatusReason("spawn npx ENOENT")).toBe("command");
    expect(classifyMcpStatusReason("fetch failed due to DNS timeout")).toBe("network");
    expect(classifyMcpStatusReason("protocol closed during initialize")).toBe("unknown");
  });
});