from __future__ import annotations

import json
from pathlib import Path

from recoil.pipeline.tools.autonomy import resource_gate


def _isolate(monkeypatch, tmp_path: Path) -> Path:
    state_dir = tmp_path / "state"
    monkeypatch.setattr(resource_gate, "STATE_DIR", state_dir)
    monkeypatch.setattr(resource_gate, "BUILDS_DIR", state_dir / "builds")
    monkeypatch.setattr(resource_gate, "BREAKER_DIR", state_dir / "breakers")
    monkeypatch.setattr(resource_gate, "USAGE_LOG", state_dir / "usage.jsonl")
    return state_dir


def test_may_start_build_true_under_cap(monkeypatch, tmp_path):
    _isolate(monkeypatch, tmp_path)

    ok, reason = resource_gate.may_start_build("2026-06-06-evening")

    assert ok is True
    assert reason == ""


def test_record_build_started_and_builds_cap(monkeypatch, tmp_path):
    _isolate(monkeypatch, tmp_path)
    night_id = "2026-06-06-evening"

    assert resource_gate.record_build_started(night_id) == 1
    assert resource_gate.builds_tonight(night_id) == 1
    assert resource_gate.record_build_started(night_id) == 2

    ok, reason = resource_gate.may_start_build(night_id)
    assert ok is False
    assert reason == "builds_cap"


def test_trip_breaker_sets_sentinel_and_blocks_night(monkeypatch, tmp_path):
    emitted = []
    _isolate(monkeypatch, tmp_path)
    monkeypatch.setattr(resource_gate.events, "emit", lambda event_type, **fields: emitted.append((event_type, fields)))
    night_id = "2026-06-06-evening"

    resource_gate.trip_breaker(night_id, "HTTP 429 rate limit exceeded")

    assert resource_gate.breaker_tripped(night_id) is True
    ok, reason = resource_gate.may_start_build(night_id)
    assert ok is False
    assert reason == "night_breaker"
    assert emitted == [
        (
            "rate_limited",
            {"night_id": night_id, "reason": "HTTP 429 rate limit exceeded"},
        )
    ]


def test_trip_breaker_emits_cap_for_non_rate_limit_reason(monkeypatch, tmp_path):
    emitted = []
    _isolate(monkeypatch, tmp_path)
    monkeypatch.setattr(resource_gate.events, "emit", lambda event_type, **fields: emitted.append((event_type, fields)))

    resource_gate.trip_breaker("2026-06-06-evening", "builds_cap")

    assert emitted[0][0] == "cap_tripped"


def test_is_rate_limit_error_matches_true_signals_and_rejects_normal_output():
    assert resource_gate.is_rate_limit_error("Error: HTTP 429 Too Many Requests")
    assert resource_gate.is_rate_limit_error("Claude usage limit reached for this account")
    assert resource_gate.is_rate_limit_error("codex failed: resource exhausted")
    assert resource_gate.is_rate_limit_error(429)
    assert not resource_gate.is_rate_limit_error("build completed successfully")
    assert not resource_gate.is_rate_limit_error(1)
    assert not resource_gate.is_rate_limit_error(None)


def test_log_usage_never_raises_and_returns_no_gating_value(monkeypatch, tmp_path):
    state_dir = _isolate(monkeypatch, tmp_path)

    result = resource_gate.log_usage(
        "run-1",
        "phase-4",
        {"input_tokens": 1000, "output_tokens": 200, "not_json": object()},
    )

    assert result is None
    rows = [
        json.loads(line)
        for line in (state_dir / "usage.jsonl").read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]
    assert len(rows) == 1
    assert rows[0]["schema"] == "autonomy.usage.v1"
    assert rows[0]["run_id"] == "run-1"
    assert rows[0]["phase"] == "phase-4"
    assert rows[0]["usage"]["input_tokens"] == 1000
    assert "usd_equivalent_estimate" in rows[0]

    monkeypatch.setattr(resource_gate, "USAGE_LOG", tmp_path / "missing" / "usage.jsonl")
    monkeypatch.setattr(resource_gate, "STATE_DIR", tmp_path / "state-file")
    (tmp_path / "state-file").write_text("not a directory", encoding="utf-8")
    assert resource_gate.log_usage("run-2", "phase-4", {}) is None


def test_wall_clock_expired(monkeypatch):
    moments = iter([100.0, 101.0, 106.0])
    monkeypatch.setattr(resource_gate.time, "monotonic", lambda: next(moments))

    clock = resource_gate.WallClock(5)

    assert clock.expired() is False
    assert clock.expired() is True
