"""Tests for v0.5 Phase C — friendly error wrappers.

Covers:
- :func:`mgf.common.add_help_note` (public helper)
- :class:`BaseAppSettings.__init__` ValidationError wrapping
- :func:`mgf.common._errors._wrap_pydantic_validation_error`
"""

from __future__ import annotations

import pytest
from pydantic import Field, ValidationError

from mgf.common import add_help_note
from mgf.common._errors import _wrap_pydantic_validation_error
from mgf.common.config import BaseAppSettings
from mgf.common.exceptions import AppConfigError

# ---------------------------------------------------------------------------
# add_help_note
# ---------------------------------------------------------------------------


class TestAddHelpNote:
    def test_appends_note_to_exception(self) -> None:
        exc = ValueError("boom")
        add_help_note(exc, "try X")
        assert "try X" in exc.__notes__

    def test_doc_ref_appended_when_provided(self) -> None:
        exc = ValueError("boom")
        add_help_note(exc, "set the env var", doc_ref="docs/reference/config.md")
        assert "docs/reference/config.md" in exc.__notes__[0]

    def test_multiple_notes_accumulate(self) -> None:
        exc = ValueError("boom")
        add_help_note(exc, "first hint")
        add_help_note(exc, "second hint")
        assert len(exc.__notes__) == 2
        assert "first hint" in exc.__notes__[0]
        assert "second hint" in exc.__notes__[1]

    def test_works_on_existing_notes_attribute(self) -> None:
        exc = ValueError("boom")
        exc.add_note("pre-existing")
        add_help_note(exc, "new note")
        assert "pre-existing" in exc.__notes__
        assert "new note" in exc.__notes__[1]


# ---------------------------------------------------------------------------
# BaseAppSettings.__init__ ValidationError wrapping
# ---------------------------------------------------------------------------


class _Settings(BaseAppSettings, env_prefix="TESTAPP_"):
    db_url: str = Field(...)  # required, no default
    max_workers: int = 4


class TestSettingsValidationError:
    def test_missing_required_field_raises_appconfigerror(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.delenv("TESTAPP_DB_URL", raising=False)
        with pytest.raises(AppConfigError) as exc_info:
            _Settings()
        # The original pydantic error is preserved as __cause__.
        assert isinstance(exc_info.value.__cause__, ValidationError)

    def test_error_message_names_settings_class(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("TESTAPP_DB_URL", raising=False)
        with pytest.raises(AppConfigError, match="_Settings") as exc_info:
            _Settings()
        assert "db_url" in str(exc_info.value)

    def test_notes_name_the_env_var(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("TESTAPP_DB_URL", raising=False)
        with pytest.raises(AppConfigError) as exc_info:
            _Settings()
        notes = exc_info.value.__notes__
        # At least one note mentions the env var.
        assert any("TESTAPP_DB_URL" in n for n in notes)
        # And gives an actionable suggestion ("Set ... or pass ...").
        assert any("Set" in n and "pass" in n for n in notes)

    def test_notes_mention_explain_command(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("TESTAPP_DB_URL", raising=False)
        with pytest.raises(AppConfigError) as exc_info:
            _Settings()
        notes = exc_info.value.__notes__
        assert any("mgf-common explain" in n for n in notes)

    def test_invalid_type_raises_appconfigerror(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("TESTAPP_DB_URL", "x")
        monkeypatch.setenv("TESTAPP_MAX_WORKERS", "not-an-int")
        with pytest.raises(AppConfigError, match="max_workers") as exc_info:
            _Settings()
        notes = exc_info.value.__notes__
        assert any("TESTAPP_MAX_WORKERS" in n for n in notes)
        # Type mismatch hint (vs. missing).
        assert any("malformed" in n.lower() or "check" in n.lower() for n in notes)

    def test_valid_settings_construct_normally(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("TESTAPP_DB_URL", "sqlite://")
        s = _Settings()
        assert s.db_url == "sqlite://"
        assert s.max_workers == 4

    def test_kwarg_override_constructs_normally(self) -> None:
        s = _Settings(db_url="sqlite://", max_workers=8)
        assert s.db_url == "sqlite://"
        assert s.max_workers == 8


class TestSettingsWithoutEnvPrefix:
    """Settings classes with no env_prefix still get sensible notes."""

    def test_no_prefix_still_wraps(self) -> None:
        class _NoPrefix(BaseAppSettings):
            required_field: str = Field(...)

        with pytest.raises(AppConfigError) as exc_info:
            _NoPrefix()
        # Without an env prefix, the env-var hint just names the field.
        notes = exc_info.value.__notes__
        assert any("REQUIRED_FIELD" in n for n in notes)


# ---------------------------------------------------------------------------
# _wrap_pydantic_validation_error — direct unit tests
# ---------------------------------------------------------------------------


class TestWrapPydanticValidationError:
    def test_summary_includes_field_paths(self) -> None:
        try:
            _Settings()
        except Exception:
            from pydantic import BaseModel

            class _Inner(BaseModel):
                a: int
                b: int

            try:
                _Inner()
            except ValidationError as ve:
                wrapped = _wrap_pydantic_validation_error(
                    ve, settings_class_name="X", env_prefix="X_"
                )
                assert "a" in str(wrapped)
                assert "b" in str(wrapped)

    def test_preserves_chain(self) -> None:
        from pydantic import BaseModel

        class _Inner(BaseModel):
            a: int

        try:
            _Inner()
        except ValidationError as ve:
            wrapped = _wrap_pydantic_validation_error(ve, settings_class_name="X", env_prefix="")
            # Wrapped is an AppConfigError; chaining is the consumer's job
            # via `raise wrapped from ve`.
            assert isinstance(wrapped, AppConfigError)
            assert wrapped.__notes__  # non-empty
