"""
Deep behavioral tests for exceptions.py pure logic.
Tests exception structure, status codes, error codes,
to_dict conversion, and exception inheritance.
"""


# --- NewsAPIException base structure ---


class TestNewsAPIExceptionStructure:
    """Tests for base NewsAPIException structure."""

    def _create_exception(
        self, message, status_code=500, error_code=None, details=None
    ):
        return {
            "message": message,
            "status_code": status_code,
            "error_code": error_code or "NewsAPIException",
            "details": details or {},
        }

    def test_has_message(self):
        exc = self._create_exception("Test error")
        assert exc["message"] == "Test error"

    def test_default_status_code_500(self):
        exc = self._create_exception("Error")
        assert exc["status_code"] == 500

    def test_custom_status_code(self):
        exc = self._create_exception("Error", status_code=400)
        assert exc["status_code"] == 400

    def test_default_error_code_from_class(self):
        exc = self._create_exception("Error")
        assert exc["error_code"] == "NewsAPIException"

    def test_custom_error_code(self):
        exc = self._create_exception("Error", error_code="CUSTOM_ERROR")
        assert exc["error_code"] == "CUSTOM_ERROR"

    def test_default_empty_details(self):
        exc = self._create_exception("Error")
        assert exc["details"] == {}

    def test_custom_details(self):
        exc = self._create_exception("Error", details={"key": "value"})
        assert exc["details"]["key"] == "value"


# --- to_dict conversion ---


class TestToDictConversion:
    """Tests for to_dict method structure."""

    def _to_dict(self, message, error_code, status_code, details):
        result = {
            "error": message,
            "error_code": error_code,
            "status_code": status_code,
        }
        if details:
            result["details"] = details
        return result

    def test_includes_error(self):
        result = self._to_dict("Test error", "CODE", 500, {})
        assert result["error"] == "Test error"

    def test_includes_error_code(self):
        result = self._to_dict("Error", "TEST_CODE", 500, {})
        assert result["error_code"] == "TEST_CODE"

    def test_includes_status_code(self):
        result = self._to_dict("Error", "CODE", 404, {})
        assert result["status_code"] == 404

    def test_includes_details_when_present(self):
        result = self._to_dict("Error", "CODE", 500, {"key": "val"})
        assert "details" in result
        assert result["details"]["key"] == "val"

    def test_excludes_details_when_empty(self):
        result = self._to_dict("Error", "CODE", 500, {})
        assert "details" not in result


# --- NewsFeatureDisabledException ---


class TestNewsFeatureDisabledException:
    """Tests for NewsFeatureDisabledException."""

    def _create_disabled_exception(self, message="News system is disabled"):
        return {
            "message": message,
            "status_code": 503,
            "error_code": "NEWS_DISABLED",
            "details": {},
        }

    def test_default_message(self):
        exc = self._create_disabled_exception()
        assert exc["message"] == "News system is disabled"

    def test_status_code_503(self):
        exc = self._create_disabled_exception()
        assert exc["status_code"] == 503

    def test_error_code_news_disabled(self):
        exc = self._create_disabled_exception()
        assert exc["error_code"] == "NEWS_DISABLED"


# --- InvalidLimitException ---


class TestInvalidLimitException:
    """Tests for InvalidLimitException."""

    def _create_limit_exception(self, limit):
        return {
            "message": f"Invalid limit: {limit}. Limit must be at least 1",
            "status_code": 400,
            "error_code": "INVALID_LIMIT",
            "details": {"provided_limit": limit, "min_limit": 1},
        }

    def test_message_includes_limit(self):
        exc = self._create_limit_exception(0)
        assert "0" in exc["message"]

    def test_status_code_400(self):
        exc = self._create_limit_exception(0)
        assert exc["status_code"] == 400

    def test_details_include_provided_limit(self):
        exc = self._create_limit_exception(-5)
        assert exc["details"]["provided_limit"] == -5

    def test_details_include_min_limit(self):
        exc = self._create_limit_exception(0)
        assert exc["details"]["min_limit"] == 1


# --- SubscriptionNotFoundException ---


class TestSubscriptionNotFoundException:
    """Tests for SubscriptionNotFoundException."""

    def _create_not_found_exception(self, subscription_id):
        return {
            "message": f"Subscription not found: {subscription_id}",
            "status_code": 404,
            "error_code": "SUBSCRIPTION_NOT_FOUND",
            "details": {"subscription_id": subscription_id},
        }

    def test_message_includes_id(self):
        exc = self._create_not_found_exception("sub-123")
        assert "sub-123" in exc["message"]

    def test_status_code_404(self):
        exc = self._create_not_found_exception("sub-123")
        assert exc["status_code"] == 404

    def test_details_include_subscription_id(self):
        exc = self._create_not_found_exception("sub-456")
        assert exc["details"]["subscription_id"] == "sub-456"


# --- SubscriptionCreationException ---


class TestSubscriptionCreationException:
    """Tests for SubscriptionCreationException."""

    def _create_creation_exception(self, message, details=None):
        return {
            "message": f"Failed to create subscription: {message}",
            "status_code": 500,
            "error_code": "SUBSCRIPTION_CREATE_FAILED",
            "details": details or {},
        }

    def test_message_prefix(self):
        exc = self._create_creation_exception("Database error")
        assert exc["message"].startswith("Failed to create subscription:")

    def test_status_code_500(self):
        exc = self._create_creation_exception("Error")
        assert exc["status_code"] == 500

    def test_error_code(self):
        exc = self._create_creation_exception("Error")
        assert exc["error_code"] == "SUBSCRIPTION_CREATE_FAILED"


# --- SubscriptionUpdateException ---


class TestSubscriptionUpdateException:
    """Tests for SubscriptionUpdateException."""

    def _create_update_exception(self, subscription_id, message):
        return {
            "message": f"Failed to update subscription {subscription_id}: {message}",
            "status_code": 500,
            "error_code": "SUBSCRIPTION_UPDATE_FAILED",
            "details": {"subscription_id": subscription_id},
        }

    def test_message_includes_id(self):
        exc = self._create_update_exception("sub-123", "Error")
        assert "sub-123" in exc["message"]

    def test_message_includes_error(self):
        exc = self._create_update_exception("sub-123", "Validation failed")
        assert "Validation failed" in exc["message"]


# --- SubscriptionDeletionException ---


class TestSubscriptionDeletionException:
    """Tests for SubscriptionDeletionException."""

    def _create_deletion_exception(self, subscription_id, message):
        return {
            "message": f"Failed to delete subscription {subscription_id}: {message}",
            "status_code": 500,
            "error_code": "SUBSCRIPTION_DELETE_FAILED",
            "details": {"subscription_id": subscription_id},
        }

    def test_message_format(self):
        exc = self._create_deletion_exception("sub-123", "Not found")
        assert "delete subscription sub-123" in exc["message"]

    def test_error_code(self):
        exc = self._create_deletion_exception("sub-123", "Error")
        assert exc["error_code"] == "SUBSCRIPTION_DELETE_FAILED"


# --- DatabaseAccessException ---


class TestDatabaseAccessException:
    """Tests for DatabaseAccessException."""

    def _create_db_exception(self, operation, message):
        return {
            "message": f"Database error during {operation}: {message}",
            "status_code": 500,
            "error_code": "DATABASE_ERROR",
            "details": {"operation": operation},
        }

    def test_message_includes_operation(self):
        exc = self._create_db_exception("insert", "Connection failed")
        assert "insert" in exc["message"]

    def test_details_include_operation(self):
        exc = self._create_db_exception("query", "Timeout")
        assert exc["details"]["operation"] == "query"

    def test_error_code(self):
        exc = self._create_db_exception("delete", "Error")
        assert exc["error_code"] == "DATABASE_ERROR"


# --- NewsFeedGenerationException ---


class TestNewsFeedGenerationException:
    """Tests for NewsFeedGenerationException."""

    def _create_feed_exception(self, message, user_id=None):
        details = {}
        if user_id:
            details["user_id"] = user_id
        return {
            "message": f"Failed to generate news feed: {message}",
            "status_code": 500,
            "error_code": "FEED_GENERATION_FAILED",
            "details": details,
        }

    def test_message_prefix(self):
        exc = self._create_feed_exception("Error")
        assert exc["message"].startswith("Failed to generate news feed:")

    def test_details_include_user_id_when_provided(self):
        exc = self._create_feed_exception("Error", user_id="user-123")
        assert exc["details"]["user_id"] == "user-123"

    def test_details_empty_when_no_user_id(self):
        exc = self._create_feed_exception("Error")
        assert exc["details"] == {}


# --- ResearchProcessingException ---


class TestResearchProcessingException:
    """Tests for ResearchProcessingException."""

    def _create_research_exception(self, message, research_id=None):
        details = {}
        if research_id:
            details["research_id"] = research_id
        return {
            "message": f"Failed to process research item: {message}",
            "status_code": 500,
            "error_code": "RESEARCH_PROCESSING_FAILED",
            "details": details,
        }

    def test_message_prefix(self):
        exc = self._create_research_exception("Parse error")
        assert exc["message"].startswith("Failed to process research item:")

    def test_details_include_research_id(self):
        exc = self._create_research_exception("Error", research_id="r-123")
        assert exc["details"]["research_id"] == "r-123"


# --- NotImplementedException ---


class TestNotImplementedException:
    """Tests for NotImplementedException."""

    def _create_not_implemented(self, feature):
        return {
            "message": f"Feature not yet implemented: {feature}",
            "status_code": 501,
            "error_code": "NOT_IMPLEMENTED",
            "details": {"feature": feature},
        }

    def test_status_code_501(self):
        exc = self._create_not_implemented("Advanced search")
        assert exc["status_code"] == 501

    def test_details_include_feature(self):
        exc = self._create_not_implemented("Real-time updates")
        assert exc["details"]["feature"] == "Real-time updates"


# --- InvalidParameterException ---


class TestInvalidParameterException:
    """Tests for InvalidParameterException."""

    def _create_invalid_param(self, parameter, value, message):
        return {
            "message": f"Invalid parameter '{parameter}': {message}",
            "status_code": 400,
            "error_code": "INVALID_PARAMETER",
            "details": {"parameter": parameter, "value": value},
        }

    def test_message_includes_parameter(self):
        exc = self._create_invalid_param("limit", -1, "Must be positive")
        assert "limit" in exc["message"]

    def test_status_code_400(self):
        exc = self._create_invalid_param("p", "v", "m")
        assert exc["status_code"] == 400

    def test_details_include_value(self):
        exc = self._create_invalid_param("sort", "invalid", "Not a valid sort")
        assert exc["details"]["value"] == "invalid"


# --- SchedulerNotificationException ---


class TestSchedulerNotificationException:
    """Tests for SchedulerNotificationException."""

    def _create_scheduler_exception(self, action, message):
        return {
            "message": f"Failed to notify scheduler about {action}: {message}",
            "status_code": 500,
            "error_code": "SCHEDULER_NOTIFICATION_FAILED",
            "details": {"action": action},
        }

    def test_message_includes_action(self):
        exc = self._create_scheduler_exception("subscription update", "Timeout")
        assert "subscription update" in exc["message"]

    def test_details_include_action(self):
        exc = self._create_scheduler_exception("job removal", "Error")
        assert exc["details"]["action"] == "job removal"


# --- Status code patterns ---


class TestStatusCodePatterns:
    """Tests for HTTP status code patterns."""

    def test_400_is_client_error(self):
        assert 400 // 100 == 4

    def test_404_is_not_found(self):
        assert 404 == 404

    def test_500_is_server_error(self):
        assert 500 // 100 == 5

    def test_501_is_not_implemented(self):
        assert 501 == 501

    def test_503_is_service_unavailable(self):
        assert 503 == 503


# --- Error code naming patterns ---


class TestErrorCodeNamingPatterns:
    """Tests for error code naming conventions."""

    def test_uppercase_with_underscores(self):
        error_codes = [
            "NEWS_DISABLED",
            "INVALID_LIMIT",
            "SUBSCRIPTION_NOT_FOUND",
            "DATABASE_ERROR",
        ]
        for code in error_codes:
            assert code == code.upper()
            assert " " not in code
