"""
Deep behavioral tests for all news exception classes.
Tests status codes, error codes, to_dict, details, and inheritance.
"""

import pytest

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


# --- Base exception ---


class TestNewsAPIException:
    """Tests for base NewsAPIException."""

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

    def test_default_status_code_500(self):
        e = NewsAPIException("err")
        assert e.status_code == 500

    def test_custom_status_code(self):
        e = NewsAPIException("err", status_code=400)
        assert e.status_code == 400

    def test_error_code_defaults_to_class_name(self):
        e = NewsAPIException("err")
        assert e.error_code == "NewsAPIException"

    def test_custom_error_code(self):
        e = NewsAPIException("err", error_code="CUSTOM")
        assert e.error_code == "CUSTOM"

    def test_details_defaults_empty(self):
        e = NewsAPIException("err")
        assert e.details == {}

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

    def test_to_dict_structure(self):
        e = NewsAPIException("broken", status_code=400, error_code="BAD")
        d = e.to_dict()
        assert d["error"] == "broken"
        assert d["error_code"] == "BAD"
        assert d["status_code"] == 400

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

    def test_to_dict_includes_details_when_present(self):
        e = NewsAPIException("err", details={"info": "extra"})
        d = e.to_dict()
        assert d["details"] == {"info": "extra"}

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

    def test_str_representation(self):
        e = NewsAPIException("message here")
        assert str(e) == "message here"


# --- Specific exceptions ---


class TestNewsFeatureDisabledException:
    """Tests for disabled feature exception."""

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

    def test_custom_message(self):
        e = NewsFeatureDisabledException("Feature X disabled")
        assert e.message == "Feature X disabled"

    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"

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


class TestInvalidLimitException:
    """Tests for invalid limit exception."""

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

    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_details_include_provided_limit(self):
        e = InvalidLimitException(-5)
        assert e.details["provided_limit"] == -5

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

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

    def test_inherits_base(self):
        assert issubclass(InvalidLimitException, NewsAPIException)


class TestSubscriptionNotFoundException:
    """Tests for subscription not found exception."""

    def test_message_includes_id(self):
        e = SubscriptionNotFoundException("sub-123")
        assert "sub-123" in e.message

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

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

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


class TestSubscriptionCreationException:
    """Tests for subscription creation exception."""

    def test_message_prefixed(self):
        e = SubscriptionCreationException("query too long")
        assert "Failed to create subscription" in e.message
        assert "query too long" in e.message

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

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

    def test_custom_details(self):
        e = SubscriptionCreationException("err", details={"query": "test"})
        assert e.details == {"query": "test"}

    def test_none_details_becomes_empty(self):
        e = SubscriptionCreationException("err", details=None)
        assert e.details == {}


class TestSubscriptionUpdateException:
    """Tests for subscription update exception."""

    def test_message_includes_id_and_reason(self):
        e = SubscriptionUpdateException("sub-1", "invalid interval")
        assert "sub-1" in e.message
        assert "invalid interval" in e.message

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

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

    def test_details_include_subscription_id(self):
        e = SubscriptionUpdateException("sub-42", "err")
        assert e.details["subscription_id"] == "sub-42"


class TestSubscriptionDeletionException:
    """Tests for subscription deletion exception."""

    def test_message_includes_id(self):
        e = SubscriptionDeletionException("sub-1", "db locked")
        assert "sub-1" in e.message

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

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

    def test_details_include_subscription_id(self):
        e = SubscriptionDeletionException("sub-7", "err")
        assert e.details["subscription_id"] == "sub-7"


class TestDatabaseAccessException:
    """Tests for database access exception."""

    def test_message_includes_operation(self):
        e = DatabaseAccessException("INSERT", "table locked")
        assert "INSERT" in e.message

    def test_status_code_500(self):
        e = DatabaseAccessException("SELECT", "err")
        assert e.status_code == 500

    def test_error_code(self):
        e = DatabaseAccessException("DELETE", "err")
        assert e.error_code == "DATABASE_ERROR"

    def test_details_include_operation(self):
        e = DatabaseAccessException("UPDATE", "err")
        assert e.details["operation"] == "UPDATE"


class TestNewsFeedGenerationException:
    """Tests for feed generation exception."""

    def test_message_prefixed(self):
        e = NewsFeedGenerationException("timeout")
        assert "Failed to generate news feed" in e.message

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

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

    def test_no_user_id_means_empty_details(self):
        e = NewsFeedGenerationException("err")
        assert e.details == {}

    def test_user_id_in_details(self):
        e = NewsFeedGenerationException("err", user_id="user-42")
        assert e.details["user_id"] == "user-42"


class TestResearchProcessingException:
    """Tests for research processing exception."""

    def test_message_prefixed(self):
        e = ResearchProcessingException("parse failed")
        assert "Failed to process research item" in e.message

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

    def test_no_research_id_means_empty_details(self):
        e = ResearchProcessingException("err")
        assert e.details == {}

    def test_research_id_in_details(self):
        e = ResearchProcessingException("err", research_id="res-1")
        assert e.details["research_id"] == "res-1"


class TestNotImplementedException:
    """Tests for not implemented exception."""

    def test_message_includes_feature(self):
        e = NotImplementedException("live streaming")
        assert "live streaming" in e.message

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

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

    def test_details_include_feature(self):
        e = NotImplementedException("video analysis")
        assert e.details["feature"] == "video analysis"


class TestInvalidParameterException:
    """Tests for invalid parameter exception."""

    def test_message_includes_parameter_name(self):
        e = InvalidParameterException("limit", -1, "must be positive")
        assert "limit" in e.message

    def test_message_includes_description(self):
        e = InvalidParameterException("limit", -1, "must be positive")
        assert "must be positive" in e.message

    def test_status_code_400(self):
        e = InvalidParameterException("p", "v", "m")
        assert e.status_code == 400

    def test_error_code(self):
        e = InvalidParameterException("p", "v", "m")
        assert e.error_code == "INVALID_PARAMETER"

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

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


class TestSchedulerNotificationException:
    """Tests for scheduler notification exception."""

    def test_message_includes_action(self):
        e = SchedulerNotificationException("user_login", "timeout")
        assert "user_login" in e.message

    def test_status_code_500(self):
        e = SchedulerNotificationException("a", "m")
        assert e.status_code == 500

    def test_error_code(self):
        e = SchedulerNotificationException("a", "m")
        assert e.error_code == "SCHEDULER_NOTIFICATION_FAILED"

    def test_details_include_action(self):
        e = SchedulerNotificationException("refresh", "m")
        assert e.details["action"] == "refresh"


# --- Cross-cutting tests ---


class TestExceptionHierarchy:
    """Test that all exceptions inherit from NewsAPIException."""

    @pytest.mark.parametrize(
        "cls",
        [
            NewsFeatureDisabledException,
            InvalidLimitException,
            SubscriptionNotFoundException,
            SubscriptionCreationException,
            SubscriptionUpdateException,
            SubscriptionDeletionException,
            DatabaseAccessException,
            NewsFeedGenerationException,
            ResearchProcessingException,
            NotImplementedException,
            InvalidParameterException,
            SchedulerNotificationException,
        ],
    )
    def test_inherits_from_base(self, cls):
        assert issubclass(cls, NewsAPIException)

    @pytest.mark.parametrize(
        "cls",
        [
            NewsFeatureDisabledException,
            InvalidLimitException,
            SubscriptionNotFoundException,
            SubscriptionCreationException,
            SubscriptionUpdateException,
            SubscriptionDeletionException,
            DatabaseAccessException,
            NewsFeedGenerationException,
            ResearchProcessingException,
            NotImplementedException,
            InvalidParameterException,
            SchedulerNotificationException,
        ],
    )
    def test_inherits_from_exception(self, cls):
        assert issubclass(cls, Exception)


class TestExceptionCatchability:
    """Test that exceptions can be caught by their base class."""

    def test_catch_by_base_class(self):
        with pytest.raises(NewsAPIException):
            raise SubscriptionNotFoundException("x")

    def test_catch_by_exception(self):
        with pytest.raises(Exception):
            raise DatabaseAccessException("op", "msg")

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