from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock

import pytest
import yaml

from msagent.cli.dispatchers.commands import CommandDispatcher
from msagent.cli.handlers import add_skill as add_skill_module
from msagent.cli.handlers.add_skill import AddSkillHandler
from msagent.skills.installer import SkillInstallError, SkillInstaller


def _write_skill(skill_dir: Path, *, name: str, description: str = "test skill") -> None:
    skill_dir.mkdir(parents=True, exist_ok=True)
    (skill_dir / "SKILL.md").write_text(
        f"---\nname: {name}\ndescription: {description}\n---\nbody\n",
        encoding="utf-8",
    )


def _build_session(tmp_path: Path) -> SimpleNamespace:
    return SimpleNamespace(
        context=SimpleNamespace(
            agent="Profiler",
            working_dir=tmp_path,
            bash_mode=False,
            thread_id="thread-1",
            current_input_tokens=None,
            current_output_tokens=None,
        ),
        prompt=SimpleNamespace(hotkeys={}),
        renderer=SimpleNamespace(
            render_help=lambda *_args, **_kwargs: None,
            render_hotkeys=lambda *_args, **_kwargs: None,
        ),
        update_context=lambda **_kwargs: None,
        running=True,
        needs_reload=False,
    )


@pytest.mark.asyncio
async def test_add_skill_handler_installs_skill_and_updates_agent_patterns(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    skill_dir = tmp_path / "external-skill"
    _write_skill(skill_dir, name="custom_skill")

    session = _build_session(tmp_path)
    handler = AddSkillHandler(session)
    messages: list[str] = []

    monkeypatch.setattr(add_skill_module.console, "print_success", messages.append)
    monkeypatch.setattr(add_skill_module.console, "print_error", messages.append)
    monkeypatch.setattr(add_skill_module.console, "print", lambda *_args, **_kwargs: None)

    await handler.handle([str(skill_dir)])

    installed_skill = tmp_path / ".msagent" / "skills" / "custom_skill" / "SKILL.md"
    assert installed_skill.exists()

    profiler_config = yaml.safe_load((tmp_path / ".msagent" / "agents" / "Profiler.yml").read_text(encoding="utf-8"))
    assert "default:custom_skill" in profiler_config["skills"]["patterns"]
    assert session.needs_reload is True
    assert session.running is False
    assert any("Installed skill 'custom_skill'" in message for message in messages)


@pytest.mark.asyncio
async def test_skill_installer_rejects_shadowed_skill(tmp_path: Path) -> None:
    workspace_skill_dir = tmp_path / "skills" / "workspace-skill"
    external_skill_dir = tmp_path / "incoming" / "external-skill"

    _write_skill(workspace_skill_dir, name="shadowed_skill")
    _write_skill(external_skill_dir, name="shadowed_skill")

    installer = SkillInstaller(tmp_path)

    with pytest.raises(SkillInstallError) as exc_info:
        await installer.install(str(external_skill_dir))

    assert "higher-priority directory" in str(exc_info.value)


@pytest.mark.asyncio
async def test_skill_installer_requires_description(tmp_path: Path) -> None:
    skill_dir = tmp_path / "invalid-skill"
    skill_dir.mkdir(parents=True)
    (skill_dir / "SKILL.md").write_text(
        "---\nname: invalid_skill\ndescription: ''\n---\nbody\n",
        encoding="utf-8",
    )

    installer = SkillInstaller(tmp_path)

    with pytest.raises(SkillInstallError) as exc_info:
        await installer.install(str(skill_dir))

    assert "non-empty 'description'" in str(exc_info.value)


@pytest.mark.asyncio
async def test_skill_installer_uses_skill_name_for_target_directory(tmp_path: Path) -> None:
    skill_dir = tmp_path / "incoming" / "mismatched-dir-name"
    _write_skill(skill_dir, name="custom_skill_name")

    installer = SkillInstaller(tmp_path)
    result = await installer.install(str(skill_dir))

    assert result.target_root == tmp_path / ".msagent" / "skills" / "custom_skill_name"
    assert (result.target_root / "SKILL.md").exists()
    assert result.warnings


@pytest.mark.asyncio
async def test_skill_installer_surfaces_invalid_frontmatter_errors(tmp_path: Path) -> None:
    skill_dir = tmp_path / "broken-skill"
    skill_dir.mkdir(parents=True)
    (skill_dir / "SKILL.md").write_text(
        "---\nname: broken-skill\ndescription: [oops\n---\nbody\n",
        encoding="utf-8",
    )

    installer = SkillInstaller(tmp_path)

    with pytest.raises(SkillInstallError) as exc_info:
        await installer.install(str(skill_dir))

    assert "Invalid frontmatter" in str(exc_info.value)


@pytest.mark.asyncio
async def test_command_dispatcher_routes_add_skill_command(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    session = _build_session(tmp_path)
    session.message_dispatcher = SimpleNamespace(dispatch=AsyncMock())
    dispatcher = CommandDispatcher(session)
    handle_mock = AsyncMock()

    monkeypatch.setattr(dispatcher.add_skill_handler, "handle", handle_mock)
    monkeypatch.setattr(add_skill_module.console, "print_error", lambda *_args: None)
    monkeypatch.setattr(add_skill_module.console, "print", lambda *_args, **_kwargs: None)

    await dispatcher.dispatch('/add-skill "/tmp/custom skill"')

    handle_mock.assert_awaited_once_with(["/tmp/custom skill"])
    assert "/add-skill" in dispatcher.commands


@pytest.mark.asyncio
async def test_add_skill_handler_reports_usage_when_no_args(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    errors: list[str] = []
    monkeypatch.setattr(add_skill_module.console, "print_error", errors.append)
    monkeypatch.setattr(add_skill_module.console, "print", lambda *_args, **_kwargs: None)

    session = _build_session(tmp_path)
    handler = AddSkillHandler(session)
    await handler.handle([])

    assert any("Usage: /add-skill" in e for e in errors)


@pytest.mark.asyncio
async def test_add_skill_handler_handles_skill_install_error(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    errors: list[str] = []
    monkeypatch.setattr(add_skill_module.console, "print_error", errors.append)
    monkeypatch.setattr(add_skill_module.console, "print", lambda *_args, **_kwargs: None)

    session = _build_session(tmp_path)
    handler = AddSkillHandler(session)

    async def fake_load_agent_config(_agent, _working_dir):
        return SimpleNamespace(name="Profiler")

    async def fake_install(_self, _path):
        raise SkillInstallError("Skill conflicts with existing one")

    monkeypatch.setattr(add_skill_module.initializer, "load_agent_config", fake_load_agent_config)
    monkeypatch.setattr(SkillInstaller, "install", fake_install)

    await handler.handle(["/tmp/skill"])

    assert any("Skill conflicts with existing one" in e for e in errors)


@pytest.mark.asyncio
async def test_add_skill_handler_handles_generic_exception(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    errors: list[str] = []
    monkeypatch.setattr(add_skill_module.console, "print_error", errors.append)
    monkeypatch.setattr(add_skill_module.console, "print", lambda *_args, **_kwargs: None)

    session = _build_session(tmp_path)
    handler = AddSkillHandler(session)

    async def fake_load_agent_config(_agent, _working_dir):
        raise RuntimeError("unexpected failure")

    monkeypatch.setattr(add_skill_module.initializer, "load_agent_config", fake_load_agent_config)

    await handler.handle(["/tmp/skill"])

    assert any("Error installing skill" in e for e in errors)