"""
Deep behavioral tests for all news exception classes.
Tests status codes, error codes, message formatting, to_dict(), and inheritance.
"""

import pytest

from local_deep_research.news.exceptions import (
    NewsAPIException,
    NewsFeatureDisabledException,
    InvalidLimitException,
    SubscriptionNotFoundException,
    SubscriptionCreationException,
    SubscriptionUpdateException,
    SubscriptionDeletionException,
    DatabaseAccessException,
    NewsFeedGenerationException,
    ResearchProcessingException,
    NotImplementedException,
    InvalidParameterException,
    SchedulerNotificationException,
)


# --- NewsAPIException base ---


class TestNewsAPIExceptionBase:
    """Tests for the base NewsAPIException."""

    def test_message_stored(self):
        e = NewsAPIException("test message")
        assert e.message == "test message"

    def test_default_status_code(self):
        e = NewsAPIException("msg")
        assert e.status_code == 500

    def test_custom_status_code(self):
        e = NewsAPIException("msg", status_code=404)
        assert e.status_code == 404

    def test_default_error_code_is_class_name(self):
        e = NewsAPIException("msg")
        assert e.error_code == "NewsAPIException"

    def test_custom_error_code(self):
        e = NewsAPIException("msg", error_code="CUSTOM_ERROR")
        assert e.error_code == "CUSTOM_ERROR"

    def test_default_details_empty(self):
        e = NewsAPIException("msg")
        assert e.details == {}

    def test_custom_details(self):
        e = NewsAPIException("msg", details={"key": "val"})
        assert e.details == {"key": "val"}

    def test_is_exception(self):
        e = NewsAPIException("msg")
        assert isinstance(e, Exception)

    def test_str_is_message(self):
        e = NewsAPIException("test message")
        assert str(e) == "test message"

    def test_to_dict_has_error(self):
        e = NewsAPIException("msg")
        d = e.to_dict()
        assert d["error"] == "msg"

    def test_to_dict_has_error_code(self):
        e = NewsAPIException("msg")
        d = e.to_dict()
        assert d["error_code"] == "NewsAPIException"

    def test_to_dict_has_status_code(self):
        e = NewsAPIException("msg", status_code=400)
        d = e.to_dict()
        assert d["status_code"] == 400

    def test_to_dict_no_details_when_empty(self):
        e = NewsAPIException("msg")
        d = e.to_dict()
        assert "details" not in d

    def test_to_dict_includes_details_when_present(self):
        e = NewsAPIException("msg", details={"foo": "bar"})
        d = e.to_dict()
        assert d["details"] == {"foo": "bar"}


# --- NewsFeatureDisabledException ---


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

    def test_inherits_from_base(self):
        e = NewsFeatureDisabledException()
        assert isinstance(e, NewsAPIException)

    def test_default_message(self):
        e = NewsFeatureDisabledException()
        assert e.message == "News system is disabled"

    def test_custom_message(self):
        e = NewsFeatureDisabledException("custom msg")
        assert e.message == "custom msg"

    def test_status_code_503(self):
        e = NewsFeatureDisabledException()
        assert e.status_code == 503

    def test_error_code(self):
        e = NewsFeatureDisabledException()
        assert e.error_code == "NEWS_DISABLED"


# --- InvalidLimitException ---


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

    def test_inherits_from_base(self):
        e = InvalidLimitException(0)
        assert isinstance(e, NewsAPIException)

    def test_status_code_400(self):
        e = InvalidLimitException(-1)
        assert e.status_code == 400

    def test_error_code(self):
        e = InvalidLimitException(0)
        assert e.error_code == "INVALID_LIMIT"

    def test_message_contains_limit(self):
        e = InvalidLimitException(-5)
        assert "-5" in e.message

    def test_details_has_provided_limit(self):
        e = InvalidLimitException(0)
        assert e.details["provided_limit"] == 0

    def test_details_has_min_limit(self):
        e = InvalidLimitException(0)
        assert e.details["min_limit"] == 1

    def test_negative_limit(self):
        e = InvalidLimitException(-100)
        assert e.details["provided_limit"] == -100

    def test_zero_limit(self):
        e = InvalidLimitException(0)
        assert "0" in e.message


# --- SubscriptionNotFoundException ---


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

    def test_inherits_from_base(self):
        e = SubscriptionNotFoundException("sub-123")
        assert isinstance(e, NewsAPIException)

    def test_status_code_404(self):
        e = SubscriptionNotFoundException("sub-123")
        assert e.status_code == 404

    def test_error_code(self):
        e = SubscriptionNotFoundException("sub-123")
        assert e.error_code == "SUBSCRIPTION_NOT_FOUND"

    def test_message_contains_id(self):
        e = SubscriptionNotFoundException("sub-abc-xyz")
        assert "sub-abc-xyz" in e.message

    def test_details_has_subscription_id(self):
        e = SubscriptionNotFoundException("sub-123")
        assert e.details["subscription_id"] == "sub-123"

    def test_to_dict_round_trip(self):
        e = SubscriptionNotFoundException("sub-123")
        d = e.to_dict()
        assert d["status_code"] == 404
        assert d["details"]["subscription_id"] == "sub-123"


# --- SubscriptionCreationException ---


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

    def test_inherits_from_base(self):
        e = SubscriptionCreationException("error")
        assert isinstance(e, NewsAPIException)

    def test_status_code_500(self):
        e = SubscriptionCreationException("error")
        assert e.status_code == 500

    def test_error_code(self):
        e = SubscriptionCreationException("error")
        assert e.error_code == "SUBSCRIPTION_CREATE_FAILED"

    def test_message_contains_reason(self):
        e = SubscriptionCreationException("DB timeout")
        assert "DB timeout" in e.message

    def test_with_details(self):
        e = SubscriptionCreationException(
            "fail", details={"query": "test", "type": "search"}
        )
        assert e.details["query"] == "test"
        assert e.details["type"] == "search"

    def test_without_details(self):
        e = SubscriptionCreationException("fail")
        assert e.details == {} or e.details is None


# --- SubscriptionUpdateException ---


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

    def test_inherits_from_base(self):
        e = SubscriptionUpdateException("sub-1", "error")
        assert isinstance(e, NewsAPIException)

    def test_status_code_500(self):
        e = SubscriptionUpdateException("sub-1", "error")
        assert e.status_code == 500

    def test_error_code(self):
        e = SubscriptionUpdateException("sub-1", "error")
        assert e.error_code == "SUBSCRIPTION_UPDATE_FAILED"

    def test_message_contains_id_and_reason(self):
        e = SubscriptionUpdateException("sub-99", "DB error")
        assert "sub-99" in e.message
        assert "DB error" in e.message

    def test_details_has_subscription_id(self):
        e = SubscriptionUpdateException("sub-1", "msg")
        assert e.details["subscription_id"] == "sub-1"


# --- SubscriptionDeletionException ---


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

    def test_inherits_from_base(self):
        e = SubscriptionDeletionException("sub-1", "error")
        assert isinstance(e, NewsAPIException)

    def test_status_code_500(self):
        e = SubscriptionDeletionException("sub-1", "error")
        assert e.status_code == 500

    def test_error_code(self):
        e = SubscriptionDeletionException("sub-1", "error")
        assert e.error_code == "SUBSCRIPTION_DELETE_FAILED"

    def test_message_contains_id(self):
        e = SubscriptionDeletionException("sub-42", "timeout")
        assert "sub-42" in e.message

    def test_details_has_subscription_id(self):
        e = SubscriptionDeletionException("sub-1", "msg")
        assert e.details["subscription_id"] == "sub-1"


# --- DatabaseAccessException ---


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

    def test_inherits_from_base(self):
        e = DatabaseAccessException("query", "timeout")
        assert isinstance(e, NewsAPIException)

    def test_status_code_500(self):
        e = DatabaseAccessException("query", "timeout")
        assert e.status_code == 500

    def test_error_code(self):
        e = DatabaseAccessException("query", "timeout")
        assert e.error_code == "DATABASE_ERROR"

    def test_message_contains_operation(self):
        e = DatabaseAccessException("get_news_feed", "connection lost")
        assert "get_news_feed" in e.message

    def test_message_contains_error(self):
        e = DatabaseAccessException("op", "connection lost")
        assert "connection lost" in e.message

    def test_details_has_operation(self):
        e = DatabaseAccessException("get_feed", "error")
        assert e.details["operation"] == "get_feed"


# --- NewsFeedGenerationException ---


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

    def test_inherits_from_base(self):
        e = NewsFeedGenerationException("error")
        assert isinstance(e, NewsAPIException)

    def test_status_code_500(self):
        e = NewsFeedGenerationException("error")
        assert e.status_code == 500

    def test_error_code(self):
        e = NewsFeedGenerationException("error")
        assert e.error_code == "FEED_GENERATION_FAILED"

    def test_message_contains_reason(self):
        e = NewsFeedGenerationException("no data")
        assert "no data" in e.message

    def test_with_user_id(self):
        e = NewsFeedGenerationException("error", user_id="user1")
        assert e.details["user_id"] == "user1"

    def test_without_user_id(self):
        e = NewsFeedGenerationException("error")
        assert e.details == {}


# --- ResearchProcessingException ---


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

    def test_inherits_from_base(self):
        e = ResearchProcessingException("error")
        assert isinstance(e, NewsAPIException)

    def test_status_code_500(self):
        e = ResearchProcessingException("error")
        assert e.status_code == 500

    def test_error_code(self):
        e = ResearchProcessingException("error")
        assert e.error_code == "RESEARCH_PROCESSING_FAILED"

    def test_with_research_id(self):
        e = ResearchProcessingException("fail", research_id="r-123")
        assert e.details["research_id"] == "r-123"

    def test_without_research_id(self):
        e = ResearchProcessingException("fail")
        assert e.details == {}


# --- NotImplementedException ---


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

    def test_inherits_from_base(self):
        e = NotImplementedException("feature_x")
        assert isinstance(e, NewsAPIException)

    def test_status_code_501(self):
        e = NotImplementedException("feature_x")
        assert e.status_code == 501

    def test_error_code(self):
        e = NotImplementedException("feature_x")
        assert e.error_code == "NOT_IMPLEMENTED"

    def test_message_contains_feature(self):
        e = NotImplementedException("dark_mode")
        assert "dark_mode" in e.message

    def test_details_has_feature(self):
        e = NotImplementedException("feature_x")
        assert e.details["feature"] == "feature_x"


# --- InvalidParameterException ---


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

    def test_inherits_from_base(self):
        e = InvalidParameterException("limit", -1, "must be positive")
        assert isinstance(e, NewsAPIException)

    def test_status_code_400(self):
        e = InvalidParameterException("limit", -1, "must be positive")
        assert e.status_code == 400

    def test_error_code(self):
        e = InvalidParameterException("limit", -1, "msg")
        assert e.error_code == "INVALID_PARAMETER"

    def test_message_contains_parameter(self):
        e = InvalidParameterException("limit", -1, "msg")
        assert "limit" in e.message

    def test_details_has_parameter(self):
        e = InvalidParameterException("limit", -1, "msg")
        assert e.details["parameter"] == "limit"

    def test_details_has_value(self):
        e = InvalidParameterException("limit", -1, "msg")
        assert e.details["value"] == -1


# --- SchedulerNotificationException ---


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

    def test_inherits_from_base(self):
        e = SchedulerNotificationException("created", "msg")
        assert isinstance(e, NewsAPIException)

    def test_status_code_500(self):
        e = SchedulerNotificationException("created", "msg")
        assert e.status_code == 500

    def test_error_code(self):
        e = SchedulerNotificationException("created", "msg")
        assert e.error_code == "SCHEDULER_NOTIFICATION_FAILED"

    def test_message_contains_action(self):
        e = SchedulerNotificationException("deleted", "error")
        assert "deleted" in e.message

    def test_details_has_action(self):
        e = SchedulerNotificationException("updated", "msg")
        assert e.details["action"] == "updated"


# --- Exception hierarchy catch patterns ---


class TestExceptionHierarchyCatching:
    """Tests that exception hierarchy works correctly for catch blocks."""

    def test_catch_invalid_limit_as_base(self):
        with pytest.raises(NewsAPIException):
            raise InvalidLimitException(0)

    def test_catch_not_found_as_base(self):
        with pytest.raises(NewsAPIException):
            raise SubscriptionNotFoundException("id")

    def test_catch_disabled_as_base(self):
        with pytest.raises(NewsAPIException):
            raise NewsFeatureDisabledException()

    def test_catch_db_error_as_base(self):
        with pytest.raises(NewsAPIException):
            raise DatabaseAccessException("op", "msg")

    def test_catch_all_as_exception(self):
        with pytest.raises(Exception):
            raise NewsFeedGenerationException("msg")

    def test_not_found_not_caught_as_value_error(self):
        """NewsAPIException should NOT be caught as ValueError."""
        with pytest.raises(NewsAPIException):
            raise SubscriptionNotFoundException("id")
        # If it were a ValueError, this test would fail differently

    def test_each_exception_has_unique_error_code(self):
        """All concrete exceptions should have unique error codes."""
        exceptions = [
            NewsFeatureDisabledException(),
            InvalidLimitException(0),
            SubscriptionNotFoundException("id"),
            SubscriptionCreationException("msg"),
            SubscriptionUpdateException("id", "msg"),
            SubscriptionDeletionException("id", "msg"),
            DatabaseAccessException("op", "msg"),
            NewsFeedGenerationException("msg"),
            ResearchProcessingException("msg"),
            NotImplementedException("feature"),
            InvalidParameterException("p", "v", "msg"),
            SchedulerNotificationException("action", "msg"),
        ]
        error_codes = [e.error_code for e in exceptions]
        assert len(error_codes) == len(set(error_codes))

    def test_all_subclasses_are_catchable_as_base(self):
        """All subclasses should be catchable as NewsAPIException."""
        exceptions = [
            NewsFeatureDisabledException(),
            InvalidLimitException(0),
            SubscriptionNotFoundException("id"),
            SubscriptionCreationException("msg"),
            SubscriptionUpdateException("id", "msg"),
            SubscriptionDeletionException("id", "msg"),
            DatabaseAccessException("op", "msg"),
            NewsFeedGenerationException("msg"),
            ResearchProcessingException("msg"),
            NotImplementedException("feature"),
            InvalidParameterException("p", "v", "msg"),
            SchedulerNotificationException("action", "msg"),
        ]
        for exc in exceptions:
            assert isinstance(exc, NewsAPIException)

    def test_to_dict_always_has_required_fields(self):
        """All exceptions' to_dict should have error, error_code, status_code."""
        exceptions = [
            NewsAPIException("msg"),
            NewsFeatureDisabledException(),
            InvalidLimitException(0),
            SubscriptionNotFoundException("id"),
            DatabaseAccessException("op", "msg"),
            NotImplementedException("feature"),
        ]
        for exc in exceptions:
            d = exc.to_dict()
            assert "error" in d
            assert "error_code" in d
            assert "status_code" in d
