"""Tests for Phase 78: Swarm Telemetry and Heatmaps (25+ tests)."""

import json
from datetime import datetime, timezone, timedelta
from pathlib import Path

import pytest

import brynq.server as srv
import brynq.swarm_telemetry as st


@pytest.fixture(autouse=True)
def isolate(tmp_path, monkeypatch):
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(st, "TELEMETRY_DIR", tmp_path / "telemetry")
    (tmp_path / "telemetry").mkdir(parents=True, exist_ok=True)


def _inject_event(tmp_path, swarm_id, agent_id, event_type, timestamp=None, data=None):
    """Write an event directly to the JSONL file for deterministic timestamps."""
    path = tmp_path / "telemetry" / f"{swarm_id}.jsonl"
    event = {
        "event_id": f"evt-{agent_id}-{event_type}",
        "swarm_id": swarm_id,
        "agent_id": agent_id,
        "event_type": event_type,
        "data": data or {},
        "timestamp": timestamp or datetime.now(timezone.utc).isoformat(),
    }
    with path.open("a", encoding="utf-8") as f:
        f.write(json.dumps(event, default=str) + "\n")
    return event


# ── record_event ─────────────────────────────────────────────────────────


class TestRecordEvent:
    def test_valid_event(self):
        result = st.record_event("s1", "agent-1", "task_start")
        assert result["ok"] is True
        assert result["event"]["swarm_id"] == "s1"
        assert result["event"]["agent_id"] == "agent-1"
        assert result["event"]["event_type"] == "task_start"

    def test_invalid_event_type(self):
        result = st.record_event("s1", "agent-1", "explode")
        assert result["ok"] is False
        assert "Invalid" in result["error"]

    def test_with_data(self):
        result = st.record_event("s1", "a1", "task_start", data={"task_id": "t1"})
        assert result["event"]["data"]["task_id"] == "t1"

    def test_persisted(self, tmp_path):
        st.record_event("s1", "a1", "task_start")
        events = st._load_events("s1")
        assert len(events) == 1

    def test_all_valid_types(self):
        for etype in st.VALID_EVENT_TYPES:
            result = st.record_event("s1", "a1", etype)
            assert result["ok"] is True

    def test_unique_event_id(self):
        r1 = st.record_event("s1", "a1", "task_start")
        r2 = st.record_event("s1", "a1", "task_start")
        assert r1["event"]["event_id"] != r2["event"]["event_id"]


# ── get_agent_timeline ───────────────────────────────────────────────────


class TestGetAgentTimeline:
    def test_empty(self):
        assert st.get_agent_timeline("s1", "a1") == []

    def test_filters_by_agent(self, tmp_path):
        _inject_event(tmp_path, "s1", "a1", "task_start")
        _inject_event(tmp_path, "s1", "a2", "task_start")
        _inject_event(tmp_path, "s1", "a1", "task_complete")
        timeline = st.get_agent_timeline("s1", "a1")
        assert len(timeline) == 2
        assert all(e["agent_id"] == "a1" for e in timeline)

    def test_chronological(self, tmp_path):
        t1 = (datetime.now(timezone.utc) - timedelta(minutes=5)).isoformat()
        t2 = datetime.now(timezone.utc).isoformat()
        _inject_event(tmp_path, "s1", "a1", "task_start", timestamp=t1)
        _inject_event(tmp_path, "s1", "a1", "task_complete", timestamp=t2)
        timeline = st.get_agent_timeline("s1", "a1")
        assert timeline[0]["event_type"] == "task_start"
        assert timeline[1]["event_type"] == "task_complete"


# ── get_swarm_timeline ───────────────────────────────────────────────────


class TestGetSwarmTimeline:
    def test_empty(self):
        assert st.get_swarm_timeline("nonexistent") == []

    def test_all_events(self, tmp_path):
        _inject_event(tmp_path, "s1", "a1", "task_start")
        _inject_event(tmp_path, "s1", "a2", "idle")
        timeline = st.get_swarm_timeline("s1")
        assert len(timeline) == 2


# ── get_bottlenecks ──────────────────────────────────────────────────────


class TestGetBottlenecks:
    def test_empty_swarm(self):
        b = st.get_bottlenecks("empty")
        assert b["idle_agents"] == []
        assert b["slow_tasks"] == []
        assert b["handoff_heavy"] == []

    def test_detects_idle_agent(self, tmp_path):
        for _ in range(4):
            _inject_event(tmp_path, "s1", "lazy", "idle")
        b = st.get_bottlenecks("s1")
        assert len(b["idle_agents"]) == 1
        assert b["idle_agents"][0]["agent_id"] == "lazy"

    def test_detects_slow_task(self, tmp_path):
        now = datetime.now(timezone.utc)
        # Three normal tasks: 10 sec each
        for i in range(3):
            agent = f"fast-{i}"
            _inject_event(tmp_path, "s1", agent, "task_start", timestamp=(now - timedelta(seconds=10)).isoformat())
            _inject_event(tmp_path, "s1", agent, "task_complete", timestamp=now.isoformat())
        # One slow task: 300 sec (avg ~82.5s, threshold 165s, 300 > 165)
        _inject_event(tmp_path, "s1", "slow", "task_start", timestamp=(now - timedelta(seconds=300)).isoformat())
        _inject_event(tmp_path, "s1", "slow", "task_complete", timestamp=now.isoformat())
        b = st.get_bottlenecks("s1")
        assert len(b["slow_tasks"]) >= 1

    def test_detects_handoff_heavy(self, tmp_path):
        for _ in range(4):
            _inject_event(tmp_path, "s1", "bouncer", "handoff")
        b = st.get_bottlenecks("s1")
        assert len(b["handoff_heavy"]) == 1
        assert b["handoff_heavy"][0]["agent_id"] == "bouncer"

    def test_below_threshold_not_flagged(self, tmp_path):
        _inject_event(tmp_path, "s1", "a1", "idle")  # only 1, need >2
        b = st.get_bottlenecks("s1")
        assert b["idle_agents"] == []


# ── get_heatmap ──────────────────────────────────────────────────────────


class TestGetHeatmap:
    def test_empty(self):
        h = st.get_heatmap("empty")
        assert h["agents"] == []
        assert h["grid"] == {}

    def test_agent_activity(self, tmp_path):
        ts = datetime(2025, 6, 15, 14, 30, 0, tzinfo=timezone.utc).isoformat()
        _inject_event(tmp_path, "s1", "a1", "task_start", timestamp=ts)
        _inject_event(tmp_path, "s1", "a1", "task_complete", timestamp=ts)
        h = st.get_heatmap("s1")
        assert "a1" in h["agents"]
        assert h["grid"]["a1"][14] == 2

    def test_multiple_agents(self, tmp_path):
        ts = datetime(2025, 6, 15, 10, 0, 0, tzinfo=timezone.utc).isoformat()
        _inject_event(tmp_path, "s1", "a1", "task_start", timestamp=ts)
        _inject_event(tmp_path, "s1", "a2", "message", timestamp=ts)
        h = st.get_heatmap("s1")
        assert len(h["agents"]) == 2

    def test_hours_are_0_to_23(self):
        h = st.get_heatmap("any")
        assert h["hours"] == list(range(24))


# ── get_throughput ───────────────────────────────────────────────────────


class TestGetThroughput:
    def test_empty(self):
        assert st.get_throughput("empty") == []

    def test_counts_completions(self, tmp_path):
        base = datetime(2025, 6, 15, 10, 0, 0, tzinfo=timezone.utc)
        for i in range(3):
            ts = (base + timedelta(minutes=i * 5)).isoformat()
            _inject_event(tmp_path, "s1", "a1", "task_complete", timestamp=ts)
        tp = st.get_throughput("s1")
        total = sum(b["completed"] for b in tp)
        assert total == 3

    def test_ignores_non_completions(self, tmp_path):
        ts = datetime.now(timezone.utc).isoformat()
        _inject_event(tmp_path, "s1", "a1", "task_start", timestamp=ts)
        _inject_event(tmp_path, "s1", "a1", "idle", timestamp=ts)
        assert st.get_throughput("s1") == []

    def test_custom_bucket(self, tmp_path):
        base = datetime(2025, 6, 15, 10, 0, 0, tzinfo=timezone.utc)
        _inject_event(tmp_path, "s1", "a1", "task_complete", timestamp=base.isoformat())
        _inject_event(tmp_path, "s1", "a1", "task_complete", timestamp=(base + timedelta(minutes=40)).isoformat())
        tp = st.get_throughput("s1", bucket_minutes=30)
        assert len(tp) >= 2


# ── get_efficiency_score ─────────────────────────────────────────────────


class TestGetEfficiencyScore:
    def test_no_data(self):
        r = st.get_efficiency_score("empty")
        assert r["score"] == 0

    def test_perfect_swarm(self, tmp_path):
        ts = datetime.now(timezone.utc).isoformat()
        for _ in range(5):
            _inject_event(tmp_path, "s1", "a1", "task_start", timestamp=ts)
            _inject_event(tmp_path, "s1", "a1", "task_complete", timestamp=ts)
        r = st.get_efficiency_score("s1")
        assert r["score"] >= 80

    def test_idle_swarm(self, tmp_path):
        ts = datetime.now(timezone.utc).isoformat()
        for _ in range(10):
            _inject_event(tmp_path, "s1", "a1", "idle", timestamp=ts)
        r = st.get_efficiency_score("s1")
        assert r["score"] < 30

    def test_breakdown_present(self, tmp_path):
        ts = datetime.now(timezone.utc).isoformat()
        _inject_event(tmp_path, "s1", "a1", "task_start", timestamp=ts)
        _inject_event(tmp_path, "s1", "a1", "task_complete", timestamp=ts)
        r = st.get_efficiency_score("s1")
        assert "idle_score" in r["breakdown"]
        assert "completion_score" in r["breakdown"]
        assert "handoff_score" in r["breakdown"]

    def test_score_range(self, tmp_path):
        ts = datetime.now(timezone.utc).isoformat()
        _inject_event(tmp_path, "s1", "a1", "task_start", timestamp=ts)
        r = st.get_efficiency_score("s1")
        assert 0 <= r["score"] <= 100


# ── compare_swarms ───────────────────────────────────────────────────────


class TestCompareSwarms:
    def test_empty_comparison(self):
        results = st.compare_swarms([])
        assert results == []

    def test_single_swarm(self, tmp_path):
        ts = datetime.now(timezone.utc).isoformat()
        _inject_event(tmp_path, "s1", "a1", "task_start", timestamp=ts)
        _inject_event(tmp_path, "s1", "a1", "task_complete", timestamp=ts)
        results = st.compare_swarms(["s1"])
        assert len(results) == 1
        assert results[0]["swarm_id"] == "s1"
        assert results[0]["total_events"] == 2

    def test_multi_swarm(self, tmp_path):
        ts = datetime.now(timezone.utc).isoformat()
        _inject_event(tmp_path, "s1", "a1", "task_start", timestamp=ts)
        _inject_event(tmp_path, "s2", "a2", "task_start", timestamp=ts)
        _inject_event(tmp_path, "s2", "a2", "task_complete", timestamp=ts)
        results = st.compare_swarms(["s1", "s2"])
        assert len(results) == 2
        by_id = {r["swarm_id"]: r for r in results}
        assert by_id["s1"]["total_events"] == 1
        assert by_id["s2"]["total_events"] == 2

    def test_includes_efficiency(self, tmp_path):
        ts = datetime.now(timezone.utc).isoformat()
        _inject_event(tmp_path, "s1", "a1", "task_start", timestamp=ts)
        results = st.compare_swarms(["s1"])
        assert "efficiency_score" in results[0]
