"""Tests for BudgetGuard thread-safe budget enforcement."""
import threading
import pytest


def test_budget_guard_basic_pre_check_and_charge():
    """Happy path: reserve, charge, remaining decreases."""
    from recoil.pipeline._lib.budget_manager import BudgetGuard
    guard = BudgetGuard(limit_usd=10.0, label="test")
    assert guard.remaining == 10.0
    assert not guard.would_exceed(5.0)
    guard.charge(5.0, reserved_amount=5.0)
    assert guard.spent == 5.0
    assert guard.remaining == 5.0


def test_budget_guard_would_exceed_blocks():
    """Pre-check returns True when cost would exceed remaining."""
    from recoil.pipeline._lib.budget_manager import BudgetGuard
    guard = BudgetGuard(limit_usd=2.0)
    assert not guard.would_exceed(1.5)
    guard.charge(1.5, reserved_amount=1.5)
    assert guard.would_exceed(1.0)  # 1.5 + 1.0 > 2.0
    assert not guard.would_exceed(0.5)  # 1.5 + 0.5 = 2.0, exact = ok


def test_budget_guard_reservation_prevents_double_spend():
    """Reservation (from would_exceed) prevents concurrent threads from over-spending."""
    from recoil.pipeline._lib.budget_manager import BudgetGuard
    guard = BudgetGuard(limit_usd=3.0)
    # First thread reserves 2.0
    assert not guard.would_exceed(2.0)  # reserves 2.0
    # Second thread tries to reserve 2.0 -- should be blocked
    assert guard.would_exceed(2.0)  # 2.0 reserved + 2.0 = 4.0 > 3.0


def test_budget_guard_release_frees_reservation():
    """Release returns unrealized reservation to the pool."""
    from recoil.pipeline._lib.budget_manager import BudgetGuard
    guard = BudgetGuard(limit_usd=3.0)
    assert not guard.would_exceed(2.0)  # reserves 2.0
    guard.release(2.0)  # API call failed, no charge
    assert guard.remaining == 3.0
    assert not guard.would_exceed(2.5)  # now 2.5 is ok again


def test_budget_guard_charge_with_reserved_amount():
    """charge() properly handles reserved_amount different from actual_cost."""
    from recoil.pipeline._lib.budget_manager import BudgetGuard
    guard = BudgetGuard(limit_usd=10.0)
    # Reserve 2.0 via would_exceed
    assert not guard.would_exceed(2.0)
    assert guard.remaining == 8.0  # 10 - 0 spent - 2 reserved
    # Actual cost was only 1.5
    guard.charge(1.5, reserved_amount=2.0)
    assert guard.spent == 1.5
    assert guard.remaining == 8.5  # 10 - 1.5 spent - 0 reserved


def test_budget_guard_charge_without_reserved_amount():
    """charge() without reserved_amount falls back to actual_cost debit."""
    from recoil.pipeline._lib.budget_manager import BudgetGuard
    guard = BudgetGuard(limit_usd=10.0)
    assert not guard.would_exceed(2.0)
    guard.charge(2.0)  # no reserved_amount -- uses actual_cost
    assert guard.spent == 2.0
    assert guard.remaining == 8.0


def test_budget_guard_at_warn_threshold():
    """Warn threshold fires at 80% of limit."""
    from recoil.pipeline._lib.budget_manager import BudgetGuard
    guard = BudgetGuard(limit_usd=10.0)
    assert not guard.at_warn_threshold
    guard.charge(7.9)
    assert not guard.at_warn_threshold
    guard.charge(0.1)
    assert guard.at_warn_threshold  # 8.0 = 80% of 10.0


def test_budget_guard_concurrent_access():
    """Multiple threads charging simultaneously stay within budget."""
    from recoil.pipeline._lib.budget_manager import BudgetGuard
    guard = BudgetGuard(limit_usd=10.0)
    errors = []

    def worker(amount):
        try:
            if not guard.would_exceed(amount):
                guard.charge(amount, reserved_amount=amount)
            else:
                pass  # budget exceeded, skip
        except Exception as e:
            errors.append(e)

    threads = [threading.Thread(target=worker, args=(1.0,)) for _ in range(15)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

    assert not errors
    assert guard.spent <= 10.0  # Must never exceed limit


def test_budget_guard_per_shot_cap():
    """BudgetGuard.per_shot_cap correctly enforces model-level ceiling."""
    from recoil.pipeline._lib.budget_manager import BudgetGuard
    guard = BudgetGuard(limit_usd=100.0, per_shot_cap_usd=2.0)
    assert not guard.would_exceed_per_shot(1.5)
    assert guard.would_exceed_per_shot(2.5)


def test_budget_exceeded_exception():
    """BudgetExceeded is a proper exception subclass."""
    from recoil.pipeline._lib.budget_manager import BudgetExceeded
    exc = BudgetExceeded("test", spent=5.0, limit=3.0)
    assert "test" in str(exc)
    assert exc.spent == 5.0
    assert exc.limit == 3.0


def test_budget_guard_uses_rlock():
    """BudgetGuard must use RLock (not Lock) to avoid deadlock in charge->at_warn_threshold."""
    from recoil.pipeline._lib.budget_manager import BudgetGuard
    guard = BudgetGuard(limit_usd=10.0)
    assert isinstance(guard._lock, type(threading.RLock()))
