"""
Deep coverage tests for DiversityExplorer (beyond existing test_diversity_explorer_coverage.py).

Tests cover missing branches in:
- _determine_category: each keyword bucket (mountain, water, park, trail, canyon,
  viewpoint, coastal, place, other)
- _filter_for_diversity: category at limit skipped; too-similar candidate skipped;
  diverse candidate kept
- _is_sufficiently_different: identical names rejected; completely different accepted
- _final_diversity_selection: empty input; balanced selection from categories
- generate_exploration_queries: delegates to _generate_diversity_queries
- _find_underrepresented_categories: empty counts returns empty; returns low-count categories
"""

from collections import defaultdict
from unittest.mock import patch


from local_deep_research.advanced_search_system.candidates.base_candidate import (
    Candidate,
)
from local_deep_research.advanced_search_system.candidate_exploration.diversity_explorer import (
    DiversityExplorer,
)

MODULE = "local_deep_research.advanced_search_system.candidate_exploration.diversity_explorer"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_explorer(**overrides):
    with patch.object(
        DiversityExplorer, "__init__", lambda self, *a, **kw: None
    ):
        e = DiversityExplorer.__new__(DiversityExplorer)
        e.diversity_threshold = overrides.get("diversity_threshold", 0.7)
        e.category_limit = overrides.get("category_limit", 10)
        e.similarity_threshold = overrides.get("similarity_threshold", 0.8)
        e.category_counts = defaultdict(int)
        e.diversity_categories = set()
        e.max_candidates = overrides.get("max_candidates", 50)
        e.max_search_time = overrides.get("max_search_time", 60.0)
        e.explored_queries = set()
        e.found_candidates = {}
    return e


def _candidate(name, category=None, relevance_score=0.5):
    meta = {"diversity_category": category} if category else {}
    c = Candidate(name=name, metadata=meta)
    c.relevance_score = relevance_score
    return c


# ---------------------------------------------------------------------------
# _determine_category
# ---------------------------------------------------------------------------


class TestDetermineCategory:
    """_determine_category maps name keywords to the correct bucket."""

    def test_mountain_keywords(self):
        e = _make_explorer()
        for name in ["Blue Mountain", "High Peak", "Summit Ridge", "Blue Hill"]:
            assert e._determine_category(Candidate(name=name)) == "mountain"

    def test_water_keywords(self):
        e = _make_explorer()
        for name in [
            "Blue Lake",
            "Snake River",
            "Bear Creek",
            "Salt Pond",
            "White Stream",
        ]:
            assert e._determine_category(Candidate(name=name)) == "water"

    def test_park_keywords(self):
        e = _make_explorer()
        for name in [
            "National Park",
            "Old Forest",
            "Wildlife Reserve",
            "Wilderness Area",
        ]:
            assert e._determine_category(Candidate(name=name)) == "park"

    def test_trail_keywords(self):
        e = _make_explorer()
        for name in ["Hiking Trail", "Winding Path", "Main Route", "The Way"]:
            assert e._determine_category(Candidate(name=name)) == "trail"

    def test_canyon_keywords(self):
        e = _make_explorer()
        for name in ["Grand Canyon", "Slot Gorge", "Deep Valley", "Wind Gap"]:
            assert e._determine_category(Candidate(name=name)) == "canyon"

    def test_viewpoint_keywords(self):
        e = _make_explorer()
        for name in [
            "Cliff Top",
            "Scenic Bluff",
            "Vista Overlook",
            "High Viewpoint",
        ]:
            assert e._determine_category(Candidate(name=name)) == "viewpoint"

    def test_coastal_keywords(self):
        e = _make_explorer()
        for name in [
            "Palm Island",
            "Sandy Beach",
            "Pacific Coast",
            "Rocky Shore",
        ]:
            assert e._determine_category(Candidate(name=name)) == "coastal"

    def test_place_keywords(self):
        e = _make_explorer()
        for name in [
            "Capital City",
            "Small Town",
            "Orange County",
            "New State",
        ]:
            assert e._determine_category(Candidate(name=name)) == "place"

    def test_other_returns_other(self):
        e = _make_explorer()
        c = Candidate(name="Completely Unrelated Name 42")
        assert e._determine_category(c) == "other"


# ---------------------------------------------------------------------------
# _filter_for_diversity
# ---------------------------------------------------------------------------


class TestFilterForDiversity:
    """_filter_for_diversity skips over-represented and similar candidates."""

    def test_category_at_limit_is_excluded(self):
        e = _make_explorer(category_limit=3)
        # Category "mountain" is already at the limit
        e.category_counts["mountain"] = 3

        new = [_candidate("Everest Peak")]  # will map to "mountain"
        result = e._filter_for_diversity(new, [])
        assert result == []

    def test_too_similar_candidate_excluded(self):
        e = _make_explorer(similarity_threshold=0.5)
        existing = [_candidate("Blue Lake")]
        new_candidate = _candidate(
            "Blue Lake"
        )  # identical name -> high similarity

        result = e._filter_for_diversity([new_candidate], existing)
        assert result == []

    def test_sufficiently_different_candidate_included(self):
        e = _make_explorer(similarity_threshold=0.8)
        existing = [_candidate("Mount Everest")]
        new_candidate = _candidate("Pacific Coast")  # completely different

        result = e._filter_for_diversity([new_candidate], existing)
        assert len(result) == 1


# ---------------------------------------------------------------------------
# _is_sufficiently_different
# ---------------------------------------------------------------------------


class TestIsSufficientlyDifferent:
    """_is_sufficiently_different Jaccard similarity check."""

    def test_identical_names_not_different(self):
        e = _make_explorer(similarity_threshold=0.5)
        c = _candidate("Blue Lake")
        existing = [_candidate("Blue Lake")]
        assert e._is_sufficiently_different(c, existing) is False

    def test_completely_different_names_are_different(self):
        e = _make_explorer(similarity_threshold=0.8)
        c = _candidate("Pacific Ocean")
        existing = [_candidate("Mountain Peak")]
        assert e._is_sufficiently_different(c, existing) is True

    def test_empty_existing_always_different(self):
        e = _make_explorer()
        c = _candidate("Anything")
        assert e._is_sufficiently_different(c, []) is True


# ---------------------------------------------------------------------------
# _final_diversity_selection
# ---------------------------------------------------------------------------


class TestFinalDiversitySelection:
    """_final_diversity_selection balances candidates across categories."""

    def test_empty_input_returns_empty(self):
        e = _make_explorer()
        assert e._final_diversity_selection([]) == []

    def test_selects_from_each_category(self):
        e = _make_explorer(max_candidates=6)
        candidates = [
            _candidate(
                f"Mountain {i}", "mountain", relevance_score=0.9 - i * 0.1
            )
            for i in range(3)
        ] + [
            _candidate(f"Lake {i}", "water", relevance_score=0.8 - i * 0.1)
            for i in range(3)
        ]

        result = e._final_diversity_selection(candidates)

        categories = {c.metadata.get("diversity_category") for c in result}
        assert "mountain" in categories
        assert "water" in categories

    def test_max_per_category_enforced(self):
        e = _make_explorer(max_candidates=2)
        # 4 candidates in one category, max_candidates=2 -> max_per_category=2//1=2
        candidates = [_candidate(f"C{i}", "mountain") for i in range(4)]

        result = e._final_diversity_selection(candidates)

        mountain_count = sum(
            1
            for c in result
            if c.metadata.get("diversity_category") == "mountain"
        )
        assert mountain_count <= 2


# ---------------------------------------------------------------------------
# generate_exploration_queries
# ---------------------------------------------------------------------------


class TestGenerateExplorationQueries:
    """generate_exploration_queries delegates to _generate_diversity_queries."""

    def test_delegates_to_diversity_queries(self):
        e = _make_explorer()
        found = [_candidate("A", "mountain")]

        with patch.object(
            e, "_generate_diversity_queries", return_value=["q1", "q2"]
        ) as mock:
            result = e.generate_exploration_queries("base query", found)

        mock.assert_called_once_with("base query", found)
        assert result == ["q1", "q2"]


# ---------------------------------------------------------------------------
# _find_underrepresented_categories
# ---------------------------------------------------------------------------


class TestFindUnderrepresentedCategories:
    """_find_underrepresented_categories returns low-count categories."""

    def test_empty_counts_returns_empty(self):
        e = _make_explorer()
        assert e._find_underrepresented_categories() == []

    def test_all_equal_no_underrepresented(self):
        e = _make_explorer()
        e.category_counts = defaultdict(int, {"a": 5, "b": 5, "c": 5})
        result = e._find_underrepresented_categories()
        assert result == []

    def test_low_count_category_returned(self):
        e = _make_explorer(category_limit=10)
        e.category_counts = defaultdict(int, {"popular": 10, "rare": 1})

        result = e._find_underrepresented_categories()

        assert "rare" in result
        assert "popular" not in result
