"""Integration tests for error recovery workflows.

Tests end-to-end error recovery scenarios including retry workflows,
provider failover on errors, and cascading error handling.
"""

from unittest.mock import MagicMock, patch

import pytest

from src.core.error_handler import (
    DownloadError,
    NetworkError,
    NonRetryableError,
    RecoveryStrategies,
    RetryableError,
    with_error_recovery,
)


class TestDownloadRetryWorkflow:
    """End-to-end tests: download fails → retries → eventually succeeds/fails."""

    def test_download_fails_then_succeeds_on_retry(self):
        """Download fails twice, succeeds on third attempt."""
        call_log = []

        @with_error_recovery(max_retries=3, context="download")
        def download_file(url: str):
            call_log.append(url)
            if len(call_log) < 3:
                raise DownloadError("connection reset")
            return f"downloaded:{url}"

        result = download_file("https://example.com/video.mp4")
        assert result == "downloaded:https://example.com/video.mp4"
        assert len(call_log) == 3

    def test_download_exhausts_retries_then_raises(self):
        """Download fails all retry attempts and raises final error."""

        @with_error_recovery(max_retries=3, context="download")
        def always_fail_download():
            raise DownloadError("server unavailable")

        with pytest.raises(DownloadError, match="server unavailable"):
            always_fail_download()

    def test_non_retryable_error_aborts_immediately(self):
        """NonRetryableError stops retry loop on first occurrence."""
        attempts = []

        @with_error_recovery(max_retries=5, context="download")
        def corrupt_download():
            attempts.append(1)
            raise NonRetryableError("file is corrupt, don't retry")

        with pytest.raises(NonRetryableError):
            corrupt_download()
        assert len(attempts) == 1


class TestNetworkRecoveryWorkflow:
    """Tests for network error recovery with RecoveryStrategies."""

    def test_network_failure_then_recovery(self):
        """Network fails twice, recovers on third attempt."""
        attempts = []

        def fetch_data():
            attempts.append(1)
            if len(attempts) < 3:
                raise NetworkError("timeout")
            return {"data": "anime_list"}

        result = RecoveryStrategies.handle_network_failure(fetch_data)
        assert result == {"data": "anime_list"}
        assert len(attempts) == 3

    def test_connection_error_then_recovery(self):
        """ConnectionError (stdlib) is handled by network recovery."""
        attempts = []

        def connect():
            attempts.append(1)
            if len(attempts) == 1:
                raise ConnectionError("refused")
            return "connected"

        result = RecoveryStrategies.handle_network_failure(connect)
        assert result == "connected"
        assert len(attempts) == 2


class TestProviderFailoverOnError:
    """Tests for provider failover when errors occur."""

    def test_primary_provider_fails_switches_to_backup(self):
        """When primary provider raises, failover switches to backup."""
        primary = MagicMock(side_effect=NetworkError("primary down"))
        backup = MagicMock(return_value="backup_result")
        providers = [primary, backup]

        result = None
        for provider in providers:
            try:
                result = provider()
                break
            except (NetworkError, ConnectionError):
                continue

        assert result == "backup_result"
        primary.assert_called_once()
        backup.assert_called_once()

    def test_all_providers_fail_raises(self):
        """When all providers fail, the last error propagates."""
        providers = [
            MagicMock(side_effect=NetworkError("p1 down")),
            MagicMock(side_effect=NetworkError("p2 down")),
            MagicMock(side_effect=NetworkError("p3 down")),
        ]

        last_error = None
        for provider in providers:
            try:
                provider()
                break
            except NetworkError as e:
                last_error = e

        assert last_error is not None
        assert "p3 down" in str(last_error)

    def test_failover_with_retry_per_provider(self):
        """Each provider gets retries before moving to next."""
        p1_calls = []
        p2_calls = []

        @with_error_recovery(max_retries=2, context="provider1")
        def provider1():
            p1_calls.append(1)
            raise NetworkError("p1 fail")

        @with_error_recovery(max_retries=2, context="provider2")
        def provider2():
            p2_calls.append(1)
            return "p2_success"

        result = None
        for provider_fn in [provider1, provider2]:
            try:
                result = provider_fn()
                break
            except NetworkError:
                continue

        assert result == "p2_success"
        assert len(p1_calls) == 2  # provider1 exhausted its retries
        assert len(p2_calls) == 1  # provider2 succeeded first try


class TestCascadingErrorHandling:
    """Tests for cascading error scenarios."""

    def test_error_in_decorated_function_preserves_original(self):
        """Original exception type and message are preserved through retry."""

        @with_error_recovery(max_retries=1, context="cascade")
        def inner_fail():
            raise ValueError("original error context")

        with pytest.raises(ValueError, match="original error context"):
            inner_fail()

    def test_nested_recovery_decorators(self):
        """Nested error recovery decorators work independently."""
        outer_attempts = []
        inner_attempts = []

        @with_error_recovery(max_retries=2, context="outer")
        def outer():
            outer_attempts.append(1)
            return inner()

        @with_error_recovery(max_retries=2, context="inner")
        def inner():
            inner_attempts.append(1)
            if len(inner_attempts) < 2:
                raise RuntimeError("inner fail")
            return "ok"

        result = outer()
        assert result == "ok"
        assert len(outer_attempts) == 1  # Outer didn't need to retry
        assert len(inner_attempts) == 2  # Inner retried once

    def test_error_recovery_with_different_error_types(self):
        """Recovery handles mixed error types across retries."""
        errors = iter([
            ConnectionError("refused"),
            TimeoutError("timed out"),
        ])

        @with_error_recovery(max_retries=3, context="mixed")
        def mixed_errors():
            try:
                raise next(errors)
            except StopIteration:
                return "recovered"

        result = mixed_errors()
        assert result == "recovered"


class TestResourceCleanupOnError:
    """Tests that resources are properly handled during error recovery."""

    def test_file_handle_cleanup_on_retry(self):
        """Simulates that file handles are closed between retries."""
        opened_files = []
        closed_files = []

        @with_error_recovery(max_retries=3, context="file_op")
        def file_operation():
            handle = MagicMock()
            opened_files.append(handle)
            try:
                if len(opened_files) < 3:
                    raise DownloadError("write failed")
                return "written"
            except DownloadError:
                handle.close()
                closed_files.append(handle)
                raise

        result = file_operation()
        assert result == "written"
        assert len(closed_files) == 2  # 2 failures closed their handles

    def test_download_progress_tracked_across_retries(self):
        """Download progress tracking works across retry attempts."""
        progress_log = []
        attempt = {"n": 0}

        @with_error_recovery(max_retries=3, context="download_progress")
        def download_with_progress():
            attempt["n"] += 1
            progress_log.append("started")
            if attempt["n"] < 3:
                progress_log.append("failed")
                raise DownloadError("interrupted")
            progress_log.append("completed")
            return "done"

        result = download_with_progress()
        assert result == "done"
        assert progress_log == [
            "started", "failed",
            "started", "failed",
            "started", "completed",
        ]


class TestErrorClassificationWorkflow:
    """Tests for correct error classification in workflows."""

    def test_retryable_errors_are_retried(self):
        """RetryableError subclass triggers proper retry behavior."""
        attempts = {"count": 0}

        @with_error_recovery(max_retries=3, context="classify")
        def operation():
            attempts["count"] += 1
            if attempts["count"] < 3:
                raise RetryableError("transient issue")
            return "success"

        assert operation() == "success"
        assert attempts["count"] == 3

    def test_non_retryable_errors_skip_retry(self):
        """NonRetryableError bypasses retry mechanism completely."""
        attempts = {"count": 0}

        @with_error_recovery(max_retries=10, context="classify")
        def operation():
            attempts["count"] += 1
            raise NonRetryableError("permanent failure")

        with pytest.raises(NonRetryableError):
            operation()
        assert attempts["count"] == 1

    def test_download_error_through_strategies(self):
        """DownloadError handled correctly by both strategies and decorator."""
        # Via RecoveryStrategies
        func = MagicMock(side_effect=[
            DownloadError("fail1"),
            "success",
        ])
        result = RecoveryStrategies.handle_download_failure(func)
        assert result == "success"

        # Via decorator
        counter = {"n": 0}

        @with_error_recovery(max_retries=3, context="dl")
        def dl():
            counter["n"] += 1
            if counter["n"] < 2:
                raise DownloadError("fail")
            return "downloaded"

        assert dl() == "downloaded"
