"""Unit tests for PyPI publishing."""

from __future__ import annotations

import subprocess
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

import pytest

from release_py.config.models import PublishConfig
from release_py.exceptions import BuildError, PublishError, UploadError
from release_py.publish.pypi import (
    build_package,
    check_pypi_version_exists,
    publish_package,
)

if TYPE_CHECKING:
    from pathlib import Path


class TestBuildPackage:
    """Tests for package building."""

    def test_build_with_uv(self, tmp_path: Path):
        """Build package using uv."""
        # Create dist directory with mock files
        dist_dir = tmp_path / "dist"
        dist_dir.mkdir()
        (dist_dir / "package-1.0.0.whl").touch()
        (dist_dir / "package-1.0.0.tar.gz").touch()

        with patch("shutil.which") as mock_which:
            mock_which.return_value = "/usr/bin/uv"
            with patch("subprocess.run") as mock_run:
                mock_run.return_value = MagicMock(returncode=0)

                result = build_package(tmp_path, clean=False)

                assert len(result) == 2
                mock_run.assert_called_once()
                assert "uv" in mock_run.call_args[0][0]

    def test_build_with_hatch_fallback(self, tmp_path: Path):
        """Fall back to hatch when uv not available."""
        dist_dir = tmp_path / "dist"
        dist_dir.mkdir()
        (dist_dir / "package-1.0.0.whl").touch()

        def which_side_effect(cmd: str) -> str | None:
            if cmd == "uv":
                return None
            if cmd == "hatch":
                return "/usr/bin/hatch"
            return None

        with patch("shutil.which", side_effect=which_side_effect):
            with patch("subprocess.run") as mock_run:
                mock_run.return_value = MagicMock(returncode=0)

                result = build_package(tmp_path, clean=False)

                assert len(result) == 1
                assert "hatch" in mock_run.call_args[0][0]

    def test_build_with_python_build_fallback(self, tmp_path: Path):
        """Fall back to python -m build."""
        dist_dir = tmp_path / "dist"
        dist_dir.mkdir()
        (dist_dir / "package-1.0.0.whl").touch()

        def which_side_effect(cmd: str) -> str | None:
            if cmd == "python":
                return "/usr/bin/python"
            return None

        with patch("shutil.which", side_effect=which_side_effect):
            with patch("subprocess.run") as mock_run:
                mock_run.return_value = MagicMock(returncode=0)

                result = build_package(tmp_path, clean=False)

                assert len(result) == 1
                call_args = mock_run.call_args[0][0]
                assert "python" in call_args
                assert "-m" in call_args
                assert "build" in call_args

    def test_build_no_tool_raises(self, tmp_path: Path):
        """Raise BuildError when no build tool found."""
        with patch("shutil.which", return_value=None):
            with pytest.raises(BuildError, match="No build tool found"):
                build_package(tmp_path)

    def test_build_cleans_dist(self, tmp_path: Path):
        """Clean dist directory before building."""
        dist_dir = tmp_path / "dist"
        dist_dir.mkdir()
        old_file = dist_dir / "old-package-0.1.0.whl"
        old_file.touch()

        with patch("shutil.which", return_value="/usr/bin/uv"):
            with patch("subprocess.run") as mock_run:
                mock_run.return_value = MagicMock(returncode=0)

                # This should clean and then fail because no files created
                with pytest.raises(BuildError, match="dist/ directory not found"):
                    build_package(tmp_path, clean=True)

                # Old file should be gone (directory removed)
                assert not old_file.exists()

    def test_build_failure_raises(self, tmp_path: Path):
        """Build failure raises BuildError."""
        with patch("shutil.which", return_value="/usr/bin/uv"):
            with patch("subprocess.run") as mock_run:
                mock_run.side_effect = subprocess.CalledProcessError(
                    1, "uv build", stderr="Build failed: missing setup.py"
                )

                with pytest.raises(BuildError, match="Build with uv failed"):
                    build_package(tmp_path)

    def test_build_no_dist_files_raises(self, tmp_path: Path):
        """Raise BuildError when no distribution files found."""
        dist_dir = tmp_path / "dist"
        dist_dir.mkdir()
        # Empty dist directory

        with patch("shutil.which", return_value="/usr/bin/uv"):
            with patch("subprocess.run") as mock_run:
                mock_run.return_value = MagicMock(returncode=0)

                with pytest.raises(BuildError, match="no distribution files found"):
                    build_package(tmp_path, clean=False)


class TestPublishPackage:
    """Tests for package publishing."""

    def test_publish_disabled(self, tmp_path: Path):
        """Skip publishing when disabled."""
        config = PublishConfig(enabled=False)

        # Should not raise or call anything
        publish_package(tmp_path, config)

    def test_publish_with_uv(self, tmp_path: Path):
        """Publish using uv."""
        dist_dir = tmp_path / "dist"
        dist_dir.mkdir()
        whl = dist_dir / "package-1.0.0.whl"
        whl.touch()

        config = PublishConfig(enabled=True, tool="uv")

        with patch("shutil.which", return_value="/usr/bin/uv"):
            with patch("subprocess.run") as mock_run:
                mock_run.return_value = MagicMock(returncode=0)

                publish_package(tmp_path, config, dist_files=[whl])

                mock_run.assert_called_once()
                call_args = mock_run.call_args[0][0]
                assert "uv" in call_args
                assert "publish" in call_args

    def test_publish_with_twine(self, tmp_path: Path):
        """Publish using twine."""
        dist_dir = tmp_path / "dist"
        dist_dir.mkdir()
        whl = dist_dir / "package-1.0.0.whl"
        whl.touch()

        config = PublishConfig(enabled=True, tool="twine")

        with patch("shutil.which", return_value="/usr/bin/twine"):
            with patch("subprocess.run") as mock_run:
                mock_run.return_value = MagicMock(returncode=0)

                publish_package(tmp_path, config, dist_files=[whl])

                mock_run.assert_called_once()
                call_args = mock_run.call_args[0][0]
                assert "twine" in call_args
                assert "upload" in call_args

    def test_publish_custom_registry(self, tmp_path: Path):
        """Publish to custom registry."""
        dist_dir = tmp_path / "dist"
        dist_dir.mkdir()
        whl = dist_dir / "package-1.0.0.whl"
        whl.touch()

        config = PublishConfig(
            enabled=True,
            tool="uv",
            registry="https://test.pypi.org/legacy/",
        )

        with patch("shutil.which", return_value="/usr/bin/uv"):
            with patch("subprocess.run") as mock_run:
                mock_run.return_value = MagicMock(returncode=0)

                publish_package(tmp_path, config, dist_files=[whl])

                call_args = mock_run.call_args[0][0]
                assert "--publish-url" in call_args
                assert "https://test.pypi.org/legacy/" in call_args

    def test_publish_no_files_raises(self, tmp_path: Path):
        """Raise PublishError when no files to publish."""
        dist_dir = tmp_path / "dist"
        dist_dir.mkdir()  # Empty

        config = PublishConfig(enabled=True, tool="uv")

        with pytest.raises(PublishError, match="No distribution files"):
            publish_package(tmp_path, config)

    def test_publish_uv_not_found_raises(self, tmp_path: Path):
        """Raise PublishError when uv not found."""
        dist_dir = tmp_path / "dist"
        dist_dir.mkdir()
        whl = dist_dir / "package-1.0.0.whl"
        whl.touch()

        config = PublishConfig(enabled=True, tool="uv")

        with patch("shutil.which", return_value=None):
            with pytest.raises(PublishError, match="uv not found"):
                publish_package(tmp_path, config, dist_files=[whl])

    def test_publish_twine_not_found_raises(self, tmp_path: Path):
        """Raise PublishError when twine not found."""
        dist_dir = tmp_path / "dist"
        dist_dir.mkdir()
        whl = dist_dir / "package-1.0.0.whl"
        whl.touch()

        config = PublishConfig(enabled=True, tool="twine")

        with patch("shutil.which", return_value=None):
            with pytest.raises(PublishError, match="twine not found"):
                publish_package(tmp_path, config, dist_files=[whl])

    def test_publish_version_exists_raises(self, tmp_path: Path):
        """Raise UploadError when version already published."""
        dist_dir = tmp_path / "dist"
        dist_dir.mkdir()
        whl = dist_dir / "package-1.0.0.whl"
        whl.touch()

        config = PublishConfig(enabled=True, tool="uv")

        with patch("shutil.which", return_value="/usr/bin/uv"):
            with patch("subprocess.run") as mock_run:
                mock_run.side_effect = subprocess.CalledProcessError(
                    1, "uv publish", stderr="File already exists"
                )

                with pytest.raises(UploadError, match="already been published"):
                    publish_package(tmp_path, config, dist_files=[whl])


class TestCheckPypiVersionExists:
    """Tests for PyPI version checking."""

    def test_version_exists(self):
        """Return True when version exists on PyPI."""
        with patch("httpx.get") as mock_get:
            mock_get.return_value = MagicMock(status_code=200)

            result = check_pypi_version_exists("requests", "2.28.0")

            assert result is True
            mock_get.assert_called_once_with(
                "https://pypi.org/pypi/requests/2.28.0/json",
                timeout=10,
            )

    def test_version_not_exists(self):
        """Return False when version doesn't exist."""
        with patch("httpx.get") as mock_get:
            mock_get.return_value = MagicMock(status_code=404)

            result = check_pypi_version_exists("requests", "99.99.99")

            assert result is False

    def test_http_error_returns_false(self):
        """Return False on HTTP errors."""
        import httpx

        with patch("httpx.get") as mock_get:
            mock_get.side_effect = httpx.HTTPError("Connection failed")

            result = check_pypi_version_exists("requests", "2.28.0")

            assert result is False

    def test_timeout_returns_false(self):
        """Return False on timeout."""
        import httpx

        with patch("httpx.get") as mock_get:
            mock_get.side_effect = httpx.TimeoutException("Timeout")

            result = check_pypi_version_exists("requests", "2.28.0")

            assert result is False
