"""Tests for api/dora_metrics.py - DORA metrics calculator."""

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

import pytest

from api.dora_metrics import (
    DataQuality,
    DORACalculator,
    DORAMetrics,
    PerformanceLevel,
    _assess_quality,
)


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


def _make_fix(status="accepted", created_at=None, applied_at=None, run_id="run-1"):
    """Helper to create a fix dict."""
    now = datetime.now(timezone.utc)
    return {
        "id": "fix-1",
        "run_id": run_id,
        "status": status,
        "created_at": (created_at or now).isoformat(),
        "applied_at": (applied_at or now + timedelta(hours=1)).isoformat(),
    }


class TestDataQuality:
    def test_assess_quality_high(self):
        q = _assess_quality(100, "test")
        assert q.confidence == "high"
        assert q.warning is None

    def test_assess_quality_medium(self):
        q = _assess_quality(25, "test")
        assert q.confidence == "medium"

    def test_assess_quality_low(self):
        q = _assess_quality(3, "test")
        assert q.confidence == "low"
        assert "Only 3" in q.warning

    def test_assess_quality_zero(self):
        q = _assess_quality(0, "deployments")
        assert q.confidence == "low"
        assert "Only 0" in q.warning

    def test_to_dict(self):
        q = DataQuality(sample_count=5, confidence="low", warning="test warning")
        d = q.to_dict()
        assert d["sample_count"] == 5
        assert d["confidence"] == "low"
        assert d["warning"] == "test warning"

    def test_to_dict_no_warning(self):
        q = DataQuality(sample_count=50, confidence="high")
        d = q.to_dict()
        assert "warning" not in d


class TestPerformanceLevels:
    def setup_method(self):
        self.calc = DORACalculator(project_id="proj-1")

    def test_deployment_frequency_elite(self):
        level = self.calc._get_level("deployment_frequency", 2.0, higher_better=True)
        assert level == PerformanceLevel.ELITE

    def test_deployment_frequency_high(self):
        level = self.calc._get_level("deployment_frequency", 0.5, higher_better=True)
        assert level == PerformanceLevel.HIGH

    def test_deployment_frequency_medium(self):
        level = self.calc._get_level("deployment_frequency", 0.05, higher_better=True)
        assert level == PerformanceLevel.MEDIUM

    def test_deployment_frequency_low(self):
        level = self.calc._get_level("deployment_frequency", 0.01, higher_better=True)
        assert level == PerformanceLevel.LOW

    def test_lead_time_elite(self):
        level = self.calc._get_level("lead_time_hours", 0.5, higher_better=False)
        assert level == PerformanceLevel.ELITE

    def test_lead_time_low(self):
        level = self.calc._get_level("lead_time_hours", 500, higher_better=False)
        assert level == PerformanceLevel.LOW

    def test_cfr_elite(self):
        level = self.calc._get_level("change_failure_rate", 3.0, higher_better=False)
        assert level == PerformanceLevel.ELITE

    def test_mttr_high(self):
        level = self.calc._get_level("mttr_hours", 12, higher_better=False)
        assert level == PerformanceLevel.HIGH


class TestDORACalculator:
    @pytest.mark.asyncio
    @patch("api.dora_metrics.db")
    async def test_calculate_with_deployments(self, mock_db):
        """Test DORA calculation with deployment data."""
        runs = [_make_run(branch="main", status="passed") for _ in range(30)]
        failed_runs = [_make_run(branch="main", status="failed") for _ in range(2)]
        all_runs = runs + failed_runs

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

        calc = DORACalculator(project_id="proj-1")
        result = await calc.calculate(days=30)

        assert result.total_deployments == 30  # only passed main runs
        assert result.deployment_frequency == 1.0  # 30/30
        assert result.deployment_frequency_level == "elite"
        assert result.failed_deployments == 2

    @pytest.mark.asyncio
    @patch("api.dora_metrics.db")
    async def test_calculate_zero_deployments(self, mock_db):
        """Edge case: no deployments in period."""
        mock_db.list_runs_for_metrics.return_value = []
        mock_db.list_fixes.return_value = []
        mock_db.get_runs_by_ids.return_value = []

        calc = DORACalculator(project_id="proj-1")
        result = await calc.calculate(days=30)

        assert result.total_deployments == 0
        assert result.deployment_frequency == 0
        assert result.deployment_frequency_level == "low"
        assert result.change_failure_rate == 0

    @pytest.mark.asyncio
    @patch("api.dora_metrics.db")
    async def test_overall_level_worst_of(self, mock_db):
        """Overall level should be the worst of all 4 metrics."""
        # elite deploys but no recovery data = low MTTR
        runs = [_make_run(branch="main", status="passed") for _ in range(60)]
        mock_db.list_runs_for_metrics.return_value = runs
        mock_db.list_fixes.return_value = []
        mock_db.get_runs_by_ids.return_value = []

        calc = DORACalculator(project_id="proj-1")
        result = await calc.calculate(days=30)

        # With 0 MTTR (no recovery data), mttr_hours=0 which is elite
        # With 0 lead_time (no PR data), lead_time=0 which is elite
        # So overall should be elite
        assert result.overall_level == "elite"

    @pytest.mark.asyncio
    @patch("api.dora_metrics.db")
    async def test_data_quality_included(self, mock_db):
        """Data quality should be populated in result."""
        runs = [_make_run(branch="main", status="passed") for _ in range(5)]
        mock_db.list_runs_for_metrics.return_value = runs
        mock_db.list_fixes.return_value = []
        mock_db.get_runs_by_ids.return_value = []

        calc = DORACalculator(project_id="proj-1")
        result = await calc.calculate(days=30)

        assert result.data_quality is not None
        assert result.data_quality["deployment_frequency"].confidence == "low"
        assert "Only 5" in result.data_quality["deployment_frequency"].warning

    @pytest.mark.asyncio
    @patch("api.dora_metrics.db")
    async def test_recovery_times(self, mock_db):
        """Test MTTR calculation from fix application times."""
        now = datetime.now(timezone.utc)
        failure_time = now - timedelta(hours=2)
        fix_time = now

        runs = [_make_run(branch="main", status="passed")]
        mock_db.list_runs_for_metrics.return_value = runs

        mock_db.list_fixes.return_value = [
            {"run_id": "run-1", "status": "accepted", "applied_at": fix_time.isoformat(), "created_at": failure_time.isoformat()},
        ]
        mock_db.get_runs_by_ids.return_value = [
            {"id": "run-1", "created_at": failure_time.isoformat()},
        ]

        calc = DORACalculator(project_id="proj-1")
        result = await calc.calculate(days=30)

        assert result.mttr_hours == pytest.approx(2.0, abs=0.1)

    @pytest.mark.asyncio
    @patch("api.dora_metrics.db")
    async def test_fetch_runs_error_returns_empty(self, mock_db):
        """Exception in fetch_runs should return empty list."""
        mock_db.list_runs_for_metrics.side_effect = Exception("DB error")
        mock_db.list_fixes.return_value = []
        mock_db.get_runs_by_ids.return_value = []

        calc = DORACalculator(project_id="proj-1")
        result = await calc.calculate(days=30)

        assert result.total_deployments == 0

    def test_count_failures_from_runs(self):
        """Count only main/master failed runs."""
        runs = [
            _make_run(branch="main", status="failed"),
            _make_run(branch="main", status="passed"),
            _make_run(branch="feature", status="failed"),
            _make_run(branch="master", status="FAILED"),
        ]
        count = DORACalculator._count_failures_from_runs(runs)
        assert count == 2  # main failed + master FAILED

    def test_to_dict(self):
        """Test DORAMetrics serialization."""
        now = datetime.now(timezone.utc)
        m = DORAMetrics(
            deployment_frequency=1.0,
            deployment_frequency_unit="per_day",
            deployment_frequency_level="elite",
            total_deployments=30,
            lead_time_hours=0.5,
            lead_time_level="elite",
            change_failure_rate=3.0,
            change_failure_rate_level="elite",
            failed_deployments=1,
            mttr_hours=0.5,
            mttr_level="elite",
            overall_level="elite",
            data_quality={"test": _assess_quality(50, "test")},
            period_days=30,
            start_date=now,
            end_date=now,
        )
        d = m.to_dict()
        assert d["deployment_frequency"] == 1.0
        assert d["overall_level"] == "elite"
        assert "data_quality" in d
        assert d["data_quality"]["test"]["confidence"] == "high"

    def test_get_benchmarks(self):
        calc = DORACalculator(project_id="proj-1")
        benchmarks = calc.get_benchmarks()
        assert "deployment_frequency" in benchmarks
        assert "lead_time" in benchmarks
        assert "change_failure_rate" in benchmarks
        assert "mttr" in benchmarks
