"""User-perspective tests for ``mgf.apiprobe.exceptions``.

Surfaces exercised:
  - The hierarchy roots (``ApiprobeError``, category bases) all
    importable from the public path.
  - Each is a subclass of ``mgf.common.exceptions.AppError`` (or
    a category descendant) so the upstream CLI exit-code
    translator handles them.
  - Construction shape — error string round-trip, raise-and-catch.
"""

from __future__ import annotations

import pytest
from mgf.common.exceptions import AppError, ConfigError, OperationError

from mgf.apiprobe.exceptions import (
    ApiprobeConfigError,
    ApiprobeError,
    AuthError,
    MatcherError,
    SchemaError,
    SuiteLogicError,
    TransportError,
)


class TestHierarchy:
    def test_root_is_app_error_subclass(self) -> None:
        assert issubclass(ApiprobeError, AppError)

    def test_config_error_chain(self) -> None:
        # ApiprobeConfigError inherits from BOTH the apiprobe root
        # AND the upstream ConfigError — so consumers catching
        # either path get it.
        assert issubclass(ApiprobeConfigError, ConfigError)
        assert issubclass(ApiprobeConfigError, ApiprobeError)

    def test_transport_error_chain(self) -> None:
        assert issubclass(TransportError, OperationError)
        assert issubclass(TransportError, ApiprobeError)

    @pytest.mark.parametrize(
        "exc_cls",
        [AuthError, MatcherError, SchemaError, SuiteLogicError],
    )
    def test_category_bases_are_apiprobe_errors(
        self,
        exc_cls: type[ApiprobeError],
    ) -> None:
        assert issubclass(exc_cls, ApiprobeError)


class TestRaiseAndCatch:
    def test_raise_apiprobe_error(self) -> None:
        with pytest.raises(ApiprobeError, match="boom"):
            raise ApiprobeError("boom")

    def test_caught_via_app_error_root(self) -> None:
        # Consumer code that catches the upstream root catches us too.
        with pytest.raises(AppError):
            raise SuiteLogicError("invariant violated")

    def test_transport_error_carries_url_and_cause(self) -> None:
        try:
            raise TransportError("https://api.example.com", "DNS failure")
        except TransportError as exc:
            assert exc.url == "https://api.example.com"
            assert exc.cause == "DNS failure"
            assert "DNS failure" in str(exc)
            assert "https://api.example.com" in str(exc)

    def test_auth_error_carries_flow_name_and_reason(self) -> None:
        try:
            raise AuthError("bearer", "token env var unset")
        except AuthError as exc:
            assert exc.flow_name == "bearer"
            assert exc.reason == "token env var unset"
            assert "bearer" in str(exc)
