"""Tests for package management utilities."""

import json
import subprocess
import sys
from pathlib import Path
from unittest.mock import patch, MagicMock

import pytest

from scry_run.packages import (
    ensure_venv,
    install_packages,
    get_installed_packages,
    get_venv_python,
)


class TestGetVenvPython:
    """Tests for get_venv_python function."""

    def test_returns_unix_path_on_unix(self, tmp_path: Path) -> None:
        """Should return bin/python on Unix systems."""
        app_dir = tmp_path / "myapp"
        result = get_venv_python(app_dir)
        if sys.platform == "win32":
            assert result == app_dir / ".venv" / "Scripts" / "python.exe"
        else:
            assert result == app_dir / ".venv" / "bin" / "python"


class TestEnsureVenv:
    """Tests for ensure_venv function."""

    def test_creates_venv_if_not_exists(self, tmp_path: Path) -> None:
        """Should create .venv directory using uv."""
        app_dir = tmp_path / "myapp"
        app_dir.mkdir()

        with patch("scry_run.packages.subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            ensure_venv(app_dir)

        mock_run.assert_called_once()
        call_args = mock_run.call_args
        assert call_args[0][0] == ["uv", "venv"]
        assert call_args[1]["cwd"] == app_dir

    def test_skips_if_venv_exists(self, tmp_path: Path) -> None:
        """Should not create venv if .venv already exists."""
        app_dir = tmp_path / "myapp"
        app_dir.mkdir()
        (app_dir / ".venv").mkdir()

        with patch("scry_run.packages.subprocess.run") as mock_run:
            ensure_venv(app_dir)

        mock_run.assert_not_called()

    def test_ensure_venv_raises_on_failure(self, tmp_path: Path) -> None:
        """Should raise RuntimeError when uv venv fails."""
        app_dir = tmp_path / "myapp"
        app_dir.mkdir()

        with patch("scry_run.packages.subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=1, stderr="some error")
            with pytest.raises(RuntimeError):
                ensure_venv(app_dir)


class TestInstallPackages:
    """Tests for install_packages function."""

    def test_installs_packages_with_uv(self, tmp_path: Path) -> None:
        """Should run uv pip install with packages targeting app's venv."""
        app_dir = tmp_path / "myapp"
        app_dir.mkdir()
        (app_dir / ".venv").mkdir()

        with patch("scry_run.packages.subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            install_packages(app_dir, ["pygame", "requests>=2.0"])

        mock_run.assert_called_once()
        call_args = mock_run.call_args
        cmd = call_args[0][0]
        assert cmd[:3] == ["uv", "pip", "install"]
        assert "--python" in cmd
        python_idx = cmd.index("--python")
        # Check for platform-appropriate path
        if sys.platform == "win32":
            assert ".venv\\Scripts\\python.exe" in cmd[python_idx + 1] or ".venv/Scripts/python.exe" in cmd[python_idx + 1]
        else:
            assert ".venv/bin/python" in cmd[python_idx + 1]
        assert "pygame" in cmd
        assert "requests>=2.0" in cmd

    def test_skips_empty_packages_list(self, tmp_path: Path) -> None:
        """Should not run uv if packages list is empty."""
        app_dir = tmp_path / "myapp"
        app_dir.mkdir()

        with patch("scry_run.packages.subprocess.run") as mock_run:
            install_packages(app_dir, [])

        mock_run.assert_not_called()

    def test_creates_venv_if_missing(self, tmp_path: Path) -> None:
        """Should create venv before installing if missing."""
        app_dir = tmp_path / "myapp"
        app_dir.mkdir()

        with patch("scry_run.packages.subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            install_packages(app_dir, ["pygame"])

        # Should have called twice: once for venv, once for install
        assert mock_run.call_count == 2

    def test_install_packages_raises_on_failure(self, tmp_path: Path) -> None:
        """Should raise RuntimeError when uv pip install fails."""
        app_dir = tmp_path / "myapp"
        app_dir.mkdir()
        (app_dir / ".venv").mkdir()

        with patch("scry_run.packages.subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=1, stderr="install error")
            with pytest.raises(RuntimeError):
                install_packages(app_dir, ["pygame"])


class TestGetInstalledPackages:
    """Tests for get_installed_packages function."""

    def test_returns_installed_packages(self, tmp_path: Path) -> None:
        """Should parse uv pip list output from app's venv."""
        app_dir = tmp_path / "myapp"
        app_dir.mkdir()
        (app_dir / ".venv").mkdir()

        mock_output = "pygame==2.5.0\nrequests==2.31.0\npip==23.0\n"

        with patch("scry_run.packages.subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(
                returncode=0,
                stdout=mock_output,
            )
            packages = get_installed_packages(app_dir)

        # Verify --python flag targets app's venv
        call_args = mock_run.call_args
        cmd = call_args[0][0]
        assert "--python" in cmd
        assert packages == ["pygame==2.5.0", "requests==2.31.0", "pip==23.0"]

    def test_returns_empty_if_no_venv(self, tmp_path: Path) -> None:
        """Should return empty list if no venv exists."""
        app_dir = tmp_path / "myapp"
        app_dir.mkdir()

        packages = get_installed_packages(app_dir)

        assert packages == []


class TestGetScryRunSourcePath:
    """Tests for get_scry_run_source_path function."""

    def test_returns_path_in_dev_mode(self) -> None:
        """Should return source path when running from source."""
        from scry_run.packages import get_scry_run_source_path
        # When running tests, we're in dev mode
        result = get_scry_run_source_path()
        assert result is not None
        assert (result / "pyproject.toml").exists()
        assert (result / "src" / "scry_run").exists()


class TestIsScryRunInstalled:
    """Tests for is_scry_run_installed function."""

    def test_returns_false_if_no_venv(self, tmp_path: Path) -> None:
        """Should return False if venv doesn't exist."""
        from scry_run.packages import is_scry_run_installed
        app_dir = tmp_path / "myapp"
        app_dir.mkdir()
        assert is_scry_run_installed(app_dir) is False

    def test_returns_false_if_not_installed(self, tmp_path: Path) -> None:
        """Should return False if scry-run not in venv."""
        from scry_run.packages import is_scry_run_installed
        app_dir = tmp_path / "myapp"
        app_dir.mkdir()
        # Create a real venv without scry-run
        import subprocess
        subprocess.run(["uv", "venv"], cwd=app_dir, capture_output=True)
        assert is_scry_run_installed(app_dir) is False


class TestEnsureScryRunInstalled:
    """Tests for ensure_scry_run_installed function."""

    def test_creates_venv_and_installs(self, tmp_path: Path) -> None:
        """Should create venv and install scry-run."""
        from scry_run.packages import ensure_scry_run_installed, is_scry_run_installed
        app_dir = tmp_path / "myapp"
        app_dir.mkdir()

        ensure_scry_run_installed(app_dir)

        assert (app_dir / ".venv").exists()
        assert is_scry_run_installed(app_dir) is True

    def test_skips_if_already_installed(self, tmp_path: Path) -> None:
        """Should not reinstall if already present."""
        from scry_run.packages import ensure_scry_run_installed, is_scry_run_installed
        app_dir = tmp_path / "myapp"
        app_dir.mkdir()

        # Install once
        ensure_scry_run_installed(app_dir)
        assert is_scry_run_installed(app_dir) is True

        # Second call should skip install (we can't easily verify no-op, but it shouldn't error)
        ensure_scry_run_installed(app_dir)
        assert is_scry_run_installed(app_dir) is True
