"""Tests for Phase 77: Enhanced Self-Healing V2 (25+ tests)."""

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

import pytest

import brynq.server as srv
import brynq.self_healing_v2 as shv2


@pytest.fixture(autouse=True)
def isolate(tmp_path, monkeypatch):
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(shv2, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(shv2, "HEALING_DIR", tmp_path / "healing")
    monkeypatch.setattr(shv2, "SESSIONS_DIR", tmp_path / "sessions")
    monkeypatch.setattr(shv2, "TASKS_DIR", tmp_path / "tasks")
    monkeypatch.setattr(shv2, "LOCKS_DIR", tmp_path / "locks")
    monkeypatch.setattr(shv2, "CHANNELS_DIR", tmp_path / "channels")
    # Create dirs
    for d in ["healing", "sessions", "tasks", "locks", "channels"]:
        (tmp_path / d).mkdir(parents=True, exist_ok=True)


def _make_session(tmp_path, session_id, heartbeat=None, name="test"):
    data = {"session_id": session_id, "name": name}
    if heartbeat:
        data["heartbeat"] = heartbeat
    (tmp_path / "sessions" / f"{session_id}.json").write_text(
        json.dumps(data), encoding="utf-8"
    )


def _make_lock(tmp_path, lock_name, session_id, filepath="somefile.py"):
    data = {"session_id": session_id, "filepath": filepath}
    (tmp_path / "locks" / f"{lock_name}.lock").write_text(
        json.dumps(data), encoding="utf-8"
    )


def _make_task(tmp_path, task_id, status="pending", accepted_at=None, title="task", assignee="agent"):
    data = {
        "task_id": task_id,
        "status": status,
        "title": title,
        "assignee_name": assignee,
    }
    if accepted_at:
        data["accepted_at"] = accepted_at
    (tmp_path / "tasks" / f"{task_id}.json").write_text(
        json.dumps(data), encoding="utf-8"
    )


def _make_channel(tmp_path, channel_name, mtime_offset_days=0):
    path = tmp_path / "channels" / f"{channel_name}.jsonl"
    path.write_text('{"msg": "hello"}\n', encoding="utf-8")
    if mtime_offset_days:
        old_time = time.time() - (mtime_offset_days * 86400)
        os.utime(str(path), (old_time, old_time))


# ── run_diagnostics ──────────────────────────────────────────────────────


class TestRunDiagnostics:
    def test_healthy_system(self, tmp_path):
        report = shv2.run_diagnostics()
        assert report["healthy"] is True
        assert report["total_issues"] == 0

    def test_detects_orphan_lock(self, tmp_path):
        _make_lock(tmp_path, "file1", "dead-session")
        report = shv2.run_diagnostics()
        assert len(report["orphan_locks"]) == 1

    def test_detects_stale_session(self, tmp_path):
        old_hb = (datetime.now(timezone.utc) - timedelta(hours=5)).isoformat()
        _make_session(tmp_path, "stale-1", heartbeat=old_hb)
        report = shv2.run_diagnostics()
        assert len(report["stale_sessions"]) == 1

    def test_detects_stuck_task(self, tmp_path):
        old_time = (datetime.now(timezone.utc) - timedelta(hours=3)).isoformat()
        _make_task(tmp_path, "stuck-1", status="accepted", accepted_at=old_time)
        report = shv2.run_diagnostics()
        assert len(report["stuck_tasks"]) == 1

    def test_detects_dead_channel(self, tmp_path):
        _make_channel(tmp_path, "old-chat", mtime_offset_days=10)
        report = shv2.run_diagnostics()
        assert len(report["dead_channels"]) == 1

    def test_healthy_session_not_flagged(self, tmp_path):
        fresh_hb = datetime.now(timezone.utc).isoformat()
        _make_session(tmp_path, "active-1", heartbeat=fresh_hb)
        report = shv2.run_diagnostics()
        assert len(report["stale_sessions"]) == 0

    def test_pending_task_not_flagged(self, tmp_path):
        _make_task(tmp_path, "ok-1", status="pending")
        report = shv2.run_diagnostics()
        assert len(report["stuck_tasks"]) == 0

    def test_active_lock_not_flagged(self, tmp_path):
        _make_session(tmp_path, "alive")
        _make_lock(tmp_path, "valid", "alive")
        report = shv2.run_diagnostics()
        assert len(report["orphan_locks"]) == 0

    def test_fresh_channel_not_flagged(self, tmp_path):
        _make_channel(tmp_path, "busy-chat", mtime_offset_days=0)
        report = shv2.run_diagnostics()
        assert len(report["dead_channels"]) == 0


# ── heal_orphan_locks ────────────────────────────────────────────────────


class TestHealOrphanLocks:
    def test_removes_orphan(self, tmp_path):
        _make_lock(tmp_path, "orphan", "gone-session")
        healed = shv2.heal_orphan_locks()
        assert len(healed) == 1
        assert not (tmp_path / "locks" / "orphan.lock").exists()

    def test_skips_valid_lock(self, tmp_path):
        _make_session(tmp_path, "alive")
        _make_lock(tmp_path, "valid", "alive")
        healed = shv2.heal_orphan_locks()
        assert len(healed) == 0
        assert (tmp_path / "locks" / "valid.lock").exists()

    def test_logs_action(self, tmp_path):
        _make_lock(tmp_path, "orphan", "gone")
        shv2.heal_orphan_locks()
        history = shv2.get_healing_history()
        assert len(history) == 1
        assert history[0]["action"] == "removed_orphan_lock"


# ── heal_stale_sessions ─────────────────────────────────────────────────


class TestHealStaleSessions:
    def test_removes_stale(self, tmp_path):
        old_hb = (datetime.now(timezone.utc) - timedelta(hours=5)).isoformat()
        _make_session(tmp_path, "stale-1", heartbeat=old_hb)
        healed = shv2.heal_stale_sessions()
        assert len(healed) == 1
        assert not (tmp_path / "sessions" / "stale-1.json").exists()

    def test_keeps_fresh(self, tmp_path):
        fresh_hb = datetime.now(timezone.utc).isoformat()
        _make_session(tmp_path, "fresh-1", heartbeat=fresh_hb)
        healed = shv2.heal_stale_sessions()
        assert len(healed) == 0
        assert (tmp_path / "sessions" / "fresh-1.json").exists()

    def test_custom_max_age(self, tmp_path):
        # Session 30 min old, threshold 0.25 hours (15 min) -> should be removed
        hb = (datetime.now(timezone.utc) - timedelta(minutes=30)).isoformat()
        _make_session(tmp_path, "mid", heartbeat=hb)
        healed = shv2.heal_stale_sessions(max_age_hours=0.25)
        assert len(healed) == 1


# ── heal_stuck_tasks ─────────────────────────────────────────────────────


class TestHealStuckTasks:
    def test_requeues_stuck(self, tmp_path):
        old_time = (datetime.now(timezone.utc) - timedelta(hours=2)).isoformat()
        _make_task(tmp_path, "stuck-1", status="accepted", accepted_at=old_time)
        healed = shv2.heal_stuck_tasks()
        assert len(healed) == 1
        # Verify task was requeued
        data = json.loads((tmp_path / "tasks" / "stuck-1.json").read_text("utf-8"))
        assert data["status"] == "pending"
        assert data["assignee_name"] is None

    def test_ignores_recent_accepted(self, tmp_path):
        recent = datetime.now(timezone.utc).isoformat()
        _make_task(tmp_path, "fresh", status="accepted", accepted_at=recent)
        healed = shv2.heal_stuck_tasks()
        assert len(healed) == 0

    def test_custom_max_age(self, tmp_path):
        t = (datetime.now(timezone.utc) - timedelta(minutes=10)).isoformat()
        _make_task(tmp_path, "short-stuck", status="accepted", accepted_at=t)
        healed = shv2.heal_stuck_tasks(max_age_minutes=5)
        assert len(healed) == 1


# ── heal_dead_channels ──────────────────────────────────────────────────


class TestHealDeadChannels:
    def test_archives_dead(self, tmp_path):
        _make_channel(tmp_path, "dead-chan", mtime_offset_days=10)
        healed = shv2.heal_dead_channels()
        assert len(healed) == 1
        assert not (tmp_path / "channels" / "dead-chan.jsonl").exists()
        assert (tmp_path / "channels" / "_archive" / "dead-chan.jsonl").exists()

    def test_keeps_active(self, tmp_path):
        _make_channel(tmp_path, "live-chan", mtime_offset_days=0)
        healed = shv2.heal_dead_channels()
        assert len(healed) == 0

    def test_custom_threshold(self, tmp_path):
        _make_channel(tmp_path, "semi-old", mtime_offset_days=3)
        healed = shv2.heal_dead_channels(max_inactive_days=2)
        assert len(healed) == 1


# ── auto_heal ────────────────────────────────────────────────────────────


class TestAutoHeal:
    def test_heals_everything(self, tmp_path):
        # Create multiple issues
        _make_lock(tmp_path, "orphan", "gone")
        old_hb = (datetime.now(timezone.utc) - timedelta(hours=5)).isoformat()
        _make_session(tmp_path, "stale", heartbeat=old_hb)
        result = shv2.auto_heal()
        assert result["fixes_applied"] >= 2

    def test_no_issues(self, tmp_path):
        result = shv2.auto_heal()
        assert result["issues_detected"] == 0
        assert result["fixes_applied"] == 0

    def test_with_explicit_issues(self, tmp_path):
        _make_lock(tmp_path, "orphan", "gone")
        issues = [{"type": "orphan_lock", "lock_file": str(tmp_path / "locks" / "orphan.lock"), "session_id": "gone"}]
        result = shv2.auto_heal(issues=issues)
        assert result["fixes_applied"] >= 1

    def test_creates_missing_index(self, tmp_path):
        target = tmp_path / "new_index"
        issues = [{"type": "missing_index", "directory": str(target), "name": "new_index"}]
        result = shv2.auto_heal(issues=issues)
        assert target.exists()
        assert result["fixes_applied"] == 1


# ── get_healing_history ──────────────────────────────────────────────────


class TestGetHealingHistory:
    def test_empty_history(self, tmp_path):
        assert shv2.get_healing_history() == []

    def test_records_actions(self, tmp_path):
        _make_lock(tmp_path, "orphan", "gone")
        shv2.heal_orphan_locks()
        history = shv2.get_healing_history()
        assert len(history) == 1

    def test_limit_parameter(self, tmp_path):
        for i in range(5):
            _make_lock(tmp_path, f"orphan{i}", f"gone{i}")
        shv2.heal_orphan_locks()
        assert len(shv2.get_healing_history(limit=2)) == 2


# ── schedule_healing / cancel_healing ────────────────────────────────────


class TestScheduleHealing:
    def test_schedule_returns_confirmation(self, tmp_path):
        result = shv2.schedule_healing(interval_minutes=999)
        assert result["scheduled"] is True
        assert result["interval_minutes"] == 999
        shv2.cancel_healing()

    def test_cancel(self, tmp_path):
        shv2.schedule_healing(interval_minutes=999)
        result = shv2.cancel_healing()
        assert result["cancelled"] is True

    def test_cancel_when_none(self, tmp_path):
        shv2.cancel_healing()  # ensure clean
        result = shv2.cancel_healing()
        assert result["cancelled"] is False
