"""Tests for ``mgf.apiprobe.core.matchers.primitives``."""

from __future__ import annotations

import re

import pytest

from mgf.apiprobe.core.matchers.primitives import (
    between,
    equals,
    gt,
    gte,
    in_,
    lt,
    lte,
    matches,
    not_equals,
    not_in,
)
from mgf.apiprobe.exceptions import MatcherError


class TestEquality:
    def test_equals_true(self) -> None:
        assert equals(42)(42) is True
        assert equals("x")("x") is True

    def test_equals_false(self) -> None:
        assert equals(42)(43) is False
        assert equals("x")("y") is False

    def test_not_equals_inverse(self) -> None:
        assert not_equals(42)(43) is True
        assert not_equals(42)(42) is False

    def test_describe(self) -> None:
        assert "==" in equals(42).describe()
        assert "42" in equals(42).describe()


class TestOrdering:
    @pytest.mark.parametrize(
        ("matcher_factory", "value", "expected"),
        [
            (gt(0), 1, True),
            (gt(0), 0, False),
            (gt(0), -1, False),
            (gte(0), 0, True),
            (gte(0), -1, False),
            (lt(10), 9, True),
            (lt(10), 10, False),
            (lte(10), 10, True),
            (lte(10), 11, False),
        ],
    )
    def test_numeric_ordering(self, matcher_factory: object, value: int, expected: bool) -> None:
        assert matcher_factory(value) is expected  # type: ignore[operator]

    def test_type_mismatch_raises(self) -> None:
        with pytest.raises(MatcherError, match="cannot compare"):
            gt(0)("string")

    def test_describe_includes_threshold(self) -> None:
        assert ">" in gt(0).describe()
        assert "0" in gt(0).describe()


class TestBetween:
    def test_inclusive_both_sides(self) -> None:
        m = between(0, 10)
        assert m(0) is True
        assert m(5) is True
        assert m(10) is True

    def test_outside_range(self) -> None:
        m = between(0, 10)
        assert m(-1) is False
        assert m(11) is False

    def test_low_high_swapped_raises(self) -> None:
        with pytest.raises(MatcherError, match="low > high"):
            between(10, 0)


class TestSetMembership:
    def test_in_(self) -> None:
        m = in_(200, 304)
        assert m(200) is True
        assert m(304) is True
        assert m(404) is False

    def test_not_in(self) -> None:
        m = not_in(404, 500)
        assert m(200) is True
        assert m(404) is False

    def test_describe_renders_set(self) -> None:
        d = in_(200, 304).describe()
        assert "200" in d
        assert "304" in d


class TestMatches:
    def test_string_pattern(self) -> None:
        m = matches(r"^[a-z]+$")
        assert m("hello") is True
        assert m("Hello") is False
        assert m("hello123") is False

    def test_compiled_pattern(self) -> None:
        m = matches(re.compile(r"\d+"))
        assert m("abc123") is True
        assert m("abcdef") is False

    def test_non_string_returns_false(self) -> None:
        # Cross-type: not an exception. Return False so the check
        # surfaces as a Finding, not a TypeError.
        assert matches(r".*")(42) is False
        assert matches(r".*")(None) is False

    def test_search_semantics_not_match(self) -> None:
        # `search`, not `match` — pattern not anchored unless caller anchors it.
        assert matches(r"world")("hello world") is True


class TestComposition:
    def test_and_chaining(self) -> None:
        m = gt(0) & lt(100)
        assert m(50) is True
        assert m(0) is False
        assert m(100) is False
        assert m(101) is False

    def test_or_chaining(self) -> None:
        m = equals(200) | equals(304)
        assert m(200) is True
        assert m(304) is True
        assert m(404) is False
