from __future__ import annotations

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

import pytest

from recoil.pipeline.tools.autonomy import claim_ledger, reaper


def _isolate(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
    ledger = tmp_path / "state" / "claim-ledger.jsonl"
    monkeypatch.setattr(claim_ledger, "CLAIM_LEDGER", ledger)
    monkeypatch.setattr(reaper, "SESSION_WORKSPACE", tmp_path / "session_workspace.sh")
    monkeypatch.setattr(reaper, "DEFAULT_WORKTREE_TTL_HOURS", 0)
    return ledger


def _read_jsonl(path: Path) -> list[dict]:
    return [
        json.loads(line)
        for line in path.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]


def _iso(delta: timedelta) -> str:
    return (
        (datetime.now(timezone.utc).replace(microsecond=0) + delta)
        .isoformat()
        .replace("+00:00", "Z")
    )


def _stub_boundaries(monkeypatch: pytest.MonkeyPatch):
    calls: dict[str, list] = {"run": [], "events": [], "blocked": []}

    def fake_run(cmd, **kwargs):
        calls["run"].append((cmd, kwargs))
        return subprocess.CompletedProcess(cmd, 0, stdout="reap: ok\n", stderr="")

    monkeypatch.setattr(reaper.subprocess, "run", fake_run)
    monkeypatch.setattr(
        reaper.events,
        "emit",
        lambda event_type, **fields: calls["events"].append((event_type, fields)),
    )
    monkeypatch.setattr(
        reaper.linear_client,
        "mark_blocked",
        lambda issue_id: calls["blocked"].append(issue_id) or True,
    )
    return calls


def test_stranded_active_claim_is_failed_and_workspace_reap_called(monkeypatch, tmp_path):
    ledger = _isolate(monkeypatch, tmp_path)
    calls = _stub_boundaries(monkeypatch)
    monkeypatch.setattr(reaper.lease, "read", lambda: None)

    assert claim_ledger.claim("issue-1", "REC-1", "run-1", "night")

    assert reaper.reap() == ["run-1"]

    rows = _read_jsonl(ledger)
    assert [row["state"] for row in rows] == ["active", "failed"]
    assert rows[-1]["failure_signature"] == "reaper:stranded"
    assert calls["run"][0][0] == [
        str(tmp_path / "session_workspace.sh"),
        "reap",
        "--ttl-hours",
        "0",
    ]
    assert any(event == "build_killed" for event, _fields in calls["events"])
    assert any(event == "maintenance_ran" for event, _fields in calls["events"])


def test_live_claim_with_fresh_matching_lease_is_untouched(monkeypatch, tmp_path):
    ledger = _isolate(monkeypatch, tmp_path)
    calls = _stub_boundaries(monkeypatch)
    monkeypatch.setattr(
        reaper.lease,
        "read",
        lambda: {
            "run_id": "run-live",
            "expires_at": _iso(timedelta(minutes=5)),
            "pid": os.getpid(),
        },
    )

    active = claim_ledger.claim("issue-1", "REC-1", "run-live", "night")

    assert reaper.reap() == []
    assert _read_jsonl(ledger) == [active]
    assert calls["run"] == []
    assert calls["events"] == []
    assert calls["blocked"] == []


def test_expired_matching_lease_with_live_pid_is_untouched(monkeypatch, tmp_path):
    ledger = _isolate(monkeypatch, tmp_path)
    calls = _stub_boundaries(monkeypatch)
    monkeypatch.setattr(
        reaper.lease,
        "read",
        lambda: {
            "run_id": "run-live",
            "expires_at": _iso(timedelta(minutes=-5)),
            "pid": os.getpid(),
        },
    )

    active = claim_ledger.claim("issue-1", "REC-1", "run-live", "night")

    assert reaper.reap() == []
    assert _read_jsonl(ledger) == [active]
    assert calls["run"] == []


def test_k_consecutive_same_signature_failures_marks_blocked(monkeypatch, tmp_path):
    _isolate(monkeypatch, tmp_path)
    calls = _stub_boundaries(monkeypatch)
    monkeypatch.setattr(reaper.lease, "read", lambda: None)
    monkeypatch.setattr(reaper, "DEFAULT_BLOCK_AFTER_FAILURES", 3)

    assert claim_ledger.claim("issue-1", "REC-1", "run-1", "night")
    assert claim_ledger.release("issue-1", "run-1", "failed", "reaper:stranded")
    assert claim_ledger.claim("issue-1", "REC-1", "run-2", "night")
    assert claim_ledger.release("issue-1", "run-2", "failed", "reaper:stranded")
    assert claim_ledger.claim("issue-1", "REC-1", "run-3", "night")

    assert reaper.reap() == ["run-3"]

    assert calls["blocked"] == ["issue-1"]
    assert any(
        event == "cap_tripped" and fields["reason"] == "consecutive_failures"
        for event, fields in calls["events"]
    )


def test_dry_run_reports_reap_without_mutation(monkeypatch, tmp_path):
    ledger = _isolate(monkeypatch, tmp_path)
    calls = _stub_boundaries(monkeypatch)
    monkeypatch.setattr(reaper.lease, "read", lambda: None)

    active = claim_ledger.claim("issue-1", "REC-1", "run-1", "night")

    assert reaper.reap(dry_run=True) == ["run-1"]
    assert _read_jsonl(ledger) == [active]
    assert calls["run"][0][0] == [
        str(tmp_path / "session_workspace.sh"),
        "reap",
        "--ttl-hours",
        "0",
        "--dry-run",
    ]
    assert calls["events"] == []
    assert calls["blocked"] == []
