"""
Tests for retry mechanism module.
"""

import pytest
import time
from unittest.mock import Mock, patch

from openHealth.core.retry import RetryHandler, retry_on_failure


class TestRetryHandler:
    """Test cases for RetryHandler class."""
    
    def test_retry_handler_initialization(self):
        """Test RetryHandler initialization with default and custom parameters."""
        # Default initialization
        handler = RetryHandler()
        assert handler.max_retries == 3
        assert handler.base_delay == 1.0
        assert handler.backoff_factor == 2.0
        
        # Custom initialization
        handler = RetryHandler(max_retries=5, base_delay=0.5, backoff_factor=1.5)
        assert handler.max_retries == 5
        assert handler.base_delay == 0.5
        assert handler.backoff_factor == 1.5
    
    def test_execute_with_retry_success_on_first_attempt(self):
        """Test retry execution when function succeeds on first attempt."""
        handler = RetryHandler(max_retries=3)
        mock_func = Mock(return_value="success")
        
        result = handler.execute_with_retry(mock_func, "arg1", kwarg1="value1")
        
        assert result == "success"
        mock_func.assert_called_once_with("arg1", kwarg1="value1")
    
    def test_execute_with_retry_success_after_retries(self):
        """Test retry execution when function succeeds after some retries."""
        handler = RetryHandler(max_retries=3, base_delay=0.1)
        mock_func = Mock()
        # Fail twice, then succeed
        mock_func.side_effect = [Exception("Fail 1"), Exception("Fail 2"), "success"]
        
        start_time = time.time()
        result = handler.execute_with_retry(mock_func)
        end_time = time.time()
        
        assert result == "success"
        assert mock_func.call_count == 3
        # Should have waited some time due to retries
        assert end_time - start_time >= 0.1  # At least base delay
    
    def test_execute_with_retry_all_attempts_fail(self):
        """Test retry execution when all attempts fail."""
        handler = RetryHandler(max_retries=2, base_delay=0.1)
        mock_func = Mock()
        always_fail = Exception("Always fail")
        mock_func.side_effect = always_fail
        
        start_time = time.time()
        with pytest.raises(Exception) as exc_info:
            handler.execute_with_retry(mock_func)
        end_time = time.time()
        
        assert exc_info.value == always_fail
        assert mock_func.call_count == 3  # 1 initial + 2 retries
        # Should have waited some time due to retries
        assert end_time - start_time >= 0.1  # At least base delay
    
    def test_execute_with_retry_exponential_backoff(self):
        """Test that retry delay increases exponentially."""
        handler = RetryHandler(max_retries=3, base_delay=0.1, backoff_factor=2.0)
        mock_func = Mock()
        
        # Record timing
        call_times = []
        
        def side_effect(*args, **kwargs):
            call_times.append(time.time())
            if len(call_times) < 4:  # Fail first 3 times
                raise Exception("Fail")
            return "success"
        
        mock_func.side_effect = side_effect
        
        handler.execute_with_retry(mock_func)
        
        # Check that delays are increasing
        if len(call_times) >= 3:
            first_delay = call_times[1] - call_times[0]
            second_delay = call_times[2] - call_times[1]
            # Second delay should be roughly twice the first (with some tolerance)
            assert second_delay >= first_delay * 1.5
    
    def test_execute_with_retry_different_exception_types(self):
        """Test retry execution with different exception types."""
        handler = RetryHandler(max_retries=2, base_delay=0.1)
        mock_func = Mock()
        
        # Test with ValueError
        mock_func.side_effect = [ValueError("Bad value"), "success"]
        result = handler.execute_with_retry(mock_func)
        assert result == "success"
        
        mock_func.reset_mock()
        
        # Test with custom exception
        custom_error = RuntimeError("Custom error")
        mock_func.side_effect = [custom_error, "success"]
        result = handler.execute_with_retry(mock_func)
        assert result == "success"
    
    def test_retry_decorator_success(self):
        """Test retry decorator with successful function."""
        @retry_on_failure(max_retries=2, base_delay=0.1)
        def test_function():
            return "decorated success"
        
        result = test_function()
        assert result == "decorated success"
    
    def test_retry_decorator_failure(self):
        """Test retry decorator with failing function."""
        @retry_on_failure(max_retries=1, base_delay=0.1)
        def failing_function():
            raise ValueError("decorated failure")
        
        with pytest.raises(ValueError) as exc_info:
            failing_function()
        assert str(exc_info.value) == "decorated failure"
    
    def test_retry_decorator_with_args_and_kwargs(self):
        """Test retry decorator with function arguments and keyword arguments."""
        @retry_on_failure(max_retries=1, base_delay=0.1)
        def function_with_args(arg1, arg2, kwarg1=None):
            return f"{arg1}-{arg2}-{kwarg1}"
        
        result = function_with_args("test1", "test2", kwarg1="kwvalue")
        assert result == "test1-test2-kwvalue"
    
    def test_retry_decorator_with_custom_exceptions(self):
        """Test retry decorator with custom exception types."""
        @retry_on_failure(max_retries=1, base_delay=0.1, exceptions=(ValueError,))
        def function_with_value_error():
            raise ValueError("value error")
        
        @retry_on_failure(max_retries=1, base_delay=0.1, exceptions=(ValueError,))
        def function_with_type_error():
            raise TypeError("type error")
        
        # Should retry ValueError
        with pytest.raises(ValueError):
            function_with_value_error()
        
        # Should not retry TypeError (will raise immediately)
        with pytest.raises(TypeError):
            function_with_type_error()
    
    def test_retry_handler_edge_cases(self):
        """Test retry handler with edge cases."""
        # Zero retries
        handler = RetryHandler(max_retries=0, base_delay=0.1)
        mock_func = Mock(side_effect=Exception("Fail"))
        
        with pytest.raises(Exception):
            handler.execute_with_retry(mock_func)
        assert mock_func.call_count == 1  # Only initial attempt, no retries
        
        # Zero base delay
        handler = RetryHandler(max_retries=2, base_delay=0, backoff_factor=2.0)
        mock_func = Mock(side_effect=[Exception("Fail 1"), "success"])
        
        start_time = time.time()
        result = handler.execute_with_retry(mock_func)
        end_time = time.time()
        
        assert result == "success"
        # Should complete quickly with zero delay
        assert end_time - start_time < 0.1
    
    @patch('openHealth.core.retry.logger')
    def test_retry_handler_logging(self, mock_logger):
        """Test that retry handler logs appropriate messages."""
        handler = RetryHandler(max_retries=1, base_delay=0.1)
        mock_func = Mock()
        mock_func.side_effect = [Exception("Fail"), "success"]
        
        handler.execute_with_retry(mock_func)
        
        # Should log a warning for the failed attempt
        mock_logger.warning.assert_called()
        warning_call = mock_logger.warning.call_args[0][0]
        assert "Attempt 1 failed" in warning_call
        
        # Should not log an error since it eventually succeeded
        mock_logger.error.assert_not_called()
    
    @patch('openHealth.core.retry.logger')
    def test_retry_handler_logging_final_failure(self, mock_logger):
        """Test that retry handler logs error when all attempts fail."""
        handler = RetryHandler(max_retries=1, base_delay=0.1)
        mock_func = Mock(side_effect=Exception("Always fail"))
        
        try:
            handler.execute_with_retry(mock_func)
        except Exception:
            pass
        
        # Should log an error for the final failure
        mock_logger.error.assert_called()
        error_call = mock_logger.error.call_args[0][0]
        assert "All 2 attempts failed" in error_call