"""Tests for cross-process rate limit coordinator (D-11, D-13)."""

from __future__ import annotations

import json
import time

import pytest
from opendeviationbar.ratelimit import (
    BUDGET_KINTSUGI,
    BUDGET_RESERVE,
    BUDGET_SEEDER,
    BUDGET_SIDECAR,
    RateLimitCoordinator,
)


@pytest.fixture
def coordinator(tmp_path):
    """Create a coordinator using an isolated state file."""
    state_file = tmp_path / "ratelimit.json"
    return RateLimitCoordinator(state_path=state_file)


# ── Test 1: report_usage creates state file ──────────────────────────


def test_report_usage_creates_state_file(coordinator, tmp_path):
    state_file = tmp_path / "ratelimit.json"
    assert not state_file.exists()
    coordinator.report_usage("sidecar", 100)
    assert state_file.exists()
    state = json.loads(state_file.read_text())
    assert state["consumers"]["sidecar"]["weight_used"] == 100


# ── Test 2: get_aggregate_usage sums all consumers ──────────────────


def test_get_aggregate_usage_sums_consumers(coordinator):
    coordinator.report_usage("sidecar", 500)
    coordinator.report_usage("kintsugi", 300)
    coordinator.report_usage("seeder", 200)
    assert coordinator.get_aggregate_usage() == 1000


# ── Test 3: stale entries excluded from aggregate ────────────────────


def test_stale_entries_excluded(coordinator, monkeypatch):
    now = time.time()
    # Report at t=0
    monkeypatch.setattr(time, "time", lambda: now)
    coordinator.report_usage("sidecar", 500)
    coordinator.report_usage("kintsugi", 300)
    # Advance 66 seconds (past WINDOW_SECONDS + STALE_GRACE_SECONDS = 65)
    monkeypatch.setattr(time, "time", lambda: now + 66)
    # Report kintsugi fresh, sidecar is stale
    coordinator.report_usage("kintsugi", 200)
    assert coordinator.get_aggregate_usage() == 200


# ── Test 4: window expiry resets all counts ──────────────────────────


def test_window_expiry_resets_counts(coordinator, monkeypatch):
    now = time.time()
    monkeypatch.setattr(time, "time", lambda: now)
    coordinator.report_usage("sidecar", 1000)
    # Advance past window (61 seconds)
    monkeypatch.setattr(time, "time", lambda: now + 61)
    assert coordinator.get_aggregate_usage() == 0


# ── Test 5: corrupt/missing state file returns 0 ────────────────────


def test_corrupt_state_file_returns_zero(coordinator, tmp_path):
    state_file = tmp_path / "ratelimit.json"
    state_file.write_text("NOT VALID JSON {{{{")
    assert coordinator.get_aggregate_usage() == 0


def test_missing_state_file_returns_zero(coordinator):
    assert coordinator.get_aggregate_usage() == 0


# ── Test 6: can_acquire returns False when over budget ───────────────


def test_can_acquire_false_when_over_budget(coordinator):
    coordinator.report_usage("sidecar", 2000)
    coordinator.report_usage("kintsugi", 1500)
    # 2000 + 1500 + 600 = 4100 > 4000 budget for this hypothetical consumer
    assert coordinator.can_acquire("seeder", 600, budget=4000) is False


# ── Test 7: can_acquire returns True when within budget ──────────────


def test_can_acquire_true_when_within_budget(coordinator):
    coordinator.report_usage("sidecar", 1000)
    assert coordinator.can_acquire("kintsugi", 500, budget=2000) is True


# ── Test 8: budget constants match D-13 ──────────────────────────────


def test_budget_constants():
    assert BUDGET_SIDECAR == 2400
    assert BUDGET_KINTSUGI == 2000
    assert BUDGET_SEEDER == 800
    assert BUDGET_RESERVE == 800
    assert BUDGET_SIDECAR + BUDGET_KINTSUGI + BUDGET_SEEDER + BUDGET_RESERVE == 6000


# ── Test 9: atomic rename does not produce partial reads ─────────────


def test_atomic_write_no_partial_reads(coordinator, tmp_path):
    """Verify state file is always valid JSON (atomic rename pattern)."""
    state_file = tmp_path / "ratelimit.json"
    # Write multiple times rapidly
    for i in range(50):
        coordinator.report_usage("sidecar", i * 10)
    # File should always be valid JSON
    state = json.loads(state_file.read_text())
    assert "consumers" in state
    assert "window_start" in state
    # Last write should have the final value
    assert state["consumers"]["sidecar"]["weight_used"] == 490
