"""Tests for ``mgf.apiprobe.schema.pydantic``."""

from __future__ import annotations

import pytest
from pydantic import BaseModel

from mgf.apiprobe.exceptions import SchemaError
from mgf.apiprobe.schema.base import SchemaSource
from mgf.apiprobe.schema.pydantic import PydanticSchemaSource


class UserResponse(BaseModel):
    id: str
    email: str
    age: int


class TestConstruction:
    def test_subclasses_schema_source(self) -> None:
        assert issubclass(PydanticSchemaSource, SchemaSource)

    def test_rejects_non_basemodel(self) -> None:
        with pytest.raises(SchemaError, match="BaseModel subclass"):
            PydanticSchemaSource(dict)  # type: ignore[arg-type]

    def test_rejects_non_class(self) -> None:
        with pytest.raises(SchemaError):
            PydanticSchemaSource("not a class")  # type: ignore[arg-type]


class TestValidate:
    def test_valid_payload_returns_no_issues(self) -> None:
        s = PydanticSchemaSource(UserResponse)
        issues = s.validate({"id": "abc", "email": "a@b.c", "age": 30})
        assert issues == []

    def test_missing_field_emits_issue(self) -> None:
        s = PydanticSchemaSource(UserResponse)
        issues = s.validate({"id": "abc", "email": "a@b.c"})
        assert len(issues) == 1
        assert issues[0].path == "$.age"
        assert "missing" in issues[0].message.lower() or "required" in issues[0].message.lower()

    def test_wrong_type_emits_issue(self) -> None:
        s = PydanticSchemaSource(UserResponse)
        issues = s.validate({"id": "abc", "email": "a@b.c", "age": "thirty"})
        assert len(issues) == 1
        assert issues[0].path == "$.age"
        assert "thirty" in issues[0].actual or "str" in issues[0].actual

    def test_multiple_issues_reported(self) -> None:
        s = PydanticSchemaSource(UserResponse)
        # Both id and age wrong types.
        issues = s.validate({"id": 123, "email": "a@b.c", "age": "x"})
        assert len(issues) == 2

    def test_top_level_non_dict(self) -> None:
        s = PydanticSchemaSource(UserResponse)
        issues = s.validate("not a dict")
        assert len(issues) >= 1
        assert issues[0].path == "$"


class TestDescribe:
    def test_describe_includes_model_qualname(self) -> None:
        s = PydanticSchemaSource(UserResponse)
        d = s.describe()
        assert "UserResponse" in d
        assert d.startswith("pydantic:")


class TestActualFormatter:
    @pytest.mark.parametrize(
        ("payload", "field", "expected_substring"),
        [
            ({"id": None, "email": "a@b.c", "age": 30}, "$.id", "null"),
            ({"id": True, "email": "a@b.c", "age": 30}, "$.id", "bool"),
            ({"id": 42, "email": "a@b.c", "age": 30}, "$.id", "int 42"),
        ],
    )
    def test_actual_renders(
        self,
        payload: dict[str, object],
        field: str,
        expected_substring: str,
    ) -> None:
        s = PydanticSchemaSource(UserResponse)
        issues = s.validate(payload)
        match = next((i for i in issues if i.path == field), None)
        assert match is not None
        assert expected_substring in match.actual

    def test_long_string_truncated(self) -> None:
        s = PydanticSchemaSource(UserResponse)
        long_value = "x" * 200
        # Send long string as `age` (wrong type) to inspect the formatter.
        issues = s.validate({"id": "abc", "email": "a@b.c", "age": long_value})
        match = next(i for i in issues if i.path == "$.age")
        assert "..." in match.actual
        assert len(match.actual) <= 80  # capped + prefix
