"""
Coverage tests for ProgressiveExplorer.

Tests cover missing branches in:
- SearchProgress.update_coverage: creates new entity_type key; appends to existing
- SearchProgress.get_uncovered_entities: all covered -> empty; partial coverage returned
- explore: parallel_search called; candidates extracted; progress updated; search_depth incremented
- _extract_candidates_from_results: quoted terms extracted; title phrases boosted; empty results
- _update_entity_coverage: matching entity updates coverage; non-matching ignored
- generate_verification_searches: empty candidates returns []; limits to max_searches;
  already-searched terms skipped
- suggest_next_searches: no candidates, temporal uncovered; candidate boosts; names+descriptors
- _parallel_search: search exception returns empty list for that query
"""

from unittest.mock import MagicMock

from local_deep_research.advanced_search_system.candidate_exploration.progressive_explorer import (
    ProgressiveExplorer,
    SearchProgress,
)

MODULE = "local_deep_research.advanced_search_system.candidate_exploration.progressive_explorer"


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


def _make_explorer(search_results=None):
    search_engine = MagicMock()
    search_engine.run.return_value = search_results or []
    model = MagicMock()
    return ProgressiveExplorer(
        search_engine=search_engine, model=model
    ), search_engine


def _constraint_mock(description="born in Germany"):
    c = MagicMock()
    c.description = description
    return c


# ---------------------------------------------------------------------------
# SearchProgress.update_coverage
# ---------------------------------------------------------------------------


class TestSearchProgressUpdateCoverage:
    """update_coverage creates and populates entity_type sets."""

    def test_creates_new_entity_type_key(self):
        sp = SearchProgress()
        sp.update_coverage("temporal", "2020")
        assert "2020" in sp.entity_coverage["temporal"]

    def test_appends_to_existing_key(self):
        sp = SearchProgress()
        sp.update_coverage("names", "Alpha")
        sp.update_coverage("names", "Beta")
        assert {"alpha", "beta"} == sp.entity_coverage["names"]

    def test_entity_lowercased(self):
        sp = SearchProgress()
        sp.update_coverage("names", "UPPER")
        assert "upper" in sp.entity_coverage["names"]


# ---------------------------------------------------------------------------
# SearchProgress.get_uncovered_entities
# ---------------------------------------------------------------------------


class TestSearchProgressGetUncoveredEntities:
    """get_uncovered_entities returns only unsearched entities."""

    def test_all_covered_returns_empty(self):
        sp = SearchProgress()
        sp.update_coverage("temporal", "2020")
        entities = {"temporal": ["2020"]}
        result = sp.get_uncovered_entities(entities)
        assert result == {}

    def test_partial_coverage_returns_remaining(self):
        sp = SearchProgress()
        sp.update_coverage("temporal", "2020")
        entities = {"temporal": ["2020", "2021", "2022"]}
        result = sp.get_uncovered_entities(entities)
        assert "temporal" in result
        assert "2021" in result["temporal"]
        assert "2022" in result["temporal"]
        assert "2020" not in result["temporal"]

    def test_empty_entities_returns_empty(self):
        sp = SearchProgress()
        result = sp.get_uncovered_entities({})
        assert result == {}


# ---------------------------------------------------------------------------
# _extract_candidates_from_results
# ---------------------------------------------------------------------------


class TestExtractCandidatesFromResults:
    """_extract_candidates_from_results extracts quoted terms and title phrases."""

    def test_quoted_term_in_snippet_extracted(self):
        explorer, _ = _make_explorer()
        results = [{"title": "", "snippet": 'The answer is "Blue Nile River"'}]
        candidates = explorer._extract_candidates_from_results(results, "query")
        assert "Blue Nile River" in candidates

    def test_proper_noun_in_title_extracted(self):
        explorer, _ = _make_explorer()
        results = [{"title": "Eiffel Tower Paris landmark", "snippet": ""}]
        candidates = explorer._extract_candidates_from_results(results, "query")
        # "Eiffel" starts with uppercase and is longer than 3 chars
        assert any("Eiffel" in k for k in candidates)

    def test_empty_results_returns_empty_dict(self):
        explorer, _ = _make_explorer()
        candidates = explorer._extract_candidates_from_results([], "query")
        assert candidates == {}

    def test_existing_candidate_confidence_updated_if_higher(self):
        explorer, _ = _make_explorer()
        # Same quoted term in two results -> confidence boosted on second occurrence
        results = [
            {"title": "", "snippet": '"Mount Everest" highest peak'},
            {"title": "", "snippet": '"Mount Everest" tallest mountain'},
        ]
        candidates = explorer._extract_candidates_from_results(results, "query")
        # Should exist with base confidence 0.3 (only appearances, no title match)
        assert "Mount Everest" in candidates


# ---------------------------------------------------------------------------
# _update_entity_coverage
# ---------------------------------------------------------------------------


class TestUpdateEntityCoverage:
    """_update_entity_coverage tracks entities found in query text."""

    def test_matching_entity_updated(self):
        explorer, _ = _make_explorer()
        explorer._update_entity_coverage(
            "Dartmouth conference 2020",
            {"temporal": ["2020"], "names": ["Dartmouth"]},
        )
        assert "2020" in explorer.progress.entity_coverage.get(
            "temporal", set()
        )
        assert "dartmouth" in explorer.progress.entity_coverage.get(
            "names", set()
        )

    def test_non_matching_entity_not_updated(self):
        explorer, _ = _make_explorer()
        explorer._update_entity_coverage(
            "completely unrelated",
            {"temporal": ["2020"]},
        )
        assert "temporal" not in explorer.progress.entity_coverage


# ---------------------------------------------------------------------------
# generate_verification_searches
# ---------------------------------------------------------------------------


class TestGenerateVerificationSearches:
    """generate_verification_searches targets top candidates."""

    def test_empty_candidates_returns_empty(self):
        explorer, _ = _make_explorer()
        result = explorer.generate_verification_searches({}, [], max_searches=5)
        assert result == []

    def test_generates_searches_for_top_candidates(self):
        explorer, _ = _make_explorer()
        candidates = {"Candidate A": 0.9, "Candidate B": 0.5}
        constraints = [_constraint_mock("born in Germany")]

        result = explorer.generate_verification_searches(
            candidates, constraints, max_searches=5
        )

        assert len(result) >= 1
        assert any("Candidate A" in s for s in result)

    def test_respects_max_searches_limit(self):
        explorer, _ = _make_explorer()
        candidates = {f"Cand{i}": 0.9 - i * 0.05 for i in range(5)}
        constraints = [_constraint_mock(f"constraint {i}") for i in range(4)]

        result = explorer.generate_verification_searches(
            candidates, constraints, max_searches=2
        )
        assert len(result) <= 2

    def test_already_searched_queries_skipped(self):
        explorer, _ = _make_explorer()
        explorer.progress.searched_terms = {'"candidate a" born in germany'}
        candidates = {"Candidate A": 0.9}
        constraints = [_constraint_mock("born in Germany")]

        result = explorer.generate_verification_searches(
            candidates, constraints, max_searches=5
        )
        assert not any("Candidate A" in s for s in result)


# ---------------------------------------------------------------------------
# suggest_next_searches
# ---------------------------------------------------------------------------


class TestSuggestNextSearches:
    """suggest_next_searches builds suggestions from uncovered entities."""

    def test_temporal_uncovered_with_no_candidates(self):
        explorer, _ = _make_explorer()
        entities = {
            "temporal": ["2020", "2021"],
            "names": ["Alpha"],
            "descriptors": [],
        }

        result = explorer.suggest_next_searches(entities, max_suggestions=5)

        assert len(result) >= 1
        assert any("2020" in s or "2021" in s for s in result)

    def test_with_candidate_combines_with_uncovered(self):
        explorer, _ = _make_explorer()
        explorer.progress.found_candidates = {"BestCandidate": 0.9}
        entities = {"temporal": ["2020"], "names": [], "descriptors": []}
        explorer.progress.update_coverage(
            "temporal", "2019"
        )  # 2020 still uncovered

        result = explorer.suggest_next_searches(entities, max_suggestions=5)

        # Should try to verify BestCandidate with 2020
        assert any("BestCandidate" in s for s in result)

    def test_names_and_descriptors_combined(self):
        explorer, _ = _make_explorer()
        entities = {
            "temporal": [],
            "names": ["Alpha"],
            "descriptors": ["ancient"],
        }

        result = explorer.suggest_next_searches(entities, max_suggestions=5)

        assert any("Alpha" in s and "ancient" in s for s in result)

    def test_max_suggestions_respected(self):
        explorer, _ = _make_explorer()
        entities = {
            "temporal": [str(y) for y in range(2000, 2010)],
            "names": ["X"],
            "descriptors": ["d"],
        }
        result = explorer.suggest_next_searches(entities, max_suggestions=3)
        assert len(result) <= 3


# ---------------------------------------------------------------------------
# explore (integration)
# ---------------------------------------------------------------------------


class TestExploreIntegration:
    """explore() executes searches and returns results + progress."""

    def test_explore_returns_tuple(self):
        explorer, search_engine = _make_explorer()
        search_engine.run.return_value = [
            {"title": "Result", "snippet": "text"}
        ]

        results, progress = explorer.explore(["query one", "query two"])

        assert isinstance(results, list)
        assert isinstance(progress, SearchProgress)

    def test_search_depth_incremented(self):
        explorer, _ = _make_explorer()
        _, progress = explorer.explore(["q"])
        assert progress.search_depth == 1

    def test_searched_terms_populated(self):
        explorer, _ = _make_explorer()
        _, progress = explorer.explore(["my search"])
        assert "my search" in progress.searched_terms


# ---------------------------------------------------------------------------
# _parallel_search error handling
# ---------------------------------------------------------------------------


class TestParallelSearchException:
    """_parallel_search catches exceptions and returns empty list for that query."""

    def test_search_exception_returns_empty_result(self):
        explorer, search_engine = _make_explorer()
        search_engine.run.side_effect = Exception("network error")

        results = explorer._parallel_search(["failing query"], max_workers=1)

        # Should return [(query, [])] instead of raising
        assert len(results) == 1
        query, result_list = results[0]
        assert query == "failing query"
        assert result_list == []
