"""Tests for LLM exception hierarchy.

Tests verify inheritance, attribute storage, and message formatting
for all exception classes in llm/exceptions.py.
"""

from __future__ import annotations

import pytest

from unfamiliar.llm.exceptions import (
    AuthenticationError,
    BriefingGenerationError,
    LLMError,
    MissionGenerationError,
    ProviderError,
    RetryableError,
)


class TestLLMError:
    """Tests for LLMError base exception."""

    def test_inherits_from_exception(self):
        """LLMError is a subclass of Exception."""
        assert issubclass(LLMError, Exception)

    def test_accepts_message_argument(self):
        """LLMError accepts and stores a message string."""
        error = LLMError("something went wrong")

        assert str(error) == "something went wrong"


class TestProviderError:
    """Tests for ProviderError exception."""

    def test_inherits_from_llm_error(self):
        """ProviderError is a subclass of LLMError."""
        assert issubclass(ProviderError, LLMError)

    def test_inherits_from_exception_transitively(self):
        """ProviderError is transitively a subclass of Exception."""
        assert issubclass(ProviderError, Exception)

    def test_accepts_message_argument(self):
        """ProviderError accepts and stores a message string."""
        error = ProviderError("unknown provider 'foo'")

        assert str(error) == "unknown provider 'foo'"


class TestAuthenticationError:
    """Tests for AuthenticationError exception."""

    def test_inherits_from_llm_error(self):
        """AuthenticationError is a subclass of LLMError."""
        assert issubclass(AuthenticationError, LLMError)

    def test_accepts_message_argument(self):
        """AuthenticationError accepts and stores a message string."""
        error = AuthenticationError("invalid API key")

        assert str(error) == "invalid API key"


class TestBriefingGenerationError:
    """Tests for BriefingGenerationError exception."""

    def test_inherits_from_llm_error(self):
        """BriefingGenerationError is a subclass of LLMError."""
        assert issubclass(BriefingGenerationError, LLMError)

    def test_default_attempts_is_one(self):
        """BriefingGenerationError defaults attempts to 1."""
        error = BriefingGenerationError("generation failed")

        assert error.attempts == 1

    def test_custom_attempts_stored(self):
        """BriefingGenerationError stores custom attempts value."""
        error = BriefingGenerationError("generation failed", attempts=3)

        assert error.attempts == 3

    def test_message_includes_attempt_count_with_default(self):
        """Message includes '(after 1 attempt(s))' for default attempts."""
        error = BriefingGenerationError("generation failed")

        assert str(error) == "generation failed (after 1 attempt(s))"

    def test_message_includes_attempt_count_with_custom(self):
        """Message includes '(after N attempt(s))' for custom attempts."""
        error = BriefingGenerationError("generation failed", attempts=5)

        assert str(error) == "generation failed (after 5 attempt(s))"


class TestMissionGenerationError:
    """Tests for MissionGenerationError exception."""

    def test_inherits_from_llm_error(self):
        """MissionGenerationError is a subclass of LLMError."""
        assert issubclass(MissionGenerationError, LLMError)

    def test_default_category_is_empty_string(self):
        """MissionGenerationError defaults category to empty string."""
        error = MissionGenerationError("generation failed")

        assert error.category == ""

    def test_default_attempts_is_one(self):
        """MissionGenerationError defaults attempts to 1."""
        error = MissionGenerationError("generation failed")

        assert error.attempts == 1

    def test_custom_category_stored(self):
        """MissionGenerationError stores custom category value."""
        error = MissionGenerationError("failed", category="architecture")

        assert error.category == "architecture"

    def test_custom_attempts_stored(self):
        """MissionGenerationError stores custom attempts value."""
        error = MissionGenerationError("failed", attempts=3)

        assert error.attempts == 3

    def test_both_custom_values_stored(self):
        """MissionGenerationError stores both category and attempts when provided."""
        error = MissionGenerationError("failed", category="architecture", attempts=3)

        assert error.category == "architecture"
        assert error.attempts == 3

    def test_message_without_category_shows_generic_text(self):
        """Message shows 'mission generation' when category is empty."""
        error = MissionGenerationError("generation failed")

        assert str(error) == (
            "generation failed (mission generation, after 1 attempt(s))"
        )

    def test_message_with_category_includes_category_name(self):
        """Message includes 'category X' when category is provided."""
        error = MissionGenerationError("failed", category="architecture")

        assert str(error) == "failed (category 'architecture', after 1 attempt(s))"

    def test_message_with_category_and_custom_attempts(self):
        """Message includes both category and custom attempt count."""
        error = MissionGenerationError("failed", category="testing", attempts=4)

        assert str(error) == "failed (category 'testing', after 4 attempt(s))"


class TestPolymorphicCatch:
    """Tests for polymorphic exception catching via LLMError."""

    def test_provider_error_catchable_as_llm_error(self):
        """ProviderError can be caught via 'except LLMError'."""
        with pytest.raises(LLMError) as exc_info:
            raise ProviderError("unknown provider")

        assert isinstance(exc_info.value, ProviderError)
        assert str(exc_info.value) == "unknown provider"

    def test_authentication_error_catchable_as_llm_error(self):
        """AuthenticationError can be caught via 'except LLMError'."""
        with pytest.raises(LLMError) as exc_info:
            raise AuthenticationError("bad key")

        assert isinstance(exc_info.value, AuthenticationError)
        assert str(exc_info.value) == "bad key"

    def test_briefing_generation_error_catchable_as_llm_error(self):
        """BriefingGenerationError can be caught via 'except LLMError'."""
        with pytest.raises(RetryableError) as exc_info:
            raise BriefingGenerationError("failed", attempts=2)

        assert isinstance(exc_info.value, BriefingGenerationError)
        assert exc_info.value.attempts == 2

    def test_mission_generation_error_catchable_as_llm_error(self):
        """MissionGenerationError can be caught via 'except LLMError'."""
        with pytest.raises(RetryableError) as exc_info:
            raise MissionGenerationError("failed", category="refactor", attempts=3)

        assert isinstance(exc_info.value, MissionGenerationError)
        assert exc_info.value.category == "refactor"
        assert exc_info.value.attempts == 3

    def test_all_exceptions_catchable_as_exception(self):
        """All LLM exceptions can be caught via 'except Exception'."""
        exceptions_to_test = [
            ProviderError("p"),
            AuthenticationError("a"),
            BriefingGenerationError("b"),
            MissionGenerationError("m"),
        ]

        for exc in exceptions_to_test:
            assert isinstance(exc, Exception)
