"""
Tests for run_research_process() core execution logic.

Covers:
- Quick vs detailed mode branching
- Settings context creation
- Progress callback invocation
- Termination handling
- Error handling
- Research status updates
"""

import pytest

from local_deep_research.web.services.research_service import (
    _DETAILED_REPORT_PROGRESS_END,
    _DETAILED_SEARCH_PROGRESS_CAP,
    _REPORT_PHASES,
)


class TestSettingsContext:
    """Tests for SettingsContext created inside run_research_process."""

    def test_settings_context_extracts_values_from_setting_objects(self):
        """SettingsContext extracts 'value' from setting dicts."""
        # Replicate the SettingsContext class logic from research_service.py
        snapshot = {
            "llm.provider": {"value": "openai", "type": "string"},
            "search.tool": "google",  # plain value
        }

        # Simulate the extraction logic
        values = {}
        for key, setting in snapshot.items():
            if isinstance(setting, dict) and "value" in setting:
                values[key] = setting["value"]
            else:
                values[key] = setting

        assert values["llm.provider"] == "openai"
        assert values["search.tool"] == "google"

    def test_settings_context_get_setting_from_snapshot(self):
        """get_setting returns value from snapshot, default for missing."""
        values = {"llm.provider": "openai"}

        def get_setting(key, default=None):
            return values.get(key, default)

        assert get_setting("llm.provider") == "openai"
        assert get_setting("missing.key", "fallback") == "fallback"

    def test_settings_context_empty_snapshot(self):
        """Empty snapshot → all get_setting calls return default."""
        values = {}

        def get_setting(key, default=None):
            return values.get(key, default)

        assert get_setting("any.key", 42) == 42


class TestProgressCallback:
    """Tests for progress callback logic."""

    def test_progress_adjusted_for_detailed_output_generation(self):
        """Detailed mode output_generation → capped at search cap."""
        mode = "detailed"
        metadata = {"phase": "output_generation"}
        progress_percent = 90

        adjusted_progress = progress_percent
        phase = metadata.get("phase", "")
        if mode == "detailed":
            if phase not in _REPORT_PHASES and progress_percent is not None:
                adjusted_progress = min(
                    _DETAILED_SEARCH_PROGRESS_CAP, progress_percent
                )

        assert adjusted_progress == _DETAILED_SEARCH_PROGRESS_CAP

    def test_progress_adjusted_for_detailed_report_generation(self):
        """Detailed mode report_generation → passes through (wrapper maps range)."""
        mode = "detailed"
        metadata = {"phase": "report_generation"}
        progress_percent = 50

        adjusted_progress = progress_percent
        phase = metadata.get("phase", "")
        if mode == "detailed" and phase not in _REPORT_PHASES:
            adjusted_progress = min(
                _DETAILED_SEARCH_PROGRESS_CAP, progress_percent
            )

        assert adjusted_progress == 50

    def test_progress_adjusted_for_quick_output_generation(self):
        """Quick mode output_generation → scaled 85-95%."""
        mode = "quick"
        metadata = {"phase": "output_generation"}
        progress_percent = 50

        adjusted_progress = progress_percent
        if (
            mode == "quick"
            and metadata.get("phase") == "output_generation"
            and progress_percent is not None
        ):
            if progress_percent > 0:
                adjusted_progress = 85 + (progress_percent / 100) * 10
            else:
                adjusted_progress = 85

        assert adjusted_progress == 90.0

    def test_quick_output_generation_with_none_progress_passes_through(self):
        """Quick mode output_generation with None progress must not crash."""
        mode = "quick"
        metadata = {"phase": "output_generation"}
        progress_percent = None

        adjusted_progress = progress_percent
        if (
            mode == "quick"
            and metadata.get("phase") == "output_generation"
            and progress_percent is not None
        ):
            if progress_percent > 0:
                adjusted_progress = 85 + (progress_percent / 100) * 10
            else:
                adjusted_progress = 85

        assert adjusted_progress is None

    def test_search_plan_extracted_from_message(self):
        """SEARCH_PLAN: in message → engines extracted."""
        message = "Planning SEARCH_PLAN: google, bing, wikipedia"
        metadata = {}

        if "SEARCH_PLAN:" in message:
            engines = message.split("SEARCH_PLAN:")[1].strip()
            metadata["planned_engines"] = engines
            metadata["phase"] = "search_planning"

        assert metadata["planned_engines"] == "google, bing, wikipedia"
        assert metadata["phase"] == "search_planning"

    def test_engine_selected_extracted_from_message(self):
        """ENGINE_SELECTED: in message → engine extracted."""
        message = "Selected ENGINE_SELECTED: google"
        metadata = {}

        if "ENGINE_SELECTED:" in message:
            engine = message.split("ENGINE_SELECTED:")[1].strip()
            metadata["selected_engine"] = engine
            metadata["phase"] = "search"

        assert metadata["selected_engine"] == "google"


def _apply_detailed_progress(progress_percent, phase):
    """Replicate detailed-mode progress adjustment from research_service.py.

    Mirrors the production logic so tests stay self-contained, matching the
    existing pattern in this file.
    """
    adjusted_progress = progress_percent
    if phase not in _REPORT_PHASES and progress_percent is not None:
        adjusted_progress = min(_DETAILED_SEARCH_PROGRESS_CAP, progress_percent)
    return adjusted_progress


class TestDetailedModeSearchCap:
    """Tests for the search-phase cap in detailed mode.

    Global monotonicity is enforced by update_progress_and_check_active in
    web/routes/globals.py; these tests cover only the closure's per-call
    capping. The integration tests in
    test_research_service_progress_integration.py exercise the full
    closure + global guard pipeline.
    """

    def test_high_search_values_clamp_to_cap(self):
        """Search progress above the cap clamps to the cap."""
        emitted = [
            _apply_detailed_progress(value, "search")
            for value in (90, 33, 50, 5, 100)
        ]
        assert emitted == [8, 8, 8, 5, 8]

    def test_low_search_values_pass_through_below_cap(self):
        """Search progress below the cap passes through unchanged."""
        assert _apply_detailed_progress(3, "search") == 3
        assert _apply_detailed_progress(7, "search") == 7
        assert _apply_detailed_progress(8, "search") == 8

    def test_none_progress_passes_through_for_error_phase(self):
        """None progress with phase='error' must not crash (regression #3806)."""
        assert _apply_detailed_progress(None, "error") is None

    def test_none_progress_passes_through_for_sub_search(self):
        """None progress from constrained-search sub-callback must not crash.

        The sub-callback in constrained_search_strategy.py emits None with
        phase in {'search_complete', 'final_results'}.
        """
        for phase in ("search_complete", "final_results"):
            assert _apply_detailed_progress(None, phase) is None, (
                f"None should pass through for phase={phase}"
            )

    def test_report_phase_unaffected_by_search_cap(self):
        """Report phases pass through; the wrapper already maps the range."""
        for phase in (
            "report_generation",
            "report_section_research",
            "report_formatting",
            "report_structure",
            "report_complete",
        ):
            assert _apply_detailed_progress(55, phase) == 55

    def test_strategy_complete_phase_does_not_jump_bar_to_100_mid_report(self):
        """phase='complete' from a strategy mid-report must NOT pin bar to 100.

        Regression: every search strategy emits {'phase': 'complete'} when its
        analyze_topic finishes (e.g. standard_strategy.py:334 at value 95).
        report_generator runs analyze_topic per subsection via
        self.search_system.analyze_topic, and the SearchSystem's
        progress_callback is the outer callback (set at research_service.py:711,
        NOT the wrapper). So a strategy 'complete' fires AFTER each subsection
        and would jump the bar to 100 mid-report if treated as the final marker.

        Expected behavior: 'complete' is treated as a search-phase emission —
        capped at the search cap. The legitimate 100% emit uses phase=
        'report_complete' (in _REPORT_PHASES) and passes through.
        """
        # First subsection's strategy finishes: emits phase='complete' at 95
        adjusted = _apply_detailed_progress(95, "complete")
        assert adjusted != 100, (
            "regression: strategy 'complete' jumped bar to 100 mid-report"
        )
        assert adjusted == _DETAILED_SEARCH_PROGRESS_CAP

    def test_legitimate_final_complete_uses_report_complete_phase(self):
        """The legitimate end-of-research 100 emit uses phase='report_complete'.

        Confirms that the production code's final detailed-mode emission
        uses a phase that's in _REPORT_PHASES, so it passes through
        unchanged to 100 — independent of the strategy-level 'complete'
        phase that is intentionally capped.
        """
        assert "report_complete" in _REPORT_PHASES
        assert "complete" not in _REPORT_PHASES
        adjusted = _apply_detailed_progress(
            _DETAILED_REPORT_PROGRESS_END, "report_complete"
        )
        assert adjusted == _DETAILED_REPORT_PROGRESS_END


class TestTerminationHandling:
    """Tests for termination checks in research process."""

    def test_termination_during_progress_raises(self):
        """Termination requested during progress → raises exception."""
        from local_deep_research.web.services.research_service import (
            ResearchTerminatedException,
        )

        with pytest.raises(ResearchTerminatedException):
            raise ResearchTerminatedException("Research was terminated by user")


def _classify_error(error_message):
    """Replicate error classification logic from run_research_process (lines 714-733)."""
    if "status code: 503" in error_message:
        return "ollama_unavailable"
    if "status code: 404" in error_message:
        return "model_not_found"
    if "status code:" in error_message:
        return "api_error"
    if "connection" in error_message.lower():
        return "connection_error"
    return "unknown"


class TestErrorClassification:
    """Tests for search error classification in run_research_process."""

    @pytest.mark.parametrize(
        "error_message, expected_type",
        [
            ("Request failed with status code: 503", "ollama_unavailable"),
            ("status code: 404 not found", "model_not_found"),
            ("status code: 429 rate limited", "api_error"),
            ("status code: 500 internal error", "api_error"),
            ("Connection refused to localhost:11434", "connection_error"),
            ("TCP connection reset by peer", "connection_error"),
            ("Something unexpected happened", "unknown"),
            ("", "unknown"),
        ],
    )
    def test_error_classification(self, error_message, expected_type):
        assert _classify_error(error_message) == expected_type

    def test_503_takes_priority_over_generic_status_code(self):
        """503 is matched specifically before generic 'status code:' pattern."""
        assert _classify_error("status code: 503") == "ollama_unavailable"

    def test_404_takes_priority_over_generic_status_code(self):
        """404 is matched specifically before generic 'status code:' pattern."""
        assert _classify_error("status code: 404") == "model_not_found"


class TestResearchModes:
    """Tests for mode-specific behavior."""

    def test_quick_mode_checks_findings(self):
        """Quick mode checks for findings or formatted_findings."""
        results = {"findings": ["f1"], "formatted_findings": "Summary text"}
        mode = "quick"

        if mode == "quick":
            has_findings = bool(
                results.get("findings") or results.get("formatted_findings")
            )
        else:
            has_findings = False

        assert has_findings is True

    def test_quick_mode_detects_error_in_findings(self):
        """Quick mode detects 'Error:' prefix in formatted_findings."""
        results = {"formatted_findings": "Error: token limit exceeded"}

        raw = results["formatted_findings"]
        is_error = isinstance(raw, str) and raw.startswith("Error:")

        assert is_error is True

    def test_quick_mode_no_error_prefix(self):
        """Normal formatted_findings → not detected as error."""
        results = {"formatted_findings": "This is a normal summary."}

        raw = results["formatted_findings"]
        is_error = isinstance(raw, str) and raw.startswith("Error:")

        assert is_error is False
