import pytest
import regex
from pytest_mock import MockerFixture

from documents.regex import safe_regex_finditer
from documents.regex import safe_regex_match
from documents.regex import safe_regex_search
from documents.regex import safe_regex_sub
from documents.regex import validate_regex_pattern


class TestValidateRegexPattern:
    def test_valid_pattern(self) -> None:
        validate_regex_pattern(r"\d+")

    def test_invalid_pattern_raises(self) -> None:
        with pytest.raises(ValueError):
            validate_regex_pattern(r"[invalid")


class TestSafeRegexSearchAndMatch:
    """Tests for safe_regex_search and safe_regex_match (same contract)."""

    @pytest.mark.parametrize(
        ("func", "pattern", "text", "expected_group"),
        [
            pytest.param(
                safe_regex_search,
                r"\d+",
                "abc123def",
                "123",
                id="search-match-found",
            ),
            pytest.param(
                safe_regex_match,
                r"\d+",
                "123abc",
                "123",
                id="match-match-found",
            ),
        ],
    )
    def test_match_found(self, func, pattern, text, expected_group) -> None:
        result = func(pattern, text)
        assert result is not None
        assert result.group() == expected_group

    @pytest.mark.parametrize(
        ("func", "pattern", "text"),
        [
            pytest.param(safe_regex_search, r"\d+", "abcdef", id="search-no-match"),
            pytest.param(safe_regex_match, r"\d+", "abc123", id="match-no-match"),
        ],
    )
    def test_no_match(self, func, pattern, text) -> None:
        assert func(pattern, text) is None

    @pytest.mark.parametrize(
        "func",
        [
            pytest.param(safe_regex_search, id="search"),
            pytest.param(safe_regex_match, id="match"),
        ],
    )
    def test_invalid_pattern_returns_none(self, func) -> None:
        assert func(r"[invalid", "test") is None

    @pytest.mark.parametrize(
        "func",
        [
            pytest.param(safe_regex_search, id="search"),
            pytest.param(safe_regex_match, id="match"),
        ],
    )
    def test_flags_respected(self, func) -> None:
        assert func(r"abc", "ABC", flags=regex.IGNORECASE) is not None

    @pytest.mark.parametrize(
        ("func", "method_name"),
        [
            pytest.param(safe_regex_search, "search", id="search"),
            pytest.param(safe_regex_match, "match", id="match"),
        ],
    )
    def test_timeout_returns_none(
        self,
        func,
        method_name,
        mocker: MockerFixture,
    ) -> None:
        mock_compile = mocker.patch("documents.regex.regex.compile")
        getattr(mock_compile.return_value, method_name).side_effect = TimeoutError
        assert func(r"\d+", "test") is None


class TestSafeRegexSub:
    @pytest.mark.parametrize(
        ("pattern", "repl", "text", "expected"),
        [
            pytest.param(r"\d+", "NUM", "abc123def456", "abcNUMdefNUM", id="basic-sub"),
            pytest.param(r"\d+", "NUM", "abcdef", "abcdef", id="no-match"),
            pytest.param(r"abc", "X", "ABC", "X", id="flags"),
        ],
    )
    def test_substitution(self, pattern, repl, text, expected) -> None:
        flags = regex.IGNORECASE if pattern == r"abc" else 0
        result = safe_regex_sub(pattern, repl, text, flags=flags)
        assert result == expected

    def test_invalid_pattern_returns_none(self) -> None:
        assert safe_regex_sub(r"[invalid", "x", "test") is None

    def test_timeout_returns_none(self, mocker: MockerFixture) -> None:
        mock_compile = mocker.patch("documents.regex.regex.compile")
        mock_compile.return_value.sub.side_effect = TimeoutError
        assert safe_regex_sub(r"\d+", "X", "test") is None


class TestSafeRegexFinditer:
    def test_yields_matches(self) -> None:
        pattern = regex.compile(r"\d+")
        matches = list(safe_regex_finditer(pattern, "a1b22c333"))
        assert [m.group() for m in matches] == ["1", "22", "333"]

    def test_no_matches(self) -> None:
        pattern = regex.compile(r"\d+")
        assert list(safe_regex_finditer(pattern, "abcdef")) == []

    def test_timeout_stops_iteration(self, mocker: MockerFixture) -> None:
        mock_pattern = mocker.MagicMock()
        mock_pattern.finditer.side_effect = TimeoutError
        mock_pattern.pattern = r"\d+"
        assert list(safe_regex_finditer(mock_pattern, "test")) == []
