import json
from pathlib import Path

import pytest

from agentmux.adapters.opencode import OpenCodeAdapter
from agentmux.core.component import Agent, Command, Instructions, MCPConfig, Rule, Skill


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


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


def test_convert_agent_output_has_description_and_mode(adapter: OpenCodeAdapter) -> None:
    agent = Agent(name="Helper", description="Helps with code", body="Be helpful.")
    results = adapter.convert_agent(agent)
    assert len(results) == 1
    result = results[0]
    assert result.path == Path("agents/Helper.md")
    assert result.content is not None
    assert "description: Helps with code" in result.content
    assert "mode:" in result.content
    assert "Be helpful." in result.content


def test_convert_agent_no_name_field_in_frontmatter(adapter: OpenCodeAdapter) -> None:
    """OpenCode agents use the filename as name; frontmatter should NOT include 'name:'."""
    agent = Agent(name="Reviewer", description="Code reviewer", body="Review carefully.")
    result = adapter.convert_agent(agent)[0]
    assert result.content is not None
    assert "name:" not in result.content


def test_convert_agent_mode_all_maps_to_all(adapter: OpenCodeAdapter) -> None:
    agent = Agent(name="A", description="d", body="b", mode="all")
    result = adapter.convert_agent(agent)[0]
    assert result.content is not None
    assert "mode: all" in result.content


def test_convert_agent_mode_primary_passthrough(adapter: OpenCodeAdapter) -> None:
    agent = Agent(name="Build", description="Builder", body="Build things.", mode="primary")
    result = adapter.convert_agent(agent)[0]
    assert result.content is not None
    assert "mode: primary" in result.content


def test_convert_agent_mode_subagent_passthrough(adapter: OpenCodeAdapter) -> None:
    agent = Agent(name="Explore", description="Explorer", body="Explore things.", mode="subagent")
    result = adapter.convert_agent(agent)[0]
    assert result.content is not None
    assert "mode: subagent" in result.content


def test_convert_agent_unknown_mode_defaults_to_all(adapter: OpenCodeAdapter) -> None:
    """Unknown agentmux modes (e.g. 'cursor-only') should fall back to 'all'."""
    agent = Agent(name="A", description="d", body="b", mode="cursor-only")
    result = adapter.convert_agent(agent)[0]
    assert result.content is not None
    assert "mode: all" in result.content


def test_convert_agent_scope_is_global(adapter: OpenCodeAdapter) -> 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: OpenCodeAdapter, tmp_path: Path) -> None:
    """source must be None so symlink strategy does not bypass mode mapping."""
    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: OpenCodeAdapter, tmp_path: Path) -> None:
    """Extra metadata fields like model and tools should be preserved."""
    f = tmp_path / "agent.md"
    f.write_text("---\nname: X\ndescription: d\nmodel: anthropic/claude-sonnet-4\n---\nbody")
    agent = Agent.from_file(f)
    result = adapter.convert_agent(agent)[0]
    assert result.content is not None
    assert "anthropic/claude-sonnet-4" in result.content


def test_convert_agent_sanitizes_name_in_path(adapter: OpenCodeAdapter) -> 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")


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


def test_convert_command_body_is_prompt_template(adapter: OpenCodeAdapter, tmp_path: Path) -> None:
    cmd_file = tmp_path / "run-tests.md"
    cmd_file.write_text(
        "---\nname: Run Tests\ndescription: Run the test suite\n---\nRun all tests."
    )
    cmd = Command.from_file(cmd_file)
    results = adapter.convert_command(cmd)
    assert len(results) == 1
    result = results[0]
    assert result.path == Path("commands/run-tests.md")
    assert result.content is not None
    assert "Run all tests." in result.content


def test_convert_command_includes_description_in_frontmatter(
    adapter: OpenCodeAdapter, 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: OpenCodeAdapter, tmp_path: Path
) -> None:
    """Commands with no description or extra meta should not have YAML 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: OpenCodeAdapter, 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: OpenCodeAdapter, 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: OpenCodeAdapter, 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: OpenCodeAdapter, tmp_path: Path) -> None:
    """Extra frontmatter fields like 'agent' and 'model' should be included."""
    cmd_file = tmp_path / "review.md"
    cmd_file.write_text("---\ndescription: Review code\nagent: plan\n---\nReview the PR.")
    cmd = Command.from_file(cmd_file)
    result = adapter.convert_command(cmd)[0]
    assert result.content is not None
    assert "agent: plan" in result.content


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


def test_convert_skill_symlinks_directory(adapter: OpenCodeAdapter, 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: OpenCodeAdapter, 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: OpenCodeAdapter, tmp_path: Path) -> None:
    """Path segment must match the source directory name, not the skill's name field."""
    skill_dir = tmp_path / "code-review-skill"
    skill_dir.mkdir()
    (skill_dir / "SKILL.md").write_text(
        "---\nname: code-review-skill\ndescription: d\n---\ncontent"
    )
    skill = Skill.from_dir(skill_dir)
    result = adapter.convert_skill(skill)[0]
    assert result.path == Path("skills/code-review-skill")


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


def test_convert_rule_returns_empty_list(adapter: OpenCodeAdapter) -> None:
    """OpenCode has no standalone rule files; convert_rule must return []."""
    rule = Rule(name="no-comments", always_apply=True, body="Short comments only.")
    result = adapter.convert_rule(rule)
    assert result == []


def test_convert_rule_returns_empty_regardless_of_globs(adapter: OpenCodeAdapter) -> None:
    rule = Rule(name="ts-rule", globs="**/*.ts", always_apply=False, body="TS rules.")
    assert adapter.convert_rule(rule) == []


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


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


def test_convert_instructions_no_frontmatter(adapter: OpenCodeAdapter) -> None:
    """AGENTS.md must be plain markdown — no YAML frontmatter."""
    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: OpenCodeAdapter, tmp_path: Path
) -> None:
    """Frontmatter in the source file must be stripped in the output."""
    f = tmp_path / "instructions.md"
    f.write_text("---\nalwaysApply: true\n---\nBe helpful.")
    instr = Instructions.from_file(f)
    result = adapter.convert_instructions(instr)[0]
    assert "alwaysApply" not in result.content  # type: ignore[operator]
    assert "Be helpful." in result.content  # type: ignore[operator]


def test_convert_instructions_scope_is_global(adapter: OpenCodeAdapter) -> None:
    instr = Instructions(body="b")
    assert adapter.convert_instructions(instr)[0].scope == "global"


def test_convert_instructions_has_no_source(adapter: OpenCodeAdapter, 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_opencode_jsonc(adapter: OpenCodeAdapter, tmp_path: Path) -> None:
    mcp_file = tmp_path / "mcp.json"
    mcp_file.write_text(json.dumps({"mcpServers": {"srv": {"type": "local", "command": ["run"]}}}))
    mcp = MCPConfig.from_file(mcp_file)
    results = adapter.convert_mcp(mcp)
    assert len(results) == 1
    assert results[0].path == Path("opencode.jsonc")


def test_convert_mcp_wraps_servers_under_mcp_key(adapter: OpenCodeAdapter, tmp_path: Path) -> None:
    """OpenCode uses 'mcp' key, not 'mcpServers'."""
    mcp_file = tmp_path / "mcp.json"
    mcp_file.write_text(
        json.dumps({"mcpServers": {"my-server": {"type": "local", "command": ["npx", "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 "mcp" in payload
    assert "mcpServers" not in payload
    assert "my-server" in payload["mcp"]


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


def test_convert_mcp_scope_is_global(adapter: OpenCodeAdapter) -> None:
    mcp = MCPConfig(servers={"srv": {"type": "local", "command": ["run"]}})
    assert adapter.convert_mcp(mcp)[0].scope == "global"


def test_convert_mcp_inline_servers(adapter: OpenCodeAdapter) -> None:
    """MCPConfig built programmatically (no source_path) should also produce the mcp wrapper."""
    mcp = MCPConfig(
        servers={
            "context7": {"type": "remote", "url": "https://mcp.context7.com/mcp"},
            "local-tool": {"type": "local", "command": ["bun", "x", "my-tool"]},
        }
    )
    result = adapter.convert_mcp(mcp)[0]
    assert result.content is not None
    payload = json.loads(result.content)
    assert set(payload["mcp"].keys()) == {"context7", "local-tool"}


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


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


def test_validate_returns_error_for_rule(adapter: OpenCodeAdapter) -> None:
    """Rules are not supported standalone in OpenCode."""
    rule = Rule(name="r", always_apply=False, body="b")
    errors = adapter.validate(rule)
    assert len(errors) == 1
    assert "rule" in errors[0].lower() or "opencode" in errors[0].lower()


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


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


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


def test_validate_returns_empty_for_skill(adapter: OpenCodeAdapter, 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: OpenCodeAdapter) -> None:
    assert adapter.name == "opencode"


def test_adapter_global_base_dir(adapter: OpenCodeAdapter) -> None:
    assert adapter.get_global_base_dir() == Path.home() / ".config" / "opencode"


def test_adapter_project_subdir(adapter: OpenCodeAdapter) -> None:
    assert adapter.get_project_subdir() == Path(".opencode")
