import { describe, expect, test, spyOn } from "bun:test"
import { convertClaudeToCopilot, transformContentForCopilot } from "../src/converters/claude-to-copilot"
import { parseFrontmatter } from "../src/utils/frontmatter"
import type { ClaudePlugin } from "../src/types/claude"

const fixturePlugin: ClaudePlugin = {
  root: "/tmp/plugin",
  manifest: { name: "fixture", version: "1.0.0" },
  agents: [
    {
      name: "Security Reviewer",
      description: "Security-focused code review agent",
      capabilities: ["Threat modeling", "OWASP"],
      model: "claude-sonnet-4-20250514",
      body: "Focus on vulnerabilities.",
      sourcePath: "/tmp/plugin/agents/security-reviewer.md",
    },
  ],
  commands: [
    {
      name: "workflows:plan",
      description: "Planning command",
      argumentHint: "[FOCUS]",
      model: "inherit",
      allowedTools: ["Read"],
      body: "Plan the work.",
      sourcePath: "/tmp/plugin/commands/workflows/plan.md",
    },
  ],
  skills: [
    {
      name: "existing-skill",
      description: "Existing skill",
      sourceDir: "/tmp/plugin/skills/existing-skill",
      skillPath: "/tmp/plugin/skills/existing-skill/SKILL.md",
    },
  ],
  hooks: undefined,
  mcpServers: undefined,
}

const defaultOptions = {
  agentMode: "subagent" as const,
  inferTemperature: false,
  permissions: "none" as const,
}

describe("convertClaudeToCopilot", () => {
  test("converts agents to .agent.md with Copilot frontmatter", () => {
    const bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)

    expect(bundle.agents).toHaveLength(1)
    const agent = bundle.agents[0]
    expect(agent.name).toBe("security-reviewer")

    const parsed = parseFrontmatter(agent.content)
    expect(parsed.data.description).toBe("Security-focused code review agent")
    expect(parsed.data.tools).toEqual(["*"])
    expect(parsed.data.infer).toBe(true)
    expect(parsed.body).toContain("Capabilities")
    expect(parsed.body).toContain("Threat modeling")
    expect(parsed.body).toContain("Focus on vulnerabilities.")
  })

  test("agent description is required, fallback generated if missing", () => {
    const plugin: ClaudePlugin = {
      ...fixturePlugin,
      agents: [
        {
          name: "basic-agent",
          body: "Do things.",
          sourcePath: "/tmp/plugin/agents/basic.md",
        },
      ],
    }

    const bundle = convertClaudeToCopilot(plugin, defaultOptions)
    const parsed = parseFrontmatter(bundle.agents[0].content)
    expect(parsed.data.description).toBe("Converted from Claude agent basic-agent")
  })

  test("agent with empty body gets default body", () => {
    const plugin: ClaudePlugin = {
      ...fixturePlugin,
      agents: [
        {
          name: "empty-agent",
          description: "Empty agent",
          body: "",
          sourcePath: "/tmp/plugin/agents/empty.md",
        },
      ],
    }

    const bundle = convertClaudeToCopilot(plugin, defaultOptions)
    const parsed = parseFrontmatter(bundle.agents[0].content)
    expect(parsed.body).toContain("Instructions converted from the empty-agent agent.")
  })

  test("agent capabilities are prepended to body", () => {
    const bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)
    const parsed = parseFrontmatter(bundle.agents[0].content)
    expect(parsed.body).toMatch(/## Capabilities\n- Threat modeling\n- OWASP/)
  })

  test("agent model field is passed through", () => {
    const bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)
    const parsed = parseFrontmatter(bundle.agents[0].content)
    expect(parsed.data.model).toBe("claude-sonnet-4-20250514")
  })

  test("agent without model omits model field", () => {
    const plugin: ClaudePlugin = {
      ...fixturePlugin,
      agents: [
        {
          name: "no-model",
          description: "No model agent",
          body: "Content.",
          sourcePath: "/tmp/plugin/agents/no-model.md",
        },
      ],
    }

    const bundle = convertClaudeToCopilot(plugin, defaultOptions)
    const parsed = parseFrontmatter(bundle.agents[0].content)
    expect(parsed.data.model).toBeUndefined()
  })

  test("agent tools defaults to [*]", () => {
    const bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)
    const parsed = parseFrontmatter(bundle.agents[0].content)
    expect(parsed.data.tools).toEqual(["*"])
  })

  test("agent infer defaults to true", () => {
    const bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)
    const parsed = parseFrontmatter(bundle.agents[0].content)
    expect(parsed.data.infer).toBe(true)
  })

  test("warns when agent body exceeds 30k characters", () => {
    const warnSpy = spyOn(console, "warn").mockImplementation(() => {})

    const plugin: ClaudePlugin = {
      ...fixturePlugin,
      agents: [
        {
          name: "large-agent",
          description: "Large agent",
          body: "x".repeat(31_000),
          sourcePath: "/tmp/plugin/agents/large.md",
        },
      ],
      commands: [],
      skills: [],
    }

    convertClaudeToCopilot(plugin, defaultOptions)
    expect(warnSpy).toHaveBeenCalledWith(
      expect.stringContaining("exceeds 30000 characters"),
    )

    warnSpy.mockRestore()
  })

  test("converts commands to skills with SKILL.md format", () => {
    const bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)

    expect(bundle.generatedSkills).toHaveLength(1)
    const skill = bundle.generatedSkills[0]
    expect(skill.name).toBe("workflows-plan")

    const parsed = parseFrontmatter(skill.content)
    expect(parsed.data.name).toBe("workflows-plan")
    expect(parsed.data.description).toBe("Planning command")
    expect(parsed.body).toContain("Plan the work.")
  })

  test("preserves namespaced command names with hyphens", () => {
    const bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)
    expect(bundle.generatedSkills[0].name).toBe("workflows-plan")
  })

  test("command name collision after normalization is deduplicated", () => {
    const plugin: ClaudePlugin = {
      ...fixturePlugin,
      commands: [
        {
          name: "workflows:plan",
          description: "Workflow plan",
          body: "Plan body.",
          sourcePath: "/tmp/plugin/commands/workflows/plan.md",
        },
        {
          name: "workflows:plan",
          description: "Duplicate plan",
          body: "Duplicate body.",
          sourcePath: "/tmp/plugin/commands/workflows/plan2.md",
        },
      ],
      agents: [],
      skills: [],
    }

    const bundle = convertClaudeToCopilot(plugin, defaultOptions)
    const names = bundle.generatedSkills.map((s) => s.name)
    expect(names).toEqual(["workflows-plan", "workflows-plan-2"])
  })

  test("namespaced and non-namespaced commands produce distinct names", () => {
    const plugin: ClaudePlugin = {
      ...fixturePlugin,
      commands: [
        {
          name: "workflows:plan",
          description: "Workflow plan",
          body: "Plan body.",
          sourcePath: "/tmp/plugin/commands/workflows/plan.md",
        },
        {
          name: "plan",
          description: "Top-level plan",
          body: "Top plan body.",
          sourcePath: "/tmp/plugin/commands/plan.md",
        },
      ],
      agents: [],
      skills: [],
    }

    const bundle = convertClaudeToCopilot(plugin, defaultOptions)
    const names = bundle.generatedSkills.map((s) => s.name)
    expect(names).toEqual(["workflows-plan", "plan"])
  })

  test("command allowedTools is silently dropped", () => {
    const bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)
    const skill = bundle.generatedSkills[0]
    expect(skill.content).not.toContain("allowedTools")
    expect(skill.content).not.toContain("allowed-tools")
  })

  test("command with argument-hint gets Arguments section", () => {
    const bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)
    const skill = bundle.generatedSkills[0]
    expect(skill.content).toContain("## Arguments")
    expect(skill.content).toContain("[FOCUS]")
  })

  test("passes through skill directories", () => {
    const bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)

    expect(bundle.skillDirs).toHaveLength(1)
    expect(bundle.skillDirs[0].name).toBe("existing-skill")
    expect(bundle.skillDirs[0].sourceDir).toBe("/tmp/plugin/skills/existing-skill")
  })

  test("skill and generated skill name collision is deduplicated", () => {
    const plugin: ClaudePlugin = {
      ...fixturePlugin,
      commands: [
        {
          name: "existing-skill",
          description: "Colliding command",
          body: "This collides with skill name.",
          sourcePath: "/tmp/plugin/commands/existing-skill.md",
        },
      ],
      agents: [],
    }

    const bundle = convertClaudeToCopilot(plugin, defaultOptions)
    // The command should get deduplicated since the skill name is reserved
    expect(bundle.generatedSkills[0].name).toBe("existing-skill-2")
    expect(bundle.skillDirs[0].name).toBe("existing-skill")
  })

  test("converts MCP servers with COPILOT_MCP_ prefix", () => {
    const plugin: ClaudePlugin = {
      ...fixturePlugin,
      agents: [],
      commands: [],
      skills: [],
      mcpServers: {
        playwright: {
          command: "npx",
          args: ["-y", "@anthropic/mcp-playwright"],
          env: { DISPLAY: ":0", API_KEY: "secret" },
        },
      },
    }

    const bundle = convertClaudeToCopilot(plugin, defaultOptions)
    expect(bundle.mcpConfig).toBeDefined()
    expect(bundle.mcpConfig!.playwright.type).toBe("local")
    expect(bundle.mcpConfig!.playwright.command).toBe("npx")
    expect(bundle.mcpConfig!.playwright.args).toEqual(["-y", "@anthropic/mcp-playwright"])
    expect(bundle.mcpConfig!.playwright.tools).toEqual(["*"])
    expect(bundle.mcpConfig!.playwright.env).toEqual({
      COPILOT_MCP_DISPLAY: ":0",
      COPILOT_MCP_API_KEY: "secret",
    })
  })

  test("MCP env vars already prefixed are not double-prefixed", () => {
    const plugin: ClaudePlugin = {
      ...fixturePlugin,
      agents: [],
      commands: [],
      skills: [],
      mcpServers: {
        server: {
          command: "node",
          args: ["server.js"],
          env: { COPILOT_MCP_TOKEN: "abc" },
        },
      },
    }

    const bundle = convertClaudeToCopilot(plugin, defaultOptions)
    expect(bundle.mcpConfig!.server.env).toEqual({ COPILOT_MCP_TOKEN: "abc" })
  })

  test("MCP servers get type field (local vs sse)", () => {
    const plugin: ClaudePlugin = {
      ...fixturePlugin,
      agents: [],
      commands: [],
      skills: [],
      mcpServers: {
        local: { command: "npx", args: ["server"] },
        remote: { url: "https://mcp.example.com/sse" },
      },
    }

    const bundle = convertClaudeToCopilot(plugin, defaultOptions)
    expect(bundle.mcpConfig!.local.type).toBe("local")
    expect(bundle.mcpConfig!.remote.type).toBe("sse")
  })

  test("MCP headers pass through for remote servers", () => {
    const plugin: ClaudePlugin = {
      ...fixturePlugin,
      agents: [],
      commands: [],
      skills: [],
      mcpServers: {
        remote: {
          url: "https://mcp.example.com/sse",
          headers: { Authorization: "Bearer token" },
        },
      },
    }

    const bundle = convertClaudeToCopilot(plugin, defaultOptions)
    expect(bundle.mcpConfig!.remote.url).toBe("https://mcp.example.com/sse")
    expect(bundle.mcpConfig!.remote.headers).toEqual({ Authorization: "Bearer token" })
  })

  test("warns when hooks are present", () => {
    const warnSpy = spyOn(console, "warn").mockImplementation(() => {})

    const plugin: ClaudePlugin = {
      ...fixturePlugin,
      agents: [],
      commands: [],
      skills: [],
      hooks: {
        hooks: {
          PreToolUse: [{ matcher: "Bash", hooks: [{ type: "command", command: "echo test" }] }],
        },
      },
    }

    convertClaudeToCopilot(plugin, defaultOptions)
    expect(warnSpy).toHaveBeenCalledWith(
      "Warning: Copilot does not support hooks. Hooks were skipped during conversion.",
    )

    warnSpy.mockRestore()
  })

  test("no warning when hooks are absent", () => {
    const warnSpy = spyOn(console, "warn").mockImplementation(() => {})

    convertClaudeToCopilot(fixturePlugin, defaultOptions)
    expect(warnSpy).not.toHaveBeenCalled()

    warnSpy.mockRestore()
  })

  test("plugin with zero agents produces empty agents array", () => {
    const plugin: ClaudePlugin = {
      ...fixturePlugin,
      agents: [],
    }

    const bundle = convertClaudeToCopilot(plugin, defaultOptions)
    expect(bundle.agents).toHaveLength(0)
  })

  test("plugin with only skills works", () => {
    const plugin: ClaudePlugin = {
      ...fixturePlugin,
      agents: [],
      commands: [],
    }

    const bundle = convertClaudeToCopilot(plugin, defaultOptions)
    expect(bundle.agents).toHaveLength(0)
    expect(bundle.generatedSkills).toHaveLength(0)
    expect(bundle.skillDirs).toHaveLength(1)
  })
})

describe("transformContentForCopilot", () => {
  test("rewrites .claude/ paths to .github/", () => {
    const input = "Read `.claude/compound-engineering.local.md` for config."
    const result = transformContentForCopilot(input)
    expect(result).toContain(".github/compound-engineering.local.md")
    expect(result).not.toContain(".claude/")
  })

  test("rewrites ~/.claude/ paths to ~/.copilot/", () => {
    const input = "Global config at ~/.claude/settings.json"
    const result = transformContentForCopilot(input)
    expect(result).toContain("~/.copilot/settings.json")
    expect(result).not.toContain("~/.claude/")
  })

  test("transforms Task agent calls to skill references", () => {
    const input = `Run agents:

- Task repo-research-analyst(feature_description)
- Task learnings-researcher(feature_description)

Task best-practices-researcher(topic)`

    const result = transformContentForCopilot(input)
    expect(result).toContain("Use the repo-research-analyst skill to: feature_description")
    expect(result).toContain("Use the learnings-researcher skill to: feature_description")
    expect(result).toContain("Use the best-practices-researcher skill to: topic")
    expect(result).not.toContain("Task repo-research-analyst(")
  })

  test("transforms namespaced Task agent calls using final segment", () => {
    const input = `Run agents:

- Task compound-engineering:research:repo-research-analyst(feature_description)
- Task compound-engineering:review:security-reviewer(code_diff)`

    const result = transformContentForCopilot(input)
    expect(result).toContain("Use the repo-research-analyst skill to: feature_description")
    expect(result).toContain("Use the security-reviewer skill to: code_diff")
    expect(result).not.toContain("compound-engineering:")
  })

  test("transforms zero-argument Task calls", () => {
    const input = `- Task compound-engineering:review:code-simplicity-reviewer()`

    const result = transformContentForCopilot(input)
    expect(result).toContain("Use the code-simplicity-reviewer skill")
    expect(result).not.toContain("compound-engineering:")
    expect(result).not.toContain("skill to:")
  })

  test("replaces colons with hyphens in slash commands", () => {
    const input = `1. Run /deepen-plan to enhance
2. Start /workflows:work to implement
3. File at /tmp/output.md`

    const result = transformContentForCopilot(input)
    expect(result).toContain("/deepen-plan")
    expect(result).toContain("/workflows-work")
    expect(result).not.toContain("/workflows:work")
    // File paths preserved
    expect(result).toContain("/tmp/output.md")
  })

  test("transforms @agent references to agent references", () => {
    const input = "Have @security-sentinel and @dhh-rails-reviewer check the code."
    const result = transformContentForCopilot(input)
    expect(result).toContain("the security-sentinel agent")
    expect(result).toContain("the dhh-rails-reviewer agent")
    expect(result).not.toContain("@security-sentinel")
  })
})
