"""Tests for orchestration.metrics — swarm metrics collector."""

from __future__ import annotations

import threading
import time

import pytest

from orxaq.orchestration.metrics import (
    MetricsCollector,
    ProviderStats,
    TaskMetric,
    TaskStatus,
    WorkerStats,
)

# ---------------------------------------------------------------------------
# TaskMetric
# ---------------------------------------------------------------------------


class TestTaskMetric:
    """TaskMetric dataclass unit tests."""

    def test_creation_defaults(self) -> None:
        m = TaskMetric(task_id="t1", task_type="code_review", provider="openai")
        assert m.task_id == "t1"
        assert m.task_type == "code_review"
        assert m.provider == "openai"
        assert m.worker == ""
        assert m.status == TaskStatus.PENDING
        assert m.tokens_in == 0
        assert m.tokens_out == 0
        assert m.cost_usd == 0.0

    def test_duration_ms_with_timestamps(self) -> None:
        m = TaskMetric(
            task_id="t2",
            task_type="gen",
            provider="anthropic",
            start_time=100.0,
            end_time=100.5,
        )
        assert m.duration_ms == pytest.approx(500.0)

    def test_duration_ms_zero_when_missing(self) -> None:
        m = TaskMetric(task_id="t3", task_type="gen", provider="p")
        assert m.duration_ms == 0.0

    def test_duration_ms_zero_when_end_zero(self) -> None:
        m = TaskMetric(task_id="t4", task_type="gen", provider="p", start_time=1.0)
        assert m.duration_ms == 0.0

    def test_to_dict_serialization(self) -> None:
        m = TaskMetric(
            task_id="t5",
            task_type="summarize",
            provider="openai",
            worker="w1",
            status=TaskStatus.COMPLETED,
            start_time=10.0,
            end_time=10.2,
            tokens_in=100,
            tokens_out=50,
            cost_usd=0.003,
        )
        d = m.to_dict()
        assert d["task_id"] == "t5"
        assert d["status"] == "completed"
        assert d["duration_ms"] == pytest.approx(200.0, abs=0.01)
        assert d["tokens_in"] == 100
        assert d["cost_usd"] == pytest.approx(0.003)
        assert d["worker"] == "w1"

    def test_to_dict_error_field(self) -> None:
        m = TaskMetric(
            task_id="t6",
            task_type="gen",
            provider="p",
            status=TaskStatus.FAILED,
            error="timeout",
        )
        d = m.to_dict()
        assert d["error"] == "timeout"
        assert d["status"] == "failed"


# ---------------------------------------------------------------------------
# ProviderStats
# ---------------------------------------------------------------------------


class TestProviderStats:
    """ProviderStats property tests."""

    def test_success_rate_empty(self) -> None:
        ps = ProviderStats(name="openai")
        assert ps.success_rate == 0.0

    def test_success_rate_computed(self) -> None:
        ps = ProviderStats(name="openai", total_tasks=10, completed=8, failed=2)
        assert ps.success_rate == pytest.approx(0.8)

    def test_avg_latency_empty(self) -> None:
        ps = ProviderStats(name="p")
        assert ps.avg_latency_ms == 0.0

    def test_avg_latency_computed(self) -> None:
        ps = ProviderStats(name="p", latencies_ms=[100.0, 200.0, 300.0])
        assert ps.avg_latency_ms == pytest.approx(200.0)

    def test_p95_latency_single_value(self) -> None:
        ps = ProviderStats(name="p", latencies_ms=[150.0])
        # Falls back to avg when < 2 samples.
        assert ps.p95_latency_ms == pytest.approx(150.0)

    def test_p95_latency_computed(self) -> None:
        # 20 values: 10, 20, ..., 200.  95th percentile index = 19.
        lats = [float(i * 10) for i in range(1, 21)]
        ps = ProviderStats(name="p", latencies_ms=lats)
        assert ps.p95_latency_ms == pytest.approx(200.0)

    def test_cost_per_1k_tokens_zero_tokens(self) -> None:
        ps = ProviderStats(name="p", total_cost_usd=1.0)
        assert ps.cost_per_1k_tokens == 0.0

    def test_cost_per_1k_tokens_computed(self) -> None:
        ps = ProviderStats(
            name="p",
            total_tokens_in=500,
            total_tokens_out=500,
            total_cost_usd=0.01,
        )
        # 0.01 / 1000 * 1000 = 0.01
        assert ps.cost_per_1k_tokens == pytest.approx(0.01)

    def test_to_dict_keys(self) -> None:
        ps = ProviderStats(name="openai")
        d = ps.to_dict()
        expected_keys = {
            "name",
            "total_tasks",
            "completed",
            "failed",
            "success_rate",
            "avg_latency_ms",
            "p95_latency_ms",
            "total_tokens_in",
            "total_tokens_out",
            "total_cost_usd",
            "cost_per_1k_tokens",
        }
        assert set(d.keys()) == expected_keys


# ---------------------------------------------------------------------------
# WorkerStats
# ---------------------------------------------------------------------------


class TestWorkerStats:
    """WorkerStats property tests."""

    def test_specialization_none_when_empty(self) -> None:
        ws = WorkerStats(name="w1")
        assert ws.specialization == "none"

    def test_specialization_most_common(self) -> None:
        ws = WorkerStats(name="w1")
        ws.task_types["code_review"] = 5
        ws.task_types["summarize"] = 3
        ws.task_types["generate"] = 1
        assert ws.specialization == "code_review"

    def test_avg_duration_empty(self) -> None:
        ws = WorkerStats(name="w1")
        assert ws.avg_duration_ms == 0.0

    def test_avg_duration_computed(self) -> None:
        ws = WorkerStats(name="w1", latencies_ms=[100.0, 300.0])
        assert ws.avg_duration_ms == pytest.approx(200.0)

    def test_to_dict_keys(self) -> None:
        ws = WorkerStats(name="w1")
        d = ws.to_dict()
        expected_keys = {
            "name",
            "total_tasks",
            "completed",
            "failed",
            "avg_duration_ms",
            "specialization",
            "task_type_distribution",
        }
        assert set(d.keys()) == expected_keys


# ---------------------------------------------------------------------------
# MetricsCollector
# ---------------------------------------------------------------------------


def _make_task(
    task_id: str = "t1",
    task_type: str = "gen",
    provider: str = "openai",
    worker: str = "",
    status: TaskStatus = TaskStatus.COMPLETED,
    start_time: float = 1.0,
    end_time: float = 1.5,
    tokens_in: int = 100,
    tokens_out: int = 50,
    cost_usd: float = 0.002,
    error: str = "",
) -> TaskMetric:
    """Helper to build a TaskMetric with sensible defaults."""
    return TaskMetric(
        task_id=task_id,
        task_type=task_type,
        provider=provider,
        worker=worker,
        status=status,
        start_time=start_time,
        end_time=end_time,
        tokens_in=tokens_in,
        tokens_out=tokens_out,
        cost_usd=cost_usd,
        error=error,
    )


class TestMetricsCollector:
    """MetricsCollector integration tests."""

    def test_empty_collector(self) -> None:
        mc = MetricsCollector()
        assert mc.total_cost() == 0.0
        assert mc.error_rate() == 0.0
        assert mc.provider_summary() == {}
        assert mc.worker_summary() == {}
        assert mc.recent_tasks() == []

    def test_record_task_updates_provider(self) -> None:
        mc = MetricsCollector()
        mc.record_task(_make_task(provider="anthropic"))
        summary = mc.provider_summary()
        assert "anthropic" in summary
        assert summary["anthropic"]["total_tasks"] == 1
        assert summary["anthropic"]["completed"] == 1

    def test_record_task_updates_worker(self) -> None:
        mc = MetricsCollector()
        mc.record_task(_make_task(worker="m4-max"))
        summary = mc.worker_summary()
        assert "m4-max" in summary
        assert summary["m4-max"]["total_tasks"] == 1

    def test_record_task_no_worker_skips_worker_stats(self) -> None:
        mc = MetricsCollector()
        mc.record_task(_make_task(worker=""))
        assert mc.worker_summary() == {}

    def test_multiple_providers_independent(self) -> None:
        mc = MetricsCollector()
        mc.record_task(_make_task(task_id="a", provider="openai"))
        mc.record_task(_make_task(task_id="b", provider="anthropic"))
        mc.record_task(_make_task(task_id="c", provider="openai"))
        summary = mc.provider_summary()
        assert summary["openai"]["total_tasks"] == 2
        assert summary["anthropic"]["total_tasks"] == 1

    def test_multiple_workers_independent(self) -> None:
        mc = MetricsCollector()
        mc.record_task(_make_task(task_id="a", worker="w1"))
        mc.record_task(_make_task(task_id="b", worker="w2"))
        mc.record_task(_make_task(task_id="c", worker="w1"))
        summary = mc.worker_summary()
        assert summary["w1"]["total_tasks"] == 2
        assert summary["w2"]["total_tasks"] == 1

    def test_error_rate(self) -> None:
        mc = MetricsCollector()
        mc.record_task(_make_task(task_id="ok1"))
        mc.record_task(_make_task(task_id="ok2"))
        mc.record_task(_make_task(task_id="fail1", status=TaskStatus.FAILED, error="boom"))
        assert mc.error_rate() == pytest.approx(1 / 3)

    def test_total_cost(self) -> None:
        mc = MetricsCollector()
        mc.record_task(_make_task(task_id="a", cost_usd=0.01))
        mc.record_task(_make_task(task_id="b", cost_usd=0.02))
        assert mc.total_cost() == pytest.approx(0.03)

    def test_tasks_per_minute(self) -> None:
        mc = MetricsCollector()
        # Backdate start_time so elapsed time is predictable.
        mc._start_time = time.time() - 60  # 60 seconds ago
        mc.record_task(_make_task(task_id="a"))
        mc.record_task(_make_task(task_id="b"))
        tpm = mc.tasks_per_minute()
        # Should be approximately 2 tasks/minute.
        assert tpm == pytest.approx(2.0, abs=0.1)

    def test_recent_tasks_limit(self) -> None:
        mc = MetricsCollector()
        for i in range(30):
            mc.record_task(_make_task(task_id=f"t{i}"))
        recent = mc.recent_tasks(limit=5)
        assert len(recent) == 5
        assert recent[0]["task_id"] == "t25"
        assert recent[4]["task_id"] == "t29"

    def test_recent_tasks_default(self) -> None:
        mc = MetricsCollector()
        for i in range(25):
            mc.record_task(_make_task(task_id=f"t{i}"))
        recent = mc.recent_tasks()
        assert len(recent) == 20  # default limit

    def test_dashboard_snapshot_structure(self) -> None:
        mc = MetricsCollector()
        mc.record_task(_make_task(provider="openai", worker="w1"))
        mc.record_task(
            _make_task(
                task_id="f1",
                provider="openai",
                worker="w1",
                status=TaskStatus.FAILED,
                error="err",
            )
        )
        snap = mc.dashboard_snapshot()
        assert snap["total_tasks"] == 2
        assert snap["completed_tasks"] == 1
        assert snap["failed_tasks"] == 1
        assert "uptime_seconds" in snap
        assert "tasks_per_minute" in snap
        assert "total_cost_usd" in snap
        assert "error_rate" in snap
        assert "providers" in snap
        assert "workers" in snap
        assert "timestamp" in snap
        assert "openai" in snap["providers"]
        assert "w1" in snap["workers"]

    def test_reset_clears_everything(self) -> None:
        mc = MetricsCollector()
        mc.record_task(_make_task())
        mc.reset()
        assert mc.total_cost() == 0.0
        assert mc.error_rate() == 0.0
        assert mc.provider_summary() == {}
        assert mc.worker_summary() == {}
        assert mc.recent_tasks() == []

    def test_failed_task_not_in_latencies(self) -> None:
        mc = MetricsCollector()
        mc.record_task(
            _make_task(
                task_id="f1",
                provider="p",
                status=TaskStatus.FAILED,
                start_time=1.0,
                end_time=2.0,
            )
        )
        ps = mc.provider_summary()["p"]
        assert ps["avg_latency_ms"] == 0.0  # failed tasks excluded

    def test_thread_safety(self) -> None:
        """Record tasks from multiple threads concurrently."""
        mc = MetricsCollector()
        errors: list[str] = []

        def record_batch(start: int) -> None:
            try:
                for i in range(50):
                    mc.record_task(
                        _make_task(
                            task_id=f"t{start + i}",
                            provider="openai",
                            worker=f"w{start % 3}",
                        )
                    )
            except Exception as exc:  # noqa: BLE001
                errors.append(str(exc))

        threads = [threading.Thread(target=record_batch, args=(i * 50,)) for i in range(4)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert not errors, f"Thread errors: {errors}"
        snap = mc.dashboard_snapshot()
        assert snap["total_tasks"] == 200


# ── Targeted coverage (line 214: zero elapsed → 0.0 tasks_per_minute) ──


class TestTasksPerMinuteZeroElapsed:
    """Cover line 214: tasks_per_minute returns 0.0 when elapsed <= 0."""

    def test_zero_elapsed_returns_zero(self):
        from unittest.mock import patch

        mc = MetricsCollector()
        mc.record_task(_make_task(task_id="t1", provider="openai"))
        # Mock time.time to return the same value as _start_time
        with patch("orxaq.orchestration.metrics.time") as mock_time:
            mock_time.time.return_value = mc._start_time
            result = mc.tasks_per_minute()
        assert result == 0.0
