"""Tests for #222 — agent health taxonomy (pure classifier + Paperclip fetch).

Pure-classifier tests need no deps. The fetch test fakes an asyncpg connection
(routing the three queries by content) so it never touches the live DB.
"""

from __future__ import annotations

import asyncio
import os
import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))  # web/

from agent_health import (  # noqa: E402
    STALL_WAKEUP_THRESHOLD,
    classify_issue_health,
    is_recovery_reason,
    zombie_escalation,
)
from agent_health import ZOMBIE_ESCALATE_MIN_RECOVERY  # noqa: E402


# ── pure classifier ────────────────────────────────────────────────────────
def test_live_run_is_working_regardless_of_wakeups():
    assert classify_issue_health(has_live_run=True, recovery_wakeups=9, total_wakeups=9) == "working"


def test_recovery_wakeup_without_live_run_is_zombie():
    # the stranded-child signature: assigned+open, nothing running, recovery firing
    assert classify_issue_health(has_live_run=False, recovery_wakeups=1, total_wakeups=1) == "zombie"


def test_repeated_wakeups_no_recovery_is_stalled():
    assert (
        classify_issue_health(
            has_live_run=False, recovery_wakeups=0, total_wakeups=STALL_WAKEUP_THRESHOLD
        )
        == "stalled"
    )


def test_quiet_open_issue_is_idle():
    assert classify_issue_health(has_live_run=False, recovery_wakeups=0, total_wakeups=1) == "idle"


# ── watchdog escalation policy (pure) ───────────────────────────────────────
def _item(health, recovery):
    return {"health": health, "recovery_wakeups": recovery, "identifier": "CMP-141", "agent_name": "writer"}


def test_zombie_escalation_medium_for_fresh_loop():
    decision = zombie_escalation(_item("zombie", ZOMBIE_ESCALATE_MIN_RECOVERY))
    assert decision is not None
    severity, reason = decision
    assert severity == "medium"
    assert "CMP-141" in reason and "recovery" in reason.lower()


def test_zombie_escalation_high_for_long_loop():
    severity, _ = zombie_escalation(_item("zombie", 5))
    assert severity == "high"


def test_zombie_below_threshold_is_not_escalated():
    assert zombie_escalation(_item("zombie", ZOMBIE_ESCALATE_MIN_RECOVERY - 1)) is None


def test_non_zombie_never_escalates():
    assert zombie_escalation(_item("stalled", 9)) is None
    assert zombie_escalation(_item("idle", 0)) is None
    assert zombie_escalation(_item("working", 9)) is None


def test_is_recovery_reason_markers():
    assert is_recovery_reason("source_scoped_recovery_action")
    assert is_recovery_reason("issue_continuation_needed")
    assert is_recovery_reason("issue_reopened_via_comment")
    assert not is_recovery_reason("start_workflow_8125-09-24")
    assert not is_recovery_reason(None)


# ── Paperclip fetch (fake DB) ───────────────────────────────────────────────
os.environ.setdefault("PAPERCLIP_DB_URL", "postgres://x:x@127.0.0.1:54329/paperclip")
pc = pytest.importorskip("paperclip_client", reason="web deps unavailable")


class _FakeConn:
    def __init__(self, issues, live, wakeups):
        self._issues, self._live, self._wakeups = issues, live, wakeups

    async def fetch(self, query, *args):
        if "FROM issues" in query:
            return self._issues
        if "heartbeat_runs" in query:
            return self._live
        if "agent_wakeup_requests" in query:
            return self._wakeups
        raise AssertionError(f"unexpected query: {query[:40]}")

    async def close(self):
        pass


def test_get_agent_health_classifies_and_sorts(monkeypatch):
    # zombie agent A (recovery wake, no live run); working agent B (live run);
    # idle agent C (quiet).
    issues = [
        {"id": "iA", "identifier": "CMP-1", "status": "blocked", "assignee_agent_id": "agA", "agent_name": "writer"},
        {"id": "iB", "identifier": "CMP-2", "status": "in_progress", "assignee_agent_id": "agB", "agent_name": "analyst"},
        {"id": "iC", "identifier": "CMP-3", "status": "todo", "assignee_agent_id": "agC", "agent_name": "qa"},
    ]
    live = [{"agent_id": "agB"}]
    wakeups = [
        {"agent_id": "agA", "issue_id": "iA", "reason": "source_scoped_recovery_action"},
        {"agent_id": "agC", "issue_id": "iC", "reason": "start_workflow_x"},
    ]

    async def _connect(_url):
        return _FakeConn(issues, live, wakeups)

    monkeypatch.setattr(pc.asyncpg, "connect", _connect)

    result = asyncio.run(pc.get_agent_health())
    assert result["ok"] is True
    by_issue = {it["issue_id"]: it["health"] for it in result["items"]}
    assert by_issue == {"iA": "zombie", "iB": "working", "iC": "idle"}
    assert result["counts"]["zombie"] == 1
    assert result["counts"]["working"] == 1
    assert result["counts"]["idle"] == 1
    # worst-first: zombie leads
    assert result["items"][0]["health"] == "zombie"
    assert result["items"][0]["recovery_wakeups"] == 1


def test_get_agent_health_empty(monkeypatch):
    async def _connect(_url):
        return _FakeConn([], [], [])

    monkeypatch.setattr(pc.asyncpg, "connect", _connect)
    result = asyncio.run(pc.get_agent_health())
    assert result == {"ok": True, "items": [], "counts": {"zombie": 0, "stalled": 0, "working": 0, "idle": 0}}


# ── recent escalations (fake DB) — parses severity + reason from system notes ──
class _FetchConn:
    def __init__(self, rows):
        self._rows = rows

    async def fetch(self, query, *args):
        return self._rows

    async def close(self):
        pass


def test_get_recent_escalations_parses_severity_and_reason(monkeypatch):
    from datetime import datetime

    rows = [
        {
            "issue_id": "iss-1",
            "identifier": "CMP-141",
            "body": "🚨 הסלמה [high] לחיים\n\nזוהתה לולאת-recovery — 4 יקיצות-שחזור; נמסר לחיים.",
            "created_at": datetime(2026, 7, 7, 3, 30, 0),
        },
        {
            "issue_id": "iss-2",
            "identifier": "CMPA-112",
            "body": "🚨 הסלמה [medium] לחיים\n\nזוהתה לולאת-recovery — 2 יקיצות.",
            "created_at": datetime(2026, 7, 7, 2, 49, 0),
        },
    ]

    async def _connect(_url):
        return _FetchConn(rows)

    monkeypatch.setattr(pc.asyncpg, "connect", _connect)
    result = asyncio.run(pc.get_recent_escalations())
    assert result["ok"] is True
    a, b = result["items"]
    assert a["identifier"] == "CMP-141" and a["severity"] == "high"
    assert "4 יקיצות" in a["reason"] and "🚨" not in a["reason"]
    assert a["created_at"] == "2026-07-07T03:30:00"
    assert b["severity"] == "medium"


def test_get_recent_escalations_defaults_severity_when_untagged(monkeypatch):
    rows = [{"issue_id": "x", "identifier": "CMP-9", "body": "🚨 הסלמה לחיים", "created_at": None}]

    async def _connect(_url):
        return _FetchConn(rows)

    monkeypatch.setattr(pc.asyncpg, "connect", _connect)
    (item,) = asyncio.run(pc.get_recent_escalations())["items"]
    assert item["severity"] == "medium"  # fallback
    assert item["created_at"] is None


# ── predecessor by case (#220 agent-facing) — resolve project then shape runs ──
class _ProjectRunsConn:
    def __init__(self, project, runs):
        self._project, self._runs = project, runs

    async def fetchrow(self, query, *args):
        return self._project

    async def fetch(self, query, *args):
        return self._runs

    async def close(self):
        pass


def test_get_predecessor_for_case_shapes_runs(monkeypatch):
    from datetime import datetime

    runs = [
        {
            "id": "run-1", "status": "succeeded",
            "started_at": datetime(2026, 7, 5, 10, 0, 0),
            "finished_at": datetime(2026, 7, 5, 10, 5, 0),
            "summary": "הלולאה נסגרה כראוי", "error_code": None,
            "session_id_after": "sess-9", "agent_name": "עוזר משפטי",
            "identifier": "CMP-141",
        },
    ]

    async def _connect(_url):
        return _ProjectRunsConn({"id": "proj-1"}, runs)

    monkeypatch.setattr(pc.asyncpg, "connect", _connect)
    result = asyncio.run(pc.get_predecessor_for_case("1043-02-26"))
    assert result["ok"] is True and result["case_number"] == "1043-02-26"
    (run,) = result["runs"]
    assert run["run_id"] == "run-1"
    assert run["identifier"] == "CMP-141"  # by-case adds which issue
    assert run["started_at"] == "2026-07-05T10:00:00"
    assert "הלולאה" in run["summary"]


def test_get_predecessor_for_case_unknown_case_is_empty(monkeypatch):
    async def _connect(_url):
        return _ProjectRunsConn(None, [])  # no project row

    monkeypatch.setattr(pc.asyncpg, "connect", _connect)
    result = asyncio.run(pc.get_predecessor_for_case("9999-99-99"))
    assert result == {"ok": True, "case_number": "9999-99-99", "runs": []}
