"""
Tests for the ParallelExplorer class.

Tests cover:
- Initialization with worker settings
- Query variation generation
- Candidate-based query generation
- Constraint-based query generation
- Query generation methods
"""

from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict
from unittest.mock import Mock


class MockConstraintType(Enum):
    """Mock constraint type for testing."""

    PROPERTY = "property"
    NAME_PATTERN = "name_pattern"


@dataclass
class MockConstraint:
    """Mock constraint for testing."""

    id: str = "c1"
    value: str = "test constraint"
    weight: float = 1.0
    type: MockConstraintType = MockConstraintType.PROPERTY
    description: str = ""


@dataclass
class MockCandidate:
    """Mock candidate for testing."""

    name: str
    metadata: Dict[str, Any] = field(default_factory=dict)


class TestParallelExplorerInit:
    """Tests for ParallelExplorer initialization."""

    def test_init_with_defaults(self):
        """Initialize with default values."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        assert explorer.max_workers == 5
        assert explorer.queries_per_round == 8
        assert explorer.max_rounds == 3

    def test_init_with_custom_max_workers(self):
        """Initialize with custom max workers."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_workers=10)

        assert explorer.max_workers == 10

    def test_init_with_custom_queries_per_round(self):
        """Initialize with custom queries per round."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(
            mock_model, mock_engine, queries_per_round=15
        )

        assert explorer.queries_per_round == 15

    def test_init_with_custom_max_rounds(self):
        """Initialize with custom max rounds."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=5)

        assert explorer.max_rounds == 5

    def test_init_stores_model_and_engine(self):
        """Stores model and search engine references."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        assert explorer.model is mock_model
        assert explorer.search_engine is mock_engine


class TestGenerateQueryVariations:
    """Tests for _generate_query_variations method."""

    def test_generates_variations_from_llm(self):
        """Generates query variations using LLM."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = """
        1. Colorado mountain trails
        2. hiking trails in Colorado
        3. Colorado hiking paths
        4. mountain trails Colorado state
        """
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        variations = explorer._generate_query_variations("Colorado trails")

        assert len(variations) == 4
        assert "Colorado mountain trails" in variations

    def test_handles_llm_exception(self):
        """Returns empty list on LLM exception."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.side_effect = Exception("LLM error")
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        variations = explorer._generate_query_variations("test query")

        assert variations == []

    def test_limits_to_four_variations(self):
        """Limits variations to 4 max."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = """
        1. var1
        2. var2
        3. var3
        4. var4
        5. var5
        6. var6
        """
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        variations = explorer._generate_query_variations("test")

        assert len(variations) <= 4

    def test_parses_numbered_list(self):
        """Parses numbered list format."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = (
            "1. first query\n2. second query\n3. third"
        )
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        variations = explorer._generate_query_variations("test")

        assert "first query" in variations
        assert "second query" in variations
        assert "third" in variations

    def test_handles_empty_response(self):
        """Handles empty LLM response."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        variations = explorer._generate_query_variations("test")

        assert variations == []


class TestGenerateCandidateBasedQueries:
    """Tests for _generate_candidate_based_queries method."""

    def test_generates_queries_from_candidates(self):
        """Generates queries based on found candidates."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.candidates.base_candidate import (
            Candidate,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        candidates = [
            Candidate(name="Heart Mountain Trail"),
            Candidate(name="Arm Creek Path"),
        ]

        queries = explorer._generate_candidate_based_queries(
            candidates, "trails"
        )

        assert len(queries) > 0
        assert any("Heart Mountain Trail" in q for q in queries)

    def test_limits_to_sample_candidates(self):
        """Only samples first 3 candidates."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.candidates.base_candidate import (
            Candidate,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        candidates = [Candidate(name=f"Candidate {i}") for i in range(10)]

        queries = explorer._generate_candidate_based_queries(candidates, "base")

        # 2 queries per candidate, 3 candidates = 6 queries max
        assert len(queries) <= 6

    def test_generates_similar_and_like_queries(self):
        """Generates 'similar to' and 'like' queries."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.candidates.base_candidate import (
            Candidate,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        candidates = [Candidate(name="Test Name")]

        queries = explorer._generate_candidate_based_queries(candidates, "base")

        assert any("similar to" in q.lower() for q in queries)
        assert any("like" in q.lower() for q in queries)


class TestGenerateConstraintQueries:
    """Tests for _generate_constraint_queries method."""

    def test_generates_queries_from_constraints(self):
        """Generates queries from constraints."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.constraints.base_constraint import (
            Constraint,
            ConstraintType,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="",
                value="ice age formation",
            ),
        ]

        queries = explorer._generate_constraint_queries(constraints, "trail")

        assert len(queries) > 0
        assert any("ice age formation" in q for q in queries)

    def test_limits_to_two_constraints(self):
        """Only uses first 2 constraints."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.constraints.base_constraint import (
            Constraint,
            ConstraintType,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        constraints = [
            Constraint(
                id=f"c{i}",
                type=ConstraintType.PROPERTY,
                description="",
                value=f"value{i}",
            )
            for i in range(5)
        ]

        queries = explorer._generate_constraint_queries(constraints, "base")

        # 2 queries per constraint, 2 constraints = 4 queries max
        assert len(queries) <= 4

    def test_generates_examples_query(self):
        """Generates 'examples' query for constraints."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.constraints.base_constraint import (
            Constraint,
            ConstraintType,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="",
                value="test value",
            ),
        ]

        queries = explorer._generate_constraint_queries(constraints, "base")

        assert any("examples" in q for q in queries)


class TestGenerateExplorationQueries:
    """Tests for generate_exploration_queries method."""

    def test_combines_all_query_sources(self):
        """Combines queries from all sources."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.candidates.base_candidate import (
            Candidate,
        )
        from local_deep_research.advanced_search_system.constraints.base_constraint import (
            Constraint,
            ConstraintType,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = "1. variation1\n2. variation2"
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        candidates = [Candidate(name="Test Candidate")]
        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="",
                value="test",
            ),
        ]

        queries = explorer.generate_exploration_queries(
            "base query", candidates, constraints
        )

        assert len(queries) > 0

    def test_excludes_already_explored_queries(self):
        """Excludes queries that have already been explored."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = (
            "1. already explored\n2. new query"
        )
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)
        explorer.explored_queries.add("already explored")

        queries = explorer.generate_exploration_queries("base", [])

        assert "already explored" not in [q.lower() for q in queries]

    def test_limits_to_queries_per_round(self):
        """Limits output to queries_per_round."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.candidates.base_candidate import (
            Candidate,
        )
        from local_deep_research.advanced_search_system.constraints.base_constraint import (
            Constraint,
            ConstraintType,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = "\n".join(
            [f"{i}. query{i}" for i in range(1, 20)]
        )
        mock_engine = Mock()

        explorer = ParallelExplorer(
            mock_model, mock_engine, queries_per_round=3
        )

        candidates = [Candidate(name=f"c{i}") for i in range(10)]
        constraints = [
            Constraint(
                id=f"c{i}",
                type=ConstraintType.PROPERTY,
                description="",
                value=f"v{i}",
            )
            for i in range(5)
        ]

        queries = explorer.generate_exploration_queries(
            "base", candidates, constraints
        )

        assert len(queries) <= 3

    def test_returns_empty_when_no_new_queries(self):
        """Returns empty list when all queries already explored."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = "1. explored1\n2. explored2"
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)
        explorer.explored_queries.add("explored1")
        explorer.explored_queries.add("explored2")

        queries = explorer.generate_exploration_queries("base", [])

        # May still have base query or be empty
        for q in queries:
            assert q.lower() not in ["explored1", "explored2"]


class TestExplorationResult:
    """Tests for exploration result structure."""

    def test_result_has_required_attributes(self):
        """ExplorationResult has required attributes."""
        from local_deep_research.advanced_search_system.candidate_exploration.base_explorer import (
            ExplorationResult,
            ExplorationStrategy,
        )

        result = ExplorationResult(
            candidates=[],
            total_searched=5,
            unique_candidates=3,
            exploration_paths=["path1", "path2"],
            metadata={"key": "value"},
            elapsed_time=1.5,
            strategy_used=ExplorationStrategy.BREADTH_FIRST,
        )

        assert result.candidates == []
        assert result.total_searched == 5
        assert result.unique_candidates == 3
        assert len(result.exploration_paths) == 2
        assert result.metadata["key"] == "value"
        assert result.elapsed_time == 1.5
        assert result.strategy_used == ExplorationStrategy.BREADTH_FIRST

    def test_result_strategy_enum(self):
        """ExplorationStrategy enum has expected values."""
        from local_deep_research.advanced_search_system.candidate_exploration.base_explorer import (
            ExplorationStrategy,
        )

        assert hasattr(ExplorationStrategy, "BREADTH_FIRST")
        assert hasattr(ExplorationStrategy, "DEPTH_FIRST")
        assert hasattr(ExplorationStrategy, "CONSTRAINT_GUIDED")


class TestExploreMethod:
    """Tests for the explore method."""

    def test_explore_returns_exploration_result(self):
        """Explore method returns ExplorationResult."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.base_explorer import (
            ExplorationResult,
        )
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()
        mock_engine.run.return_value = []

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=1)

        with patch.object(explorer, "_execute_search", return_value=[]):
            with patch.object(
                explorer, "_extract_candidates_from_results", return_value=[]
            ):
                result = explorer.explore("test query")

        assert isinstance(result, ExplorationResult)

    def test_explore_uses_breadth_first_strategy(self):
        """Explore method uses breadth-first strategy."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.base_explorer import (
            ExplorationStrategy,
        )
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=1)

        with patch.object(explorer, "_execute_search", return_value=[]):
            with patch.object(
                explorer, "_extract_candidates_from_results", return_value=[]
            ):
                result = explorer.explore("test query")

        assert result.strategy_used == ExplorationStrategy.BREADTH_FIRST

    def test_explore_includes_metadata(self):
        """Explore method includes strategy metadata."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()

        explorer = ParallelExplorer(
            mock_model, mock_engine, max_workers=3, max_rounds=1
        )

        with patch.object(explorer, "_execute_search", return_value=[]):
            with patch.object(
                explorer, "_extract_candidates_from_results", return_value=[]
            ):
                result = explorer.explore("test query", entity_type="place")

        assert result.metadata["strategy"] == "parallel"
        assert result.metadata["max_workers"] == 3
        assert result.metadata["entity_type"] == "place"

    def test_explore_tracks_total_searched(self):
        """Explore method tracks total searched count."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=1)

        with patch.object(explorer, "_execute_search", return_value=[]):
            with patch.object(
                explorer, "_extract_candidates_from_results", return_value=[]
            ):
                result = explorer.explore("test query")

        # Should have searched at least the initial query
        assert result.total_searched >= 1

    def test_explore_records_exploration_paths(self):
        """Explore method records exploration paths."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.candidates.base_candidate import (
            Candidate,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=1)

        candidates = [Candidate(name="Test")]

        with patch.object(
            explorer, "_execute_search", return_value=[{"title": "T"}]
        ):
            with patch.object(
                explorer,
                "_extract_candidates_from_results",
                return_value=candidates,
            ):
                result = explorer.explore("test query")

        assert len(result.exploration_paths) > 0
        assert "Round 1" in result.exploration_paths[0]

    def test_explore_deduplicates_candidates(self):
        """Explore method deduplicates candidates."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.candidates.base_candidate import (
            Candidate,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=1)

        # Same candidate returned multiple times
        candidates = [Candidate(name="Duplicate"), Candidate(name="Duplicate")]

        with patch.object(explorer, "_execute_search", return_value=[]):
            with patch.object(
                explorer,
                "_extract_candidates_from_results",
                return_value=candidates,
            ):
                with patch.object(
                    explorer,
                    "_deduplicate_candidates",
                    return_value=[Candidate(name="Duplicate")],
                ) as mock_dedup:
                    explorer.explore("test query")

        mock_dedup.assert_called()

    def test_explore_limits_to_max_candidates(self):
        """Explore method limits results to max_candidates."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.candidates.base_candidate import (
            Candidate,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=1)
        explorer.max_candidates = 2

        candidates = [Candidate(name=f"C{i}") for i in range(10)]

        with patch.object(explorer, "_execute_search", return_value=[]):
            with patch.object(
                explorer,
                "_extract_candidates_from_results",
                return_value=candidates,
            ):
                with patch.object(
                    explorer, "_deduplicate_candidates", return_value=candidates
                ):
                    with patch.object(
                        explorer,
                        "_rank_candidates_by_relevance",
                        return_value=candidates,
                    ):
                        result = explorer.explore("test query")

        assert len(result.candidates) <= 2

    def test_explore_handles_search_exception(self):
        """Explore method handles exceptions from search."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=1)

        def raise_error(query):
            raise Exception("Search error")

        with patch.object(explorer, "_execute_search", side_effect=raise_error):
            # Should not raise, but handle gracefully
            result = explorer.explore("test query")

        assert result is not None

    def test_explore_multiple_rounds(self):
        """Explore method executes multiple rounds."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.candidates.base_candidate import (
            Candidate,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = "1. query1\n2. query2"
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=2)

        candidates = [Candidate(name="Test")]

        call_count = [0]

        def mock_execute(query):
            call_count[0] += 1
            return []

        with patch.object(
            explorer, "_execute_search", side_effect=mock_execute
        ):
            with patch.object(
                explorer,
                "_extract_candidates_from_results",
                return_value=candidates,
            ):
                with patch.object(
                    explorer,
                    "generate_exploration_queries",
                    return_value=["query1", "query2"],
                ):
                    explorer.explore("test query")

        # Multiple searches should be executed
        assert call_count[0] >= 1

    def test_explore_stops_when_no_more_queries(self):
        """Explore method stops when no more queries to explore."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=5)

        with patch.object(explorer, "_execute_search", return_value=[]):
            with patch.object(
                explorer, "_extract_candidates_from_results", return_value=[]
            ):
                with patch.object(
                    explorer, "generate_exploration_queries", return_value=[]
                ):
                    result = explorer.explore("test query")

        # Should complete without exploring all rounds
        assert result.metadata["rounds"] <= 5

    def test_explore_tracks_elapsed_time(self):
        """Explore method tracks elapsed time."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=1)

        with patch.object(explorer, "_execute_search", return_value=[]):
            with patch.object(
                explorer, "_extract_candidates_from_results", return_value=[]
            ):
                result = explorer.explore("test query")

        assert result.elapsed_time >= 0

    def test_explore_with_constraints(self):
        """Explore method works with constraints."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.constraints.base_constraint import (
            Constraint,
            ConstraintType,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=1)

        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="test",
                value="test value",
            )
        ]

        with patch.object(explorer, "_execute_search", return_value=[]):
            with patch.object(
                explorer, "_extract_candidates_from_results", return_value=[]
            ):
                result = explorer.explore("test query", constraints=constraints)

        assert result is not None

    def test_explore_respects_should_continue(self):
        """Explore method respects _should_continue_exploration."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=10)

        with patch.object(explorer, "_execute_search", return_value=[]):
            with patch.object(
                explorer, "_extract_candidates_from_results", return_value=[]
            ):
                with patch.object(
                    explorer, "_should_continue_exploration", return_value=False
                ):
                    result = explorer.explore("test query")

        # Should stop early due to _should_continue_exploration returning False
        assert result.metadata["rounds"] == 1

    def test_explore_ranks_candidates(self):
        """Explore method ranks candidates by relevance."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.candidates.base_candidate import (
            Candidate,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=1)

        candidates = [Candidate(name="A"), Candidate(name="B")]
        ranked = [Candidate(name="B"), Candidate(name="A")]

        with patch.object(explorer, "_execute_search", return_value=[]):
            with patch.object(
                explorer,
                "_extract_candidates_from_results",
                return_value=candidates,
            ):
                with patch.object(
                    explorer, "_deduplicate_candidates", return_value=candidates
                ):
                    with patch.object(
                        explorer,
                        "_rank_candidates_by_relevance",
                        return_value=ranked,
                    ) as mock_rank:
                        explorer.explore("test query")

        mock_rank.assert_called()

    def test_explore_tracks_unique_candidates(self):
        """Explore method tracks unique candidate count."""
        from unittest.mock import patch

        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )
        from local_deep_research.advanced_search_system.candidates.base_candidate import (
            Candidate,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = ""
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine, max_rounds=1)
        explorer.max_candidates = 100

        candidates = [Candidate(name=f"C{i}") for i in range(5)]
        unique = [Candidate(name=f"C{i}") for i in range(3)]

        with patch.object(explorer, "_execute_search", return_value=[]):
            with patch.object(
                explorer,
                "_extract_candidates_from_results",
                return_value=candidates,
            ):
                with patch.object(
                    explorer, "_deduplicate_candidates", return_value=unique
                ):
                    with patch.object(
                        explorer,
                        "_rank_candidates_by_relevance",
                        return_value=unique,
                    ):
                        result = explorer.explore("test query")

        assert result.unique_candidates == 3


class TestParallelExplorerInheritance:
    """Tests for ParallelExplorer inheritance from BaseCandidateExplorer."""

    def test_inherits_from_base(self):
        """ParallelExplorer inherits from BaseCandidateExplorer."""
        from local_deep_research.advanced_search_system.candidate_exploration.base_explorer import (
            BaseCandidateExplorer,
        )
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        assert issubclass(ParallelExplorer, BaseCandidateExplorer)

    def test_has_explored_queries_set(self):
        """ParallelExplorer has explored_queries set from base."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        assert hasattr(explorer, "explored_queries")
        assert isinstance(explorer.explored_queries, set)

    def test_has_max_candidates_from_base(self):
        """ParallelExplorer has max_candidates from base."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        assert hasattr(explorer, "max_candidates")


class TestEdgeCases:
    """Tests for edge cases in ParallelExplorer."""

    def test_empty_candidate_list_handling(self):
        """Handles empty candidate list."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        queries = explorer._generate_candidate_based_queries([], "base")

        assert queries == []

    def test_empty_constraint_list_handling(self):
        """Handles empty constraint list."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        queries = explorer._generate_constraint_queries([], "base")

        assert queries == []

    def test_query_variations_with_special_chars(self):
        """Handles special characters in query."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = (
            "1. query with 'quotes'\n2. query"
        )
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        variations = explorer._generate_query_variations("test's query")

        # Should not raise
        assert isinstance(variations, list)

    def test_handles_none_constraints(self):
        """Handles None constraints in generate_exploration_queries."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = "1. query1"
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        queries = explorer.generate_exploration_queries("base", [], None)

        assert isinstance(queries, list)

    def test_handles_malformed_llm_response(self):
        """Handles malformed LLM response in query variations."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_model.invoke.return_value.content = "Not a numbered list at all"
        mock_engine = Mock()

        explorer = ParallelExplorer(mock_model, mock_engine)

        variations = explorer._generate_query_variations("test")

        # Should return empty list for malformed response
        assert variations == []

    def test_parallel_execution_with_zero_max_workers(self):
        """Handles zero max_workers gracefully."""
        from local_deep_research.advanced_search_system.candidate_exploration.parallel_explorer import (
            ParallelExplorer,
        )

        mock_model = Mock()
        mock_engine = Mock()

        # Zero workers should default or handle gracefully
        explorer = ParallelExplorer(mock_model, mock_engine, max_workers=1)

        assert explorer.max_workers == 1
