"""
Deep behavioral tests for NewsSearchCallback and search integration.
Tests quality calculation, tracking control, and search wrapper behavior.
"""

from unittest.mock import Mock

from local_deep_research.news.core.search_integration import (
    NewsSearchCallback,
    create_search_wrapper,
)


# --- NewsSearchCallback init ---


class TestNewsSearchCallbackInit:
    """Tests for NewsSearchCallback initialization."""

    def test_tracking_initially_none(self):
        cb = NewsSearchCallback()
        assert cb._tracking_enabled is None

    def test_tracking_defaults_to_false(self):
        cb = NewsSearchCallback()
        assert cb.tracking_enabled is False

    def test_tracking_cached_after_first_access(self):
        cb = NewsSearchCallback()
        _ = cb.tracking_enabled
        assert cb._tracking_enabled is not None


# --- _calculate_quality ---


class TestCalculateQuality:
    """Tests for search result quality calculation."""

    def test_empty_findings_returns_zero(self):
        cb = NewsSearchCallback()
        result = {"findings": []}
        assert cb._calculate_quality(result) == 0.0

    def test_no_findings_key_returns_zero(self):
        cb = NewsSearchCallback()
        assert cb._calculate_quality({}) == 0.0

    def test_single_finding_with_content(self):
        cb = NewsSearchCallback()
        result = {"findings": [{"content": "Some content"}]}
        score = cb._calculate_quality(result)
        # count_score = min(1/10, 1.0) = 0.1
        # content_score = 1.0 (has content)
        # (0.1 + 1.0) / 2 = 0.55
        assert abs(score - 0.55) < 0.01

    def test_single_finding_without_content(self):
        cb = NewsSearchCallback()
        result = {"findings": [{"title": "No content field"}]}
        score = cb._calculate_quality(result)
        # count_score = 0.1
        # content_score = 0.5 (no content)
        # (0.1 + 0.5) / 2 = 0.3
        assert abs(score - 0.3) < 0.01

    def test_ten_findings_maxes_count_score(self):
        cb = NewsSearchCallback()
        findings = [{"content": f"content {i}"} for i in range(10)]
        result = {"findings": findings}
        score = cb._calculate_quality(result)
        # count_score = min(10/10, 1.0) = 1.0
        # content_score = 1.0
        # (1.0 + 1.0) / 2 = 1.0
        assert score == 1.0

    def test_twenty_findings_still_capped(self):
        cb = NewsSearchCallback()
        findings = [{"content": f"content {i}"} for i in range(20)]
        result = {"findings": findings}
        score = cb._calculate_quality(result)
        # count_score = min(20/10, 1.0) = 1.0 (capped)
        assert score == 1.0

    def test_five_findings_with_content(self):
        cb = NewsSearchCallback()
        findings = [{"content": f"content {i}"} for i in range(5)]
        result = {"findings": findings}
        score = cb._calculate_quality(result)
        # count_score = 0.5, content_score = 1.0
        # (0.5 + 1.0) / 2 = 0.75
        assert abs(score - 0.75) < 0.01

    def test_five_findings_without_content(self):
        cb = NewsSearchCallback()
        findings = [{"title": f"title {i}"} for i in range(5)]
        result = {"findings": findings}
        score = cb._calculate_quality(result)
        # count_score = 0.5, content_score = 0.5
        # (0.5 + 0.5) / 2 = 0.5
        assert abs(score - 0.5) < 0.01

    def test_content_check_only_first_five(self):
        """Content check looks at only the first 5 findings."""
        cb = NewsSearchCallback()
        # First 5 have no content, rest do
        findings = [{"title": f"t {i}"} for i in range(5)]
        findings += [{"content": f"c {i}"} for i in range(5)]
        result = {"findings": findings}
        score = cb._calculate_quality(result)
        # count_score = min(10/10, 1.0) = 1.0
        # content_score = 0.5 (first 5 have no content)
        # (1.0 + 0.5) / 2 = 0.75
        assert abs(score - 0.75) < 0.01

    def test_content_in_first_finding_counts(self):
        cb = NewsSearchCallback()
        findings = [
            {"content": "has content"},
            {"title": "no content"},
            {"title": "no content"},
        ]
        result = {"findings": findings}
        score = cb._calculate_quality(result)
        # count_score = 0.3, content_score = 1.0
        # (0.3 + 1.0) / 2 = 0.65
        assert abs(score - 0.65) < 0.01

    def test_empty_content_is_falsy(self):
        cb = NewsSearchCallback()
        findings = [{"content": ""}, {"content": ""}]
        result = {"findings": findings}
        score = cb._calculate_quality(result)
        # Empty strings are falsy
        # count_score = 0.2, content_score = 0.5
        assert score < 0.5

    def test_none_content_is_falsy(self):
        cb = NewsSearchCallback()
        findings = [{"content": None}]
        result = {"findings": findings}
        score = cb._calculate_quality(result)
        # None is falsy
        # count_score = 0.1, content_score = 0.5
        assert abs(score - 0.3) < 0.01


# --- __call__ ---


class TestNewsSearchCallbackCall:
    """Tests for the callback invocation."""

    def test_does_not_track_when_disabled(self):
        cb = NewsSearchCallback()
        cb._tracking_enabled = False
        # Should not raise even without tracking infrastructure
        cb("test query", {"findings": []})

    def test_does_not_track_non_user_search(self):
        cb = NewsSearchCallback()
        cb._tracking_enabled = True
        context = {"is_user_search": False}
        # Should not attempt tracking for non-user searches
        cb("test query", {"findings": []}, context)

    def test_handles_no_context(self):
        cb = NewsSearchCallback()
        cb._tracking_enabled = False
        cb("test query", {"findings": []}, None)

    def test_handles_empty_context(self):
        cb = NewsSearchCallback()
        cb._tracking_enabled = False
        cb("test query", {"findings": []}, {})


# --- create_search_wrapper ---


class TestCreateSearchWrapper:
    """Tests for search method wrapping."""

    def test_wrapper_is_callable(self):
        def original(self, query, **kwargs):
            return {"findings": []}

        wrapped = create_search_wrapper(original)
        assert callable(wrapped)

    def test_preserves_function_name(self):
        def my_search(self, query, **kwargs):
            return {}

        wrapped = create_search_wrapper(my_search)
        assert wrapped.__name__ == "my_search"

    def test_calls_original_method(self):
        call_tracker = {"called": False}

        def original(self, query, **kwargs):
            call_tracker["called"] = True
            return {"findings": []}

        wrapped = create_search_wrapper(original)
        wrapped(Mock(), "test query")
        assert call_tracker["called"]

    def test_returns_original_result(self):
        expected = {"findings": ["result1"]}

        def original(self, query, **kwargs):
            return expected

        wrapped = create_search_wrapper(original)
        result = wrapped(Mock(), "test query")
        assert result == expected

    def test_strips_news_kwargs(self):
        """News-specific kwargs should be stripped before passing to original."""
        received_kwargs = {}

        def original(self, query, **kwargs):
            received_kwargs.update(kwargs)
            return {}

        wrapped = create_search_wrapper(original)
        wrapped(
            Mock(),
            "test",
            is_user_search=True,
            is_news_search=False,
            user_id="u1",
        )
        assert "is_user_search" not in received_kwargs
        assert "is_news_search" not in received_kwargs
        assert "user_id" not in received_kwargs

    def test_callback_error_does_not_propagate(self):
        """If callback fails, the search result should still be returned."""

        def original(self, query, **kwargs):
            return {"findings": []}

        wrapped = create_search_wrapper(original)
        result = wrapped(Mock(), "test query")
        assert result is not None
