"""
Tests for Phase 40: Continuous Health Monitoring & Alerting.
"""

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

import pytest


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect all broker directories to tmp_path so tests are fully isolated."""
    channels = tmp_path / "channels"
    sessions = tmp_path / "sessions"
    tasks = tmp_path / "tasks"
    api_keys = tmp_path / "api_keys"
    profiles = tmp_path / "profiles"
    health = tmp_path / "health"
    for d in (channels, sessions, tasks, api_keys, profiles, health):
        d.mkdir(parents=True, exist_ok=True)

    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(srv, "CHANNELS_DIR", channels)
    monkeypatch.setattr(srv, "SESSIONS_DIR", sessions)

    import brynq.health_monitor as hm
    monkeypatch.setattr(hm, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(hm, "CHANNELS_DIR", channels)
    monkeypatch.setattr(hm, "SESSIONS_DIR", sessions)
    monkeypatch.setattr(hm, "TASKS_DIR", tasks)
    monkeypatch.setattr(hm, "API_KEYS_DIR", api_keys)
    monkeypatch.setattr(hm, "PROFILES_DIR", profiles)
    monkeypatch.setattr(hm, "HEALTH_DIR", health)
    monkeypatch.setattr(hm, "CHECKS_FILE", health / "checks.jsonl")
    monkeypatch.setattr(hm, "ALERTS_FILE", health / "alerts.jsonl")
    monkeypatch.setattr(hm, "SLA_FILE", health / "sla.jsonl")

    # Reset module-level state between tests
    hm._alert_rules.clear()
    hm._alert_cooldowns.clear()
    hm._monitor_stop_event.set()
    monkeypatch.setattr(hm, "_monitor_thread", None)

    yield


# ── Helpers ──────────────────────────────────────────────────────────────


def _write_session(tmp_path, session_id, heartbeat_iso=None):
    """Create a session JSON file."""
    data = {"session_id": session_id, "name": f"agent-{session_id}", "platform": "test"}
    if heartbeat_iso:
        data["heartbeat"] = heartbeat_iso
    path = tmp_path / "sessions" / f"{session_id}.json"
    path.write_text(json.dumps(data), encoding="utf-8")
    return path


def _write_task(tmp_path, task_id, status="pending", accepted_at_iso=None):
    """Create a task JSON file."""
    data = {"task_id": task_id, "title": f"Task {task_id}", "status": status}
    if accepted_at_iso:
        data["accepted_at"] = accepted_at_iso
    path = tmp_path / "tasks" / f"{task_id}.json"
    path.write_text(json.dumps(data), encoding="utf-8")
    return path


def _write_api_key(tmp_path, key_id):
    path = tmp_path / "api_keys" / f"{key_id}.json"
    path.write_text(json.dumps({"key_id": key_id}), encoding="utf-8")
    return path


def _write_profile(tmp_path, profile_id):
    path = tmp_path / "profiles" / f"{profile_id}.json"
    path.write_text(json.dumps({"profile_id": profile_id}), encoding="utf-8")
    return path


# ── 1. Health Check Tests ────────────────────────────────────────────────


class TestRunHealthCheck:
    def test_returns_valid_report(self, tmp_path):
        from brynq.health_monitor import run_health_check
        report = run_health_check()
        assert report["status"] in ("healthy", "degraded", "unhealthy")
        assert 0 <= report["score"] <= 100
        assert "checks" in report
        assert "timestamp" in report

    def test_healthy_system_scores_high(self, tmp_path):
        from brynq.health_monitor import run_health_check
        report = run_health_check()
        assert report["score"] >= 80
        assert report["status"] == "healthy"

    def test_check_includes_all_subsystems(self, tmp_path):
        from brynq.health_monitor import run_health_check
        report = run_health_check()
        checks = report["checks"]
        assert "broker" in checks
        assert "sessions" in checks
        assert "tasks" in checks
        assert "disk" in checks
        assert "api_keys" in checks
        assert "profiles" in checks

    def test_detects_stale_sessions(self, tmp_path):
        from brynq.health_monitor import run_health_check
        old = (datetime.now(timezone.utc) - timedelta(hours=2)).isoformat()
        for i in range(5):
            _write_session(tmp_path, f"stale-{i}", heartbeat_iso=old)
        report = run_health_check()
        assert report["checks"]["sessions"]["stale"] == 5

    def test_stale_sessions_degrade_score(self, tmp_path):
        from brynq.health_monitor import run_health_check
        old = (datetime.now(timezone.utc) - timedelta(hours=2)).isoformat()
        for i in range(5):
            _write_session(tmp_path, f"stale-{i}", heartbeat_iso=old)
        report = run_health_check()
        # >3 stale sessions should degrade
        assert report["checks"]["sessions"]["status"] == "degraded"

    def test_detects_stuck_tasks(self, tmp_path):
        from brynq.health_monitor import run_health_check
        old = (datetime.now(timezone.utc) - timedelta(minutes=45)).isoformat()
        _write_task(tmp_path, "stuck-1", status="accepted", accepted_at_iso=old)
        _write_task(tmp_path, "stuck-2", status="accepted", accepted_at_iso=old)
        report = run_health_check()
        assert report["checks"]["tasks"]["stuck"] == 2

    def test_counts_task_statuses(self, tmp_path):
        from brynq.health_monitor import run_health_check
        _write_task(tmp_path, "p1", status="pending")
        _write_task(tmp_path, "p2", status="pending")
        _write_task(tmp_path, "c1", status="completed")
        _write_task(tmp_path, "f1", status="failed")
        report = run_health_check()
        tasks = report["checks"]["tasks"]
        assert tasks["pending"] == 2
        assert tasks["completed"] == 1
        assert tasks["failed"] == 1

    def test_counts_api_keys(self, tmp_path):
        from brynq.health_monitor import run_health_check
        _write_api_key(tmp_path, "key-1")
        _write_api_key(tmp_path, "key-2")
        report = run_health_check()
        assert report["checks"]["api_keys"]["count"] == 2

    def test_counts_profiles(self, tmp_path):
        from brynq.health_monitor import run_health_check
        _write_profile(tmp_path, "prof-1")
        report = run_health_check()
        assert report["checks"]["profiles"]["count"] == 1

    def test_disk_check_has_free_mb(self, tmp_path):
        from brynq.health_monitor import run_health_check
        report = run_health_check()
        disk = report["checks"]["disk"]
        assert "free_mb" in disk
        assert disk["free_mb"] > 0

    def test_degraded_system_scores_lower(self, tmp_path):
        from brynq.health_monitor import run_health_check
        # Lots of stuck tasks to degrade score
        old = (datetime.now(timezone.utc) - timedelta(minutes=45)).isoformat()
        for i in range(8):
            _write_task(tmp_path, f"stuck-{i}", status="accepted", accepted_at_iso=old)
        report = run_health_check()
        assert report["score"] < 100

    def test_report_persisted_to_history(self, tmp_path):
        from brynq.health_monitor import run_health_check, CHECKS_FILE
        run_health_check()
        assert CHECKS_FILE.exists()
        lines = CHECKS_FILE.read_text("utf-8").strip().splitlines()
        assert len(lines) >= 1


# ── 2. Continuous Monitor Tests ──────────────────────────────────────────


class TestMonitor:
    def test_start_stop(self, tmp_path):
        from brynq.health_monitor import start_monitor, stop_monitor, is_monitoring
        assert is_monitoring() is False
        start_monitor(interval_seconds=60)
        assert is_monitoring() is True
        stop_monitor()
        # Give thread time to finish
        time.sleep(0.1)
        assert is_monitoring() is False

    def test_double_start_noop(self, tmp_path):
        from brynq.health_monitor import start_monitor, stop_monitor, is_monitoring
        import brynq.health_monitor as hm
        start_monitor(interval_seconds=60)
        thread1 = hm._monitor_thread
        start_monitor(interval_seconds=60)
        thread2 = hm._monitor_thread
        assert thread1 is thread2
        stop_monitor()

    def test_monitor_runs_check(self, tmp_path):
        from brynq.health_monitor import start_monitor, stop_monitor, CHECKS_FILE
        start_monitor(interval_seconds=0.05)
        time.sleep(0.2)
        stop_monitor()
        assert CHECKS_FILE.exists()
        lines = CHECKS_FILE.read_text("utf-8").strip().splitlines()
        assert len(lines) >= 1


# ── 3. Alert Rules Tests ────────────────────────────────────────────────


class TestAlertRules:
    def test_add_and_list_rules(self, tmp_path):
        from brynq.health_monitor import add_alert_rule, list_alert_rules
        add_alert_rule("test_rule", lambda r: True, severity="critical")
        rules = list_alert_rules()
        assert len(rules) == 1
        assert rules[0]["name"] == "test_rule"
        assert rules[0]["severity"] == "critical"

    def test_remove_rule(self, tmp_path):
        from brynq.health_monitor import add_alert_rule, remove_alert_rule, list_alert_rules
        add_alert_rule("temp", lambda r: True)
        assert remove_alert_rule("temp") is True
        assert remove_alert_rule("nonexistent") is False
        assert list_alert_rules() == []

    def test_rule_triggers_alert(self, tmp_path):
        from brynq.health_monitor import add_alert_rule, run_health_check, _evaluate_alert_rules, get_alerts
        add_alert_rule("always_fire", lambda r: True, severity="warning", cooldown_minutes=0)
        report = run_health_check()
        triggered = _evaluate_alert_rules(report)
        assert len(triggered) == 1
        assert triggered[0]["rule_name"] == "always_fire"
        # Should appear in persistent alerts
        alerts = get_alerts()
        assert any(a["rule_name"] == "always_fire" for a in alerts)

    def test_rule_does_not_trigger_when_false(self, tmp_path):
        from brynq.health_monitor import add_alert_rule, run_health_check, _evaluate_alert_rules
        add_alert_rule("never_fire", lambda r: False, cooldown_minutes=0)
        report = run_health_check()
        triggered = _evaluate_alert_rules(report)
        assert len(triggered) == 0

    def test_cooldown_prevents_spam(self, tmp_path):
        from brynq.health_monitor import add_alert_rule, run_health_check, _evaluate_alert_rules
        # Cooldown of 60 minutes ensures second eval is blocked
        add_alert_rule("cooldown_test", lambda r: True, cooldown_minutes=60)
        report = run_health_check()
        first = _evaluate_alert_rules(report)
        assert len(first) == 1
        second = _evaluate_alert_rules(report)
        assert len(second) == 0  # Blocked by cooldown

    def test_zero_cooldown_allows_repeated(self, tmp_path):
        from brynq.health_monitor import add_alert_rule, run_health_check, _evaluate_alert_rules
        add_alert_rule("rapid", lambda r: True, cooldown_minutes=0)
        report = run_health_check()
        first = _evaluate_alert_rules(report)
        second = _evaluate_alert_rules(report)
        assert len(first) == 1
        assert len(second) == 1


# ── 4. Alert History & Acknowledgment ────────────────────────────────────


class TestAlertHistory:
    def test_get_alerts_empty(self, tmp_path):
        from brynq.health_monitor import get_alerts
        assert get_alerts() == []

    def test_get_alerts_with_severity_filter(self, tmp_path):
        from brynq.health_monitor import add_alert_rule, run_health_check, _evaluate_alert_rules, get_alerts
        add_alert_rule("warn", lambda r: True, severity="warning", cooldown_minutes=0)
        add_alert_rule("crit", lambda r: True, severity="critical", cooldown_minutes=0)
        report = run_health_check()
        _evaluate_alert_rules(report)
        warnings = get_alerts(severity="warning")
        criticals = get_alerts(severity="critical")
        assert all(a["severity"] == "warning" for a in warnings)
        assert all(a["severity"] == "critical" for a in criticals)

    def test_acknowledge_alert(self, tmp_path):
        from brynq.health_monitor import add_alert_rule, run_health_check, _evaluate_alert_rules, acknowledge_alert, get_alerts
        add_alert_rule("ack_test", lambda r: True, cooldown_minutes=0)
        report = run_health_check()
        triggered = _evaluate_alert_rules(report)
        alert_id = triggered[0]["alert_id"]
        assert acknowledge_alert(alert_id) is True
        alerts = get_alerts()
        found = [a for a in alerts if a["alert_id"] == alert_id]
        assert found[0]["acknowledged"] is True
        assert found[0]["acknowledged_at"] is not None

    def test_acknowledge_nonexistent(self, tmp_path):
        from brynq.health_monitor import acknowledge_alert
        assert acknowledge_alert("does-not-exist") is False

    def test_get_active_alerts(self, tmp_path):
        from brynq.health_monitor import (
            add_alert_rule, run_health_check, _evaluate_alert_rules,
            acknowledge_alert, get_active_alerts,
        )
        add_alert_rule("active_test", lambda r: True, cooldown_minutes=0)
        report = run_health_check()
        triggered = _evaluate_alert_rules(report)
        alert_id = triggered[0]["alert_id"]

        active = get_active_alerts()
        assert len(active) >= 1

        acknowledge_alert(alert_id)
        active = get_active_alerts()
        assert all(a["alert_id"] != alert_id for a in active)

    def test_alerts_limit(self, tmp_path):
        from brynq.health_monitor import add_alert_rule, run_health_check, _evaluate_alert_rules, get_alerts
        add_alert_rule("spam", lambda r: True, cooldown_minutes=0)
        report = run_health_check()
        for _ in range(10):
            _evaluate_alert_rules(report)
        limited = get_alerts(limit=3)
        assert len(limited) == 3


# ── 5. Built-in Rules ───────────────────────────────────────────────────


class TestBuiltinRules:
    def test_disk_space_low(self, tmp_path):
        from brynq.health_monitor import _rule_disk_space_low
        report = {"checks": {"disk": {"free_mb": 100}}}
        assert _rule_disk_space_low(report) is True
        report["checks"]["disk"]["free_mb"] = 1000
        assert _rule_disk_space_low(report) is False

    def test_stuck_tasks_rule(self, tmp_path):
        from brynq.health_monitor import _rule_stuck_tasks
        report = {"checks": {"tasks": {"stuck": 6}}}
        assert _rule_stuck_tasks(report) is True
        report["checks"]["tasks"]["stuck"] = 2
        assert _rule_stuck_tasks(report) is False

    def test_stale_sessions_rule(self, tmp_path):
        from brynq.health_monitor import _rule_stale_sessions
        report = {"checks": {"sessions": {"stale": 4}}}
        assert _rule_stale_sessions(report) is True
        report["checks"]["sessions"]["stale"] = 1
        assert _rule_stale_sessions(report) is False

    def test_high_error_rate_rule(self, tmp_path):
        from brynq.health_monitor import _rule_high_error_rate
        # 30% failure rate
        report = {"checks": {"tasks": {"completed": 7, "failed": 3}}}
        assert _rule_high_error_rate(report) is True
        # 10% failure rate
        report["checks"]["tasks"] = {"completed": 9, "failed": 1}
        assert _rule_high_error_rate(report) is False

    def test_high_error_rate_zero_tasks(self, tmp_path):
        from brynq.health_monitor import _rule_high_error_rate
        report = {"checks": {"tasks": {"completed": 0, "failed": 0}}}
        assert _rule_high_error_rate(report) is False

    def test_register_builtin_rules(self, tmp_path):
        from brynq.health_monitor import register_builtin_rules, list_alert_rules
        register_builtin_rules()
        rules = list_alert_rules()
        names = {r["name"] for r in rules}
        assert "disk_space_low" in names
        assert "stuck_tasks" in names
        assert "stale_sessions" in names
        assert "high_error_rate" in names


# ── 6. SLA Tracking Tests ───────────────────────────────────────────────


class TestSLA:
    def test_record_sla_event(self, tmp_path):
        from brynq.health_monitor import record_sla_event
        event = record_sla_event("uptime", 99.9, 99.5)
        assert event["metric"] == "uptime"
        assert event["met"] is True

    def test_sla_event_not_met(self, tmp_path):
        from brynq.health_monitor import record_sla_event
        event = record_sla_event("uptime", 98.0, 99.5)
        assert event["met"] is False

    def test_sla_report(self, tmp_path):
        from brynq.health_monitor import record_sla_event, get_sla_report
        record_sla_event("uptime", 99.9, 99.5)
        record_sla_event("uptime", 99.8, 99.5)
        record_sla_event("uptime", 98.0, 99.5)
        report = get_sla_report(hours=24)
        assert report["period_hours"] == 24
        assert "uptime" in report["metrics"]
        uptime = report["metrics"]["uptime"]
        assert uptime["events"] == 3
        assert uptime["met"] == 2
        assert uptime["compliance_pct"] == pytest.approx(66.7, abs=0.1)

    def test_sla_report_empty(self, tmp_path):
        from brynq.health_monitor import get_sla_report
        report = get_sla_report()
        assert report["metrics"] == {}

    def test_sla_multiple_metrics(self, tmp_path):
        from brynq.health_monitor import record_sla_event, get_sla_report
        record_sla_event("uptime", 100, 99.5)
        record_sla_event("task_completion_rate", 95, 90)
        report = get_sla_report()
        assert len(report["metrics"]) == 2


# ── 7. Health History & Trend Tests ──────────────────────────────────────


class TestHealthHistory:
    def test_history_returns_checks(self, tmp_path):
        from brynq.health_monitor import run_health_check, get_health_history
        run_health_check()
        run_health_check()
        history = get_health_history(hours=24)
        assert len(history) >= 2

    def test_history_most_recent_first(self, tmp_path):
        from brynq.health_monitor import run_health_check, get_health_history
        run_health_check()
        run_health_check()
        history = get_health_history()
        if len(history) >= 2:
            t0 = datetime.fromisoformat(history[0]["timestamp"])
            t1 = datetime.fromisoformat(history[1]["timestamp"])
            assert t0 >= t1

    def test_history_limit(self, tmp_path):
        from brynq.health_monitor import run_health_check, get_health_history
        for _ in range(5):
            run_health_check()
        history = get_health_history(limit=2)
        assert len(history) == 2

    def test_trend_unknown_insufficient_data(self, tmp_path):
        from brynq.health_monitor import get_health_trend
        assert get_health_trend() == "unknown"

    def test_trend_stable(self, tmp_path):
        from brynq.health_monitor import run_health_check, get_health_trend
        # All healthy checks should produce stable trend
        for _ in range(5):
            run_health_check()
        trend = get_health_trend()
        assert trend == "stable"

    def test_trend_degrading(self, tmp_path):
        """Simulate degrading trend by writing synthetic scores to checks file."""
        from brynq.health_monitor import get_health_trend, CHECKS_FILE, _ensure_health_dir
        from brynq.server import _append_jsonl_sync
        _ensure_health_dir()
        now = datetime.now(timezone.utc)
        # First half: high scores
        for i in range(5):
            _append_jsonl_sync(CHECKS_FILE, {
                "score": 95,
                "timestamp": (now - timedelta(minutes=10 - i)).isoformat(),
            })
        # Second half: low scores
        for i in range(5):
            _append_jsonl_sync(CHECKS_FILE, {
                "score": 50,
                "timestamp": (now - timedelta(minutes=5 - i)).isoformat(),
            })
        trend = get_health_trend()
        assert trend == "degrading"

    def test_trend_improving(self, tmp_path):
        """Simulate improving trend by writing synthetic scores to checks file."""
        from brynq.health_monitor import get_health_trend, CHECKS_FILE, _ensure_health_dir
        from brynq.server import _append_jsonl_sync
        _ensure_health_dir()
        now = datetime.now(timezone.utc)
        # First half: low scores
        for i in range(5):
            _append_jsonl_sync(CHECKS_FILE, {
                "score": 50,
                "timestamp": (now - timedelta(minutes=10 - i)).isoformat(),
            })
        # Second half: high scores
        for i in range(5):
            _append_jsonl_sync(CHECKS_FILE, {
                "score": 95,
                "timestamp": (now - timedelta(minutes=5 - i)).isoformat(),
            })
        trend = get_health_trend()
        assert trend == "improving"
