"""
Tests for constrained search strategy extended functionality.

Tests cover:
- Constraint parsing and application
- Domain and date filtering
- Boolean operators
- Constraint relaxation
"""

from unittest.mock import Mock
from datetime import datetime


class TestConstraintParsing:
    """Tests for constraint parsing."""

    def test_constraint_parsing(self):
        """Constraints are parsed from query."""
        query = "climate change site:nature.com after:2023"

        constraints = {}
        parts = query.split()

        for part in parts:
            if part.startswith("site:"):
                constraints["domain"] = part.split(":")[1]
            elif part.startswith("after:"):
                constraints["after"] = part.split(":")[1]

        assert constraints["domain"] == "nature.com"
        assert constraints["after"] == "2023"

    def test_constraint_parsing_multiple(self):
        """Multiple constraints are parsed."""
        query = "AI research site:arxiv.org filetype:pdf language:en"

        constraints = {}
        for part in query.split():
            if ":" in part:
                key, value = part.split(":", 1)
                constraints[key] = value

        assert len(constraints) == 3

    def test_constraint_parsing_quoted_values(self):
        """Quoted constraint values are preserved."""
        # Simulate parsing quoted value
        constraint = 'author:"John Smith"'

        key, value = constraint.split(":", 1)
        value = value.strip('"')

        assert value == "John Smith"

    def test_constraint_parsing_empty_value(self):
        """Empty constraint values are handled."""
        constraint = "site:"

        if ":" in constraint:
            key, value = constraint.split(":", 1)
            if not value:
                value = None

        assert value is None


class TestConstraintApplication:
    """Tests for constraint application."""

    def test_constraint_application(self):
        """Constraints are applied to results."""
        results = [
            {"url": "https://nature.com/article1", "domain": "nature.com"},
            {"url": "https://example.com/article2", "domain": "example.com"},
            {"url": "https://nature.com/article3", "domain": "nature.com"},
        ]

        constraint = {"domain": "nature.com"}

        filtered = [r for r in results if r["domain"] == constraint["domain"]]

        assert len(filtered) == 2

    def test_constraint_application_no_matches(self):
        """No matches returns empty results."""
        results = [
            {"domain": "example.com"},
            {"domain": "test.com"},
        ]

        constraint = {"domain": "nonexistent.com"}

        filtered = [r for r in results if r["domain"] == constraint["domain"]]

        assert len(filtered) == 0


class TestDomainFiltering:
    """Tests for domain filtering constraints."""

    def test_constraint_domain_filtering(self):
        """Domain constraint filters results."""
        allowed_domains = ["nature.com", "science.org"]
        results = [
            {"url": "https://nature.com/a"},
            {"url": "https://random.com/b"},
            {"url": "https://science.org/c"},
        ]

        filtered = [
            r for r in results if any(d in r["url"] for d in allowed_domains)
        ]

        assert len(filtered) == 2

    def test_constraint_domain_exclusion(self):
        """Excluded domains are filtered out."""
        excluded_domains = ["spam.com", "ads.net"]
        results = [
            {"url": "https://nature.com/a", "domain": "nature.com"},
            {"url": "https://spam.com/b", "domain": "spam.com"},
        ]

        filtered = [r for r in results if r["domain"] not in excluded_domains]

        assert len(filtered) == 1

    def test_constraint_domain_subdomain_handling(self):
        """Subdomains are handled correctly."""
        domain = "nature.com"
        urls = [
            "https://www.nature.com/article",
            "https://api.nature.com/data",
            "https://nature.com/main",
        ]

        matching = [u for u in urls if domain in u]

        assert len(matching) == 3


class TestDateFiltering:
    """Tests for date range filtering."""

    def test_constraint_date_range_filtering(self):
        """Date range constraint filters results."""
        after = datetime(2023, 1, 1)
        before = datetime(2024, 1, 1)

        results = [
            {"date": datetime(2022, 6, 1)},
            {"date": datetime(2023, 6, 1)},
            {"date": datetime(2024, 6, 1)},
        ]

        filtered = [r for r in results if after <= r["date"] < before]

        assert len(filtered) == 1

    def test_constraint_date_after_only(self):
        """After date constraint works alone."""
        after = datetime(2023, 1, 1)

        results = [
            {"date": datetime(2022, 6, 1)},
            {"date": datetime(2023, 6, 1)},
        ]

        filtered = [r for r in results if r["date"] >= after]

        assert len(filtered) == 1

    def test_constraint_date_before_only(self):
        """Before date constraint works alone."""
        before = datetime(2023, 1, 1)

        results = [
            {"date": datetime(2022, 6, 1)},
            {"date": datetime(2023, 6, 1)},
        ]

        filtered = [r for r in results if r["date"] < before]

        assert len(filtered) == 1


class TestSourceTypeFiltering:
    """Tests for source type filtering."""

    def test_constraint_source_type_filtering(self):
        """Source type constraint filters results."""
        allowed_types = ["pdf", "html"]

        results = [
            {"type": "pdf"},
            {"type": "html"},
            {"type": "video"},
        ]

        filtered = [r for r in results if r["type"] in allowed_types]

        assert len(filtered) == 2

    def test_constraint_filetype_extension(self):
        """Filetype extension is detected."""
        urls = [
            "https://example.com/doc.pdf",
            "https://example.com/page.html",
            "https://example.com/file.docx",
        ]

        filetype = "pdf"
        matching = [u for u in urls if u.endswith(f".{filetype}")]

        assert len(matching) == 1


class TestLanguageFiltering:
    """Tests for language filtering."""

    def test_constraint_language_filtering(self):
        """Language constraint filters results."""
        language = "en"

        results = [
            {"lang": "en", "title": "English Article"},
            {"lang": "de", "title": "German Article"},
            {"lang": "en", "title": "Another English"},
        ]

        filtered = [r for r in results if r["lang"] == language]

        assert len(filtered) == 2

    def test_constraint_language_detection(self):
        """Language is detected from content."""
        # Simulate language detection
        content = "This is English text"

        # Simple heuristic
        if "the" in content.lower() or "is" in content.lower():
            detected_lang = "en"
        else:
            detected_lang = "unknown"

        assert detected_lang == "en"


class TestBooleanOperators:
    """Tests for boolean operators in constraints."""

    def test_constraint_boolean_operators(self):
        """Boolean operators work in queries."""
        query = "climate AND change"

        terms = query.split(" AND ")

        assert len(terms) == 2

    def test_constraint_boolean_or(self):
        """OR operator expands results."""
        query = "global OR climate"
        terms = query.split(" OR ")

        results = [
            {"text": "global warming"},
            {"text": "climate change"},
            {"text": "weather patterns"},
        ]

        matching = [
            r
            for r in results
            if any(term.lower() in r["text"].lower() for term in terms)
        ]

        assert len(matching) == 2

    def test_constraint_boolean_not(self):
        """NOT operator excludes results."""
        include_term = "climate"
        exclude_term = "change"

        results = [
            {"text": "climate patterns"},
            {"text": "climate change"},
            {"text": "weather change"},
        ]

        filtered = [
            r
            for r in results
            if include_term in r["text"] and exclude_term not in r["text"]
        ]

        assert len(filtered) == 1


class TestNegationHandling:
    """Tests for negation handling."""

    def test_constraint_negation_handling(self):
        """Negated constraints exclude results."""
        constraint = "-site:spam.com"

        excluded_domain = constraint[1:].split(":")[1]

        results = [
            {"domain": "nature.com"},
            {"domain": "spam.com"},
        ]

        filtered = [r for r in results if r["domain"] != excluded_domain]

        assert len(filtered) == 1

    def test_constraint_negation_multiple(self):
        """Multiple negations are applied."""
        excluded = ["spam.com", "ads.net"]

        results = [
            {"domain": "nature.com"},
            {"domain": "spam.com"},
            {"domain": "ads.net"},
        ]

        filtered = [r for r in results if r["domain"] not in excluded]

        assert len(filtered) == 1


class TestWildcardMatching:
    """Tests for wildcard matching."""

    def test_constraint_wildcard_matching(self):
        """Wildcards match patterns."""
        import re

        pattern = "clim*"
        regex = pattern.replace("*", ".*")

        texts = ["climate", "climbing", "claim", "weather"]

        matching = [t for t in texts if re.match(regex, t)]

        # "climate" and "climbing" match "clim*"
        assert len(matching) == 2

    def test_constraint_wildcard_suffix(self):
        """Suffix wildcards work."""
        import re

        pattern = "*ing"
        regex = pattern.replace("*", ".*")

        texts = ["running", "swimming", "run", "swim"]

        matching = [t for t in texts if re.match(regex, t)]

        assert len(matching) == 2


class TestCaseSensitivity:
    """Tests for case sensitivity."""

    def test_constraint_case_sensitivity(self):
        """Case sensitivity is configurable."""
        query = "Climate"
        case_sensitive = False

        text = "climate change"

        if case_sensitive:
            matches = query in text
        else:
            matches = query.lower() in text.lower()

        assert matches

    def test_constraint_case_insensitive_default(self):
        """Default is case insensitive."""
        query = "CLIMATE"
        text = "climate change"

        matches = query.lower() in text.lower()

        assert matches


class TestConstraintRelaxation:
    """Tests for constraint relaxation."""

    def test_constraint_relaxation_strategy(self):
        """Constraints are relaxed when no results."""
        constraints = {
            "domain": "nature.com",
            "after": "2023",
            "filetype": "pdf",
        }

        # Relaxation order

        # Simulate relaxation
        relaxed = constraints.copy()
        del relaxed["filetype"]

        assert "filetype" not in relaxed
        assert "domain" in relaxed

    def test_constraint_relaxation_levels(self):
        """Multiple relaxation levels are tried."""
        constraint_levels = [
            {"domain": "nature.com", "after": "2023", "type": "pdf"},
            {"domain": "nature.com", "after": "2023"},
            {"domain": "nature.com"},
            {},
        ]

        level = 0
        results = []

        while not results and level < len(constraint_levels):
            # Simulate search
            if level >= 2:
                results = [{"result": "found"}]
            level += 1

        assert level == 3
        assert len(results) == 1


class TestConstraintValidation:
    """Tests for constraint validation."""

    def test_constraint_violation_detection(self):
        """Constraint violations are detected."""
        constraint = {"domain": "nature.com"}
        result = {"domain": "example.com"}

        violation = result["domain"] != constraint["domain"]

        assert violation

    def test_constraint_result_validation(self):
        """Results are validated against constraints."""
        constraints = {"min_words": 100}
        result = {"word_count": 50}

        valid = result["word_count"] >= constraints["min_words"]

        assert not valid


class TestLLMQueryRefinement:
    """Tests for LLM-based query refinement."""

    def test_constraint_llm_query_refinement(self):
        """LLM refines query with constraints."""
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(
            content="climate change research site:arxiv.org after:2023"
        )

        original_query = "climate change"

        mock_llm.invoke(f"Add constraints to: {original_query}")

        assert mock_llm.invoke.called

    def test_constraint_llm_error_handling(self):
        """LLM errors use fallback refinement."""
        llm_available = False

        if llm_available:
            refined_query = "llm_refined"
        else:
            # Fallback: simple concatenation
            query = "climate change"
            constraints = ["site:nature.com"]
            refined_query = query + " " + " ".join(constraints)

        assert "site:nature.com" in refined_query


class TestErrorHandling:
    """Tests for constraint error handling."""

    def test_constraint_error_handling(self):
        """Errors in constraint processing are handled."""
        errors = []

        try:
            constraint = "invalid::constraint"
            parts = constraint.split(":")
            if len(parts) != 2:
                raise ValueError("Invalid constraint format")
        except ValueError as e:
            errors.append(str(e))

        assert len(errors) == 1

    def test_constraint_invalid_date(self):
        """Invalid dates are handled."""
        date_str = "not-a-date"

        try:
            from datetime import datetime

            datetime.strptime(date_str, "%Y-%m-%d")
            valid = True
        except ValueError:
            valid = False

        assert not valid

    def test_constraint_malformed_query(self):
        """Malformed queries are handled."""
        query = "site:  filetype:"

        constraints = {}
        for part in query.split():
            if ":" in part:
                key, value = part.split(":", 1)
                if value.strip():
                    constraints[key] = value

        # No valid constraints extracted
        assert len(constraints) == 0


class TestExtractConfidenceFromResponse:
    """Tests for confidence extraction from LLM responses (PR #2020).

    PR #2020 changed bare `except:` to `except ValueError:` in
    _extract_confidence_from_response of EarlyStopConstrainedStrategy.
    """

    def test_extracts_decimal_confidence(self):
        """Should extract decimal confidence from response."""
        import re

        response = "Based on the evidence, confidence is 0.85"
        pattern = r"\b0?\.\d+\b|\b1\.0\b|\b1\b"
        matches = re.findall(pattern, response)

        assert len(matches) > 0
        confidence = float(matches[-1])
        assert confidence == 0.85

    def test_extracts_last_number(self):
        """Should extract the last number found."""
        import re

        response = "Score: 0.3 but after further review: 0.7"
        pattern = r"\b0?\.\d+\b|\b1\.0\b|\b1\b"
        matches = re.findall(pattern, response)

        confidence = float(matches[-1])
        assert confidence == 0.7

    def test_returns_none_for_no_match(self):
        """Should not find matches in text without numbers."""
        import re

        response = "I cannot determine a confidence score"
        pattern = r"\b0?\.\d+\b|\b1\.0\b|\b1\b"
        matches = re.findall(pattern, response)

        assert len(matches) == 0

    def test_handles_perfect_confidence(self):
        """Should handle 1.0 confidence."""
        import re

        response = "This is certain: 1.0"
        pattern = r"\b0?\.\d+\b|\b1\.0\b|\b1\b"
        matches = re.findall(pattern, response)

        assert len(matches) > 0
        confidence = float(matches[-1])
        assert confidence == 1.0

    def test_handles_integer_one(self):
        """Should handle integer 1 as confidence."""
        import re

        response = "Confidence: 1"
        pattern = r"\b0?\.\d+\b|\b1\.0\b|\b1\b"
        matches = re.findall(pattern, response)

        assert len(matches) > 0
        confidence = float(matches[-1])
        assert confidence == 1.0
