"""Unit tests for MakePython module."""

from __future__ import annotations

import logging
import subprocess
import tempfile
from pathlib import Path
from unittest.mock import Mock, patch

import pytest

from sfi.makepython.makepython import (
    BuildTool,
    CommandType,
    MakePythonConfig,
    SubprocessExecutor,
    _activate_py_env,
    _bump_version,
    _clean,
    _generate_coverage_report,
    _generate_distribution,
    _get_build_command,
    _get_project_name,
    _run_command,
    _write_to_env_file,
)
from sfi.pyprojectparse.models.project import Project


class TestEnums:
    """Test enumeration classes."""

    def test_build_tool_values(self) -> None:
        """Test BuildTool enum values."""
        assert BuildTool.UV.value == "uv"
        assert BuildTool.POETRY.value == "poetry"
        assert BuildTool.HATCH.value == "hatch"

    def test_command_type_values(self) -> None:
        """Test CommandType enum values."""
        assert CommandType.BUILD.value == "build"
        assert CommandType.CLEAN.value == "clean"
        assert CommandType.TEST.value == "test"
        assert CommandType.PUBLISH.value == "publish"
        assert CommandType.BUMP_VERSION.value == "bumpversion"
        assert CommandType.TOKEN.value == "token"


class TestProjectParsing:
    """Test Project parsing from pyproject.toml."""

    def test_from_toml_file_with_valid_file(self) -> None:
        """Test creating Project from valid pyproject.toml."""
        with tempfile.TemporaryDirectory() as temp_dir:
            project_dir = Path(temp_dir)
            pyproject_path = project_dir / "pyproject.toml"

            pyproject_content = """
[project]
name = "test-project"
version = "1.0.0"

[build-system]
build-backend = "hatchling.build"
"""
            pyproject_path.write_text(pyproject_content)

            project = Project.from_toml_file(pyproject_path)

            assert project is not None
            assert project.name == "test-project"
            assert project.version == "1.0.0"
            assert project.build_backend == "hatchling.build"

    def test_from_toml_file_without_build_system(self) -> None:
        """Test Project from pyproject.toml without build-system."""
        with tempfile.TemporaryDirectory() as temp_dir:
            project_dir = Path(temp_dir)
            pyproject_path = project_dir / "pyproject.toml"

            pyproject_content = """
[project]
name = "test-project"
version = "1.0.0"
"""
            pyproject_path.write_text(pyproject_content)

            project = Project.from_toml_file(pyproject_path)

            assert project is not None
            assert project.name == "test-project"
            assert project.version == "1.0.0"
            assert project.build_backend == ""

    def test_from_toml_file_nonexistent_file(self) -> None:
        """Test Project from non-existent pyproject.toml."""
        with tempfile.TemporaryDirectory() as temp_dir:
            project_dir = Path(temp_dir)
            pyproject_path = project_dir / "pyproject.toml"
            project = Project.from_toml_file(pyproject_path)
            # Should return empty Project when file doesn't exist
            assert project is not None
            assert project.name == ""

    def test_from_toml_file_invalid_toml(self) -> None:
        """Test Project from invalid TOML file."""
        with tempfile.TemporaryDirectory() as temp_dir:
            project_dir = Path(temp_dir)
            pyproject_path = project_dir / "pyproject.toml"
            pyproject_path.write_text("invalid [ toml content")

            project = Project.from_toml_file(pyproject_path)
            # Should return empty Project when parsing fails
            assert project is not None
            assert project.name == ""


class TestMakePythonConfig:
    """Test MakePythonConfig dataclass."""

    def test_default_values(self) -> None:
        """Test default configuration values."""
        # Mock CONFIG_FILE to ensure we test default values without local config interference
        with tempfile.TemporaryDirectory() as temp_dir:
            mock_config_file = Path(temp_dir) / "nonexistent_config.json"
            with patch("sfi.makepython.makepython.CONFIG_FILE", mock_config_file):
                config = MakePythonConfig()

                assert config.default_build_tool == BuildTool.UV
                assert config.auto_detect_tool is True
                assert config.verbose_output is False
                assert config.max_retries == 3
                assert config.timeout_seconds == 300

    def test_save_and_load_config(self) -> None:
        """Test saving and loading configuration."""
        with tempfile.TemporaryDirectory() as temp_dir:
            config_file = Path(temp_dir) / "makepython.json"

            with patch("sfi.makepython.makepython.CONFIG_FILE", config_file):
                # Create and save config
                config = MakePythonConfig(
                    default_build_tool=BuildTool.POETRY,
                    verbose_output=True,
                    max_retries=5,
                )
                config.save()

                # Load config
                loaded_config = MakePythonConfig()

                assert loaded_config.default_build_tool == BuildTool.POETRY
                assert loaded_config.verbose_output is True
                assert loaded_config.max_retries == 5

    def test_cached_supported_tools(self) -> None:
        """Test cached property for supported tools."""
        config = MakePythonConfig()

        # Mock shutil.which to return available tools
        with patch("shutil.which") as mock_which:
            mock_which.side_effect = lambda x: (
                x in ["uv", "poetry"]
            )  # hatch not available

            supported = config.supported_tools

            assert len(supported) == 2
            assert BuildTool.UV in supported
            assert BuildTool.POETRY in supported
            assert BuildTool.HATCH not in supported


class TestSubprocessExecutor:
    """Test SubprocessExecutor class."""

    def test_execute_successful_command(self) -> None:
        """Test executing a successful command."""
        config = MakePythonConfig()
        executor = SubprocessExecutor(config)

        with patch("subprocess.run") as mock_run:
            mock_result = Mock()
            mock_result.stdout = "success output"
            mock_result.stderr = ""
            mock_run.return_value = mock_result

            result = executor.execute(["echo", "hello"], Path("."))

            assert result == mock_result
            mock_run.assert_called_once_with(
                ["echo", "hello"],
                cwd=Path("."),
                capture_output=True,
                text=True,
                encoding="utf-8",
                errors="replace",
                timeout=300,
                check=True,
            )

    def test_execute_failing_command(self) -> None:
        """Test executing a failing command."""
        config = MakePythonConfig()
        executor = SubprocessExecutor(config)

        with patch("subprocess.run") as mock_run:
            mock_run.side_effect = Exception("Command failed")

            with pytest.raises(Exception):
                executor.execute(["false"], Path("."))


class TestUtilityFunctions:
    """Test utility functions."""

    def test_clean_function(self) -> None:
        """Test _clean function."""
        with tempfile.TemporaryDirectory() as temp_dir:
            test_dir = Path(temp_dir)

            # Create some files to clean
            (test_dir / "dist").mkdir()
            (test_dir / "build").mkdir()
            (test_dir / "test.egg-info").touch()

            # This should not raise an exception even if files don't exist
            _clean(test_dir)

            # Files should still exist (this test just ensures no exceptions)
            assert test_dir.exists()

    def test_write_to_env_file_windows(self) -> None:
        """Test writing environment variables on Windows."""
        with patch("sfi.makepython.makepython.IS_WINDOWS", True):
            with patch("subprocess.run") as mock_run:
                _write_to_env_file("TEST_VAR", "test_value")
                mock_run.assert_called_once()

    def test_write_to_env_file_unix(self) -> None:
        """Test writing environment variables on Unix-like systems."""
        with patch("sfi.makepython.makepython.IS_WINDOWS", False), patch(
            "sfi.makepython.makepython._write_to_shell_config"
        ) as mock_write:
            _write_to_env_file("TEST_VAR", "test_value")
            mock_write.assert_called_once_with("export TEST_VAR='test_value'")

    def test_run_command_success(self) -> None:
        """Test successful command execution."""
        with patch("subprocess.run") as mock_run:
            mock_result = Mock()
            mock_result.stdout = "output"
            mock_result.stderr = ""
            mock_run.return_value = mock_result

            _run_command(["echo", "test"], Path("."))

            # Should print the output
            mock_run.assert_called_once()

    def test_get_build_command_integration(self) -> None:
        """Test getting build command integration."""
        with tempfile.TemporaryDirectory() as temp_dir:
            project_dir = Path(temp_dir)
            pyproject_path = project_dir / "pyproject.toml"

            pyproject_content = """
[project]
name = "test-project"

[build-system]
build-backend = "poetry.core.masonry.api"
"""
            pyproject_path.write_text(pyproject_content)

            config = MakePythonConfig()
            result = _get_build_command(project_dir, config)
            assert result == "poetry"

    def test_get_build_command_with_config(self) -> None:
        """Test getting build command with configuration."""
        with tempfile.TemporaryDirectory() as temp_dir:
            project_dir = Path(temp_dir)

            config = MakePythonConfig()

            # Mock supported tools
            with patch.object(config, "supported_tools", [BuildTool.UV]):
                # Since there's no pyproject.toml, it should fall back to available tools
                result = _get_build_command(project_dir, config)
                assert result == "uv"


class TestExceptionHandling:
    """Test exception handling and error scenarios."""

    def test_build_tool_not_found_error(self) -> None:
        """Test BuildToolNotFoundError exception."""
        from sfi.makepython.makepython import BuildToolNotFoundError

        error = BuildToolNotFoundError("Test error message")
        assert error.message == "Test error message"
        assert error.error_code == 1
        assert str(error) == "Test error message"

    def test_configuration_error(self) -> None:
        """Test ConfigurationError exception."""
        from sfi.makepython.makepython import ConfigurationError

        error = ConfigurationError("Config error", error_code=2)
        assert error.message == "Config error"
        assert error.error_code == 2

    def test_command_execution_error(self) -> None:
        """Test CommandExecutionError exception."""
        from sfi.makepython.makepython import CommandExecutionError

        error = CommandExecutionError(
            "Command failed", return_code=127, stdout="test output", stderr="test error"
        )
        assert error.message == "Command failed"
        assert error.error_code == 127
        assert error.stdout == "test output"
        assert error.stderr == "test error"

    def test_user_facing_error(self) -> None:
        """Test UserFacingError exception."""
        from sfi.makepython.makepython import UserFacingError

        error = UserFacingError("User error", suggestion="Try this solution")
        assert error.message == "User error"
        assert error.suggestion == "Try this solution"
        assert error.error_code == 1


class TestPerformanceMonitor:
    """Test PerformanceMonitor functionality."""

    def test_performance_monitor_basic_operations(self) -> None:
        """Test basic performance monitoring operations."""
        from sfi.makepython.makepython import PerformanceMonitor

        monitor = PerformanceMonitor()

        # Test starting operation
        monitor.start_operation("test_op")
        assert "test_op" in monitor.start_times

        # Test ending operation
        import time

        time.sleep(0.01)  # Small delay to ensure measurable time
        duration = monitor.end_operation("test_op")
        assert duration > 0
        assert "test_op" in monitor.metrics
        assert len(monitor.metrics["test_op"]) == 1

        # Test getting stats
        stats = monitor.get_stats("test_op")
        assert stats is not None
        assert stats["count"] == 1
        assert stats["average"] > 0

        # Test non-existent operation
        assert monitor.get_stats("nonexistent") is None

    def test_performance_monitor_multiple_operations(self) -> None:
        """Test performance monitor with multiple operations."""
        from sfi.makepython.makepython import PerformanceMonitor

        monitor = PerformanceMonitor()

        # Record multiple operations
        for i in range(3):
            monitor.start_operation(f"op_{i}")
            import time

            time.sleep(0.001)  # Small delay
            monitor.end_operation(f"op_{i}")

        # Check all operations recorded
        for i in range(3):
            stats = monitor.get_stats(f"op_{i}")
            assert stats is not None
            assert stats["count"] == 1


class TestUtilityFunctionsExtended:
    """Extended tests for utility functions."""

    def test_format_duration(self) -> None:
        """Test duration formatting function."""
        from sfi.makepython.makepython import _format_duration

        # Test milliseconds
        assert _format_duration(0.5) == "500ms"

        # Test seconds
        assert _format_duration(30.5) == "30.5s"

        # Test minutes
        assert _format_duration(125.5) == "2m 5.5s"

        # Test zero
        assert _format_duration(0) == "0ms"

    def test_validate_token_format(self) -> None:
        """Test PyPI token format validation."""
        from sfi.makepython.makepython import _validate_token_format

        # Test valid tokens
        is_valid, message = _validate_token_format("pypi-validtoken123")
        assert is_valid is True
        assert isinstance(message, str)

        is_valid, message = _validate_token_format("PYPI-VALIDTOKEN123")
        assert is_valid is True

        # Test invalid tokens
        is_valid, message = _validate_token_format("")
        assert is_valid is False
        assert "empty" in message.lower() or "空" in message

        is_valid, message = _validate_token_format("short")
        assert is_valid is False
        assert "short" in message.lower() or "短" in message

        # Test warning case
        is_valid, message = _validate_token_format("invalid-prefix-token")
        assert is_valid is True  # Still valid despite warning
        assert isinstance(message, str)

    def test_show_progress(self) -> None:
        """Test progress display function."""
        from sfi.makepython.makepython import _show_progress

        # Capture logger output
        logger = logging.getLogger("sfi.makepython.makepython")
        with patch.object(logger, "info") as mock_info:
            # Test simple message
            _show_progress("Processing files")
            mock_info.assert_called_with("Processing files")

            # Reset mock
            mock_info.reset_mock()

            # Test progress with numbers
            _show_progress("Processing", 5, 10)
            expected_msg = "Processing [5/10] (50.0%)"
            mock_info.assert_called_with(expected_msg)


class TestProjectAnalysis:
    """Test project analysis and type detection."""

    def test_get_project_info_with_gui_project(self) -> None:
        """Test project info extraction for GUI project."""
        from sfi.makepython.makepython import _get_project_info

        with tempfile.TemporaryDirectory() as temp_dir:
            project_dir = Path(temp_dir)
            pyproject_path = project_dir / "pyproject.toml"

            pyproject_content = """
[project]
name = "test-gui-app"
version = "1.0.0"
dependencies = ["PySide2"]

[build-system]
build-backend = "setuptools.build_meta"
"""
            pyproject_path.write_text(pyproject_content)

            with patch("sfi.makepython.makepython.CURRENT_WORKING_DIR", project_dir):
                project = _get_project_info()
                assert project is not None
                assert project.name == "test-gui-app"
                # Note: project type detection depends on projectparse implementation

    def test_get_project_info_nonexistent(self) -> None:
        """Test project info extraction when pyproject.toml doesn't exist."""
        from sfi.makepython.makepython import _get_project_info

        with tempfile.TemporaryDirectory() as temp_dir:
            project_dir = Path(temp_dir)
            with patch("sfi.makepython.makepython.CURRENT_WORKING_DIR", project_dir):
                project = _get_project_info()
                assert project is None

    def test_get_project_info_invalid(self) -> None:
        """Test project info extraction with invalid pyproject.toml."""
        from sfi.makepython.makepython import _get_project_info

        with tempfile.TemporaryDirectory() as temp_dir:
            project_dir = Path(temp_dir)
            pyproject_path = project_dir / "pyproject.toml"
            pyproject_path.write_text("invalid toml content")

            with patch("sfi.makepython.makepython.CURRENT_WORKING_DIR", project_dir):
                project = _get_project_info()
                assert project is None


class TestBuildCommands:
    """Test build command detection and optimization."""

    def test_get_optimized_build_command_with_project_info(self) -> None:
        """Test optimized build command with project information."""
        from sfi.makepython.makepython import (
            BuildTool,
            MakePythonConfig,
            _get_optimized_build_command,
        )

        with tempfile.TemporaryDirectory() as temp_dir:
            project_dir = Path(temp_dir)
            config = MakePythonConfig()

            # Mock supported tools
            with patch.object(config, "supported_tools", [BuildTool.UV]):
                with patch(
                    "sfi.makepython.makepython._get_project_info"
                ) as mock_get_info:
                    # Mock project info
                    mock_project = Mock()
                    mock_project.project_type = "窗口应用"
                    mock_project.project_type_emoji = "🖥️"
                    mock_get_info.return_value = mock_project

                    result = _get_optimized_build_command(project_dir, config)
                    assert result == "uv"

    def test_get_optimized_build_command_no_project(self) -> None:
        """Test optimized build command when no project info available."""
        from sfi.makepython.makepython import (
            BuildTool,
            MakePythonConfig,
            _get_optimized_build_command,
        )

        with tempfile.TemporaryDirectory() as temp_dir:
            project_dir = Path(temp_dir)
            config = MakePythonConfig()

            with patch.object(config, "supported_tools", [BuildTool.POETRY]):
                with patch(
                    "sfi.makepython.makepython._get_project_info"
                ) as mock_get_info:
                    mock_get_info.return_value = None

                    result = _get_optimized_build_command(project_dir, config)
                    assert result == "poetry"


class TestNewFunctionality:
    """Test newly added functionality from makepythonold."""

    def test_activate_virtual_env(self) -> None:
        """Test virtual environment activation function."""
        # This should not raise exceptions even if venv doesn't exist
        with patch("sfi.makepython.makepython._run_command"):
            _activate_py_env()
            # Should attempt to run activation script
            assert True  # Will depend on system

    def test_bump_version_function(self) -> None:
        """Test version bumping function."""
        with patch("sfi.makepython.makepython._run_command") as mock_run:
            _bump_version("patch")
            # Should call bump2version or bumpversion
            assert mock_run.called

    def test_get_project_name(self) -> None:
        """Test project name extraction."""
        with tempfile.TemporaryDirectory() as temp_dir:
            project_dir = Path(temp_dir)
            pyproject_path = project_dir / "pyproject.toml"

            pyproject_content = """
[project]
name = "test-project"
version = "1.0.0"
"""
            pyproject_path.write_text(pyproject_content)

            with patch("sfi.makepython.makepython.CURRENT_WORKING_DIR", project_dir):
                project_name = _get_project_name()
                assert project_name == "test-project"

    def test_coverage_report_generation(self) -> None:
        """Test coverage report generation function."""
        with patch("sfi.makepython.makepython._run_command") as mock_run:
            with patch("webbrowser.open"):
                _generate_coverage_report(False)
                # Should call pytest with coverage options
                assert mock_run.called

    def test_distribution_generation(self) -> None:
        """Test distribution package generation."""
        with patch("sfi.makepython.makepython._get_build_command") as mock_get_build:
            with patch("sfi.makepython.makepython._run_command") as mock_run:
                mock_get_build.return_value = "uv"
                _generate_distribution()
                # Should call build and list dist directory
                assert mock_run.called


class TestBuildDateUpdater:
    """Test build date updater functionality."""

    def test_build_date_config_constants(self) -> None:
        """Test build date configuration constants."""
        from sfi.makepython.makepython import _BuildDateConfig

        config = _BuildDateConfig()
        assert config.SRC_DIR_NAME == "src"
        assert config.INIT_FILENAME == "__init__.py"
        assert config.DATE_FORMAT == "%Y-%m-%d"
        assert config.BUILD_DATE_VAR == "__build_date__"

    def test_parse_result_dataclass(self) -> None:
        """Test ParseResult dataclass."""
        from sfi.makepython.makepython import _ParseResult

        # Test default values
        result = _ParseResult(needs_update=True)
        assert result.needs_update is True
        assert result.new_content == ""

        # Test with custom values
        result = _ParseResult(needs_update=False, new_content="test content")
        assert result.needs_update is False
        assert result.new_content == "test content"

    def test_validate_date_format(self) -> None:
        """Test date format validation."""
        from sfi.makepython.makepython import _validate_date_format

        # Test valid dates
        assert _validate_date_format("2024-01-15") is True
        assert _validate_date_format("2023-12-31") is True

        # Test invalid dates
        assert _validate_date_format("invalid") is False
        assert _validate_date_format("2024-13-01") is False  # Invalid month
        assert _validate_date_format("2024-01-32") is False  # Invalid day
        assert _validate_date_format("") is False
        assert _validate_date_format("2024/01/15") is False  # Wrong format

    def test_get_build_date_pattern(self) -> None:
        """Test build date regex pattern generation."""
        import re

        from sfi.makepython.makepython import _get_build_date_pattern

        pattern = _get_build_date_pattern()
        assert isinstance(pattern, re.Pattern)

        # Test pattern matching
        test_content = "    __build_date__ = '2024-01-15'"
        match = pattern.search(test_content)
        assert match is not None
        assert match.group(2) == "__build_date__"
        assert match.group(4) == "2024-01-15"


class TestCommandLineInterface:
    """Test command line interface functionality."""

    def test_command_dataclass(self) -> None:
        """Test Command dataclass creation and attributes."""
        from sfi.makepython.makepython import Command, CommandType

        # Test basic command
        cmd = Command(
            name="test",
            alias="t",
            command_type=CommandType.TEST,
            description="Test command",
        )
        assert cmd.name == "test"
        assert cmd.alias == "t"
        assert cmd.command_type == CommandType.TEST
        assert cmd.description == "Test command"
        assert cmd.cmds is None

        # Test command with commands list
        cmd_with_cmds = Command(
            name="build",
            alias="b",
            command_type=CommandType.BUILD,
            cmds=["uv", "build"],
            description="Build command",
        )
        assert cmd_with_cmds.cmds == ["uv", "build"]

    def test_enum_values(self) -> None:
        """Test enum value correctness."""
        from sfi.makepython.makepython import BuildTool, CommandType

        # Test BuildTool enum
        assert BuildTool.UV.value == "uv"
        assert BuildTool.POETRY.value == "poetry"
        assert BuildTool.HATCH.value == "hatch"

        # Test CommandType enum
        assert CommandType.BUILD.value == "build"
        assert CommandType.CLEAN.value == "clean"
        assert CommandType.TEST.value == "test"
        assert CommandType.PUBLISH.value == "publish"
        assert CommandType.BUMP_VERSION.value == "bumpversion"
        assert CommandType.TOKEN.value == "token"


class TestConfigurationManagement:
    """Test configuration management functionality."""

    def test_makepython_config_default_values(self) -> None:
        """Test MakePythonConfig default values."""
        from sfi.makepython.makepython import BuildTool, MakePythonConfig

        # Mock CONFIG_FILE to avoid interference
        with tempfile.TemporaryDirectory() as temp_dir:
            mock_config_file = Path(temp_dir) / "nonexistent.json"
            with patch("sfi.makepython.makepython.CONFIG_FILE", mock_config_file):
                config = MakePythonConfig()

                assert config.default_build_tool == BuildTool.UV
                assert config.auto_detect_tool is True
                assert config.verbose_output is False
                assert config.max_retries == 3
                assert config.timeout_seconds == 300

    def test_makepython_config_post_init(self) -> None:
        """Test MakePythonConfig post-initialization behavior."""
        from sfi.makepython.makepython import MakePythonConfig

        with tempfile.TemporaryDirectory() as temp_dir:
            mock_config_file = Path(temp_dir) / "nonexistent.json"
            with patch("sfi.makepython.makepython.CONFIG_FILE", mock_config_file):
                config = MakePythonConfig()
                # Test that save method is registered
                assert hasattr(config, "save")


class TestToolDetection:
    """Test tool detection and availability checking."""

    def test_is_tool_available_cache(self) -> None:
        """Test tool availability cache functionality."""
        from sfi.makepython.makepython import _is_tool_available

        # Test with existing tool (python should always be available)
        result = _is_tool_available("python")
        assert isinstance(result, bool)

        # The actual result depends on system, but shouldn't raise exception

    def test_tool_availability_cache_class(self) -> None:
        """Test ToolAvailabilityCache class functionality."""
        from sfi.makepython.makepython import ToolAvailabilityCache

        cache = ToolAvailabilityCache()

        # Test initial state
        assert cache.is_available("python") is True  # Python should be available

        # Test cache size methods
        cache.invalidate("python")
        cache.clear()
        # These should not raise exceptions


class TestCachingSystem:
    """Test caching system components."""

    def test_project_cache_basic_operations(self) -> None:
        """Test ProjectCache basic operations."""
        from sfi.makepython.models.cache import ProjectCache
        from sfi.pyprojectparse.models.project import Project

        cache = ProjectCache(max_size=2, ttl_seconds=1)

        # Test with mock project
        mock_project = Mock(spec=Project)
        mock_project.name = "test-project"

        test_path = Path("/test/path/pyproject.toml")

        # Test put and get
        cache.put(test_path, mock_project)
        retrieved = cache.get(test_path)
        assert retrieved == mock_project

        # Test size property
        assert cache.size == 1

        # Test invalidate
        cache.invalidate(test_path)
        assert cache.get(test_path) is None

        # Test clear
        cache.clear()
        assert cache.size == 0

    def test_config_cache_basic_operations(self) -> None:
        """Test ConfigCache basic operations."""
        from sfi.makepython.makepython import ConfigCache

        cache = ConfigCache()

        test_config = {"test": "value"}
        test_path = Path("/test/config.json")

        # Test put and get
        cache.put(test_path, test_config)
        retrieved = cache.get(test_path)
        assert retrieved == test_config

        # Test clear
        cache.clear()
        assert cache.get(test_path) is None


class TestFileOperations:
    """Test file operation utilities."""

    def test_write_to_env_file_windows(self) -> None:
        """Test writing to environment file on Windows."""
        with patch("sfi.makepython.makepython.IS_WINDOWS", True):
            with patch("sfi.makepython.makepython._execute_command") as mock_execute:
                # Mock successful command execution
                mock_execute.return_value = None
                _write_to_env_file("TEST_VAR", "test_value")
                # Should call setx command through _execute_command
                mock_execute.assert_called()

    def test_write_to_env_file_unix(self) -> None:
        """Test writing to environment file on Unix-like systems."""
        with patch("sfi.makepython.makepython.IS_WINDOWS", False), patch(
            "sfi.makepython.makepython._write_to_shell_config"
        ) as mock_write:
            _write_to_env_file("TEST_VAR", "test_value")
            mock_write.assert_called_once_with("export TEST_VAR='test_value'")

    def test_get_shell_config_path(self) -> None:
        """Test shell config path detection."""
        from sfi.makepython.makepython import _get_shell_config_path

        # Test with zsh
        with patch.dict("os.environ", {"SHELL": "/bin/zsh"}):
            path = _get_shell_config_path()
            assert path.name == ".zshrc"

        # Test with bash/default
        with patch.dict("os.environ", {"SHELL": "/bin/bash"}):
            path = _get_shell_config_path()
            assert path.name == ".bashrc"


class TestEdgeCases:
    """Test edge cases and boundary conditions."""

    def test_empty_command_list(self) -> None:
        """Test handling of empty command list."""
        from sfi.makepython.makepython import _run_command

        with pytest.raises(ValueError, match="Invalid command list"):
            _run_command([], Path("."))

    def test_invalid_directory(self) -> None:
        """Test handling of invalid directory."""
        from sfi.makepython.makepython import _run_command

        with pytest.raises(ValueError, match="Invalid directory"):
            _run_command(["echo", "test"], Path("/nonexistent/directory"))

    def test_non_string_command_elements(self) -> None:
        """Test handling of non-string command elements."""
        from sfi.makepython.makepython import _run_command

        with pytest.raises(ValueError, match="Invalid command list"):
            _run_command(["echo", 123], Path("."))

    def test_bump_version_invalid_type(self) -> None:
        """Test bump version with invalid version type."""
        from sfi.makepython.makepython import _bump_version

        # Test that it handles invalid types gracefully
        # We'll check that it doesn't crash
        try:
            _bump_version("invalid")
            # Should not raise exception
        except Exception:
            pytest.fail("Should not raise exception for invalid version type")

    def test_get_build_command_no_tools(self) -> None:
        """Test get_build_command when no tools are available."""
        from sfi.makepython.makepython import (
            BuildToolNotFoundError,
            MakePythonConfig,
            _get_build_command,
        )

        with tempfile.TemporaryDirectory() as temp_dir:
            project_dir = Path(temp_dir)
            config = MakePythonConfig()

            # Mock no supported tools
            with patch.object(config, "supported_tools", []):
                with pytest.raises(BuildToolNotFoundError):
                    _get_build_command(project_dir, config)


class TestMainFunction:
    """Test main function and CLI argument parsing."""

    @patch("sys.argv", ["makepython", "--version"])
    def test_main_version_flag(self) -> None:
        """Test main function with version flag."""

        from sfi.makepython.makepython import main

        # This should exit with SystemExit
        with pytest.raises(SystemExit) as exc_info:
            main()
        assert exc_info.value.code == 0

    @patch("sys.argv", ["makepython", "--info"])
    @patch("sfi.makepython.makepython._show_project_summary")
    def test_main_info_flag(self, mock_show_summary) -> None:
        """Test main function with info flag."""
        from sfi.makepython.makepython import main

        main()
        mock_show_summary.assert_called_once()

    @patch("sys.argv", ["makepython"])
    @patch("sfi.makepython.makepython.argparse.ArgumentParser.print_help")
    def test_main_no_command(self, mock_print_help) -> None:
        """Test main function when no command is provided."""
        import logging

        from sfi.makepython.makepython import main

        # Mock the specific logger that will be used
        with patch.object(
            logging.getLogger("sfi.makepython.makepython"), "error"
        ) as mock_log:
            with pytest.raises(SystemExit) as exc_info:
                main()
            assert exc_info.value.code == 1
            mock_print_help.assert_called_once()
            # Check that error was logged with the expected message
            mock_log.assert_called_once()
            call_args = mock_log.call_args[0][0]
            assert "No command specified" in call_args

    @patch("sys.argv", ["makepython", "unknown-command"])
    def test_main_unknown_command(self) -> None:
        """Test main function with unknown command."""
        import logging

        from sfi.makepython.makepython import main

        with patch.object(logging.getLogger(), "error"):
            with pytest.raises(SystemExit) as exc_info:
                main()
            # argparse exits with code 2 for unknown arguments
            assert exc_info.value.code == 2
            # The error is logged by argparse, not our code
            # So we don't assert on mock_log here

    @patch("sys.argv", ["makepython", "--stats", "build"])
    @patch("sfi.makepython.makepython._get_build_command")
    @patch("sfi.makepython.makepython._dispatch_command")
    @patch("sfi.makepython.makepython.PerformanceMonitor.print_summary")
    def test_main_with_stats_flag(
        self, mock_print_summary, mock_dispatch, mock_get_build
    ) -> None:
        """Test main function with stats flag."""
        from sfi.makepython.makepython import main

        mock_get_build.return_value = "uv"
        main()
        mock_print_summary.assert_called_once()

    @patch("sys.argv", ["makepython", "--quiet", "build"])
    @patch("sfi.makepython.makepython._get_build_command")
    @patch("sfi.makepython.makepython._dispatch_command")
    def test_main_quiet_mode(self, mock_dispatch, mock_get_build) -> None:
        """Test main function with quiet mode."""
        from sfi.makepython.makepython import logger, main

        mock_get_build.return_value = "uv"
        with patch.object(logger, "setLevel") as mock_set_level:
            main()
            mock_set_level.assert_called_with(logging.ERROR)

    @patch("sys.argv", ["makepython", "--debug", "build"])
    @patch("sfi.makepython.makepython._get_build_command")
    @patch("sfi.makepython.makepython._dispatch_command")
    def test_main_debug_mode(self, mock_dispatch, mock_get_build) -> None:
        """Test main function with debug mode."""
        from sfi.makepython.makepython import MakePythonConfig, logger, main

        mock_get_build.return_value = "uv"
        with patch.object(logger, "setLevel") as mock_set_level, patch(
            "sfi.makepython.makepython.MakePythonConfig"
        ) as mock_config_class:
            mock_config = Mock(spec=MakePythonConfig)
            mock_config_class.return_value = mock_config
            main()
            mock_set_level.assert_called_with(logging.DEBUG)
            assert mock_config.verbose_output is True

    @patch("sys.argv", ["makepython", "--verbose", "build"])
    @patch("sfi.makepython.makepython._get_build_command")
    @patch("sfi.makepython.makepython._dispatch_command")
    def test_main_verbose_mode(self, mock_dispatch, mock_get_build) -> None:
        """Test main function with verbose mode."""
        from sfi.makepython.makepython import MakePythonConfig, logger, main

        mock_get_build.return_value = "uv"
        with patch.object(logger, "setLevel") as mock_set_level, patch(
            "sfi.makepython.makepython.MakePythonConfig"
        ) as mock_config_class:
            mock_config = Mock(spec=MakePythonConfig)
            mock_config_class.return_value = mock_config
            main()
            mock_set_level.assert_called_with(logging.INFO)
            assert mock_config.verbose_output is True

    @patch("sys.argv", ["makepython", "--realtime", "test"])
    @patch("sfi.makepython.makepython._get_build_command")
    @patch("sfi.makepython.makepython._dispatch_command")
    def test_main_realtime_flag(self, mock_dispatch, mock_get_build) -> None:
        """Test main function with realtime flag."""
        from sfi.makepython.makepython import main

        mock_get_build.return_value = "uv"
        main()
        # Should call dispatch with realtime=True
        mock_dispatch.assert_called_once()


class TestTokenManagement:
    """Test token management functionality."""

    def test_validate_token_format_comprehensive(self) -> None:
        """Test comprehensive token format validation."""
        from sfi.makepython.makepython import _validate_token_format

        # Test valid tokens
        test_cases = [
            ("pypi-validtoken123", True),
            ("PYPI-VALIDTOKEN123", True),
            ("pypi-AaBbCc123456", True),
            (
                "pypi-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
                True,
            ),
        ]

        for token, expected_valid in test_cases:
            is_valid, message = _validate_token_format(token)
            assert is_valid == expected_valid
            assert isinstance(message, str)

        # Test invalid tokens
        invalid_cases = [
            ("", False),
            ("short", False),
            ("invalid-prefix-token", True),  # Valid but with warning
            ("pypi-invalid_chars!", True),  # Still valid despite special chars
            ("pypi-", False),
        ]

        for token, expected_valid in invalid_cases:
            is_valid, message = _validate_token_format(token)
            if expected_valid:
                assert is_valid is True
                assert isinstance(message, str)
            else:
                assert is_valid is False
                assert isinstance(message, str)

    def test_set_build_tool_token_uv(self) -> None:
        """Test setting token for UV build tool."""
        from sfi.makepython.makepython import BuildTool, _set_build_tool_token

        with patch("sfi.makepython.makepython._write_to_env_file") as mock_write_env:
            with patch(
                "sfi.makepython.makepython._save_token_to_file"
            ) as mock_save_file:
                _set_build_tool_token("test-token-123", BuildTool.UV)
                mock_write_env.assert_called_once_with(
                    "UV_PUBLISH_TOKEN", "test-token-123"
                )
                mock_save_file.assert_called_once()

    def test_set_build_tool_token_poetry(self) -> None:
        """Test setting token for Poetry build tool."""
        from sfi.makepython.makepython import BuildTool, _set_build_tool_token

        with patch("sfi.makepython.makepython._write_to_env_file") as mock_write_env:
            with patch("sfi.makepython.makepython._run_command") as mock_run:
                _set_build_tool_token("test-token-123", BuildTool.POETRY)
                mock_write_env.assert_called_once_with(
                    "POETRY_PYPI_TOKEN_PYPI", "test-token-123"
                )
                mock_run.assert_called_once()

    def test_set_build_tool_token_hatch(self) -> None:
        """Test setting token for Hatch build tool."""
        from sfi.makepython.makepython import BuildTool, _set_build_tool_token

        with patch("sfi.makepython.makepython._save_token_to_file") as mock_save_file:
            _set_build_tool_token("test-token-123", BuildTool.HATCH)
            mock_save_file.assert_called_once()

    def test_save_token_to_file_success(self) -> None:
        """Test successful token file saving."""
        from sfi.makepython.makepython import _save_token_to_file

        with tempfile.TemporaryDirectory() as temp_dir:
            test_file = Path(temp_dir) / "test_token.txt"
            _save_token_to_file(test_file, "test content", "test desc")
            assert test_file.exists()
            assert test_file.read_text() == "test content"

    def test_save_token_to_file_failure(self) -> None:
        """Test token file saving failure."""
        from sfi.makepython.makepython import _save_token_to_file

        # Test with a path that should definitely fail on Windows
        # Using a reserved device name that will always fail
        invalid_path = (
            Path("CON") / "token.txt"
        )  # CON is a reserved device name on Windows
        with pytest.raises(OSError):
            _save_token_to_file(invalid_path, "content", "desc")


class TestDocumentationGeneration:
    """Test documentation generation functionality."""

    def test_generate_documentation_basic(self) -> None:
        """Test basic documentation generation."""
        from sfi.makepython.makepython import _generate_documentation

        with patch("sfi.makepython.makepython._run_command") as mock_run:
            with patch("webbrowser.open"):
                with patch(
                    "sfi.makepython.makepython._get_project_name"
                ) as mock_get_name:
                    # Mock project name to ensure function proceeds
                    mock_get_name.return_value = "test-project"
                    _generate_documentation()
                    # Should call sphinx-build and sphinx-autobuild commands
                    assert mock_run.call_count >= 2
                    # Browser opening is handled by sphinx-autobuild itself,
                    # not by explicit webbrowser.open() call in our code
                    # So we don't expect webbrowser.open to be called directly

    def test_generate_documentation_with_errors(self) -> None:
        """Test documentation generation with build errors."""
        from sfi.makepython.makepython import _generate_documentation

        with patch("sfi.makepython.makepython._run_command") as mock_run:
            with patch("webbrowser.open") as mock_browser:
                with patch("sfi.makepython.makepython.logger.error") as mock_error:
                    # Simulate build failure
                    mock_run.side_effect = Exception("Build failed")
                    _generate_documentation()
                    # Browser should NOT be opened when build fails
                    mock_browser.assert_not_called()
                    # Error should be logged
                    mock_error.assert_called()


class TestBuildFunctions:
    """Test build and publish related functions."""

    def test_handle_publish_command_success(self) -> None:
        """Test successful publish command handling."""
        from sfi.makepython.makepython import _handle_publish_command

        with patch("sfi.makepython.makepython._check_pypi_token") as mock_check_token:
            with patch("sfi.makepython.makepython._set_token") as mock_set_token:
                with patch("sfi.makepython.makepython._run_command") as mock_run:
                    with patch.object(
                        logging.getLogger("sfi.makepython.makepython"), "warning"
                    ) as mock_warn:
                        # Mock token check to return False (trigger token setting)
                        mock_check_token.return_value = False
                        # Mock _set_token to not require input
                        mock_set_token.return_value = None

                        _handle_publish_command("uv")

                        # Should call token check, token setting, and publish command
                        assert mock_check_token.called
                        assert mock_set_token.called
                        assert mock_run.called
                        mock_warn.assert_called_with(
                            "PyPI token not configured. Starting configuration process..."
                        )

    def test_handle_publish_command_with_errors(self) -> None:
        """Test publish command handling with git errors."""
        from sfi.makepython.makepython import _handle_publish_command

        with patch("sfi.makepython.makepython._check_pypi_token") as mock_check_token:
            with patch("sfi.makepython.makepython._set_token"):
                with patch("sfi.makepython.makepython._run_command") as mock_run:
                    with patch.object(
                        logging.getLogger("sfi.makepython.makepython"), "warning"
                    ):
                        # Mock token check to return True (skip token setting)
                        mock_check_token.return_value = True
                        # Simulate publish command failure
                        mock_run.side_effect = Exception("Publish command failed")

                        with pytest.raises(Exception, match="Publish command failed"):
                            _handle_publish_command("uv")

                        mock_run.assert_called_once_with(["uv", "publish"], Path.cwd())

    def test_bump_publish_workflow(self) -> None:
        """Test bump and publish workflow."""
        from sfi.makepython.makepython import _bump_publish

        with patch("sfi.makepython.makepython._bump_version") as mock_bump, patch(
            "sfi.makepython.makepython._handle_publish_command"
        ) as mock_publish:
            # Mock _handle_publish_command to avoid stdin input requirement
            mock_publish.return_value = None
            _bump_publish()
            mock_bump.assert_called_once_with("patch")
            mock_publish.assert_called_once()

    def test_publish_with_cleanup_success(self) -> None:
        """Test successful publish with cleanup."""
        from sfi.makepython.makepython import _publish_with_cleanup

        with patch("sfi.makepython.makepython._run_command") as mock_run, patch(
            "sfi.makepython.makepython.logger.info"
        ) as mock_log:
            _publish_with_cleanup()
            # Should run publish command
            mock_run.assert_called()
            mock_log.assert_called()

    def test_publish_with_cleanup_failure(self) -> None:
        """Test publish with cleanup when publish fails."""
        from sfi.makepython.makepython import (
            _publish_with_cleanup,
        )

        with patch("sfi.makepython.makepython._run_command") as mock_run, patch(
            "sfi.makepython.makepython.logger.error"
        ) as mock_error:
            # Simulate publish failure with subprocess.CalledProcessError
            def side_effect(*args, **kwargs):
                cmd_str = (
                    " ".join(str(x) for x in args[0])
                    if isinstance(args[0], (list, tuple))
                    else str(args[0])
                )
                if "publish" in cmd_str.lower():
                    raise subprocess.CalledProcessError(returncode=1, cmd=args[0])
                # Let other commands succeed (like git push)

            mock_run.side_effect = side_effect

            # The function should catch the exception and log it, not re-raise
            _publish_with_cleanup()
            mock_error.assert_called()


class TestProjectInitialization:
    """Test project initialization functionality."""

    def test_initialize_project_success(self) -> None:
        """Test successful project initialization."""
        from sfi.makepython.makepython import _initialize_project

        with patch("sfi.makepython.makepython._run_command") as mock_run, patch(
            "sfi.makepython.makepython.logger.info"
        ) as mock_log:
            _initialize_project()
            # Should run git init and pre-commit install
            assert mock_run.call_count >= 2
            mock_log.assert_called()

    def test_initialize_project_git_failure(self) -> None:
        """Test project initialization when git init fails."""
        from sfi.makepython.makepython import _initialize_project

        with patch("sfi.makepython.makepython._run_command") as mock_run, patch(
            "sfi.makepython.makepython.logger.error"
        ) as mock_error:
            # Make git init fail but other commands succeed
            def side_effect(*args, **kwargs):
                if args[0] == ["git", "init"]:
                    raise Exception("Git init failed")

            mock_run.side_effect = side_effect

            _initialize_project()
            # Check that error was logged (not warning)
            mock_error.assert_called_once()
            call_args = mock_error.call_args[0][0]
            assert "Git init failed" in call_args


class TestCodeLinting:
    """Test code linting functionality."""

    def test_lint_code_success(self) -> None:
        """Test successful code linting."""
        from sfi.makepython.makepython import _lint_code

        with patch("sfi.makepython.makepython._run_command") as mock_run, patch(
            "sfi.makepython.makepython.logger.info"
        ) as mock_log:
            _lint_code()
            mock_run.assert_called()
            mock_log.assert_called()

    def test_lint_code_with_fix(self) -> None:
        """Test code linting with auto-fix."""
        from sfi.makepython.makepython import _lint_code

        with patch("sfi.makepython.makepython._run_command") as mock_run:
            _lint_code()
            # Should call ruff with check and fix options
            call_args = mock_run.call_args[0][0]
            assert "ruff" in call_args
            assert "check" in call_args
            assert "--fix" in call_args


class TestIntegration:
    """Integration tests."""

    def test_complete_workflow(self) -> None:
        """Test a complete workflow scenario."""
        with tempfile.TemporaryDirectory() as temp_dir:
            project_dir = Path(temp_dir)

            # Create a basic project structure
            pyproject_path = project_dir / "pyproject.toml"
            pyproject_content = """
[project]
name = "integration-test"
version = "0.1.0"

[build-system]
build-backend = "hatchling.build"
"""
            pyproject_path.write_text(pyproject_content)

            # Test project info extraction
            pyproject_path = project_dir / "pyproject.toml"
            project = Project.from_toml_file(pyproject_path)
            assert project is not None
            assert project.name == "integration-test"
            assert project.build_backend == "hatchling.build"

            # Test configuration with mocked config file to avoid local config interference
            mock_config_file = Path(temp_dir) / "nonexistent_config.json"
            with patch("sfi.makepython.makepython.CONFIG_FILE", mock_config_file):
                config = MakePythonConfig()
                assert config.default_build_tool == BuildTool.UV

            # Test build command detection
            build_cmd = _get_build_command(project_dir, config)
            assert build_cmd == "hatch"  # Should detect from pyproject.toml


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
