from typing import Annotated
from uuid import UUID

import pytest
from pydantic import BaseModel, ValidationError

from slices_typeid import TypeID
from slices_typeid.pydantic import TypeIDSerializer, TypeIdType, typeid_validator

# ---------------------------------------------------------------------------
# Shared fixtures
# ---------------------------------------------------------------------------

SUFFIX = "01kjsxktfrf70agz88x0k1vzy5"
EXPECTED_UUID = UUID("019cb3d9-e9f8-79c0-a87d-08e8261dffc5")
TYPEID_STR = f"r_example.com_{SUFFIX}"
TYPEID = TypeID(type="r", domain="example.com", suffix=SUFFIX)


# ---------------------------------------------------------------------------
# Models used across tests
# ---------------------------------------------------------------------------


class TypeIdTypeModel(BaseModel):
    """Model constrained to type='r' and domain='example.com'."""

    tid: Annotated[TypeID, TypeIdType("r", "example.com")]


class TypeIdTypeModelTypeOnly(BaseModel):
    """Model constrained to type='r', any domain."""

    tid: Annotated[TypeID, TypeIdType("r")]


class ValidatorModel(BaseModel):
    """Model using typeid_validator alongside TypeIdType."""

    tid: Annotated[
        TypeID,
        TypeIdType("r", "example.com"),
        typeid_validator("r", "example.com"),
        TypeIDSerializer,
    ]


class ValidatorModelTypeOnly(BaseModel):
    """Model using typeid_validator with no domain constraint."""

    tid: Annotated[
        TypeID,
        TypeIdType("r"),
        typeid_validator("r", None),
        TypeIDSerializer,
    ]


class ValidatorModelNoSerializer(BaseModel):
    """Model using typeid_validator alongside TypeIdType."""

    tid: Annotated[
        TypeID,
        TypeIdType("r", "example.com"),
        typeid_validator("r", "example.com"),
    ]


# ===========================================================================
# TypeIdType tests
# ===========================================================================


class TestTypeIdTypeValidInput:
    def test_accepts_valid_string(self):
        m = TypeIdTypeModel(tid=TYPEID_STR)  # pyright: ignore[reportArgumentType]
        assert isinstance(m.tid, TypeID)
        assert m.tid == TYPEID

    def test_accepts_typeid_instance(self):
        m = TypeIdTypeModel(tid=TYPEID)
        assert m.tid == TYPEID

    def test_parsed_fields_are_correct(self):
        m = TypeIdTypeModel(tid=TYPEID_STR)  # pyright: ignore[reportArgumentType]
        assert m.tid.type == "r"
        assert m.tid.domain == "example.com"
        assert m.tid.suffix == SUFFIX
        assert m.tid.uuid == EXPECTED_UUID

    def test_accepts_any_domain_when_unconstrained(self):
        m = TypeIdTypeModelTypeOnly(tid="r_other.org_01kjsxktfrf70agz88x0k1vzy5")  # pyright: ignore[reportArgumentType]
        assert m.tid.domain == "other.org"


class TestTypeIdTypeInvalidInput:
    def test_rejects_wrong_type(self):
        with pytest.raises(ValidationError):
            TypeIdTypeModel(tid=f"x_example.com_{SUFFIX}")  # pyright: ignore[reportArgumentType]

    def test_rejects_wrong_domain(self):
        with pytest.raises(ValidationError):
            TypeIdTypeModel(tid=f"r_other.org_{SUFFIX}")  # pyright: ignore[reportArgumentType]

    def test_rejects_malformed_string(self):
        with pytest.raises(ValidationError):
            TypeIdTypeModel(tid="not-a-typeid")  # pyright: ignore[reportArgumentType]


class TestTypeIdTypeJsonSchema:
    def test_schema_has_type_string(self):
        schema = TypeIdTypeModel.model_json_schema()
        assert schema["properties"]["tid"]["type"] == "string"

    def test_schema_pattern_includes_type_and_domain(self):
        schema = TypeIdTypeModel.model_json_schema()
        pattern = schema["properties"]["tid"]["pattern"]
        assert pattern.startswith("r_example.com_")

    def test_schema_type_only_pattern_includes_domain_regex(self):
        schema = TypeIdTypeModelTypeOnly.model_json_schema()
        pattern = schema["properties"]["tid"]["pattern"]
        assert pattern.startswith("r_")
        assert "example.com" not in pattern  # no fixed domain

    def test_schema_contains_example(self):
        schema = TypeIdTypeModel.model_json_schema()
        assert "example" in schema["properties"]["tid"]


class TestTypeIdTypeJsonRoundTrip:
    def test_json_serialize_produces_string(self):
        m = TypeIdTypeModel(tid=TYPEID_STR)  # pyright: ignore[reportArgumentType]
        data = m.model_dump()
        assert data["tid"] == TYPEID_STR

    def test_json_dump_and_load(self):
        class SerializableModel(BaseModel):
            tid: Annotated[TypeID, TypeIdType("r", "example.com"), TypeIDSerializer]

        m = SerializableModel(tid=TYPEID_STR)  # pyright: ignore[reportArgumentType]
        dumped = m.model_dump_json()
        assert f'"{TYPEID_STR}"' in dumped

        loaded = SerializableModel.model_validate_json(dumped)
        assert loaded.tid == TYPEID


# ===========================================================================
# typeid_validator tests
# ===========================================================================


class TestTypeidValidatorValidInput:
    def test_accepts_typeid_string(self):
        m = ValidatorModel(tid=TYPEID_STR)  # pyright: ignore[reportArgumentType]
        assert m.tid == TYPEID

    def test_accepts_typeid_instance(self):
        m = ValidatorModel(tid=TYPEID)
        assert m.tid == TYPEID

    def test_accepts_uuid_instance(self):
        m = ValidatorModel(tid=EXPECTED_UUID)  # pyright: ignore[reportArgumentType]
        assert m.tid.uuid == EXPECTED_UUID
        assert m.tid.type == "r"
        assert m.tid.domain == "example.com"

    def test_accepts_uuid_string(self):
        m = ValidatorModel(tid="019cb3d9-e9f8-79c0-a87d-08e8261dffc5")  # pyright: ignore[reportArgumentType]
        assert m.tid.uuid == EXPECTED_UUID
        assert m.tid.type == "r"
        assert m.tid.domain == "example.com"

    def test_accepts_any_domain_when_unconstrained(self):
        m = ValidatorModelTypeOnly(tid="r_other.org_01kjsxktfrf70agz88x0k1vzy5")  # pyright: ignore[reportArgumentType]
        assert m.tid.type == "r"
        assert m.tid.domain == "other.org"


class TestTypeidValidatorInvalidInput:
    def test_rejects_wrong_type(self):
        with pytest.raises(ValidationError):
            ValidatorModel(tid=f"x_example.com_{SUFFIX}")  # pyright: ignore[reportArgumentType]

    def test_rejects_wrong_domain(self):
        with pytest.raises(ValidationError):
            ValidatorModel(tid=TypeID(type="r", domain="other.org", suffix=SUFFIX))

    def test_type_only_rejects_wrong_type(self):
        with pytest.raises(ValidationError):
            ValidatorModelTypeOnly(tid=f"x_other.org_{SUFFIX}")  # pyright: ignore[reportArgumentType]


class TestTypeidValidatorJsonRoundTrip:
    def test_json_dump_and_load(self):
        m = ValidatorModel(tid=TYPEID_STR)  # pyright: ignore[reportArgumentType]
        dumped = m.model_dump_json()
        assert f'"{TYPEID_STR}"' in dumped

        loaded = ValidatorModel.model_validate_json(dumped)
        assert loaded.tid == TYPEID


class TestTypeidValidatorNoSerializerJsonRoundTrip:
    def test_json_dump_and_load(self):
        m = ValidatorModelNoSerializer(tid=TYPEID_STR)  # pyright: ignore[reportArgumentType]
        dumped = m.model_dump_json()
        assert f'"{TYPEID_STR}"' in dumped

        loaded = ValidatorModelNoSerializer.model_validate_json(dumped)
        assert loaded.tid == TYPEID
