"""High-value pure logic tests for modular_strategy.py.

Tests CandidateConfidence dataclass, LLMConstraintProcessor parsing,
and EarlyRejectionManager decision logic without any LLM or network calls.
"""

import json
from unittest.mock import MagicMock


from local_deep_research.advanced_search_system.strategies.modular_strategy import (
    CandidateConfidence,
    EarlyRejectionManager,
    LLMConstraintProcessor,
)


# ---------------------------------------------------------------------------
# CandidateConfidence dataclass
# ---------------------------------------------------------------------------
class TestCandidateConfidence:
    """Tests for the CandidateConfidence dataclass fields and defaults."""

    def test_required_fields(self):
        cc = CandidateConfidence(
            candidate="item_a",
            positive_confidence=0.9,
            negative_confidence=0.1,
        )
        assert cc.candidate == "item_a"
        assert cc.positive_confidence == 0.9
        assert cc.negative_confidence == 0.1

    def test_default_rejection_reason_is_none(self):
        cc = CandidateConfidence(
            candidate="x", positive_confidence=0.5, negative_confidence=0.5
        )
        assert cc.rejection_reason is None

    def test_default_should_continue_is_true(self):
        cc = CandidateConfidence(
            candidate="x", positive_confidence=0.5, negative_confidence=0.5
        )
        assert cc.should_continue is True

    def test_explicit_rejection_reason(self):
        cc = CandidateConfidence(
            candidate="x",
            positive_confidence=0.1,
            negative_confidence=0.9,
            rejection_reason="Too unlikely",
        )
        assert cc.rejection_reason == "Too unlikely"

    def test_explicit_should_continue_false(self):
        cc = CandidateConfidence(
            candidate="x",
            positive_confidence=0.1,
            negative_confidence=0.9,
            should_continue=False,
        )
        assert cc.should_continue is False

    def test_candidate_accepts_any_object(self):
        obj = {"name": "test", "score": 42}
        cc = CandidateConfidence(
            candidate=obj, positive_confidence=0.7, negative_confidence=0.3
        )
        assert cc.candidate is obj


# ---------------------------------------------------------------------------
# LLMConstraintProcessor parsing methods
# ---------------------------------------------------------------------------
class TestLLMConstraintProcessorParseDecomposition:
    """Tests for LLMConstraintProcessor._parse_decomposition()."""

    def setup_method(self):
        self.processor = LLMConstraintProcessor(model=MagicMock())

    def test_valid_json_dict(self):
        content = json.dumps(
            {
                "constraint_1": {
                    "atomic_elements": ["a", "b"],
                    "variations": ["v1"],
                    "granular_specifics": ["2020"],
                }
            }
        )
        result = self.processor._parse_decomposition(content)
        assert isinstance(result, dict)
        assert "constraint_1" in result

    def test_json_embedded_in_text(self):
        content = 'Here is the result:\n```json\n{"key": "value"}\n```\nDone.'
        result = self.processor._parse_decomposition(content)
        assert isinstance(result, dict)
        assert result.get("key") == "value"

    def test_invalid_json_returns_empty_dict(self):
        content = "This is not valid JSON at all {{{broken"
        result = self.processor._parse_decomposition(content)
        assert result == {}

    def test_empty_string_returns_empty_dict(self):
        result = self.processor._parse_decomposition("")
        assert result == {}

    def test_json_list_returns_empty_dict_because_expected_type_is_dict(self):
        content = json.dumps(["a", "b", "c"])
        result = self.processor._parse_decomposition(content)
        # extract_json with expected_type=dict should reject a list -> fallback {}
        assert result == {}

    def test_empty_dict_json(self):
        content = json.dumps({})
        result = self.processor._parse_decomposition(content)
        assert result == {}


class TestLLMConstraintProcessorParseCombinations:
    """Tests for LLMConstraintProcessor._parse_combinations()."""

    def setup_method(self):
        self.processor = LLMConstraintProcessor(model=MagicMock())

    def test_valid_json_list(self):
        content = json.dumps(["query1", "query2", "query3"])
        result = self.processor._parse_combinations(content)
        assert result == ["query1", "query2", "query3"]

    def test_json_embedded_in_markdown(self):
        content = 'Some text\n```json\n["a", "b"]\n```'
        result = self.processor._parse_combinations(content)
        assert result == ["a", "b"]

    def test_invalid_json_returns_empty_list(self):
        content = "not json [[[broken"
        result = self.processor._parse_combinations(content)
        assert result == []

    def test_empty_string_returns_empty_list(self):
        result = self.processor._parse_combinations("")
        assert result == []

    def test_json_dict_returns_empty_list_because_expected_type_is_list(self):
        content = json.dumps({"key": "value"})
        result = self.processor._parse_combinations(content)
        # extract_json with expected_type=list should reject a dict -> fallback []
        assert result == []

    def test_empty_list_json(self):
        content = json.dumps([])
        result = self.processor._parse_combinations(content)
        assert result == []


# ---------------------------------------------------------------------------
# EarlyRejectionManager
# ---------------------------------------------------------------------------
class TestEarlyRejectionManagerShouldRejectEarly:
    """Tests for EarlyRejectionManager.should_reject_early()."""

    def setup_method(self):
        self.manager = EarlyRejectionManager(model=MagicMock())

    def test_reject_when_negative_above_threshold(self):
        reject, reason = self.manager.should_reject_early(
            {"negative_confidence": 0.86}
        )
        assert reject is True
        assert reason is not None
        assert "0.86" in reason

    def test_no_reject_at_exact_boundary(self):
        """0.85 is NOT greater than 0.85, so should not reject."""
        reject, reason = self.manager.should_reject_early(
            {"negative_confidence": 0.85}
        )
        assert reject is False
        assert reason is None

    def test_no_reject_just_below_boundary(self):
        reject, reason = self.manager.should_reject_early(
            {"negative_confidence": 0.84}
        )
        assert reject is False
        assert reason is None

    def test_no_reject_low_negative(self):
        reject, reason = self.manager.should_reject_early(
            {"negative_confidence": 0.1}
        )
        assert reject is False
        assert reason is None

    def test_reject_at_1_0(self):
        reject, reason = self.manager.should_reject_early(
            {"negative_confidence": 1.0}
        )
        assert reject is True

    def test_missing_negative_confidence_uses_default(self):
        """Missing key defaults to 0.3 via dict.get, so no rejection."""
        reject, reason = self.manager.should_reject_early({})
        assert reject is False
        assert reason is None


class TestEarlyRejectionManagerShouldContinueSearch:
    """Tests for EarlyRejectionManager.should_continue_search()."""

    def setup_method(self):
        self.manager = EarlyRejectionManager(model=MagicMock())

    def test_stop_when_5_high_confidence(self):
        candidates = list(range(10))
        should_continue, reason = self.manager.should_continue_search(
            candidates, 5
        )
        assert should_continue is False
        assert "high-confidence" in reason

    def test_stop_when_more_than_5_high_confidence(self):
        candidates = list(range(20))
        should_continue, reason = self.manager.should_continue_search(
            candidates, 7
        )
        assert should_continue is False

    def test_continue_when_4_high_confidence(self):
        candidates = list(range(10))
        should_continue, reason = self.manager.should_continue_search(
            candidates, 4
        )
        assert should_continue is True
        assert reason is None

    def test_stop_when_over_50_candidates_zero_high_confidence(self):
        candidates = list(range(51))
        should_continue, reason = self.manager.should_continue_search(
            candidates, 0
        )
        assert should_continue is False
        assert "low-quality" in reason

    def test_continue_at_exactly_50_candidates_zero_high_confidence(self):
        """50 is not > 50, so search should continue."""
        candidates = list(range(50))
        should_continue, reason = self.manager.should_continue_search(
            candidates, 0
        )
        assert should_continue is True
        assert reason is None

    def test_continue_with_few_candidates_zero_high_confidence(self):
        candidates = list(range(5))
        should_continue, reason = self.manager.should_continue_search(
            candidates, 0
        )
        assert should_continue is True

    def test_continue_with_many_candidates_some_high_confidence(self):
        """51 candidates but 1 high confidence - should continue (not 0)."""
        candidates = list(range(51))
        should_continue, reason = self.manager.should_continue_search(
            candidates, 1
        )
        assert should_continue is True
        assert reason is None


class TestEarlyRejectionManagerParseConfidence:
    """Tests for EarlyRejectionManager._parse_confidence()."""

    def setup_method(self):
        self.manager = EarlyRejectionManager(model=MagicMock())

    def test_valid_json(self):
        content = json.dumps(
            {
                "positive_confidence": 0.8,
                "negative_confidence": 0.2,
                "reasoning": "looks good",
            }
        )
        result = self.manager._parse_confidence(content)
        assert result["positive_confidence"] == 0.8
        assert result["negative_confidence"] == 0.2

    def test_invalid_json_returns_fallback(self):
        result = self.manager._parse_confidence("totally broken json {{{")
        assert result["positive_confidence"] == 0.5
        assert result["negative_confidence"] == 0.3
        assert result["reasoning"] == "parse_error"

    def test_empty_string_returns_fallback(self):
        result = self.manager._parse_confidence("")
        assert result["positive_confidence"] == 0.5
        assert result["negative_confidence"] == 0.3
