"""Tests for api/dx_metrics.py - Developer Experience metrics calculator."""

from datetime import datetime, timedelta, timezone
from unittest.mock import patch

import pytest

from api.dx_metrics import (
    DXMetricsCalculator,
    TimeRange,
    _assess_quality,
)


def _make_run(status="passed", flaky=False, created_at=None, has_transcript=False, is_regression=False):
    """Helper to create a run dict."""
    return {
        "id": "run-1",
        "status": status,
        "flaky": flaky,
        "created_at": (created_at or datetime.now(timezone.utc)).isoformat(),
        "has_transcript": has_transcript,
        "is_regression": is_regression,
    }


def _make_fix(status="accepted", auto_applied=False, created_at=None, applied_at=None, modified=False, flaky=False):
    """Helper to create a fix dict."""
    now = datetime.now(timezone.utc)
    return {
        "id": "fix-1",
        "status": status,
        "auto_applied": auto_applied,
        "created_at": (created_at or now).isoformat(),
        "applied_at": (applied_at or now + timedelta(minutes=5)).isoformat(),
        "modified": modified,
        "flaky": flaky,
    }


class TestDataQuality:
    def test_high(self):
        q = _assess_quality(100, "runs")
        assert q.confidence == "high"

    def test_medium(self):
        q = _assess_quality(30, "runs")
        assert q.confidence == "medium"

    def test_low(self):
        q = _assess_quality(5, "runs")
        assert q.confidence == "low"
        assert q.warning is not None


class TestTimeRange:
    def test_last_7_days(self):
        tr = TimeRange.last_7_days()
        delta = tr.end - tr.start
        assert abs(delta.days - 7) <= 1

    def test_last_30_days(self):
        tr = TimeRange.last_30_days()
        delta = tr.end - tr.start
        assert abs(delta.days - 30) <= 1

    def test_last_90_days(self):
        tr = TimeRange.last_90_days()
        delta = tr.end - tr.start
        assert abs(delta.days - 90) <= 1


class TestCIStabilityMetrics:
    @pytest.mark.asyncio
    @patch("api.dx_metrics.db")
    async def test_with_flaky_runs(self, mock_db):
        """Flake rate correctly calculated."""
        now = datetime.now(timezone.utc)
        tr = TimeRange(start=now - timedelta(days=7), end=now)

        runs = [_make_run(created_at=now - timedelta(days=i)) for i in range(1, 8)]
        flaky_runs = [_make_run(status="flaky", created_at=now - timedelta(days=i)) for i in range(1, 4)]
        all_runs = runs + flaky_runs

        mock_db.list_runs_for_metrics.return_value = all_runs
        mock_db.list_fixes.return_value = []
        mock_db.list_verified_fixes.return_value = []

        calc = DXMetricsCalculator(org_id="org-1")
        result = await calc.get_ci_stability_metrics(tr)

        assert result.total_runs == 10
        assert result.flaky_runs == 3
        assert result.flake_rate == pytest.approx(30.0)
        assert result.data_quality is not None
        assert result.data_quality.confidence == "medium"  # 10 samples

    @pytest.mark.asyncio
    @patch("api.dx_metrics.db")
    async def test_no_runs(self, mock_db):
        """Zero runs should return safe defaults."""
        now = datetime.now(timezone.utc)
        tr = TimeRange(start=now - timedelta(days=7), end=now)

        mock_db.list_runs_for_metrics.return_value = []
        mock_db.list_fixes.return_value = []
        mock_db.list_verified_fixes.return_value = []

        calc = DXMetricsCalculator(org_id="org-1")
        result = await calc.get_ci_stability_metrics(tr)

        assert result.total_runs == 0
        assert result.flake_rate == 0.0
        assert result.stability_score == 100.0

    @pytest.mark.asyncio
    @patch("api.dx_metrics.db")
    async def test_stability_score_formula(self, mock_db):
        """Stability score: 0% flake = 100, 20% flake = 0."""
        now = datetime.now(timezone.utc)
        tr = TimeRange(start=now - timedelta(days=7), end=now)

        # 20% flake rate
        runs = [_make_run(created_at=now - timedelta(days=1)) for _ in range(80)]
        flaky = [_make_run(status="flaky", created_at=now - timedelta(days=1)) for _ in range(20)]

        mock_db.list_runs_for_metrics.return_value = runs + flaky
        mock_db.list_fixes.return_value = []
        mock_db.list_verified_fixes.return_value = []

        calc = DXMetricsCalculator(org_id="org-1")
        result = await calc.get_ci_stability_metrics(tr)

        assert result.stability_score == 0.0

    @pytest.mark.asyncio
    @patch("api.dx_metrics.db")
    async def test_flake_rate_change(self, mock_db):
        """Flake rate change compares current vs previous period."""
        now = datetime.now(timezone.utc)
        tr = TimeRange(start=now - timedelta(days=7), end=now)

        # Current period: 10% flaky
        current = [_make_run(created_at=now - timedelta(days=1)) for _ in range(9)]
        current.append(_make_run(status="flaky", created_at=now - timedelta(days=1)))

        # Previous period: 20% flaky
        prev = [_make_run(created_at=now - timedelta(days=10)) for _ in range(8)]
        prev.extend([_make_run(status="flaky", created_at=now - timedelta(days=10)) for _ in range(2)])

        mock_db.list_runs_for_metrics.return_value = current + prev
        mock_db.list_fixes.return_value = []
        mock_db.list_verified_fixes.return_value = []

        calc = DXMetricsCalculator(org_id="org-1")
        result = await calc.get_ci_stability_metrics(tr)

        # flake_rate_change = current(10%) - prev(20%) = -10%
        assert result.flake_rate_change == pytest.approx(-10.0)


class TestTimeToFixMetrics:
    @pytest.mark.asyncio
    @patch("api.dx_metrics.db")
    @patch("api.dx_metrics.parse_fix_time")
    async def test_p50_correct(self, mock_parse, mock_db):
        """P50 should use upper-median (len//2) for even lists."""
        now = datetime.now(timezone.utc)
        tr = TimeRange(start=now - timedelta(days=7), end=now)

        fixes = [_make_fix(created_at=now - timedelta(days=1)) for _ in range(4)]
        mock_db.list_runs_for_metrics.return_value = []
        mock_db.list_fixes.return_value = fixes
        mock_db.list_verified_fixes.return_value = []

        # Return fix times: [10, 20, 30, 40] - p50 should be at index 2 = 30
        mock_parse.side_effect = [10.0, 20.0, 30.0, 40.0, 10.0, 20.0, 30.0, 40.0]

        calc = DXMetricsCalculator(org_id="org-1")
        result = await calc.get_time_to_fix_metrics(tr)

        assert result.p50_seconds == 30.0  # index 4//2 = 2 -> value 30

    @pytest.mark.asyncio
    @patch("api.dx_metrics.db")
    async def test_empty_fixes(self, mock_db):
        """No fixes should return zeros."""
        now = datetime.now(timezone.utc)
        tr = TimeRange(start=now - timedelta(days=7), end=now)

        mock_db.list_runs_for_metrics.return_value = []
        mock_db.list_fixes.return_value = []
        mock_db.list_verified_fixes.return_value = []

        calc = DXMetricsCalculator(org_id="org-1")
        result = await calc.get_time_to_fix_metrics(tr)

        assert result.mttf_seconds == 0
        assert result.p50_seconds == 0
        assert result.auto_fixed_count == 0

    @pytest.mark.asyncio
    @patch("api.dx_metrics.db")
    @patch("api.dx_metrics.parse_fix_time")
    async def test_data_quality_on_fixes(self, mock_parse, mock_db):
        """Data quality indicator based on fix count."""
        now = datetime.now(timezone.utc)
        tr = TimeRange(start=now - timedelta(days=7), end=now)

        fixes = [_make_fix(created_at=now - timedelta(days=1)) for _ in range(3)]
        mock_db.list_runs_for_metrics.return_value = []
        mock_db.list_fixes.return_value = fixes
        mock_db.list_verified_fixes.return_value = []
        mock_parse.side_effect = [10.0, 20.0, 30.0, 10.0, 20.0, 30.0]

        calc = DXMetricsCalculator(org_id="org-1")
        result = await calc.get_time_to_fix_metrics(tr)

        assert result.data_quality is not None
        assert result.data_quality.confidence == "low"


class TestProductivityMetrics:
    @pytest.mark.asyncio
    @patch("api.dx_metrics.db")
    @patch("api.dx_metrics.parse_fix_time")
    async def test_overall_score_weighted(self, mock_parse, mock_db):
        """Overall score is equally weighted average of 4 dimensions."""
        now = datetime.now(timezone.utc)
        tr = TimeRange(start=now - timedelta(days=7), end=now)

        mock_db.list_runs_for_metrics.return_value = []
        mock_db.list_fixes.return_value = []
        mock_db.list_verified_fixes.return_value = []
        mock_parse.return_value = None

        calc = DXMetricsCalculator(org_id="org-1")
        result = await calc.get_productivity_metrics(tr)

        # With no data: speed=0, effectiveness=0, quality=50 (default), impact=0
        # overall = (0 + 0 + 50 + 0) / 4 = 12.5
        assert result.overall_score == 12.5
        assert result.speed_score == 0.0


class TestPrefetchCaching:
    @pytest.mark.asyncio
    @patch("api.dx_metrics.db")
    async def test_prefetch_called_once(self, mock_db):
        """DB should only be called once per calculator instance."""
        now = datetime.now(timezone.utc)
        tr = TimeRange(start=now - timedelta(days=7), end=now)

        mock_db.list_runs_for_metrics.return_value = []
        mock_db.list_fixes.return_value = []
        mock_db.list_verified_fixes.return_value = []

        calc = DXMetricsCalculator(org_id="org-1")
        await calc.get_ci_stability_metrics(tr)
        await calc.get_ci_stability_metrics(tr)

        # list_runs_for_metrics should be called only once
        assert mock_db.list_runs_for_metrics.call_count == 1


class TestFilterByRange:
    def test_excludes_out_of_range(self):
        now = datetime.now(timezone.utc)
        tr = TimeRange(start=now - timedelta(days=7), end=now)

        items = [
            {"created_at": (now - timedelta(days=1)).isoformat()},  # in range
            {"created_at": (now - timedelta(days=10)).isoformat()},  # out of range
        ]

        calc = DXMetricsCalculator(org_id="org-1")
        calc._all_runs = items  # manually set cache
        result = calc._filter_by_range(items, tr)
        assert len(result) == 1

    def test_missing_created_at(self):
        now = datetime.now(timezone.utc)
        tr = TimeRange(start=now - timedelta(days=7), end=now)

        items = [{"id": "no-date"}]
        calc = DXMetricsCalculator(org_id="org-1")
        result = calc._filter_by_range(items, tr)
        assert len(result) == 0


class TestBuildTrend:
    @pytest.mark.asyncio
    @patch("api.dx_metrics.db")
    async def test_trend_has_correct_points(self, mock_db):
        """Trend should have the specified number of data points."""
        now = datetime.now(timezone.utc)
        tr = TimeRange(start=now - timedelta(days=7), end=now)

        mock_db.list_runs_for_metrics.return_value = []
        mock_db.list_fixes.return_value = []
        mock_db.list_verified_fixes.return_value = []

        calc = DXMetricsCalculator(org_id="org-1")
        trend = await calc.get_flake_rate_trend(tr, points=5)

        assert len(trend) == 5
        # All zero since no data
        for point in trend:
            assert point.value == 0.0
