import json
from pathlib import Path

import pytest

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

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


def test_agent_from_file_with_frontmatter(tmp_path: Path) -> None:
    f = tmp_path / "my-agent.md"
    f.write_text("---\nname: My Agent\ndescription: Does stuff\n---\nBe helpful.\n")
    agent = Agent.from_file(f)
    assert agent.name == "My Agent"
    assert agent.description == "Does stuff"
    assert agent.body == "Be helpful."
    assert agent.source_path == f


def test_agent_preserves_extra_meta(tmp_path: Path) -> None:
    f = tmp_path / "agent.md"
    f.write_text("---\nname: X\ndescription: d\nmodel: gpt-4o\nreadonly: true\n---\nbody")
    agent = Agent.from_file(f)
    assert agent.extra_meta == {"model": "gpt-4o", "readonly": True}


def test_agent_extra_meta_excludes_known_keys(tmp_path: Path) -> None:
    f = tmp_path / "agent.md"
    f.write_text("---\nname: X\ndescription: d\nmode: cursor\ncustom: val\n---\nbody")
    agent = Agent.from_file(f)
    assert "name" not in agent.extra_meta
    assert "description" not in agent.extra_meta
    assert "mode" not in agent.extra_meta
    assert agent.extra_meta == {"custom": "val"}


def test_agent_from_file_falls_back_to_stem(tmp_path: Path) -> None:
    f = tmp_path / "fallback-agent.md"
    f.write_text("---\ndescription: No name field\n---\nbody")
    agent = Agent.from_file(f)
    assert agent.name == "fallback-agent"


def test_agent_from_file_no_frontmatter(tmp_path: Path) -> None:
    f = tmp_path / "plain.md"
    f.write_text("Just the body")
    agent = Agent.from_file(f)
    assert agent.name == "plain"
    assert agent.body == "Just the body"


def test_agent_to_markdown() -> None:
    agent = Agent(name="foo", description="bar", body="Do it.")
    md = agent.to_markdown()
    assert "name: foo" in md
    assert "description: bar" in md
    assert "Do it." in md
    assert "mode:" not in md  # default mode not serialized


def test_agent_to_markdown_round_trips_extra_meta(tmp_path: Path) -> None:
    f = tmp_path / "agent.md"
    f.write_text("---\nname: X\ndescription: d\nmodel: gpt-4o\nreadonly: true\n---\nbody")
    agent = Agent.from_file(f)
    md = agent.to_markdown()
    assert "model: gpt-4o" in md
    assert "readonly: true" in md


def test_agent_validation_requires_name() -> None:
    with pytest.raises(Exception):  # noqa: B017 - pydantic raises different types across versions
        Agent(description="no name", body="body")  # type: ignore[call-arg]


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


def test_command_from_file_with_frontmatter(tmp_path: Path) -> None:
    f = tmp_path / "fix-tests.md"
    f.write_text("---\nname: Fix Tests\ndescription: Runs fixes\n---\nFix all failing tests.")
    cmd = Command.from_file(f)
    assert cmd.name == "Fix Tests"
    assert cmd.description == "Runs fixes"
    assert cmd.body == "Fix all failing tests."


def test_command_preserves_extra_meta(tmp_path: Path) -> None:
    f = tmp_path / "cmd.md"
    f.write_text("---\nname: Cmd\ndescription: d\ntemperature: 0.5\n---\nDo it.")
    cmd = Command.from_file(f)
    assert cmd.extra_meta == {"temperature": 0.5}


def test_command_from_file_plain_markdown(tmp_path: Path) -> None:
    f = tmp_path / "my-cmd.md"
    f.write_text("Do something useful.")
    cmd = Command.from_file(f)
    assert cmd.name == "my-cmd"
    assert cmd.body == "Do something useful."
    assert cmd.description == ""


def test_command_from_file_stem_as_name(tmp_path: Path) -> None:
    f = tmp_path / "review-pr.md"
    f.write_text("Review the PR carefully.")
    cmd = Command.from_file(f)
    assert cmd.name == "review-pr"


def test_command_to_markdown_round_trips_extra_meta(tmp_path: Path) -> None:
    f = tmp_path / "cmd.md"
    f.write_text("---\nname: Cmd\ndescription: d\ntemperature: 0.5\n---\nDo it.")
    cmd = Command.from_file(f)
    md = cmd.to_markdown()
    assert "temperature: 0.5" in md


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


def test_skill_from_dir(tmp_path: Path) -> None:
    skill_dir = tmp_path / "my-skill"
    skill_dir.mkdir()
    (skill_dir / "SKILL.md").write_text(
        "---\nname: My Skill\ndescription: A cool skill\n---\nUse me wisely."
    )
    skill = Skill.from_dir(skill_dir)
    assert skill.name == "My Skill"
    assert skill.description == "A cool skill"
    assert skill.body == "Use me wisely."
    assert skill.source_dir == skill_dir


def test_skill_from_dir_falls_back_to_dirname(tmp_path: Path) -> None:
    skill_dir = tmp_path / "no-name-skill"
    skill_dir.mkdir()
    (skill_dir / "SKILL.md").write_text("Just content, no frontmatter.")
    skill = Skill.from_dir(skill_dir)
    assert skill.name == "no-name-skill"


def test_skill_preserves_extra_meta(tmp_path: Path) -> None:
    skill_dir = tmp_path / "my-skill"
    skill_dir.mkdir()
    (skill_dir / "SKILL.md").write_text(
        "---\nname: My Skill\ndescription: d\nmin_version: 1.2\n---\ncontent"
    )
    skill = Skill.from_dir(skill_dir)
    assert skill.extra_meta == {"min_version": 1.2}


def test_skill_extra_meta_excludes_known_keys(tmp_path: Path) -> None:
    skill_dir = tmp_path / "my-skill"
    skill_dir.mkdir()
    (skill_dir / "SKILL.md").write_text(
        "---\nname: My Skill\ndescription: d\ncustom: val\n---\ncontent"
    )
    skill = Skill.from_dir(skill_dir)
    assert "name" not in skill.extra_meta
    assert "description" not in skill.extra_meta
    assert skill.extra_meta == {"custom": "val"}


def test_skill_from_dir_missing_skill_md(tmp_path: Path) -> None:
    skill_dir = tmp_path / "bad-skill"
    skill_dir.mkdir()
    # No SKILL.md created
    with pytest.raises(FileNotFoundError, match="SKILL.md not found"):
        Skill.from_dir(skill_dir)


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


def test_rule_from_file(tmp_path: Path) -> None:
    f = tmp_path / "no-comments.md"
    f.write_text(
        "---\nname: No Comments\ndescription: Avoid verbose comments\nalwaysApply: true\n---\nDo not over-comment."
    )
    rule = Rule.from_file(f)
    assert rule.name == "No Comments"
    assert rule.always_apply is True
    assert rule.body == "Do not over-comment."


def test_rule_from_file_with_globs(tmp_path: Path) -> None:
    f = tmp_path / "ts-rule.md"
    f.write_text("---\nname: TS Rule\nglobs: '**/*.ts'\n---\nTS content.")
    rule = Rule.from_file(f)
    assert rule.globs == "**/*.ts"


def test_rule_from_file_snake_case_always_apply(tmp_path: Path) -> None:
    f = tmp_path / "rule.md"
    f.write_text("---\nname: r\nalways_apply: true\n---\nbody")
    rule = Rule.from_file(f)
    assert rule.always_apply is True


def test_rule_to_mdc() -> None:
    rule = Rule(name="foo", description="bar", always_apply=True, body="content")
    mdc = rule.to_mdc()
    assert "alwaysApply: true" in mdc
    assert "description: bar" in mdc
    assert "content" in mdc


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


def test_instructions_from_file_strips_frontmatter(tmp_path: Path) -> None:
    f = tmp_path / "instructions.md"
    f.write_text("---\nalwaysApply: true\n---\nBe concise.")
    instr = Instructions.from_file(f)
    assert instr.body == "Be concise."
    assert instr.source_path == f


def test_instructions_from_file_no_frontmatter(tmp_path: Path) -> None:
    f = tmp_path / "instructions.md"
    f.write_text("Just instructions.")
    instr = Instructions.from_file(f)
    assert instr.body == "Just instructions."


# ── MCPConfig ─────────────────────────────────────────────────────────────────


def test_mcp_from_file_standard_format(tmp_path: Path) -> None:
    f = tmp_path / "mcp.json"
    f.write_text(json.dumps({"mcpServers": {"server1": {"command": "foo"}}}))
    mcp = MCPConfig.from_file(f)
    assert "server1" in mcp.servers
    assert mcp.source_path == f


def test_mcp_from_file_bare_dict(tmp_path: Path) -> None:
    f = tmp_path / "mcp.json"
    f.write_text(json.dumps({"server1": {"command": "foo"}}))
    mcp = MCPConfig.from_file(f)
    assert "server1" in mcp.servers


def test_mcp_to_json() -> None:
    mcp = MCPConfig(servers={"srv": {"command": "run"}})
    output = json.loads(mcp.to_json())
    assert output == {"mcpServers": {"srv": {"command": "run"}}}


def test_mcp_from_file_rejects_non_object_values(tmp_path: Path) -> None:
    """Bare JSON without mcpServers key must have dict values for all keys."""
    f = tmp_path / "bad-mcp.json"
    f.write_text(json.dumps({"name": "my-project", "version": "1.0"}))
    with pytest.raises(ValueError, match="do not look like server configs"):
        MCPConfig.from_file(f)


def test_mcp_from_file_rejects_nested_dicts_without_server_keys(tmp_path: Path) -> None:
    """Bare format must reject nested dicts that lack any recognised server config key."""
    f = tmp_path / "bad-mcp.json"
    f.write_text(json.dumps({"metadata": {"version": "1.0"}, "config": {"debug": True}}))
    with pytest.raises(ValueError, match="do not look like server configs"):
        MCPConfig.from_file(f)


def test_mcp_from_file_rejects_non_dict_root(tmp_path: Path) -> None:
    f = tmp_path / "bad-mcp.json"
    f.write_text(json.dumps([{"command": "foo"}]))
    with pytest.raises(ValueError, match="must be a JSON object"):
        MCPConfig.from_file(f)
