"""
Comprehensive unit tests for the refactored Storm Logger configuration system.
Tests all the new abstractions and dependency injection features.
"""

from pathlib import Path
from unittest.mock import patch

import pytest

from storm_logger.config import (
    ConfigManager,
    EnvironmentReader,
    FileSystemOps,
    Framework,
    FrameworkDetector,
    ImportBasedFrameworkDetector,
    LogLevel,
    OSEnvironmentReader,
    RealFileSystemOps,
    StormLoggerConfig,
    StormTheme,
    get_config,
    get_config_manager,
    reset_config,
    set_config,
)

# Mock implementations for testing


class MockEnvironmentReader(EnvironmentReader):
    """Mock environment reader for testing."""

    def __init__(self, env_vars: dict[str, str] = None):
        self.env_vars = env_vars or {}

    def get(self, key: str, default: str | None = None) -> str | None:
        return self.env_vars.get(key, default)


class MockFrameworkDetector(FrameworkDetector):
    """Mock framework detector for testing."""

    def __init__(self, framework: Framework = Framework.STANDALONE):
        self.framework = framework

    def detect(self) -> Framework:
        return self.framework


class MockFileSystemOps(FileSystemOps):
    """Mock file system operations for testing."""

    def __init__(self):
        self.created_dirs = []
        self.created_files = []
        self.deleted_files = []
        self.existing_paths = set()
        self.should_fail = False
        self.fail_on_path = None

    def mkdir(self, path: Path, parents: bool = False, exist_ok: bool = False) -> None:
        if self.should_fail or (self.fail_on_path and str(path) == str(self.fail_on_path)):
            raise PermissionError(f"Cannot create directory: {path}")
        self.created_dirs.append((path, parents, exist_ok))
        self.existing_paths.add(str(path))

    def touch(self, path: Path) -> None:
        if self.should_fail:
            raise PermissionError(f"Cannot create file: {path}")
        self.created_files.append(path)
        self.existing_paths.add(str(path))

    def unlink(self, path: Path) -> None:
        if self.should_fail:
            raise PermissionError(f"Cannot delete file: {path}")
        self.deleted_files.append(path)
        self.existing_paths.discard(str(path))

    def exists(self, path: Path) -> bool:
        return str(path) in self.existing_paths


class TestEnvironmentReader:
    """Test the environment reader abstractions."""

    def test_os_environment_reader(self):
        """Test the real OS environment reader."""
        with patch.dict("os.environ", {"TEST_VAR": "test_value"}):
            reader = OSEnvironmentReader()
            assert reader.get("TEST_VAR") == "test_value"
            assert reader.get("MISSING_VAR") is None
            assert reader.get("MISSING_VAR", "default") == "default"

    def test_mock_environment_reader(self):
        """Test the mock environment reader."""
        reader = MockEnvironmentReader({"KEY1": "value1", "KEY2": "value2"})
        assert reader.get("KEY1") == "value1"
        assert reader.get("KEY2") == "value2"
        assert reader.get("KEY3") is None
        assert reader.get("KEY3", "default") == "default"


class TestFrameworkDetector:
    """Test the framework detector abstractions."""

    def test_import_based_detector_django(self):
        """Test Django framework detection."""
        with patch(
            "importlib.util.find_spec", side_effect=lambda name: True if name == "django" else None
        ):
            detector = ImportBasedFrameworkDetector()
            assert detector.detect() == Framework.DJANGO

    def test_import_based_detector_flask(self):
        """Test Flask framework detection."""

        def mock_find_spec(name):
            if name == "django":
                return None
            elif name == "flask":
                return True  # Spec found
            else:
                return None

        with patch("importlib.util.find_spec", side_effect=mock_find_spec):
            detector = ImportBasedFrameworkDetector()
            assert detector.detect() == Framework.FLASK

    def test_import_based_detector_fastapi(self):
        """Test FastAPI framework detection."""

        def mock_find_spec(name):
            if name in ["django", "flask"]:
                return None
            elif name == "fastapi":
                return True  # Spec found
            else:
                return None

        with patch("importlib.util.find_spec", side_effect=mock_find_spec):
            detector = ImportBasedFrameworkDetector()
            assert detector.detect() == Framework.FASTAPI

    def test_import_based_detector_standalone(self):
        """Test standalone detection when no frameworks are available."""
        with patch("importlib.util.find_spec", return_value=None):
            detector = ImportBasedFrameworkDetector()
            assert detector.detect() == Framework.STANDALONE

    def test_mock_framework_detector(self):
        """Test the mock framework detector."""
        for framework in Framework:
            detector = MockFrameworkDetector(framework)
            assert detector.detect() == framework


class TestFileSystemOps:
    """Test the file system operations abstractions."""

    def test_real_file_system_ops(self, tmp_path):
        """Test real file system operations."""
        fs_ops = RealFileSystemOps()

        # Test mkdir
        test_dir = tmp_path / "test_dir"
        fs_ops.mkdir(test_dir, parents=True, exist_ok=True)
        assert test_dir.exists()

        # Test touch
        test_file = test_dir / "test.txt"
        fs_ops.touch(test_file)
        assert test_file.exists()

        # Test exists
        assert fs_ops.exists(test_file)
        assert not fs_ops.exists(test_dir / "nonexistent.txt")

        # Test unlink
        fs_ops.unlink(test_file)
        assert not test_file.exists()

    def test_mock_file_system_ops(self):
        """Test mock file system operations."""
        fs_ops = MockFileSystemOps()

        # Test mkdir
        test_path = Path("/test/dir")
        fs_ops.mkdir(test_path, parents=True, exist_ok=True)
        assert (test_path, True, True) in fs_ops.created_dirs
        assert fs_ops.exists(test_path)

        # Test touch
        test_file = Path("/test/file.txt")
        fs_ops.touch(test_file)
        assert test_file in fs_ops.created_files
        assert fs_ops.exists(test_file)

        # Test unlink
        fs_ops.unlink(test_file)
        assert test_file in fs_ops.deleted_files
        assert not fs_ops.exists(test_file)

        # Test failure mode
        fs_ops.should_fail = True
        with pytest.raises(PermissionError):
            fs_ops.mkdir(Path("/fail/dir"))
        with pytest.raises(PermissionError):
            fs_ops.touch(Path("/fail/file"))
        with pytest.raises(PermissionError):
            fs_ops.unlink(Path("/fail/file"))


class TestStormLoggerConfig:
    """Test the StormLoggerConfig with dependency injection."""

    def test_basic_initialization(self):
        """Test basic config initialization."""
        config = StormLoggerConfig()
        assert config.log_level == LogLevel.INFO
        assert config.enable_console is True
        assert config.storm_theme == StormTheme.DEFAULT

    def test_dependency_injection(self):
        """Test that dependencies can be injected."""
        mock_env = MockEnvironmentReader({"DEBUG": "true"})
        mock_detector = MockFrameworkDetector(Framework.DJANGO)
        mock_fs = MockFileSystemOps()

        # Create config with a framework specified to avoid auto-detection
        config = StormLoggerConfig(framework=Framework.STANDALONE)
        # Set dependencies
        config._env_reader = mock_env
        config._framework_detector = mock_detector
        config._fs_ops = mock_fs

        # Test framework detection
        config.framework = None  # Reset to trigger detection
        config._detect_framework_if_needed()
        assert config.framework == Framework.DJANGO

        # Test debug mode initialization
        config.debug_mode = False
        config.production_mode = False
        config._initialize_debug_mode()
        assert config.debug_mode is True
        assert config.production_mode is False

    def test_from_env_with_custom_reader(self):
        """Test creating config from environment with custom reader."""
        env_vars = {
            "STORM_LOG_LEVEL": "DEBUG",
            "STORM_LOG_DIR": "/custom/logs",
            "STORM_ENABLE_CONSOLE": "false",
            "STORM_THEME": "professional",
            "STORM_MAX_FILE_SIZE": "5242880",  # 5MB
            "STORM_BACKUP_COUNT": "10",
            "DEBUG": "true",
            "STORM_PRODUCTION": "false",
        }

        mock_env = MockEnvironmentReader(env_vars)
        config = StormLoggerConfig.from_env(mock_env)

        assert config.log_level == LogLevel.DEBUG
        assert config.log_directory == Path("/custom/logs")
        assert config.enable_console is False
        assert config.storm_theme == StormTheme.PROFESSIONAL
        assert config.max_file_size == 5242880
        assert config.backup_count == 10
        assert config.debug_mode is True
        assert config.production_mode is False

    def test_from_env_defaults(self):
        """Test creating config from environment with defaults."""
        mock_env = MockEnvironmentReader({})  # Empty environment
        config = StormLoggerConfig.from_env(mock_env)

        assert config.log_level == LogLevel.INFO
        assert config.enable_console is True
        assert config.debug_mode is False
        # When debug_mode is False and not explicitly set, production_mode becomes True
        assert config.production_mode is True

    def test_from_dict(self):
        """Test creating config from dictionary."""
        config_dict = {
            "log_level": LogLevel.ERROR,
            "enable_console": False,
            "max_file_size": 1024,
            "invalid_key": "should_be_ignored",
        }

        config = StormLoggerConfig.from_dict(config_dict)
        assert config.log_level == LogLevel.ERROR
        assert config.enable_console is False
        assert config.max_file_size == 1024
        # Invalid key should be ignored
        assert not hasattr(config, "invalid_key")

    def test_to_dict(self):
        """Test converting config to dictionary."""
        config = StormLoggerConfig(
            log_level=LogLevel.WARNING,
            log_directory=Path("/test/logs"),
            storm_theme=StormTheme.MINIMAL,
        )

        result = config.to_dict()
        assert result["log_level"] == "WARNING"
        assert result["log_directory"] == "/test/logs"
        assert result["storm_theme"] == "minimal"

    def test_get_log_handlers(self):
        """Test getting log handlers based on configuration."""
        # Note: log_directory is always set to a default in __post_init__ if None
        # So we test with different configurations

        # With file logging (default)
        config = StormLoggerConfig(enable_console=True)
        # log_directory is set automatically, so file handlers are included
        assert set(config.get_log_handlers()) >= {"console", "file", "error_file"}

        # Debug mode
        config = StormLoggerConfig(enable_console=True, debug_mode=True, production_mode=False)
        assert set(config.get_log_handlers()) >= {"console", "file", "error_file", "debug_file"}

        # Production Django
        config = StormLoggerConfig(
            enable_console=True, production_mode=True, framework=Framework.DJANGO
        )
        expected = {"console", "file", "error_file", "mail_admins", "syslog"}
        assert set(config.get_log_handlers()) == expected

        # Console only (when file logging is explicitly disabled)
        # Note: When creating config, it auto-sets production_mode=True if debug_mode=False
        config = StormLoggerConfig(
            enable_console=True,
            enable_file_logging=False,
            debug_mode=True,  # This prevents production_mode from being auto-set
            production_mode=False,
        )
        config.log_directory = None  # Force no log directory
        assert config.get_log_handlers() == ["console"]

    def test_get_logging_level(self):
        """Test getting Python logging level."""
        import logging

        test_cases = [
            (LogLevel.DEBUG, logging.DEBUG),
            (LogLevel.INFO, logging.INFO),
            (LogLevel.WARNING, logging.WARNING),
            (LogLevel.ERROR, logging.ERROR),
            (LogLevel.CRITICAL, logging.CRITICAL),
        ]

        for log_level, expected in test_cases:
            config = StormLoggerConfig(log_level=log_level)
            assert config.get_logging_level() == expected

    def test_create_log_directories_success(self):
        """Test successful directory creation."""
        mock_fs = MockFileSystemOps()

        config = StormLoggerConfig(
            log_directory=Path("/test/logs"),
            archive_directory=Path("/test/archive"),
            framework=Framework.DJANGO,
        )
        config._fs_ops = mock_fs

        config.create_log_directories()

        # Check all directories were created
        created_paths = [str(path) for path, _, _ in mock_fs.created_dirs]
        assert "/test/logs" in created_paths
        assert str(Path("/test/logs/app")) in created_paths
        assert str(Path("/test/logs/errors")) in created_paths
        assert str(Path("/test/logs/django")) in created_paths
        assert "/test/archive" in created_paths

    def test_create_log_directories_failure(self):
        """Test directory creation failure handling."""
        mock_fs = MockFileSystemOps()
        mock_fs.should_fail = True

        config = StormLoggerConfig(log_directory=Path("/test/logs"))
        config._fs_ops = mock_fs

        with patch("warnings.warn") as mock_warn:
            config.create_log_directories()

        # Should disable file logging on failure
        assert config.enable_file_logging is False
        # Should issue a warning
        mock_warn.assert_called_once()
        warning_msg = str(mock_warn.call_args[0][0])
        assert "Cannot create log directory" in warning_msg

    def test_validate_success(self):
        """Test successful validation."""
        mock_fs = MockFileSystemOps()

        config = StormLoggerConfig(
            log_directory=Path("/test/logs"), max_file_size=1024, backup_count=5
        )
        config._fs_ops = mock_fs

        errors = config.validate()
        assert errors == []

        # Check that test file was created and deleted
        assert any(str(p).endswith("test_write.tmp") for p in mock_fs.created_files)
        assert any(str(p).endswith("test_write.tmp") for p in mock_fs.deleted_files)

    def test_validate_with_errors(self):
        """Test validation with various errors."""
        # Test directory not writable
        mock_fs = MockFileSystemOps()
        mock_fs.should_fail = True

        config = StormLoggerConfig(log_directory=Path("/test/logs"))
        config._fs_ops = mock_fs

        errors = config.validate()
        assert len(errors) == 1
        assert "Log directory not writable" in errors[0]

        # Test invalid file size
        config = StormLoggerConfig(max_file_size=0)
        errors = config.validate()
        assert "max_file_size must be greater than 0" in errors

        # Test invalid backup count
        config = StormLoggerConfig(backup_count=-1)
        errors = config.validate()
        assert "backup_count must be non-negative" in errors

        # Test async logging validation
        config = StormLoggerConfig(enable_async_logging=True, batch_size=0, flush_interval=0)
        errors = config.validate()
        assert any("batch_size must be greater than 0" in e for e in errors)
        assert any("flush_interval must be greater than 0" in e for e in errors)

    def test_path_normalization(self):
        """Test that string paths are converted to Path objects."""
        config = StormLoggerConfig(
            log_directory="/test/logs", archive_directory="/test/archive"  # String  # String
        )

        assert isinstance(config.log_directory, Path)
        assert isinstance(config.archive_directory, Path)
        assert str(config.log_directory) == "/test/logs"
        assert str(config.archive_directory) == "/test/archive"

    def test_log_level_normalization(self):
        """Test that string log levels are converted to enum."""
        config = StormLoggerConfig(log_level="debug")  # Lowercase string
        assert config.log_level == LogLevel.DEBUG

        config = StormLoggerConfig(log_level="ERROR")  # Uppercase string
        assert config.log_level == LogLevel.ERROR

    def test_validate_log_level_method_directly(self):
        """Test _validate_log_level method directly to ensure string conversion branch is covered."""
        # Create a config object without going through normal initialization
        config = StormLoggerConfig.__new__(StormLoggerConfig)

        # Set the log_level as a string directly (simulating what would happen internally)
        config.log_level = "warning"  # lowercase string

        # Call the validation method directly
        config._validate_log_level()

        # Verify it was converted to the enum
        assert config.log_level == LogLevel.WARNING
        assert isinstance(config.log_level, LogLevel)

        # Test with mixed case
        config.log_level = "CrItIcAl"  # mixed case string
        config._validate_log_level()
        assert config.log_level == LogLevel.CRITICAL

        # Test when already an enum (should not change)
        config.log_level = LogLevel.INFO
        config._validate_log_level()
        assert config.log_level == LogLevel.INFO

    def test_async_logging_validation_branches(self):
        """Test async logging validation to cover missing branches."""
        # Test with valid async settings (both batch_size and flush_interval > 0)
        config = StormLoggerConfig(enable_async_logging=True, batch_size=100, flush_interval=5.0)
        errors = config.validate()
        # Should have no async-related errors
        assert not any("batch_size" in e or "flush_interval" in e for e in errors)

        # Test with invalid batch_size only
        config = StormLoggerConfig(
            enable_async_logging=True, batch_size=0, flush_interval=5.0  # Invalid  # Valid
        )
        errors = config.validate()
        assert any("batch_size must be greater than 0" in e for e in errors)
        assert not any("flush_interval must be greater than 0" in e for e in errors)

        # Test with invalid flush_interval only
        config = StormLoggerConfig(
            enable_async_logging=True, batch_size=100, flush_interval=0  # Valid  # Invalid
        )
        errors = config.validate()
        assert not any("batch_size must be greater than 0" in e for e in errors)
        assert any("flush_interval must be greater than 0" in e for e in errors)

    def test_validate_no_log_directory(self):
        """Test validation when log_directory is None to cover missing branch."""
        config = StormLoggerConfig()
        config.log_directory = None  # Force None
        config._fs_ops = MockFileSystemOps()

        errors = config.validate()
        # Should not attempt to validate a None log_directory
        # No errors should be related to log directory
        assert not any("Log directory" in e for e in errors)

    def test_create_log_directories_no_log_directory(self):
        """Test create_log_directories when log_directory is None."""
        config = StormLoggerConfig()
        config.log_directory = None  # Force None
        config._fs_ops = MockFileSystemOps()

        # Should not raise any errors
        config.create_log_directories()

        # No directories should have been created
        assert len(config._fs_ops.created_dirs) == 0

    def test_get_log_handlers_with_enable_console_false(self):
        """Test get_log_handlers when enable_console is False."""
        config = StormLoggerConfig(
            enable_console=False,
            enable_file_logging=False,
            debug_mode=True,  # This prevents production_mode from being auto-set to True
            production_mode=False,
        )
        handlers = config.get_log_handlers()
        assert "console" not in handlers
        assert handlers == []  # No handlers at all

    def test_django_framework_archive_directory_none(self):
        """Test Django framework with archive_directory=None."""
        mock_fs = MockFileSystemOps()

        config = StormLoggerConfig(
            log_directory=Path("/test/logs"),
            framework=Framework.DJANGO,
            archive_directory=None,  # No archive directory
        )
        config._fs_ops = mock_fs

        config.create_log_directories()

        # Check Django directory was created but not archive
        created_paths = [str(path) for path, _, _ in mock_fs.created_dirs]
        assert str(Path("/test/logs/django")) in created_paths
        assert not any("archive" in path for path in created_paths)

    def test_non_django_framework_directory_creation(self):
        """Test directory creation when framework is NOT Django to cover the false branch."""
        mock_fs = MockFileSystemOps()

        # Test with different frameworks
        for framework in [Framework.FLASK, Framework.FASTAPI, Framework.STANDALONE]:
            mock_fs.created_dirs.clear()  # Reset for each test

            config = StormLoggerConfig(
                log_directory=Path("/test/logs"),
                framework=framework,  # Not Django
                archive_directory=Path("/test/archive"),
            )
            config._fs_ops = mock_fs

            config.create_log_directories()

            # Check that Django directory was NOT created
            created_paths = [str(path) for path, _, _ in mock_fs.created_dirs]
            assert str(Path("/test/logs/django")) not in created_paths
            # But other directories should be created
            assert "/test/logs" in created_paths
            assert str(Path("/test/logs/app")) in created_paths
            assert str(Path("/test/logs/errors")) in created_paths
            assert "/test/archive" in created_paths


class TestConfigManager:
    """Test the ConfigManager class."""

    def test_basic_operations(self):
        """Test basic config manager operations."""
        manager = ConfigManager()

        # Initially no config
        assert manager._config is None

        # Set config
        config = StormLoggerConfig(log_level=LogLevel.WARNING)
        manager.set_config(config)
        assert manager.get_config() == config

        # Reset
        manager.reset()
        assert manager._config is None

    def test_lazy_initialization(self):
        """Test that config is created lazily from environment."""
        mock_env = MockEnvironmentReader({"STORM_LOG_LEVEL": "ERROR"})

        manager = ConfigManager()
        manager.with_env_reader(mock_env)

        # Config not created yet
        assert manager._config is None

        # Get config triggers creation
        config = manager.get_config()
        assert config.log_level == LogLevel.ERROR
        assert manager._config is not None

    def test_dependency_injection_methods(self):
        """Test the fluent interface for dependency injection."""
        mock_env = MockEnvironmentReader()
        mock_detector = MockFrameworkDetector()
        mock_fs = MockFileSystemOps()

        manager = (
            ConfigManager()
            .with_env_reader(mock_env)
            .with_framework_detector(mock_detector)
            .with_fs_ops(mock_fs)
        )

        assert manager._env_reader == mock_env
        assert manager._framework_detector == mock_detector
        assert manager._fs_ops == mock_fs


class TestGlobalFunctions:
    """Test the global configuration functions."""

    def setup_method(self):
        """Reset config before each test."""
        reset_config()

    def test_get_config(self):
        """Test getting global config."""
        config = get_config()
        assert isinstance(config, StormLoggerConfig)

    def test_set_and_get_config(self):
        """Test setting and getting global config."""
        custom_config = StormLoggerConfig(log_level=LogLevel.CRITICAL)
        set_config(custom_config)

        retrieved = get_config()
        assert retrieved == custom_config
        assert retrieved.log_level == LogLevel.CRITICAL

    def test_reset_config(self):
        """Test resetting global config."""
        # Set a custom config
        set_config(StormLoggerConfig(log_level=LogLevel.ERROR))

        # Reset
        reset_config()

        # Next get_config should create a new one from environment
        new_config = get_config()
        # Should have default values (not ERROR)
        assert new_config.log_level != LogLevel.ERROR

    def test_get_config_manager(self):
        """Test getting the config manager for testing."""
        manager = get_config_manager()
        assert isinstance(manager, ConfigManager)

        # Should be the same instance
        assert manager is get_config_manager()

    def test_config_manager_integration(self):
        """Test using config manager for testing scenarios."""
        # Set up custom environment for testing
        mock_env = MockEnvironmentReader(
            {"STORM_LOG_LEVEL": "DEBUG", "STORM_ENABLE_CONSOLE": "false"}
        )

        manager = get_config_manager()
        manager.reset()
        manager.with_env_reader(mock_env)

        # Now get_config should use our mock environment
        config = get_config()
        assert config.log_level == LogLevel.DEBUG
        assert config.enable_console is False


class TestIntegrationScenarios:
    """Test complete integration scenarios."""

    def setup_method(self):
        """Reset config before each test."""
        reset_config()

    def test_complete_django_setup(self):
        """Test a complete Django setup scenario."""
        # Mock environment for Django production
        env_vars = {
            "STORM_LOG_LEVEL": "INFO",
            "STORM_LOG_DIR": "/var/log/myapp",
            "STORM_PRODUCTION": "true",
            "DEBUG": "false",
        }

        mock_env = MockEnvironmentReader(env_vars)
        mock_detector = MockFrameworkDetector(Framework.DJANGO)
        mock_fs = MockFileSystemOps()

        # Configure the manager
        manager = get_config_manager()
        manager.reset()
        manager.with_env_reader(mock_env)
        manager.with_framework_detector(mock_detector)
        manager.with_fs_ops(mock_fs)

        # Create config - this will use our mocks
        config = get_config()

        # Verify configuration
        assert config.log_level == LogLevel.INFO
        assert config.log_directory == Path("/var/log/myapp")
        assert config.production_mode is True
        assert config.debug_mode is False
        assert config.framework == Framework.DJANGO

        # Create directories
        config.create_log_directories()

        # Verify all Django directories were created
        created_paths = [str(path) for path, _, _ in mock_fs.created_dirs]
        assert "/var/log/myapp" in created_paths
        assert str(Path("/var/log/myapp/django")) in created_paths

        # Validate
        errors = config.validate()
        assert errors == []

    def test_development_setup_with_errors(self):
        """Test a development setup with permission errors."""
        # Mock environment for development
        env_vars = {
            "DEBUG": "true",
            "STORM_LOG_DIR": "/restricted/logs",
        }

        mock_env = MockEnvironmentReader(env_vars)
        mock_fs = MockFileSystemOps()
        mock_fs.fail_on_path = Path("/restricted/logs")

        # Configure the manager
        manager = get_config_manager()
        manager.reset()
        manager.with_env_reader(mock_env)

        # Create config
        config = get_config()
        config._fs_ops = mock_fs

        # Try to create directories (should fail)
        with patch("warnings.warn") as mock_warn:
            config.create_log_directories()

        # Should fall back to console only
        assert config.enable_file_logging is False
        mock_warn.assert_called_once()

        # Handlers should not include file handlers when enable_file_logging is False
        handlers = config.get_log_handlers()
        assert "console" in handlers
        assert "file" not in handlers
        assert "error_file" not in handlers


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