import json
import re
from pathlib import Path

import pytest

from agentmux.adapters.claude_code import ClaudeCodeAdapter, _to_claude_agent_name
from agentmux.core.component import Agent, Command, Instructions, MCPConfig, Rule, Skill


@pytest.fixture
def adapter() -> ClaudeCodeAdapter:
    return ClaudeCodeAdapter()


# ── Agent ─────────────────────────────────────────────────────────────────────


def test_convert_agent_output_has_name_and_description(adapter: ClaudeCodeAdapter) -> None:
    agent = Agent(
        name="code-reviewer", description="Reviews code quality", body="Review carefully."
    )
    results = adapter.convert_agent(agent)
    assert len(results) == 1
    result = results[0]
    assert result.path == Path("agents/code-reviewer.md")
    assert result.content is not None
    assert "name: code-reviewer" in result.content
    assert "description: Reviews code quality" in result.content
    assert "Review carefully." in result.content


def test_convert_agent_default_model_is_inherit(adapter: ClaudeCodeAdapter) -> None:
    """When no model is specified, the adapter injects model: inherit per Claude Code defaults."""
    agent = Agent(name="helper", description="Helps", body="Be helpful.")
    result = adapter.convert_agent(agent)[0]
    assert result.content is not None
    assert "model: inherit" in result.content


def test_convert_agent_explicit_model_overrides_default(adapter: ClaudeCodeAdapter) -> None:
    """Explicit model in extra_meta must override the 'inherit' default."""
    agent = Agent(
        name="sonnet-agent",
        description="Uses sonnet",
        body="body",
        extra_meta={"model": "sonnet"},
    )
    result = adapter.convert_agent(agent)[0]
    assert result.content is not None
    assert "model: sonnet" in result.content
    # Must not have both — only one model line
    assert result.content.count("model:") == 1


def test_convert_agent_scope_is_global(adapter: ClaudeCodeAdapter) -> None:
    agent = Agent(name="a", description="d", body="b")
    assert adapter.convert_agent(agent)[0].scope == "global"


def test_convert_agent_has_no_source(adapter: ClaudeCodeAdapter, tmp_path: Path) -> None:
    """source must be None so symlink strategy does not bypass content transformation."""
    f = tmp_path / "agent.md"
    f.write_text("---\nname: Helper\ndescription: d\n---\nbody")
    agent = Agent.from_file(f)
    result = adapter.convert_agent(agent)[0]
    assert result.source is None


def test_convert_agent_preserves_extra_meta(adapter: ClaudeCodeAdapter, tmp_path: Path) -> None:
    """Extra metadata fields like tools and disallowedTools should be preserved."""
    f = tmp_path / "agent.md"
    f.write_text(
        "---\nname: reviewer\ndescription: Reviewer\ndisallowedTools: Write, Edit\n---\nReview."
    )
    agent = Agent.from_file(f)
    result = adapter.convert_agent(agent)[0]
    assert result.content is not None
    assert "disallowedTools:" in result.content


def test_convert_agent_sanitizes_name_in_path(adapter: ClaudeCodeAdapter) -> None:
    agent = Agent(name="my agent/helper", description="d", body="b")
    result = adapter.convert_agent(agent)[0]
    assert "/" not in result.path.name
    assert result.path.parent == Path("agents")


def test_convert_agent_path_uses_sanitized_name(adapter: ClaudeCodeAdapter) -> None:
    """Filename uses sanitize_component_name; the frontmatter name uses the lowercase-hyphen form."""
    agent = Agent(name="Code Reviewer", description="d", body="b")
    result = adapter.convert_agent(agent)[0]
    # Filename: sanitize_component_name preserves case but removes unsafe chars
    assert result.path == Path("agents/Code Reviewer.md")
    # Frontmatter name: must be lowercase-hyphen form
    assert result.content is not None
    assert "name: code-reviewer" in result.content


# ── _to_claude_agent_name ─────────────────────────────────────────────────────


def test_to_claude_agent_name_lowercases_input() -> None:
    assert _to_claude_agent_name("Helper") == "helper"


def test_to_claude_agent_name_replaces_spaces_with_hyphens() -> None:
    assert _to_claude_agent_name("Code Reviewer") == "code-reviewer"


def test_to_claude_agent_name_replaces_underscores_with_hyphens() -> None:
    assert _to_claude_agent_name("my_agent") == "my-agent"


def test_to_claude_agent_name_collapses_consecutive_hyphens() -> None:
    assert _to_claude_agent_name("my  agent") == "my-agent"


def test_to_claude_agent_name_strips_leading_trailing_hyphens() -> None:
    assert _to_claude_agent_name("  My Agent  ") == "my-agent"


def test_to_claude_agent_name_already_valid_passthrough() -> None:
    assert _to_claude_agent_name("code-reviewer") == "code-reviewer"


def test_to_claude_agent_name_result_matches_lowercase_hyphen_pattern() -> None:
    """Output must only contain lowercase letters, digits, and hyphens."""
    result = _to_claude_agent_name("My Complex Agent (v2)!")
    assert re.fullmatch(r"[a-z0-9][a-z0-9-]*[a-z0-9]|[a-z0-9]", result), (
        f"Result {result!r} contains invalid characters"
    )


# ── Command ───────────────────────────────────────────────────────────────────


def test_convert_command_body_is_prompt_content(adapter: ClaudeCodeAdapter, tmp_path: Path) -> None:
    cmd_file = tmp_path / "review-pr.md"
    cmd_file.write_text(
        "---\nname: Review PR\ndescription: Review a pull request\n---\nReview the PR carefully."
    )
    cmd = Command.from_file(cmd_file)
    results = adapter.convert_command(cmd)
    assert len(results) == 1
    result = results[0]
    assert result.path == Path("commands/review-pr.md")
    assert result.content is not None
    assert "Review the PR carefully." in result.content


def test_convert_command_includes_description_in_frontmatter(
    adapter: ClaudeCodeAdapter, tmp_path: Path
) -> None:
    cmd_file = tmp_path / "fix.md"
    cmd_file.write_text("---\ndescription: Fix failing tests\n---\nFix all failures.")
    cmd = Command.from_file(cmd_file)
    result = adapter.convert_command(cmd)[0]
    assert result.content is not None
    assert "description: Fix failing tests" in result.content


def test_convert_command_no_frontmatter_when_no_description(
    adapter: ClaudeCodeAdapter, tmp_path: Path
) -> None:
    """Commands with no description or extra meta should produce plain markdown without frontmatter."""
    cmd_file = tmp_path / "plain.md"
    cmd_file.write_text("Do the thing.")
    cmd = Command.from_file(cmd_file)
    result = adapter.convert_command(cmd)[0]
    assert result.content is not None
    assert "---" not in result.content
    assert "Do the thing." in result.content


def test_convert_command_preserves_filename(adapter: ClaudeCodeAdapter, tmp_path: Path) -> None:
    cmd_file = tmp_path / "review-pr.md"
    cmd_file.write_text("Review it.")
    cmd = Command.from_file(cmd_file)
    result = adapter.convert_command(cmd)[0]
    assert result.path.name == "review-pr.md"


def test_convert_command_has_no_source(adapter: ClaudeCodeAdapter, tmp_path: Path) -> None:
    """source must be None so the sync engine does not symlink the raw file."""
    cmd_file = tmp_path / "fix.md"
    cmd_file.write_text("---\ndescription: Fix things\n---\nFix it.")
    cmd = Command.from_file(cmd_file)
    result = adapter.convert_command(cmd)[0]
    assert result.source is None


def test_convert_command_scope_is_global(adapter: ClaudeCodeAdapter, tmp_path: Path) -> None:
    cmd_file = tmp_path / "cmd.md"
    cmd_file.write_text("Do something.")
    cmd = Command.from_file(cmd_file)
    assert adapter.convert_command(cmd)[0].scope == "global"


def test_convert_command_preserves_extra_meta(adapter: ClaudeCodeAdapter, tmp_path: Path) -> None:
    """Extra frontmatter fields like 'argument-hint' and 'allowed-tools' should be included."""
    cmd_file = tmp_path / "explain.md"
    cmd_file.write_text(
        "---\ndescription: Explain code\nargument-hint: '[filename]'\n---\nExplain the code."
    )
    cmd = Command.from_file(cmd_file)
    result = adapter.convert_command(cmd)[0]
    assert result.content is not None
    assert "argument-hint:" in result.content


# ── Skill ─────────────────────────────────────────────────────────────────────


def test_convert_skill_symlinks_directory(adapter: ClaudeCodeAdapter, tmp_path: Path) -> None:
    skill_dir = tmp_path / "my-skill"
    skill_dir.mkdir()
    (skill_dir / "SKILL.md").write_text("---\nname: my-skill\ndescription: d\n---\ncontent")
    skill = Skill.from_dir(skill_dir)
    results = adapter.convert_skill(skill)
    assert len(results) == 1
    result = results[0]
    assert result.path == Path("skills/my-skill")
    assert result.content is None
    assert result.source == skill_dir
    assert result.is_directory is True


def test_convert_skill_scope_is_global(adapter: ClaudeCodeAdapter, tmp_path: Path) -> None:
    skill_dir = tmp_path / "my-skill"
    skill_dir.mkdir()
    (skill_dir / "SKILL.md").write_text("---\nname: my-skill\ndescription: d\n---\ncontent")
    skill = Skill.from_dir(skill_dir)
    assert adapter.convert_skill(skill)[0].scope == "global"


def test_convert_skill_uses_source_dir_name(adapter: ClaudeCodeAdapter, tmp_path: Path) -> None:
    """Path segment must match the source directory name, not the skill's name field."""
    skill_dir = tmp_path / "security-audit-skill"
    skill_dir.mkdir()
    (skill_dir / "SKILL.md").write_text("---\nname: security-audit\ndescription: d\n---\ncontent")
    skill = Skill.from_dir(skill_dir)
    result = adapter.convert_skill(skill)[0]
    assert result.path == Path("skills/security-audit-skill")


# ── Rule ──────────────────────────────────────────────────────────────────────


def test_convert_rule_produces_md_file(adapter: ClaudeCodeAdapter) -> None:
    rule = Rule(name="code-style", description="Code style rules", body="Use consistent style.")
    results = adapter.convert_rule(rule)
    assert len(results) == 1
    result = results[0]
    assert result.path == Path("rules/code-style.md")
    assert result.content is not None
    assert "Use consistent style." in result.content


def test_convert_rule_unconditional_has_no_paths_frontmatter(adapter: ClaudeCodeAdapter) -> None:
    """Rules without globs must produce plain markdown — no 'paths' frontmatter.

    Claude Code: omitting 'paths' makes a rule apply unconditionally.
    """
    rule = Rule(name="general", always_apply=True, body="Always do this.")
    result = adapter.convert_rule(rule)[0]
    assert result.content is not None
    assert "paths:" not in result.content


def test_convert_rule_with_globs_maps_to_paths_list(adapter: ClaudeCodeAdapter) -> None:
    """rule.globs (single string) must be mapped to paths: [<glob>] in Claude Code format."""
    rule = Rule(name="ts-rules", globs="**/*.ts", always_apply=False, body="TypeScript rules.")
    result = adapter.convert_rule(rule)[0]
    assert result.content is not None
    assert "paths:" in result.content
    assert "**/*.ts" in result.content


def test_convert_rule_no_always_apply_field(adapter: ClaudeCodeAdapter) -> None:
    """Claude Code has no 'alwaysApply' concept — must not appear in output."""
    rule = Rule(name="r", always_apply=True, body="b")
    result = adapter.convert_rule(rule)[0]
    assert result.content is not None
    assert "alwaysApply" not in result.content


def test_convert_rule_scope_is_project(adapter: ClaudeCodeAdapter) -> None:
    """Rules are project-scoped — they live in .claude/rules/ and are committed to git."""
    rule = Rule(name="r", always_apply=False, body="b")
    assert adapter.convert_rule(rule)[0].scope == "project"


def test_convert_rule_has_no_source(adapter: ClaudeCodeAdapter, tmp_path: Path) -> None:
    """source must be None — transformation (paths mapping) is always applied."""
    rule_file = tmp_path / "my-rule.md"
    rule_file.write_text("---\nalwaysApply: false\nglobs: '**/*.py'\n---\nPython rules.")
    rule = Rule.from_file(rule_file)
    result = adapter.convert_rule(rule)[0]
    assert result.source is None


def test_convert_rule_sanitizes_name_in_path(adapter: ClaudeCodeAdapter) -> None:
    rule = Rule(name="my rule/path", always_apply=False, body="b")
    result = adapter.convert_rule(rule)[0]
    assert "/" not in result.path.name
    assert result.path.parent == Path("rules")


# ── Instructions ──────────────────────────────────────────────────────────────


def test_convert_instructions_produces_claude_md(adapter: ClaudeCodeAdapter) -> None:
    instr = Instructions(body="Be concise and accurate.")
    results = adapter.convert_instructions(instr)
    assert len(results) == 1
    result = results[0]
    assert result.path == Path("CLAUDE.md")
    assert result.content is not None
    assert "Be concise and accurate." in result.content


def test_convert_instructions_no_frontmatter(adapter: ClaudeCodeAdapter) -> None:
    """CLAUDE.md must be plain markdown — no YAML frontmatter added by the adapter."""
    instr = Instructions(body="Use TypeScript.")
    result = adapter.convert_instructions(instr)[0]
    assert result.content is not None
    assert "---" not in result.content


def test_convert_instructions_strips_source_frontmatter(
    adapter: ClaudeCodeAdapter, tmp_path: Path
) -> None:
    """Frontmatter in the source file must be stripped in the output CLAUDE.md."""
    f = tmp_path / "instructions.md"
    f.write_text("---\nalwaysApply: true\n---\nFollow best practices.")
    instr = Instructions.from_file(f)
    result = adapter.convert_instructions(instr)[0]
    assert result.content is not None
    assert "alwaysApply" not in result.content
    assert "Follow best practices." in result.content


def test_convert_instructions_scope_is_project(adapter: ClaudeCodeAdapter) -> None:
    """CLAUDE.md is project-scoped — written to .claude/CLAUDE.md."""
    instr = Instructions(body="b")
    assert adapter.convert_instructions(instr)[0].scope == "project"


def test_convert_instructions_has_no_source(adapter: ClaudeCodeAdapter, tmp_path: Path) -> None:
    """source must be None — content is always written directly."""
    f = tmp_path / "instructions.md"
    f.write_text("Follow best practices.")
    instr = Instructions.from_file(f)
    result = adapter.convert_instructions(instr)[0]
    assert result.source is None


# ── MCP ───────────────────────────────────────────────────────────────────────


def test_convert_mcp_targets_project_root_mcp_json(adapter: ClaudeCodeAdapter) -> None:
    """MCP must target ../.mcp.json (resolves to project root relative to .claude/ base_dir)."""
    mcp = MCPConfig(servers={"srv": {"command": "run"}})
    results = adapter.convert_mcp(mcp)
    assert len(results) == 1
    assert results[0].path == Path("../.mcp.json")


def test_convert_mcp_wraps_servers_under_mcp_servers_key(
    adapter: ClaudeCodeAdapter, tmp_path: Path
) -> None:
    """Claude Code .mcp.json uses the 'mcpServers' wrapper (per official docs)."""
    mcp_file = tmp_path / "mcp.json"
    mcp_file.write_text(
        json.dumps({"mcpServers": {"my-server": {"command": "npx", "args": ["my-mcp"]}}})
    )
    mcp = MCPConfig.from_file(mcp_file)
    result = adapter.convert_mcp(mcp)[0]
    assert result.content is not None
    payload = json.loads(result.content)
    assert "mcpServers" in payload
    assert "my-server" in payload["mcpServers"]


def test_convert_mcp_merge_flag_is_true(adapter: ClaudeCodeAdapter) -> None:
    """merge=True so the sync engine deep-merges into any existing .mcp.json."""
    mcp = MCPConfig(servers={"srv": {"command": "run"}})
    result = adapter.convert_mcp(mcp)[0]
    assert result.merge is True


def test_convert_mcp_scope_is_project(adapter: ClaudeCodeAdapter) -> None:
    """.mcp.json is project-scoped — it lives at the project root."""
    mcp = MCPConfig(servers={"srv": {"command": "run"}})
    assert adapter.convert_mcp(mcp)[0].scope == "project"


def test_convert_mcp_inline_servers(adapter: ClaudeCodeAdapter) -> None:
    """MCPConfig built programmatically (no source_path) should use the mcpServers wrapper."""
    mcp = MCPConfig(
        servers={
            "github": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"]},
            "postgres": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-postgres"]},
        }
    )
    result = adapter.convert_mcp(mcp)[0]
    assert result.content is not None
    payload = json.loads(result.content)
    assert set(payload["mcpServers"].keys()) == {"github", "postgres"}


def test_convert_mcp_from_source_file(adapter: ClaudeCodeAdapter, tmp_path: Path) -> None:
    """MCPConfig loaded from a file should still produce the correct mcpServers wrapper."""
    mcp_file = tmp_path / "mcp.json"
    mcp_file.write_text(
        json.dumps(
            {
                "mcpServers": {
                    "slack": {
                        "type": "http",
                        "url": "https://mcp.slack.com/mcp",
                    }
                }
            }
        )
    )
    mcp = MCPConfig.from_file(mcp_file)
    result = adapter.convert_mcp(mcp)[0]
    assert result.content is not None
    payload = json.loads(result.content)
    assert "mcpServers" in payload
    assert "slack" in payload["mcpServers"]


# ── Validate ──────────────────────────────────────────────────────────────────


def test_validate_returns_empty_for_agent(adapter: ClaudeCodeAdapter) -> None:
    agent = Agent(name="a", description="d", body="b")
    assert adapter.validate(agent) == []


def test_validate_returns_empty_for_command(adapter: ClaudeCodeAdapter) -> None:
    cmd = Command(name="c", body="b")
    assert adapter.validate(cmd) == []


def test_validate_returns_empty_for_rule(adapter: ClaudeCodeAdapter) -> None:
    """Claude Code supports rules — validate must return no errors."""
    rule = Rule(name="r", always_apply=False, body="b")
    assert adapter.validate(rule) == []


def test_validate_returns_empty_for_instructions(adapter: ClaudeCodeAdapter) -> None:
    instr = Instructions(body="b")
    assert adapter.validate(instr) == []


def test_validate_returns_empty_for_mcp(adapter: ClaudeCodeAdapter) -> None:
    mcp = MCPConfig(servers={})
    assert adapter.validate(mcp) == []


def test_validate_returns_empty_for_skill(adapter: ClaudeCodeAdapter, tmp_path: Path) -> None:
    skill_dir = tmp_path / "my-skill"
    skill_dir.mkdir()
    (skill_dir / "SKILL.md").write_text("---\nname: my-skill\ndescription: d\n---\ncontent")
    skill = Skill.from_dir(skill_dir)
    assert adapter.validate(skill) == []


# ── Adapter identity ──────────────────────────────────────────────────────────


def test_adapter_name(adapter: ClaudeCodeAdapter) -> None:
    assert adapter.name == "claude-code"


def test_adapter_global_base_dir(adapter: ClaudeCodeAdapter) -> None:
    assert adapter.get_global_base_dir() == Path.home() / ".claude"


def test_adapter_project_subdir(adapter: ClaudeCodeAdapter) -> None:
    assert adapter.get_project_subdir() == Path(".claude")
