"""
Coverage-focused tests for benchmarks/metrics/calculation.py.

Targets the ~37% uncovered code, specifically:
- evaluate_benchmark_quality (with mocked runner)
- measure_execution_time (with mocked SearchSystem)
- calculate_quality_metrics (delegates to evaluate_benchmark_quality)
- calculate_speed_metrics (delegates to measure_execution_time)
- calculate_resource_metrics edge cases not yet covered
- calculate_combined_score with weights that need normalization
- calculate_metrics: malformed JSON mixed with valid lines, integer confidence,
  confidence as None, confidence as a list (TypeError path)
"""

import json
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest


# ---------------------------------------------------------------------------
# Helper to write JSONL files
# ---------------------------------------------------------------------------


def _write_jsonl(path, records):
    """Write a list of dicts (or raw strings) to a JSONL file."""
    with open(path, "w") as f:
        for r in records:
            if isinstance(r, str):
                f.write(r + "\n")
            else:
                f.write(json.dumps(r) + "\n")


# ===========================================================================
# calculate_metrics – coverage-gap tests
# ===========================================================================


class TestCalculateMetricsCoverageGaps:
    """Tests targeting lines not yet exercised by existing suites."""

    def test_malformed_json_among_valid_lines_returns_error(self, tmp_path):
        """A single malformed JSON line causes json.loads to raise,
        which is caught by the broad except and returns an error dict."""
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_metrics,
        )

        results_file = tmp_path / "mixed.jsonl"
        _write_jsonl(
            results_file,
            [
                '{"is_correct": true}',
                "NOT JSON AT ALL",
            ],
        )
        result = calculate_metrics(str(results_file))
        # The function catches *any* exception while iterating and returns error
        assert "error" in result

    def test_confidence_as_integer_value(self, tmp_path):
        """Integer confidence (not string) is truthy when non-zero,
        and int() on an int succeeds."""
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_metrics,
        )

        results_file = tmp_path / "int_conf.jsonl"
        _write_jsonl(
            results_file,
            [
                {"confidence": 75},
                {"confidence": 25},
            ],
        )
        metrics = calculate_metrics(str(results_file))
        assert metrics["average_confidence"] == 50

    def test_confidence_none_is_skipped(self, tmp_path):
        """confidence=None is falsy, so it should be skipped."""
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_metrics,
        )

        results_file = tmp_path / "none_conf.jsonl"
        _write_jsonl(
            results_file,
            [
                {"confidence": None},
                {"confidence": "60"},
            ],
        )
        metrics = calculate_metrics(str(results_file))
        assert metrics["average_confidence"] == 60

    def test_confidence_as_list_triggers_type_error(self, tmp_path):
        """confidence=[1,2] is truthy, but int([1,2]) raises TypeError,
        which is caught and skipped."""
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_metrics,
        )

        results_file = tmp_path / "list_conf.jsonl"
        _write_jsonl(
            results_file,
            [
                {"confidence": [1, 2]},
                {"confidence": "50"},
            ],
        )
        metrics = calculate_metrics(str(results_file))
        assert metrics["average_confidence"] == 50

    def test_confidence_as_float_string(self, tmp_path):
        """confidence='85.5' – int('85.5') raises ValueError, skipped."""
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_metrics,
        )

        results_file = tmp_path / "float_conf.jsonl"
        _write_jsonl(
            results_file,
            [
                {"confidence": "85.5"},
                {"confidence": "100"},
            ],
        )
        metrics = calculate_metrics(str(results_file))
        # "85.5" is skipped (ValueError on int()), only "100" counted
        assert metrics["average_confidence"] == 100

    def test_no_categories_key_absent_from_metrics(self, tmp_path):
        """When no result has 'category', metrics should not contain 'categories'."""
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_metrics,
        )

        results_file = tmp_path / "no_cat.jsonl"
        _write_jsonl(
            results_file,
            [
                {"is_correct": True},
                {"is_correct": False},
            ],
        )
        metrics = calculate_metrics(str(results_file))
        assert "categories" not in metrics

    def test_processing_time_zero_included(self, tmp_path):
        """processing_time=0 is in the result (key exists), so it counts."""
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_metrics,
        )

        results_file = tmp_path / "zero_time.jsonl"
        _write_jsonl(
            results_file,
            [
                {"processing_time": 0},
                {"processing_time": 10.0},
            ],
        )
        metrics = calculate_metrics(str(results_file))
        assert metrics["average_processing_time"] == 5.0

    def test_multiple_categories(self, tmp_path):
        """Three categories, each with different accuracy."""
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_metrics,
        )

        results_file = tmp_path / "multi_cat.jsonl"
        _write_jsonl(
            results_file,
            [
                {"is_correct": True, "category": "A"},
                {"is_correct": True, "category": "A"},
                {"is_correct": False, "category": "B"},
                {"is_correct": True, "category": "C"},
                {"is_correct": False, "category": "C"},
                {"is_correct": False, "category": "C"},
            ],
        )
        metrics = calculate_metrics(str(results_file))
        assert metrics["categories"]["A"]["accuracy"] == 1.0
        assert metrics["categories"]["B"]["accuracy"] == 0.0
        assert metrics["categories"]["C"]["accuracy"] == pytest.approx(1 / 3)

    def test_large_number_of_results(self, tmp_path):
        """Ensure it handles a larger dataset correctly."""
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_metrics,
        )

        results_file = tmp_path / "large.jsonl"
        records = [
            {"is_correct": i % 2 == 0, "processing_time": float(i)}
            for i in range(100)
        ]
        _write_jsonl(results_file, records)
        metrics = calculate_metrics(str(results_file))
        assert metrics["total_examples"] == 100
        assert metrics["graded_examples"] == 100
        assert metrics["correct"] == 50
        assert metrics["accuracy"] == 0.5

    def test_error_field_with_is_correct(self, tmp_path):
        """A result can have both 'error' and 'is_correct'."""
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_metrics,
        )

        results_file = tmp_path / "both.jsonl"
        _write_jsonl(
            results_file,
            [
                {"is_correct": True, "error": "partial failure"},
            ],
        )
        metrics = calculate_metrics(str(results_file))
        assert metrics["error_count"] == 1
        assert metrics["graded_examples"] == 1
        assert metrics["correct"] == 1


# ===========================================================================
# evaluate_benchmark_quality – mocked runner
# ===========================================================================


class TestEvaluateBenchmarkQuality:
    """Tests for evaluate_benchmark_quality with mocked run_simpleqa_benchmark."""

    def test_returns_accuracy_and_quality_score(self, tmp_path):
        from local_deep_research.benchmarks.metrics.calculation import (
            evaluate_benchmark_quality,
        )

        mock_results = {"metrics": {"accuracy": 0.75}}
        with patch(
            "local_deep_research.benchmarks.runners.run_simpleqa_benchmark",
            return_value=mock_results,
        ) as mock_run:
            # Provide an output_dir so no tempdir is created
            result = evaluate_benchmark_quality(
                system_config={"iterations": 1},
                num_examples=5,
                output_dir=str(tmp_path),
            )

        assert result["accuracy"] == 0.75
        assert result["quality_score"] == 0.75
        mock_run.assert_called_once()

    def test_uses_temp_dir_when_output_dir_none(self):
        from local_deep_research.benchmarks.metrics.calculation import (
            evaluate_benchmark_quality,
        )

        mock_results = {"metrics": {"accuracy": 0.5}}
        with patch(
            "local_deep_research.benchmarks.runners.run_simpleqa_benchmark",
            return_value=mock_results,
        ):
            result = evaluate_benchmark_quality(
                system_config={},
                num_examples=2,
                output_dir=None,
            )

        assert result["accuracy"] == 0.5

    def test_exception_returns_zero_scores(self, tmp_path):
        from local_deep_research.benchmarks.metrics.calculation import (
            evaluate_benchmark_quality,
        )

        with patch(
            "local_deep_research.benchmarks.runners.run_simpleqa_benchmark",
            side_effect=RuntimeError("boom"),
        ):
            result = evaluate_benchmark_quality(
                system_config={},
                num_examples=2,
                output_dir=str(tmp_path),
            )

        assert result["accuracy"] == 0.0
        assert result["quality_score"] == 0.0
        assert "error" in result

    def test_missing_metrics_key_defaults_to_zero(self, tmp_path):
        from local_deep_research.benchmarks.metrics.calculation import (
            evaluate_benchmark_quality,
        )

        # run_simpleqa_benchmark returns dict without "metrics"
        with patch(
            "local_deep_research.benchmarks.runners.run_simpleqa_benchmark",
            return_value={},
        ):
            result = evaluate_benchmark_quality(
                system_config={},
                num_examples=2,
                output_dir=str(tmp_path),
            )

        assert result["accuracy"] == 0.0
        assert result["quality_score"] == 0.0

    def test_config_values_passed_to_runner(self, tmp_path):
        from local_deep_research.benchmarks.metrics.calculation import (
            evaluate_benchmark_quality,
        )

        with patch(
            "local_deep_research.benchmarks.runners.run_simpleqa_benchmark",
            return_value={"metrics": {"accuracy": 0.9}},
        ) as mock_run:
            evaluate_benchmark_quality(
                system_config={
                    "iterations": 5,
                    "questions_per_iteration": 3,
                    "search_strategy": "custom",
                    "search_tool": "google",
                    "model_name": "gpt-4",
                    "provider": "openai",
                },
                num_examples=10,
                output_dir=str(tmp_path),
            )

        call_kwargs = mock_run.call_args[1]
        assert call_kwargs["num_examples"] == 10
        search_config = call_kwargs["search_config"]
        assert search_config["iterations"] == 5
        assert search_config["questions_per_iteration"] == 3
        assert search_config["search_strategy"] == "custom"
        assert search_config["search_tool"] == "google"
        assert search_config["model_name"] == "gpt-4"
        assert search_config["provider"] == "openai"

    def test_temp_dir_cleaned_up_on_success(self):
        """When output_dir is None, a temp dir is created and cleaned up."""
        from local_deep_research.benchmarks.metrics.calculation import (
            evaluate_benchmark_quality,
        )

        created_dirs = []

        original_mkdtemp = __import__("tempfile").mkdtemp

        def tracking_mkdtemp(**kwargs):
            d = original_mkdtemp(**kwargs)
            created_dirs.append(d)
            return d

        with (
            patch(
                "local_deep_research.benchmarks.runners.run_simpleqa_benchmark",
                return_value={"metrics": {"accuracy": 0.5}},
            ),
            patch(
                "tempfile.mkdtemp",
                side_effect=tracking_mkdtemp,
            ),
        ):
            evaluate_benchmark_quality(
                system_config={},
                num_examples=1,
                output_dir=None,
            )

        assert len(created_dirs) == 1
        # The temp dir should have been cleaned up
        assert not Path(created_dirs[0]).exists()


# ===========================================================================
# measure_execution_time – mocked SearchSystem
# ===========================================================================


class TestMeasureExecutionTime:
    """Tests for measure_execution_time with mocked SearchSystem."""

    def _patch_search_system(self, search_time=0.1):
        """Return a context manager that patches AdvancedSearchSystem and its deps."""
        from contextlib import ExitStack

        mock_system = MagicMock()
        mock_system.search.return_value = "result"

        mock_cls = MagicMock(return_value=mock_system)

        class CombinedPatcher:
            def __enter__(self2):
                self2.stack = ExitStack().__enter__()
                self2.stack.enter_context(
                    patch(
                        "local_deep_research.config.llm_config.get_llm",
                        return_value=MagicMock(),
                    )
                )
                self2.stack.enter_context(
                    patch(
                        "local_deep_research.config.search_config.get_search",
                        return_value=MagicMock(),
                    )
                )
                self2.stack.enter_context(
                    patch(
                        "local_deep_research.search_system.AdvancedSearchSystem",
                        mock_cls,
                    )
                )
                return self2

            def __exit__(self2, *args):
                self2.stack.__exit__(*args)

        return (
            CombinedPatcher(),
            mock_cls,
            mock_system,
        )

    def test_basic_speed_measurement(self):
        from local_deep_research.benchmarks.metrics.calculation import (
            measure_execution_time,
        )

        patcher, mock_cls, mock_system = self._patch_search_system()
        with patcher:
            result = measure_execution_time(
                system_config={"iterations": 1},
                query="test",
                num_runs=1,
            )

        assert "average_time" in result
        assert "speed_score" in result
        assert "min_time" in result
        assert "max_time" in result
        assert result["average_time"] >= 0
        assert 0 < result["speed_score"] <= 1.0
        mock_system.search.assert_called_once_with("test", full_response=False)

    def test_multiple_runs_averaged(self):
        from local_deep_research.benchmarks.metrics.calculation import (
            measure_execution_time,
        )

        patcher, mock_cls, mock_system = self._patch_search_system()
        with patcher:
            result = measure_execution_time(
                system_config={},
                query="multi",
                num_runs=3,
            )

        assert mock_system.search.call_count == 3
        assert result["average_time"] >= 0
        assert result["min_time"] <= result["max_time"]

    def test_search_tool_override(self):
        from local_deep_research.benchmarks.metrics.calculation import (
            measure_execution_time,
        )

        config = {"iterations": 2}
        patcher, mock_cls, mock_system = self._patch_search_system()
        with patcher:
            measure_execution_time(
                system_config=config,
                search_tool="duckduckgo",
                num_runs=1,
            )

        # search_tool should have been set on the config
        assert config["search_tool"] == "duckduckgo"

    def test_exception_returns_zero_scores(self):
        from local_deep_research.benchmarks.metrics.calculation import (
            measure_execution_time,
        )

        mock_system = MagicMock()
        mock_system.search.side_effect = RuntimeError("connection failed")
        mock_cls = MagicMock(return_value=mock_system)

        patcher, _, _ = self._patch_search_system()
        # Override the mock_cls with our exception-raising one
        with patcher:
            with patch(
                "local_deep_research.search_system.AdvancedSearchSystem",
                mock_cls,
            ):
                result = measure_execution_time(
                    system_config={},
                    num_runs=1,
                )

        assert result["average_time"] == 0.0
        assert result["speed_score"] == 0.0
        assert "error" in result

    def test_speed_score_formula(self):
        """Verify speed_score = 1/(1 + avg_time/30)."""
        from local_deep_research.benchmarks.metrics.calculation import (
            measure_execution_time,
        )

        patcher, mock_cls, mock_system = self._patch_search_system()
        mock_system.search.return_value = "r"

        with patcher:
            result = measure_execution_time(
                system_config={},
                num_runs=1,
            )

        avg = result["average_time"]
        expected_score = 1.0 / (1.0 + avg / 30.0)
        assert result["speed_score"] == pytest.approx(expected_score, abs=0.01)


# ===========================================================================
# calculate_quality_metrics – delegates to evaluate_benchmark_quality
# ===========================================================================


class TestCalculateQualityMetrics:
    """Tests for calculate_quality_metrics."""

    def test_returns_quality_and_accuracy(self, tmp_path):
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_quality_metrics,
        )

        with patch(
            "local_deep_research.benchmarks.metrics.calculation.evaluate_benchmark_quality",
            return_value={"quality_score": 0.85, "accuracy": 0.85},
        ):
            result = calculate_quality_metrics(
                system_config={"iterations": 1},
                num_examples=5,
                output_dir=str(tmp_path),
            )

        assert result["quality_score"] == 0.85
        assert result["accuracy"] == 0.85

    def test_defaults_to_zero_on_missing_keys(self, tmp_path):
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_quality_metrics,
        )

        with patch(
            "local_deep_research.benchmarks.metrics.calculation.evaluate_benchmark_quality",
            return_value={},
        ):
            result = calculate_quality_metrics(
                system_config={},
                output_dir=str(tmp_path),
            )

        assert result["quality_score"] == 0.0
        assert result["accuracy"] == 0.0


# ===========================================================================
# calculate_speed_metrics – delegates to measure_execution_time
# ===========================================================================


class TestCalculateSpeedMetrics:
    """Tests for calculate_speed_metrics."""

    def test_returns_speed_score_and_time(self):
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_speed_metrics,
        )

        with patch(
            "local_deep_research.benchmarks.metrics.calculation.measure_execution_time",
            return_value={"speed_score": 0.7, "average_time": 12.0},
        ):
            result = calculate_speed_metrics(
                system_config={},
                query="hello",
                num_runs=2,
            )

        assert result["speed_score"] == 0.7
        assert result["average_time"] == 12.0

    def test_defaults_to_zero_on_missing_keys(self):
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_speed_metrics,
        )

        with patch(
            "local_deep_research.benchmarks.metrics.calculation.measure_execution_time",
            return_value={},
        ):
            result = calculate_speed_metrics(system_config={})

        assert result["speed_score"] == 0.0
        assert result["average_time"] == 0.0

    def test_passes_search_tool_and_query(self):
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_speed_metrics,
        )

        with patch(
            "local_deep_research.benchmarks.metrics.calculation.measure_execution_time",
            return_value={"speed_score": 0.5, "average_time": 20.0},
        ) as mock_measure:
            calculate_speed_metrics(
                system_config={"iterations": 3},
                query="deep query",
                search_tool="brave",
                num_runs=5,
            )

        mock_measure.assert_called_once_with(
            system_config={"iterations": 3},
            query="deep query",
            search_tool="brave",
            num_runs=5,
        )


# ===========================================================================
# calculate_resource_metrics – additional coverage
# ===========================================================================


class TestCalculateResourceMetricsAdditional:
    """Additional resource metric tests for coverage gaps."""

    def test_search_tool_parameter_ignored_in_heuristic(self):
        """search_tool and query params exist but don't affect the heuristic."""
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_resource_metrics,
        )

        config = {
            "iterations": 2,
            "questions_per_iteration": 2,
            "max_results": 50,
        }
        r1 = calculate_resource_metrics(
            config, query="query A", search_tool="brave"
        )
        r2 = calculate_resource_metrics(
            config, query="query B", search_tool="google"
        )
        assert r1["resource_score"] == r2["resource_score"]
        assert r1["estimated_complexity"] == r2["estimated_complexity"]

    def test_fractional_max_results(self):
        """max_results can be a float; formula still works."""
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_resource_metrics,
        )

        config = {
            "iterations": 1,
            "questions_per_iteration": 1,
            "max_results": 25,
        }
        metrics = calculate_resource_metrics(config)
        expected_complexity = 1 * 1 * (25 / 50)
        assert metrics["estimated_complexity"] == pytest.approx(
            expected_complexity
        )


# ===========================================================================
# calculate_combined_score – additional coverage
# ===========================================================================


class TestCalculateCombinedScoreAdditional:
    """Additional combined score tests for coverage gaps."""

    def test_weights_with_only_some_matching_metrics(self):
        """Weights for categories not in metrics are harmless."""
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_combined_score,
        )

        metrics = {"quality": {"quality_score": 1.0}}
        weights = {"quality": 0.5, "speed": 0.3, "resource": 0.2}
        score = calculate_combined_score(metrics, weights)
        # Only quality matches: 1.0 * (0.5/1.0) = 0.5
        assert score == pytest.approx(0.5)

    def test_all_weights_equal(self):
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_combined_score,
        )

        metrics = {
            "quality": {"quality_score": 0.9},
            "speed": {"speed_score": 0.6},
            "resource": {"resource_score": 0.3},
        }
        weights = {"quality": 1, "speed": 1, "resource": 1}
        score = calculate_combined_score(metrics, weights)
        expected = (0.9 + 0.6 + 0.3) / 3.0
        assert score == pytest.approx(expected)

    def test_very_large_weights_still_normalize(self):
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_combined_score,
        )

        metrics = {
            "quality": {"quality_score": 0.8},
            "speed": {"speed_score": 0.4},
            "resource": {"resource_score": 0.2},
        }
        weights = {"quality": 1000, "speed": 500, "resource": 500}
        score = calculate_combined_score(metrics, weights)
        # norm: quality=0.5, speed=0.25, resource=0.25
        expected = 0.8 * 0.5 + 0.4 * 0.25 + 0.2 * 0.25
        assert score == pytest.approx(expected)

    def test_empty_weights_dict_returns_zero(self):
        from local_deep_research.benchmarks.metrics.calculation import (
            calculate_combined_score,
        )

        metrics = {"quality": {"quality_score": 1.0}}
        score = calculate_combined_score(metrics, weights={})
        assert score == 0.0
