"""
Comprehensive unit tests for base.py module.
Tests all methods and edge cases to achieve 100% coverage.
"""

import pytest
import logging
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch, call
from typing import Any, Dict, Optional

from storm_logger.core.base import (
    BaseStormLogger, check_optional_dependency, get_colored_formatter,
    get_json_formatter, create_base_logging_config, setup_logging_directories
)
from storm_logger.core.base_interfaces import (
    LoggerFactory, DependencyChecker, FormatterFactory,
    ConfigurationBuilder, DirectoryManager
)
from storm_logger.config import StormLoggerConfig, LogLevel


class ConcreteStormLogger(BaseStormLogger):
    """Concrete implementation for testing abstract base class."""
    
    def _configure_handlers(self) -> None:
        """Test implementation of abstract method."""
        self._test_handlers_configured = True
    
    def _configure_formatters(self) -> None:
        """Test implementation of abstract method."""
        self._test_formatters_configured = True


class MockLoggerFactory(LoggerFactory):
    """Mock implementation of LoggerFactory for testing."""
    
    def __init__(self):
        self.logger = Mock()
        self.logger.level = logging.INFO
        self.log_calls = []
    
    def get_logger(self, name: str) -> Any:
        self.logger.name = name
        return self.logger
    
    def set_level(self, logger: Any, level: int) -> None:
        logger.level = level
    
    def get_level(self, logger: Any) -> int:
        return logger.level
    
    def is_enabled_for(self, logger: Any, level: int) -> bool:
        return logger.level <= level
    
    def log(self, logger: Any, level: int, message: str, **kwargs) -> None:
        self.log_calls.append({
            'logger': logger,
            'level': level,
            'message': message,
            'kwargs': kwargs
        })


class MockDirectoryManager(DirectoryManager):
    """Mock implementation of DirectoryManager for testing."""
    
    def __init__(self):
        self.directories_created = False
        self.created_paths = []
    
    def create_directories(self, config: Any) -> None:
        self.directories_created = True
        self.created_paths.append(config.log_directory)
    
    def ensure_directory_exists(self, path: Path) -> None:
        self.created_paths.append(path)


class MockDependencyChecker(DependencyChecker):
    """Mock implementation of DependencyChecker for testing."""
    
    def __init__(self, available_modules=None):
        self.available_modules = available_modules or []
    
    def is_available(self, module_name: str) -> bool:
        return module_name in self.available_modules


class TestBaseStormLogger:
    """Test suite for BaseStormLogger class."""
    
    @pytest.fixture
    def mock_config(self):
        """Provide mock configuration."""
        config = Mock(spec=StormLoggerConfig)
        config.get_logging_level.return_value = logging.INFO
        config.log_directory = Path("/test/logs")
        return config
    
    @pytest.fixture
    def mock_logger_factory(self):
        """Provide mock logger factory."""
        return MockLoggerFactory()
    
    @pytest.fixture
    def mock_directory_manager(self):
        """Provide mock directory manager."""
        return MockDirectoryManager()
    
    @pytest.fixture
    def logger(self, mock_config, mock_logger_factory, mock_directory_manager):
        """Provide configured ConcreteStormLogger with mocks."""
        return ConcreteStormLogger(
            name="test_logger",
            config=mock_config,
            logger_factory=mock_logger_factory,
            directory_manager=mock_directory_manager
        )
    
    def test_init_with_defaults(self):
        """Test initialization with default dependencies."""
        with patch('storm_logger.core.base.get_config') as mock_get_config:
            mock_get_config.return_value = Mock()
            logger = ConcreteStormLogger(name="test")
            assert logger.name == "test"
            assert logger._logger is None
            assert logger._handlers_configured is False
    
    def test_init_with_custom_dependencies(self, mock_config):
        """Test initialization with custom dependencies."""
        mock_factory = Mock()
        mock_manager = Mock()
        
        logger = ConcreteStormLogger(
            name="custom",
            config=mock_config,
            logger_factory=mock_factory,
            directory_manager=mock_manager
        )
        
        assert logger.name == "custom"
        assert logger.config is mock_config
        assert logger._logger_factory is mock_factory
        assert logger._directory_manager is mock_manager
    
    def test_logger_property_lazy_initialization(self, logger, mock_logger_factory):
        """Test logger property creates logger on first access."""
        assert logger._logger is None
        
        # First access should create logger
        result = logger.logger
        assert result is mock_logger_factory.logger
        assert logger._handlers_configured is True
        
        # Second access should return same instance
        result2 = logger.logger
        assert result2 is result
    
    def test_configure_logger(self, logger, mock_config, mock_logger_factory, mock_directory_manager):
        """Test logger configuration."""
        logger._configure_logger()
        
        # Should set level
        assert mock_logger_factory.logger.level == logging.INFO
        
        # Should create directories
        assert mock_directory_manager.directories_created is True
        
        # Should configure handlers and formatters
        assert hasattr(logger, '_test_handlers_configured')
        assert hasattr(logger, '_test_formatters_configured')
        assert logger._handlers_configured is True
    
    def test_configure_logger_only_once(self, logger):
        """Test that configuration only happens once."""
        logger._configure_logger()
        logger._test_counter = 1
        
        # Second call should not reconfigure
        logger._configure_logger()
        assert logger._test_counter == 1
    
    def test_debug(self, logger, mock_logger_factory):
        """Test debug logging."""
        logger.debug("Debug message", extra_data="test")
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == logging.DEBUG
        assert call['message'] == "Debug message"
        assert call['kwargs'] == {'extra_data': 'test'}
    
    def test_info(self, logger, mock_logger_factory):
        """Test info logging."""
        logger.info("Info message", key="value")
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == logging.INFO
        assert call['message'] == "Info message"
        assert call['kwargs'] == {'key': 'value'}
    
    def test_warning(self, logger, mock_logger_factory):
        """Test warning logging."""
        logger.warning("Warning message")
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == logging.WARNING
        assert call['message'] == "Warning message"
    
    def test_error_without_exc_info(self, logger, mock_logger_factory):
        """Test error logging without exception info."""
        logger.error("Error message")
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == logging.ERROR
        assert call['message'] == "Error message"
        assert call['kwargs']['exc_info'] is False
    
    def test_error_with_exc_info(self, logger, mock_logger_factory):
        """Test error logging with exception info."""
        logger.error("Error with traceback", exc_info=True)
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == logging.ERROR
        assert call['kwargs']['exc_info'] is True
    
    def test_critical_without_exc_info(self, logger, mock_logger_factory):
        """Test critical logging without exception info."""
        logger.critical("Critical message")
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == logging.CRITICAL
        assert call['message'] == "Critical message"
        assert call['kwargs']['exc_info'] is False
    
    def test_critical_with_exc_info(self, logger, mock_logger_factory):
        """Test critical logging with exception info."""
        logger.critical("Critical with traceback", exc_info=True, custom="data")
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == logging.CRITICAL
        assert call['kwargs']['exc_info'] is True
        assert call['kwargs']['custom'] == "data"
    
    def test_exception(self, logger, mock_logger_factory):
        """Test exception logging."""
        logger.exception("Exception occurred", request_id="123")
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == logging.ERROR
        assert call['message'] == "Exception occurred"
        assert call['kwargs']['exc_info'] is True
        assert call['kwargs']['request_id'] == "123"
    
    def test_log_custom_level(self, logger, mock_logger_factory):
        """Test logging at custom level."""
        custom_level = 35
        logger.log(custom_level, "Custom level message")
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == custom_level
        assert call['message'] == "Custom level message"
    
    def test_success(self, logger, mock_logger_factory):
        """Test success message logging."""
        logger.success("Operation completed", duration=1.5)
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == logging.INFO
        assert call['message'] == "✅ SUCCESS: Operation completed"
        assert call['kwargs']['duration'] == 1.5
    
    def test_test_pass(self, logger, mock_logger_factory):
        """Test passed test logging."""
        logger.test_pass("test_feature", test_id="001")
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == logging.INFO
        assert call['message'] == "✅ PASS: test_feature"
        assert call['kwargs']['test_id'] == "001"
    
    def test_test_fail_without_error_message(self, logger, mock_logger_factory):
        """Test failed test logging without error message."""
        logger.test_fail("test_broken_feature")
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == logging.ERROR
        assert call['message'] == "❌ FAIL: test_broken_feature"
    
    def test_test_fail_with_error_message(self, logger, mock_logger_factory):
        """Test failed test logging with error message."""
        logger.test_fail("test_validation", "Expected 5 but got 3", line=42)
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == logging.ERROR
        assert call['message'] == "❌ FAIL: test_validation - Expected 5 but got 3"
        assert call['kwargs']['line'] == 42
    
    def test_helpful_tip(self, logger, mock_logger_factory):
        """Test helpful tip logging."""
        logger.helpful_tip("Use caching for better performance")
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == logging.INFO
        assert call['message'] == "💡 TIP: Use caching for better performance"
    
    def test_teaching_moment(self, logger, mock_logger_factory):
        """Test educational content logging."""
        logger.teaching_moment("Dependency injection improves testability", topic="design")
        
        assert len(mock_logger_factory.log_calls) == 1
        call = mock_logger_factory.log_calls[0]
        assert call['level'] == logging.INFO
        assert call['message'] == "📚 LEARN: Dependency injection improves testability"
        assert call['kwargs']['topic'] == "design"
    
    def test_is_enabled_for(self, logger, mock_logger_factory):
        """Test checking if level is enabled."""
        mock_logger_factory.logger.level = logging.INFO
        
        assert logger.isEnabledFor(logging.ERROR) is True
        assert logger.isEnabledFor(logging.INFO) is True
        assert logger.isEnabledFor(logging.DEBUG) is False
    
    def test_set_level(self, logger, mock_logger_factory):
        """Test setting log level."""
        logger.setLevel(logging.DEBUG)
        assert mock_logger_factory.logger.level == logging.DEBUG
        
        logger.setLevel(logging.WARNING)
        assert mock_logger_factory.logger.level == logging.WARNING
    
    def test_get_level(self, logger, mock_logger_factory):
        """Test getting current log level."""
        # Set level after initialization
        logger.setLevel(logging.ERROR)
        assert logger.getLevel() == logging.ERROR


class TestModuleFunctions:
    """Test module-level functions."""
    
    def test_check_optional_dependency_available(self):
        """Test checking available dependency."""
        mock_checker = MockDependencyChecker(['colorlog', 'pytest'])
        result = check_optional_dependency('colorlog', mock_checker)
        assert result is True
    
    def test_check_optional_dependency_not_available(self):
        """Test checking unavailable dependency."""
        mock_checker = MockDependencyChecker(['pytest'])
        result = check_optional_dependency('colorlog', mock_checker)
        assert result is False
    
    def test_check_optional_dependency_default_checker(self):
        """Test with default dependency checker."""
        # Test with a module that exists  
        result = check_optional_dependency('os')
        assert result is True
        
        # Test with a module that doesn't exist
        result = check_optional_dependency('non_existent_module_xyz')
        assert result is False
    
    def test_get_colored_formatter_with_colorlog(self):
        """Test getting colored formatter when colorlog is available."""
        mock_checker = MockDependencyChecker(['colorlog'])
        result = get_colored_formatter(mock_checker)
        
        assert '()' in result
        assert result['()'] == 'colorlog.ColoredFormatter'
        assert 'log_colors' in result
    
    def test_get_colored_formatter_without_colorlog(self):
        """Test getting colored formatter fallback."""
        mock_checker = MockDependencyChecker([])
        result = get_colored_formatter(mock_checker)
        
        assert 'format' in result
        assert '[{levelname}]' in result['format']
        assert '()' not in result  # No special formatter class
    
    def test_get_json_formatter_with_module(self):
        """Test getting JSON formatter when module is available."""
        mock_checker = MockDependencyChecker(['pythonjsonlogger'])
        result = get_json_formatter(mock_checker)
        
        assert '()' in result
        assert result['()'] == 'pythonjsonlogger.jsonlogger.JsonFormatter'
    
    def test_get_json_formatter_without_module(self):
        """Test getting JSON formatter fallback."""
        mock_checker = MockDependencyChecker([])
        result = get_json_formatter(mock_checker)
        
        assert 'format' in result
        assert 'Process:' in result['format']
        assert 'Thread:' in result['format']
    
    def test_create_base_logging_config_minimal(self):
        """Test creating minimal logging configuration."""
        config = Mock(spec=StormLoggerConfig)
        config.log_directory = Path("/logs")
        config.log_level = Mock(value="INFO")
        config.console_colors = False
        config.enable_file_logging = False
        config.debug_mode = False
        
        result = create_base_logging_config(config)
        
        assert result['version'] == 1
        assert result['disable_existing_loggers'] is False
        assert 'console' in result['handlers']
        assert 'app_file' not in result['handlers']
        assert len(result['loggers']) == 2
    
    def test_create_base_logging_config_with_files(self):
        """Test creating configuration with file logging."""
        config = Mock(spec=StormLoggerConfig)
        config.log_directory = Path("/logs")
        config.log_level = Mock(value="DEBUG")
        config.console_colors = True
        config.enable_file_logging = True
        config.max_file_size = 10485760
        config.backup_count = 5
        config.debug_mode = False
        
        result = create_base_logging_config(config)
        
        assert 'app_file' in result['handlers']
        assert 'error_file' in result['handlers']
        assert result['handlers']['app_file']['maxBytes'] == 10485760
        assert result['handlers']['error_file']['maxBytes'] == 5242880
    
    def test_create_base_logging_config_debug_mode(self):
        """Test creating configuration in debug mode."""
        config = Mock(spec=StormLoggerConfig)
        config.log_directory = Path("/logs")
        config.log_level = Mock(value="DEBUG")
        config.console_colors = True
        config.enable_file_logging = True
        config.max_file_size = 1048576
        config.backup_count = 3
        config.debug_mode = True
        
        result = create_base_logging_config(config)
        
        assert 'debug_file' in result['handlers']
        assert result['handlers']['debug_file']['level'] == 'DEBUG'
        assert result['handlers']['debug_file']['maxBytes'] == 2097152
        
        # Debug handler should be added to all loggers
        for logger_config in result['loggers'].values():
            assert 'debug_file' in logger_config['handlers']
    
    def test_create_base_logging_config_custom_builder(self):
        """Test with custom configuration builder."""
        config = Mock()
        mock_builder = Mock(spec=ConfigurationBuilder)
        mock_builder.build_base_config.return_value = {'custom': 'config'}
        
        result = create_base_logging_config(config, mock_builder)
        
        assert result == {'custom': 'config'}
        mock_builder.build_base_config.assert_called_once_with(config)
    
    def test_setup_logging_directories(self):
        """Test setting up logging directories."""
        config = Mock()
        mock_manager = MockDirectoryManager()
        
        setup_logging_directories(config, mock_manager)
        
        assert mock_manager.directories_created is True
    
    def test_setup_logging_directories_default_manager(self):
        """Test with default directory manager."""
        config = Mock()
        config.create_log_directories = Mock()
        
        with patch('storm_logger.core.base.DefaultDirectoryManager') as MockManager:
            mock_instance = Mock()
            MockManager.return_value = mock_instance
            
            setup_logging_directories(config)
            mock_instance.create_directories.assert_called_once_with(config)


class TestBaseStormLoggerAbstractMethods:
    """Test that abstract methods must be implemented."""
    
    def test_cannot_instantiate_abstract_class(self):
        """Test that BaseStormLogger cannot be instantiated directly."""
        with pytest.raises(TypeError) as exc_info:
            BaseStormLogger("test")
        assert "Can't instantiate abstract class" in str(exc_info.value)
    
    def test_concrete_class_must_implement_abstract_methods(self):
        """Test that concrete classes must implement abstract methods."""
        
        class IncompleteLogger(BaseStormLogger):
            """Missing _configure_formatters implementation."""
            def _configure_handlers(self):
                pass
        
        with pytest.raises(TypeError) as exc_info:
            IncompleteLogger("test")
        assert "_configure_formatters" in str(exc_info.value)


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