import pytest
from dataclasses import field
from typing import Annotated, Iterator

from monk import monk, validate
from monk.exceptions import ValidationError
from monk.constraints import (
    Each,
    Len,
    LowerCase,
    Email,
    Unique,
    Contains,
    OneOf,
    MultipleOf,
    Nullable,
    NotNull,
    UpperCase,
)


# --- Raw Constraint Tests ---


def test_len_constraint_success() -> None:
    constraint = Len(min_len=2, max_len=4)

    # Should work on lists, strings, and dictionaries
    constraint.validate([1, 2])
    constraint.validate([1, 2, 3, 4])
    constraint.validate("abc")
    constraint.validate({"a": 1, "b": 2})


def test_len_constraint_failure() -> None:
    constraint = Len(min_len=2, max_len=4)

    with pytest.raises(ValueError):
        constraint.validate([1])

    with pytest.raises(ValueError):
        constraint.validate([1, 2, 3, 4, 5])


def test_len_init_failure() -> None:
    with pytest.raises(ValueError):
        Len(min_len=5, max_len=2)


def test_len_type_error() -> None:
    constraint = Len(min_len=1)
    with pytest.raises(TypeError):
        constraint.validate(123)  # Integers have no length


def test_contains_constraint_success() -> None:
    constraint = Contains("apple")

    # Lists
    constraint.validate(["apple", "banana"])
    # Strings (substrings)
    constraint.validate("I ate an apple pie")
    # Dicts (checks keys)
    constraint.validate({"apple": 1, "orange": 2})


def test_contains_constraint_failure() -> None:
    constraint = Contains("apple")

    with pytest.raises(ValueError):
        constraint.validate(["banana", "orange"])


def test_contains_type_error() -> None:
    constraint = Contains("apple")
    with pytest.raises(TypeError):
        constraint.validate(123)


def test_unique_constraint_success() -> None:
    Unique().validate([1, 2, 3, 4])
    Unique().validate(("a", "b", "c"))
    Unique().validate({1, 2, 3})

    # Deeply nested unhashable structures
    Unique().validate([[1, 2], [3, 4]])
    Unique().validate([[{"a": [1]}, {"b": [2]}], [{"c": [3]}]])


def test_unique_constraint_failure() -> None:
    with pytest.raises(ValueError):
        Unique().validate([1, 2, 2, 3])
    with pytest.raises(ValueError):
        Unique().validate([[1, 2], [1, 2]])
    with pytest.raises(ValueError):
        Unique().validate([[{"a": [1]}], [{"a": [1]}]])
    with pytest.raises(ValueError):
        Unique().validate([[1, [2, 3]], [1, [2, 3]]])


def test_unique_type_error() -> None:
    with pytest.raises(TypeError):
        Unique().validate(123)


def test_unique_non_sized_iterable() -> None:
    """Tests that a re-usable Iterable without __len__ is safely converted to a tuple."""

    class ReusableIterable:
        def __init__(self, data: list[int]):
            self.data = data

        def __iter__(self) -> Iterator[int]:
            for item in self.data:
                yield item

    # Not an Iterator, not Sized, but IS an Iterable!
    Unique().validate(ReusableIterable([1, 2, 3]))

    with pytest.raises(ValueError):
        Unique().validate(ReusableIterable([1, 1]))


def test_exhaustible_iterator_rejection() -> None:
    """Ensure constraints that iterate over values refuse to silently consume generators."""
    gen = (x for x in [1, 2, 3])

    with pytest.raises(TypeError, match="Cannot eagerly validate exhaustible iterator"):
        Each(LowerCase).validate(gen)
    with pytest.raises(TypeError, match="Cannot eagerly validate exhaustible iterator"):
        Contains(2).validate(gen)
    with pytest.raises(TypeError, match="Cannot eagerly validate exhaustible iterator"):
        Unique().validate(gen)


def test_one_of_constraint_success() -> None:
    constraint = OneOf(["active", "pending", "closed"])

    constraint.validate("active")
    constraint.validate("closed")


def test_one_of_constraint_failure() -> None:
    constraint = OneOf(["active", "pending", "closed"])

    with pytest.raises(ValueError):
        constraint.validate("archived")


def test_one_of_init_failure() -> None:
    # Developer experience check: ensuring they don't create empty constraints
    with pytest.raises(ValueError):
        OneOf([])


def test_one_of_generator_conversion() -> None:
    # Prove that passing an exhaustible generator safely converts to a tuple internally
    constraint = OneOf(x for x in ["a", "b"])
    constraint.validate("a")


def test_each_init_and_type_failure() -> None:
    with pytest.raises(ValueError):
        Each()

    with pytest.raises(TypeError):
        Each(LowerCase).validate(123)

    with pytest.raises(TypeError, match="missing required arguments"):
        Each(MultipleOf)


def test_each_nullability_failure() -> None:
    # Prove that the Each constraint does NOT skip None items, and properly raises NotNull errors
    with pytest.raises(ValidationError) as exc:
        Each(LowerCase).validate(["hello", None, "world"])

    errors = exc.value.errors
    assert len(errors) == 1
    assert errors[0]["field"] == "[1]"
    assert errors[0]["code"] == "NotNull"
    assert "cannot be null" in errors[0]["message"]


def test_nullable_marker() -> None:
    # 0. Cover the dummy validate method required by the MonkConstraint Protocol
    Nullable().validate("anything")
    NotNull().validate("anything")

    # 1. Success & Nullability
    constraint = Each(Nullable, LowerCase)
    constraint.validate(["hello", None])

    # 2. Failure
    with pytest.raises(ValidationError) as exc:
        constraint.validate(["HELLO", None])
    assert "islower" in exc.value.errors[0]["message"]

    # 3. Requires other constraints
    with pytest.raises(ValueError, match="functional constraint besides markers"):
        Each(Nullable)

    # 4. NotNull inside Each
    constraint_not_null = Each(NotNull, LowerCase)
    with pytest.raises(ValidationError) as exc_nn:
        constraint_not_null.validate(["hello", None])
    assert exc_nn.value.errors[0]["field"] == "[1]"
    assert exc_nn.value.errors[0]["code"] == "NotNull"


# --- Dataclass Integration Tests ---


@monk
class Team:
    # Multiple constraints on each element
    members: Annotated[list[str], Each(Len(min_len=3), LowerCase)]
    # Combining collection-level constraints (Unique) with item-level constraints (Each)
    contact_emails: Annotated[list[str], Unique, Each(Email)]
    # 2D Nested Array check
    matrix: Annotated[list[list[str]], Each(Each(LowerCase))]
    # Collection-level constraints
    department: Annotated[str, OneOf(["engineering", "sales", "marketing"])]
    tags: Annotated[list[str], Contains("core")]
    # Ensure we skip validation if None is present
    managers: Annotated[list[str] | None, Nullable, Each(Len(max_len=50))] = None
    # Ensure we gracefully handle explicit nested nulls
    sparse_matrix: Annotated[list[list[str | None]], Each(Each(Nullable, LowerCase))] = field(default_factory=list)


def test_each_constraint_success() -> None:
    team = Team(
        members=["alice", "bob", "charlie"],
        contact_emails=["a@b.com", "test@domain.com"],
        matrix=[["a", "b"], ["c", "d"]],
        department="engineering",
        tags=["backend", "core"],
        sparse_matrix=[["a", None], [None, "b"]],
    )
    validate(team)  # Should not raise


def test_each_constraint_failure() -> None:
    team = Team(
        members=["al", "Bob", "charlie"],
        contact_emails=["a@b.com", "not-email"],
        matrix=[["a", "B"]],
        department="engineeri",
        tags=["backend"],
        sparse_matrix=[["A", None]],
    )

    with pytest.raises(ValidationError) as exc:
        validate(team)

    errors = exc.value.errors
    assert len(errors) == 7

    assert "members" in errors[0]["field"]  # 'al' failed length
    assert "members" in errors[1]["field"]  # 'Bob' failed LowerCase
    assert "contact_emails" in errors[2]["field"]  # 'not-email' failed Email
    assert "matrix" in errors[3]["field"]  # 'B' failed LowerCase in the nested array
    assert "department" in errors[4]["field"]  # 'engineeri' fails OneOf
    assert "tags" in errors[5]["field"]  # Does not contain 'core'
    assert "sparse_matrix[0][0]" in errors[6]["field"]  # 'A' failed LowerCase


def test_contains_keys_constraint() -> None:
    from monk.constraints import ContainsKeys

    constraint = ContainsKeys(["a", "b"])

    constraint.validate({"a": 1, "b": 2, "c": 3})

    with pytest.raises(ValueError, match="missing required keys"):
        constraint.validate({"a": 1})

    with pytest.raises(TypeError):
        constraint.validate(["a", "b"])


def test_subset_constraint() -> None:
    from monk.constraints import Subset

    constraint = Subset(["A", "B", "C"])

    constraint.validate(["A", "B"])
    constraint.validate(("A",))
    constraint.validate(set())

    with pytest.raises(ValueError, match="Contains unallowed items."):
        constraint.validate(["A", "D"])
    with pytest.raises(TypeError):
        constraint.validate(123)
    with pytest.raises(TypeError, match="Cannot eagerly validate exhaustible iterator"):
        constraint.validate((x for x in ["A"]))


def test_exact_len_constraint() -> None:
    from monk.constraints import ExactLen

    ExactLen(5).validate("12345")
    ExactLen(3).validate([1, 2, 3])

    with pytest.raises(ValueError, match="Must have an exact length of 5."):
        ExactLen(5).validate("1234")
    with pytest.raises(TypeError):
        ExactLen(5).validate(123)


def test_dictof_constraint() -> None:
    from monk.constraints import DictOf, LowerCase, URL, Len, Each

    constraint = DictOf(key=LowerCase, value=URL)

    # 1. Success
    constraint.validate({"tag": "https://example.com"})
    constraint.validate({})  # Empty dict passes gracefully

    # 2. Key failure (uses the <key: ...> format)
    with pytest.raises(ValidationError) as exc:
        constraint.validate({"TAG": "https://example.com"})
    assert exc.value.errors[0]["field"] == "<key: 'TAG'>"
    assert exc.value.errors[0]["code"] == "Predicate"

    # 3. Value failure
    with pytest.raises(ValidationError) as exc2:
        constraint.validate({"tag": "not-a-url"})
    assert exc2.value.errors[0]["field"] == "['tag']"
    assert exc2.value.errors[0]["code"] == "URL"

    # 4. Iterable of constraints for Keys and Values
    complex_constraint = DictOf(key=[LowerCase, Len(min_len=3)], value=Len(max_len=10))
    with pytest.raises(ValidationError) as exc3:
        complex_constraint.validate({"ta": "this is way too long"})
    assert len(exc3.value.errors) == 2
    assert exc3.value.errors[0]["field"] == "<key: 'ta'>"  # Failed min_len on key
    assert exc3.value.errors[1]["field"] == "['ta']"  # Failed max_len on value

    # 5. Inner ValidationError aggregation
    nested_constraint = DictOf(value=Each(LowerCase))
    with pytest.raises(ValidationError) as exc4:
        nested_constraint.validate({"tags": ["a", "B"]})
    assert exc4.value.errors[0]["field"] == "['tags'][1]"

    # 6. Key raises a ValidationError internally
    nested_constraint = DictOf(key=Each(UpperCase))
    with pytest.raises(ValidationError) as exc5:
        nested_constraint.validate({("tags", "labels"): ["a", "B"]})
    assert len(exc5.value.errors) == 2
    assert exc5.value.errors[0]["field"] == "<key: ('tags', 'labels')>[0]"
    assert exc5.value.errors[1]["field"] == "<key: ('tags', 'labels')>[1]"

    # 6. Edge cases
    with pytest.raises(TypeError):
        constraint.validate(123)
    with pytest.raises(TypeError, match="missing required arguments"):
        DictOf(key=MultipleOf)
