"""Tests for the installer module."""

from unittest.mock import patch

import pytest

from skilz.errors import InstallError
from skilz.installer import copy_skill_files, install_skill
from skilz.registry import SkillInfo


class TestCopySkillFiles:
    """Tests for copy_skill_files function."""

    def test_copy_files_success(self, temp_dir):
        """Test successful file copy."""
        source = temp_dir / "source"
        source.mkdir()
        (source / "SKILL.md").write_text("# Test Skill")
        (source / "subdir").mkdir()
        (source / "subdir" / "file.txt").write_text("content")

        target = temp_dir / "target"

        copy_skill_files(source, target, verbose=False)

        assert target.exists()
        assert (target / "SKILL.md").exists()
        assert (target / "SKILL.md").read_text() == "# Test Skill"
        assert (target / "subdir" / "file.txt").exists()
        assert (target / "subdir" / "file.txt").read_text() == "content"

    def test_copy_source_not_exists(self, temp_dir):
        """Test error when source doesn't exist."""
        source = temp_dir / "nonexistent"
        target = temp_dir / "target"

        with pytest.raises(InstallError) as exc:
            copy_skill_files(source, target)

        assert "does not exist" in str(exc.value)

    def test_copy_source_not_directory(self, temp_dir):
        """Test error when source is a file."""
        source = temp_dir / "source_file"
        source.write_text("not a directory")
        target = temp_dir / "target"

        with pytest.raises(InstallError) as exc:
            copy_skill_files(source, target)

        assert "not a directory" in str(exc.value)

    def test_copy_replaces_existing(self, temp_dir):
        """Test that existing target is replaced."""
        source = temp_dir / "source"
        source.mkdir()
        (source / "new.txt").write_text("new content")

        target = temp_dir / "target"
        target.mkdir()
        (target / "old.txt").write_text("old content")

        copy_skill_files(source, target, verbose=False)

        assert (target / "new.txt").exists()
        assert (target / "new.txt").read_text() == "new content"
        assert not (target / "old.txt").exists()

    def test_copy_verbose_output(self, temp_dir, capsys):
        """Test verbose output during copy."""
        source = temp_dir / "source"
        source.mkdir()
        (source / "SKILL.md").write_text("# Test")

        target = temp_dir / "target"

        copy_skill_files(source, target, verbose=True)

        captured = capsys.readouterr()
        assert "Copying" in captured.out

    def test_copy_verbose_removes_existing(self, temp_dir, capsys):
        """Test verbose output when removing existing target."""
        source = temp_dir / "source"
        source.mkdir()
        (source / "SKILL.md").write_text("# Test")

        target = temp_dir / "target"
        target.mkdir()
        (target / "old.txt").write_text("old")

        copy_skill_files(source, target, verbose=True)

        captured = capsys.readouterr()
        assert "Removing existing" in captured.out

    def test_copy_creates_parent_directory(self, temp_dir):
        """Test that parent directories are created."""
        source = temp_dir / "source"
        source.mkdir()
        (source / "SKILL.md").write_text("# Test")

        target = temp_dir / "deep" / "nested" / "target"

        copy_skill_files(source, target, verbose=False)

        assert target.exists()
        assert (target / "SKILL.md").exists()


class TestInstallSkill:
    """Tests for install_skill function."""

    @pytest.fixture
    def mock_skill_info(self):
        """Create a mock SkillInfo for testing."""
        return SkillInfo(
            skill_id="test/skill",
            git_repo="https://github.com/test/repo.git",
            skill_path="/main/skills/test-skill",
            git_sha="abc123def456789012345678901234567890abcd",
        )

    @pytest.fixture
    def mock_dependencies(self, temp_dir, mock_skill_info):
        """Set up common mocks for install_skill."""
        cache_path = temp_dir / "cache" / "repo"
        cache_path.mkdir(parents=True)

        source_path = cache_path / "skills" / "test-skill"
        source_path.mkdir(parents=True)
        (source_path / "SKILL.md").write_text("# Test Skill")

        skills_dir = temp_dir / ".claude" / "skills"
        skills_dir.mkdir(parents=True)

        return {
            "cache_path": cache_path,
            "source_path": source_path,
            "skills_dir": skills_dir,
            "skill_info": mock_skill_info,
        }

    def test_install_new_skill(self, mock_dependencies):
        """Test installing a new skill."""
        deps = mock_dependencies

        with (
            patch("skilz.installer.detect_agent", return_value="claude"),
            patch("skilz.installer.get_agent_display_name", return_value="Claude Code"),
            patch("skilz.installer.lookup_skill", return_value=deps["skill_info"]),
            patch("skilz.installer.ensure_skills_dir", return_value=deps["skills_dir"]),
            patch("skilz.installer.needs_install", return_value=(True, "not_installed")),
            patch("skilz.installer.clone_or_fetch", return_value=deps["cache_path"]),
            patch("skilz.installer.parse_skill_path", return_value=("main", "skills/test-skill")),
            patch("skilz.installer.checkout_sha"),
            patch("skilz.installer.get_skill_source_path", return_value=deps["source_path"]),
            patch("skilz.installer.write_manifest"),
        ):
            install_skill("test/skill", project_level=True)

    def test_install_already_installed(self, mock_dependencies, capsys):
        """Test when skill is already installed."""
        deps = mock_dependencies

        with (
            patch("skilz.installer.detect_agent", return_value="claude"),
            patch("skilz.installer.get_agent_display_name", return_value="Claude Code"),
            patch("skilz.installer.lookup_skill", return_value=deps["skill_info"]),
            patch("skilz.installer.ensure_skills_dir", return_value=deps["skills_dir"]),
            patch("skilz.installer.needs_install", return_value=(False, "up_to_date")),
        ):
            install_skill("test/skill", project_level=True)

        captured = capsys.readouterr()
        assert "Already installed" in captured.out

    def test_install_update_existing(self, mock_dependencies, capsys):
        """Test updating an existing skill."""
        deps = mock_dependencies

        with (
            patch("skilz.installer.detect_agent", return_value="claude"),
            patch("skilz.installer.get_agent_display_name", return_value="Claude Code"),
            patch("skilz.installer.lookup_skill", return_value=deps["skill_info"]),
            patch("skilz.installer.ensure_skills_dir", return_value=deps["skills_dir"]),
            patch("skilz.installer.needs_install", return_value=(True, "sha_mismatch")),
            patch("skilz.installer.clone_or_fetch", return_value=deps["cache_path"]),
            patch("skilz.installer.parse_skill_path", return_value=("main", "skills/test-skill")),
            patch("skilz.installer.checkout_sha"),
            patch("skilz.installer.get_skill_source_path", return_value=deps["source_path"]),
            patch("skilz.installer.write_manifest"),
        ):
            install_skill("test/skill", project_level=True)

        captured = capsys.readouterr()
        assert "Updated" in captured.out

    def test_install_source_not_found(self, temp_dir, mock_dependencies):
        """Test error when skill source path doesn't exist."""
        deps = mock_dependencies
        nonexistent = temp_dir / "nonexistent"

        with (
            patch("skilz.installer.detect_agent", return_value="claude"),
            patch("skilz.installer.get_agent_display_name", return_value="Claude Code"),
            patch("skilz.installer.lookup_skill", return_value=deps["skill_info"]),
            patch("skilz.installer.ensure_skills_dir", return_value=deps["skills_dir"]),
            patch("skilz.installer.needs_install", return_value=(True, "not_installed")),
            patch("skilz.installer.clone_or_fetch", return_value=deps["cache_path"]),
            patch("skilz.installer.parse_skill_path", return_value=("main", "skills/test-skill")),
            patch("skilz.installer.checkout_sha"),
            patch("skilz.installer.get_skill_source_path", return_value=nonexistent),
        ):
            with pytest.raises(InstallError) as exc:
                install_skill("test/skill", project_level=True)

            assert "not found in repository" in str(exc.value)

    def test_install_with_explicit_agent(self, mock_dependencies, capsys):
        """Test installing with explicitly specified agent."""
        deps = mock_dependencies

        with (
            patch("skilz.installer.get_agent_display_name", return_value="OpenCode"),
            patch("skilz.installer.lookup_skill", return_value=deps["skill_info"]),
            patch("skilz.installer.ensure_skills_dir", return_value=deps["skills_dir"]),
            patch("skilz.installer.needs_install", return_value=(True, "not_installed")),
            patch("skilz.installer.clone_or_fetch", return_value=deps["cache_path"]),
            patch("skilz.installer.parse_skill_path", return_value=("main", "skills/test-skill")),
            patch("skilz.installer.checkout_sha"),
            patch("skilz.installer.get_skill_source_path", return_value=deps["source_path"]),
            patch("skilz.installer.write_manifest"),
        ):
            # Note: detect_agent is NOT called because agent is explicit
            install_skill("test/skill", agent="opencode", project_level=True)

        captured = capsys.readouterr()
        assert "Installed" in captured.out

    def test_install_verbose_output(self, mock_dependencies, capsys):
        """Test verbose output during installation."""
        deps = mock_dependencies

        with (
            patch("skilz.installer.detect_agent", return_value="claude"),
            patch("skilz.installer.get_agent_display_name", return_value="Claude Code"),
            patch("skilz.installer.lookup_skill", return_value=deps["skill_info"]),
            patch("skilz.installer.ensure_skills_dir", return_value=deps["skills_dir"]),
            patch("skilz.installer.needs_install", return_value=(True, "not_installed")),
            patch("skilz.installer.clone_or_fetch", return_value=deps["cache_path"]),
            patch("skilz.installer.parse_skill_path", return_value=("main", "skills/test-skill")),
            patch("skilz.installer.checkout_sha"),
            patch("skilz.installer.get_skill_source_path", return_value=deps["source_path"]),
            patch("skilz.installer.write_manifest"),
        ):
            install_skill("test/skill", project_level=True, verbose=True)

        captured = capsys.readouterr()
        assert "Auto-detected agent" in captured.out
        assert "Looking up skill" in captured.out

    def test_install_verbose_sha_mismatch(self, mock_dependencies, capsys):
        """Test verbose output for SHA mismatch update."""
        deps = mock_dependencies

        with (
            patch("skilz.installer.detect_agent", return_value="claude"),
            patch("skilz.installer.get_agent_display_name", return_value="Claude Code"),
            patch("skilz.installer.lookup_skill", return_value=deps["skill_info"]),
            patch("skilz.installer.ensure_skills_dir", return_value=deps["skills_dir"]),
            patch("skilz.installer.needs_install", return_value=(True, "sha_mismatch")),
            patch("skilz.installer.clone_or_fetch", return_value=deps["cache_path"]),
            patch("skilz.installer.parse_skill_path", return_value=("main", "skills/test-skill")),
            patch("skilz.installer.checkout_sha"),
            patch("skilz.installer.get_skill_source_path", return_value=deps["source_path"]),
            patch("skilz.installer.write_manifest"),
        ):
            install_skill("test/skill", project_level=True, verbose=True)

        captured = capsys.readouterr()
        assert "Updating" in captured.out or "SHA changed" in captured.out

    def test_install_verbose_no_manifest(self, mock_dependencies, capsys):
        """Test verbose output for no manifest reinstall."""
        deps = mock_dependencies

        with (
            patch("skilz.installer.detect_agent", return_value="claude"),
            patch("skilz.installer.get_agent_display_name", return_value="Claude Code"),
            patch("skilz.installer.lookup_skill", return_value=deps["skill_info"]),
            patch("skilz.installer.ensure_skills_dir", return_value=deps["skills_dir"]),
            patch("skilz.installer.needs_install", return_value=(True, "no_manifest")),
            patch("skilz.installer.clone_or_fetch", return_value=deps["cache_path"]),
            patch("skilz.installer.parse_skill_path", return_value=("main", "skills/test-skill")),
            patch("skilz.installer.checkout_sha"),
            patch("skilz.installer.get_skill_source_path", return_value=deps["source_path"]),
            patch("skilz.installer.write_manifest"),
        ):
            install_skill("test/skill", project_level=True, verbose=True)

        captured = capsys.readouterr()
        assert "Reinstalling" in captured.out or "no manifest" in captured.out

    def test_install_user_level(self, mock_dependencies, capsys):
        """Test user-level installation."""
        deps = mock_dependencies

        with (
            patch("skilz.installer.detect_agent", return_value="claude"),
            patch("skilz.installer.get_agent_display_name", return_value="Claude Code"),
            patch("skilz.installer.lookup_skill", return_value=deps["skill_info"]),
            patch("skilz.installer.ensure_skills_dir", return_value=deps["skills_dir"]),
            patch("skilz.installer.needs_install", return_value=(True, "not_installed")),
            patch("skilz.installer.clone_or_fetch", return_value=deps["cache_path"]),
            patch("skilz.installer.parse_skill_path", return_value=("main", "skills/test-skill")),
            patch("skilz.installer.checkout_sha"),
            patch("skilz.installer.get_skill_source_path", return_value=deps["source_path"]),
            patch("skilz.installer.write_manifest"),
        ):
            install_skill("test/skill", project_level=False)

        captured = capsys.readouterr()
        assert "user" in captured.out

    def test_install_writes_manifest(self, mock_dependencies):
        """Test that manifest is written after installation."""
        deps = mock_dependencies

        with (
            patch("skilz.installer.detect_agent", return_value="claude"),
            patch("skilz.installer.get_agent_display_name", return_value="Claude Code"),
            patch("skilz.installer.lookup_skill", return_value=deps["skill_info"]),
            patch("skilz.installer.ensure_skills_dir", return_value=deps["skills_dir"]),
            patch("skilz.installer.needs_install", return_value=(True, "not_installed")),
            patch("skilz.installer.clone_or_fetch", return_value=deps["cache_path"]),
            patch("skilz.installer.parse_skill_path", return_value=("main", "skills/test-skill")),
            patch("skilz.installer.checkout_sha"),
            patch("skilz.installer.get_skill_source_path", return_value=deps["source_path"]),
            patch("skilz.installer.write_manifest") as mock_write,
        ):
            install_skill("test/skill", project_level=True)

            mock_write.assert_called_once()
            args = mock_write.call_args
            manifest = args[0][1]
            assert manifest.skill_id == "test/skill"
            assert manifest.git_sha == deps["skill_info"].git_sha
