"""
Extended Tests for Failure Classifier System

Phase 16: Download Management Deep Coverage - Failure Classification Tests
Tests failure classification, domain-specific logic, and pattern matching.
"""

from datetime import datetime, timedelta, UTC

from local_deep_research.library.download_management.failure_classifier import (
    PermanentFailure,
    TemporaryFailure,
    RateLimitFailure,
    FailureClassifier,
)


class TestFailureClassification:
    """Tests for HTTP status code and error type classification"""

    def test_classify_http_404_not_found(self):
        """Test 404 status code is classified as permanent failure"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="http_error",
            status_code=404,
            url="https://example.com/resource.pdf",
        )

        assert isinstance(failure, PermanentFailure)
        assert failure.error_type == "not_found"
        assert "404" in failure.message
        assert failure.is_permanent() is True

    def test_classify_http_403_forbidden(self):
        """Test 403 status code is classified as permanent failure"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="http_error",
            status_code=403,
            url="https://example.com/forbidden.pdf",
        )

        assert isinstance(failure, PermanentFailure)
        assert failure.error_type == "forbidden"
        assert "403" in failure.message
        assert failure.is_permanent() is True

    def test_classify_http_429_rate_limit(self):
        """Test 429 status code is classified as rate limit failure"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="http_error",
            status_code=429,
            url="https://arxiv.org/pdf/2301.00001.pdf",
        )

        assert isinstance(failure, RateLimitFailure)
        assert failure.error_type == "rate_limited"
        assert failure.is_permanent() is False
        assert failure.domain == "arxiv.org"

    def test_classify_http_500_server_error(self):
        """Test 500 status code is classified as temporary (unknown error)"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="http_error",
            status_code=500,
            url="https://example.com/error.pdf",
        )

        # 500 doesn't have a specific handler, falls through to unknown
        assert isinstance(failure, TemporaryFailure)
        assert failure.is_permanent() is False

    def test_classify_http_503_service_unavailable(self):
        """Test 503 status code is classified as temporary server error"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="http_error",
            status_code=503,
            url="https://example.com/unavailable.pdf",
        )

        assert isinstance(failure, TemporaryFailure)
        assert failure.error_type == "server_error"
        assert "503" in failure.message
        assert failure.is_permanent() is False
        # Should have 1-hour cooldown
        assert failure.retry_after == timedelta(hours=1)

    def test_classify_timeout_error(self):
        """Test timeout errors are classified as temporary failures"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="TimeoutError",
            url="https://example.com/slow.pdf",
        )

        assert isinstance(failure, TemporaryFailure)
        assert failure.error_type == "timeout"
        assert failure.is_permanent() is False
        assert failure.retry_after == timedelta(minutes=30)

    def test_classify_connection_refused(self):
        """Test connection errors are classified as temporary failures"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="ConnectionError",
            url="https://example.com/unreachable.pdf",
            details="Connection refused",
        )

        assert isinstance(failure, TemporaryFailure)
        assert failure.error_type == "network_error"
        assert failure.is_permanent() is False
        assert failure.retry_after == timedelta(minutes=5)

    def test_classify_dns_resolution_failure(self):
        """Test DNS resolution failures are classified as network errors"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="NetworkError",
            url="https://nonexistent.invalid/file.pdf",
            details="DNS resolution failed",
        )

        assert isinstance(failure, TemporaryFailure)
        assert failure.error_type == "network_error"
        assert failure.is_permanent() is False

    def test_classify_ssl_certificate_error(self):
        """Test SSL errors fall to unknown temporary failure"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="SSLError",
            url="https://expired-cert.example.com/file.pdf",
            details="SSL certificate verification failed",
        )

        # SSL errors don't have specific handling, fall to unknown
        assert isinstance(failure, TemporaryFailure)
        assert failure.is_permanent() is False

    def test_classify_arxiv_recaptcha_detection(self):
        """Test arXiv reCAPTCHA detection results in 3-day cooldown"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="ArxivError",
            url="https://arxiv.org/pdf/2301.00001.pdf",
            details="reCAPTCHA protection detected",
        )

        assert isinstance(failure, TemporaryFailure)
        assert failure.error_type == "recaptcha_protection"
        assert failure.retry_after == timedelta(days=3)
        assert failure.is_permanent() is False

    def test_classify_incompatible_format_pdf(self):
        """Test incompatible PDF format is classified as permanent"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="ArxivError",
            url="https://arxiv.org/abs/2301.00001",
            details="not a PDF file",
        )

        assert isinstance(failure, PermanentFailure)
        assert failure.error_type == "incompatible_format"
        assert failure.is_permanent() is True

    def test_classify_incompatible_format_html(self):
        """Test HTML instead of PDF is classified as permanent"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="ArxivError",
            url="https://arxiv.org/abs/2301.00001",
            details="Received HTML content instead of PDF",
        )

        assert isinstance(failure, PermanentFailure)
        assert failure.error_type == "incompatible_format"
        assert failure.is_permanent() is True

    def test_classify_permanent_failure_detection(self):
        """Test 410 Gone is classified as permanent"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="http_error",
            status_code=410,
            url="https://example.com/removed.pdf",
        )

        assert isinstance(failure, PermanentFailure)
        assert failure.error_type == "gone"
        assert "410" in failure.message
        assert failure.is_permanent() is True

    def test_classify_temporary_failure_detection(self):
        """Test temporary failures are properly detected"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="temporary_issue",
            url="https://example.com/temp.pdf",
        )

        assert isinstance(failure, TemporaryFailure)
        assert failure.is_permanent() is False
        assert failure.retry_after is not None

    def test_classify_unknown_error_fallback(self):
        """Test unknown errors default to temporary 1-hour cooldown"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="WeirdUnknownError",
            url="https://example.com/mystery.pdf",
            details="Something unusual happened",
        )

        assert isinstance(failure, TemporaryFailure)
        assert failure.error_type == "unknown_error"
        assert failure.retry_after == timedelta(hours=1)
        assert failure.is_permanent() is False


class TestDomainSpecificLogic:
    """Tests for domain-specific cooldown and handling logic"""

    def test_domain_cooldown_arxiv(self):
        """Test arXiv-specific rate limit cooldown of 6 hours"""
        failure = RateLimitFailure("arxiv.org")

        assert failure.domain == "arxiv.org"
        assert failure.retry_after == timedelta(hours=6)
        assert "arxiv.org" in failure.message

    def test_domain_cooldown_semantic_scholar(self):
        """Test Semantic Scholar rate limit cooldown of 4 hours"""
        failure = RateLimitFailure("semanticscholar.org")

        assert failure.domain == "semanticscholar.org"
        assert failure.retry_after == timedelta(hours=4)

    def test_domain_cooldown_pubmed(self):
        """Test PubMed rate limit cooldown of 2 hours"""
        failure = RateLimitFailure("pubmed.ncbi.nlm.nih.gov")

        assert failure.domain == "pubmed.ncbi.nlm.nih.gov"
        assert failure.retry_after == timedelta(hours=2)

    def test_domain_cooldown_ieee(self):
        """Test IEEE (unknown domain) gets default 1-hour cooldown"""
        failure = RateLimitFailure("ieeexplore.ieee.org")

        assert failure.domain == "ieeexplore.ieee.org"
        # IEEE not in specific domains, falls to default
        assert failure.retry_after == timedelta(hours=1)

    def test_domain_cooldown_springer(self):
        """Test Springer (unknown domain) gets default cooldown"""
        failure = RateLimitFailure("link.springer.com")

        assert failure.domain == "link.springer.com"
        assert failure.retry_after == timedelta(hours=1)

    def test_domain_cooldown_default(self):
        """Test unknown domains get default 1-hour cooldown"""
        failure = RateLimitFailure("unknown-domain.com")

        assert failure.domain == "unknown-domain.com"
        assert failure.retry_after == timedelta(hours=1)

    def test_rate_limit_failure_initialization(self):
        """Test RateLimitFailure initializes with proper attributes"""
        failure = RateLimitFailure("biorxiv.org", "Too many requests")

        assert failure.error_type == "rate_limited"
        assert failure.domain == "biorxiv.org"
        assert "biorxiv.org" in failure.message
        assert "Too many requests" in failure.message
        assert failure.retry_after == timedelta(hours=6)

    def test_pattern_matching_null_values(self):
        """Test classifier handles None/null values gracefully"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="",
            status_code=None,
            url="",
            details="",
        )

        # Should default to unknown temporary failure
        assert isinstance(failure, TemporaryFailure)
        assert failure.is_permanent() is False

    def test_pattern_matching_mixed_patterns(self):
        """Test classifier with mixed patterns in details"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="arxiv_download_error",
            url="https://arxiv.org/pdf/test.pdf",
            details="Captcha required - please solve the reCAPTCHA",
        )

        assert isinstance(failure, TemporaryFailure)
        assert failure.error_type == "recaptcha_protection"
        assert failure.retry_after == timedelta(days=3)

    def test_custom_domain_handling(self):
        """Test custom domain with details in RateLimitFailure"""
        failure = RateLimitFailure("researchgate.net", "Account rate limited")

        assert failure.domain == "researchgate.net"
        assert failure.retry_after == timedelta(hours=12)
        assert "Account rate limited" in failure.message

    def test_failure_message_extraction(self):
        """Test failure message is properly set"""
        failure = PermanentFailure("test_error", "This is a test message")

        assert failure.message == "This is a test message"
        assert failure.error_type == "test_error"

    def test_failure_type_enumeration(self):
        """Test various failure types can be created"""
        # Permanent types
        perm1 = PermanentFailure("not_found", "Not found")
        perm2 = PermanentFailure("forbidden", "Forbidden")
        perm3 = PermanentFailure("gone", "Gone")
        perm4 = PermanentFailure("incompatible_format", "Wrong format")

        assert all(f.is_permanent() for f in [perm1, perm2, perm3, perm4])

        # Temporary types
        temp1 = TemporaryFailure("timeout", "Timeout", timedelta(minutes=30))
        temp2 = TemporaryFailure(
            "network_error", "Network", timedelta(minutes=5)
        )

        assert not any(f.is_permanent() for f in [temp1, temp2])

    def test_retry_eligible_classification(self):
        """Test correct identification of retry-eligible failures"""
        temp_failure = TemporaryFailure(
            "temp", "Temporary", timedelta(seconds=1)
        )
        perm_failure = PermanentFailure("perm", "Permanent")

        # Wait briefly for cooldown to expire
        import time

        time.sleep(0.1)

        # After cooldown, temporary should be retryable
        # Permanent should never be retryable
        assert temp_failure.is_permanent() is False
        assert perm_failure.can_retry_now() is False
        assert perm_failure.is_permanent() is True

    def test_permanent_failure_reasons(self):
        """Test permanent failure has expected attributes"""
        failure = PermanentFailure("test_permanent", "Test permanent failure")

        assert failure.retry_after is None
        assert failure.is_permanent() is True
        assert failure.can_retry_now() is False
        assert failure.get_cooldown_remaining() is None

    def test_failure_to_dict_serialization(self):
        """Test failure can be serialized to dictionary"""
        failure = TemporaryFailure(
            "test_error", "Test message", timedelta(hours=2)
        )

        result = failure.to_dict()

        assert result["error_type"] == "test_error"
        assert result["message"] == "Test message"
        assert result["is_permanent"] is False
        assert "created_at" in result
        assert result["retry_after_timestamp"] is not None


class TestBaseFailureMethods:
    """Tests for BaseFailure base class methods"""

    def test_is_permanent_for_permanent_failure(self):
        """Test is_permanent returns True for PermanentFailure"""
        failure = PermanentFailure("test", "Test")
        assert failure.is_permanent() is True

    def test_is_permanent_for_temporary_failure(self):
        """Test is_permanent returns False for TemporaryFailure"""
        failure = TemporaryFailure("test", "Test", timedelta(hours=1))
        assert failure.is_permanent() is False

    def test_can_retry_now_permanent(self):
        """Test can_retry_now returns False for permanent failures"""
        failure = PermanentFailure("test", "Test")
        assert failure.can_retry_now() is False

    def test_can_retry_now_cooldown_active(self):
        """Test can_retry_now returns False during active cooldown"""
        failure = TemporaryFailure("test", "Test", timedelta(hours=1))
        assert failure.can_retry_now() is False

    def test_can_retry_now_cooldown_expired(self):
        """Test can_retry_now returns True after cooldown expires"""
        failure = TemporaryFailure("test", "Test", timedelta(seconds=0))
        assert failure.can_retry_now() is True

    def test_get_cooldown_remaining_permanent(self):
        """Test get_cooldown_remaining returns None for permanent"""
        failure = PermanentFailure("test", "Test")
        assert failure.get_cooldown_remaining() is None

    def test_get_cooldown_remaining_expired(self):
        """Test get_cooldown_remaining returns None after expiry"""
        failure = TemporaryFailure("test", "Test", timedelta(seconds=0))
        assert failure.get_cooldown_remaining() is None

    def test_get_cooldown_remaining_active(self):
        """Test get_cooldown_remaining returns time during cooldown"""
        failure = TemporaryFailure("test", "Test", timedelta(hours=1))
        remaining = failure.get_cooldown_remaining()

        assert remaining is not None
        assert remaining > timedelta(0)
        assert remaining <= timedelta(hours=1)

    def test_created_at_timestamp(self):
        """Test created_at is set to current UTC time"""
        before = datetime.now(UTC)
        failure = TemporaryFailure("test", "Test", timedelta(hours=1))
        after = datetime.now(UTC)

        assert before <= failure.created_at <= after

    def test_to_dict_complete_structure(self):
        """Test to_dict returns complete structure"""
        failure = TemporaryFailure("test", "Test message", timedelta(hours=2))
        result = failure.to_dict()

        expected_keys = [
            "error_type",
            "message",
            "retry_after_timestamp",
            "is_permanent",
            "created_at",
        ]

        for key in expected_keys:
            assert key in result, f"Missing key: {key}"


class TestClassifyFromException:
    """Tests for classify_from_exception method"""

    def test_classify_from_timeout_exception(self):
        """Test classification from TimeoutError exception"""
        classifier = FailureClassifier()
        exc = TimeoutError("Connection timed out")

        failure = classifier.classify_from_exception(exc, "https://example.com")

        assert isinstance(failure, TemporaryFailure)
        assert failure.error_type == "timeout"

    def test_classify_from_connection_exception(self):
        """Test classification from ConnectionError exception"""
        classifier = FailureClassifier()
        exc = ConnectionError("Connection refused")

        failure = classifier.classify_from_exception(exc, "https://example.com")

        assert isinstance(failure, TemporaryFailure)
        assert failure.error_type == "network_error"

    def test_classify_from_generic_exception(self):
        """Test classification from generic Exception"""
        classifier = FailureClassifier()
        exc = Exception("Something went wrong")

        failure = classifier.classify_from_exception(exc, "https://example.com")

        assert isinstance(failure, TemporaryFailure)
        assert failure.is_permanent() is False

    def test_classify_from_exception_with_empty_url(self):
        """Test classify_from_exception with empty URL"""
        classifier = FailureClassifier()
        exc = ValueError("Invalid data")

        failure = classifier.classify_from_exception(exc, "")

        assert isinstance(failure, TemporaryFailure)
        assert failure.is_permanent() is False

    def test_classify_from_exception_extracts_message(self):
        """Test exception message is included in details"""
        classifier = FailureClassifier()
        exc = RuntimeError("Specific runtime error message")

        failure = classifier.classify_from_exception(exc, "https://example.com")

        # The exception message should influence classification
        assert failure.message is not None


class TestEdgeCases:
    """Tests for edge cases and boundary conditions"""

    def test_case_insensitive_error_matching(self):
        """Test error matching is case-insensitive"""
        classifier = FailureClassifier()

        # Test uppercase TIMEOUT
        failure1 = classifier.classify_failure(
            error_type="TIMEOUT",
            url="https://example.com",
        )

        # Test mixed case TimeOut
        failure2 = classifier.classify_failure(
            error_type="TimeOut",
            url="https://example.com",
        )

        assert failure1.error_type == "timeout"
        assert failure2.error_type == "timeout"

    def test_arxiv_pattern_in_details(self):
        """Test arXiv detection from details field"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="download_error",
            url="https://example.com",
            details="Error from arxiv - captcha detected",
        )

        assert failure.error_type == "recaptcha_protection"

    def test_url_domain_extraction_for_rate_limit(self):
        """Test domain is correctly extracted from URL for rate limits"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="http_error",
            status_code=429,
            url="https://api.semanticscholar.org/v1/paper/123",
        )

        assert isinstance(failure, RateLimitFailure)
        assert failure.domain == "api.semanticscholar.org"

    def test_url_with_port_number(self):
        """Test URL with port number is handled"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="http_error",
            status_code=429,
            url="https://localhost:8080/api/download",
        )

        assert isinstance(failure, RateLimitFailure)
        assert failure.domain == "localhost:8080"

    def test_malformed_url_handling(self):
        """Test malformed URLs don't cause crashes"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="http_error",
            status_code=429,
            url="not-a-valid-url",
        )

        # Should still create a failure object
        assert isinstance(failure, RateLimitFailure)

    def test_very_long_details_string(self):
        """Test handling of very long details strings"""
        classifier = FailureClassifier()
        long_details = "x" * 10000

        failure = classifier.classify_failure(
            error_type="error",
            url="https://example.com",
            details=long_details,
        )

        assert isinstance(failure, TemporaryFailure)

    def test_unicode_in_error_message(self):
        """Test unicode characters in error messages"""
        classifier = FailureClassifier()
        failure = classifier.classify_failure(
            error_type="error",
            url="https://example.com",
            details="Error: \u4e2d\u6587\u6d4b\u8bd5 (Chinese test)",
        )

        assert isinstance(failure, TemporaryFailure)

    def test_html_pdf_exclusion(self):
        """Test HTML is flagged but application/pdf is allowed"""
        classifier = FailureClassifier()

        # This should NOT be classified as incompatible (has application/pdf)
        failure1 = classifier.classify_failure(
            error_type="ArxivError",
            url="https://arxiv.org/pdf/test.pdf",
            details="Content-Type: html with application/pdf fallback",
        )

        # failure1 should not be permanent incompatible format
        assert (
            failure1.error_type != "incompatible_format"
            or not failure1.is_permanent()
        )

        # This SHOULD be classified as incompatible (no application/pdf)
        failure2 = classifier.classify_failure(
            error_type="ArxivError",
            url="https://arxiv.org/abs/test",
            details="Received HTML instead of expected format",
        )

        assert isinstance(failure2, PermanentFailure)
        assert failure2.error_type == "incompatible_format"
