"""Tests for Phase 82: Distributed Rate Limiting."""

import json
import time
import pytest
from pathlib import Path
from unittest.mock import patch

import brynq.distributed_rate_limit as rl


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect storage to tmp_path."""
    monkeypatch.setattr(rl, "LIMITS_DIR", tmp_path)
    monkeypatch.setattr(rl, "CONSUMPTION_LOG", tmp_path / "consumption.jsonl")
    rl.reset()


# ── create_bucket ──────────────────────────────────────────────────────────

class TestCreateBucket:
    def test_create_basic(self):
        b = rl.create_bucket("api", 100, 10.0)
        assert b["name"] == "api"
        assert b["capacity"] == 100
        assert b["refill_rate_per_sec"] == 10.0
        assert b["tokens"] == 100  # starts full
        assert b["scope"] == "global"

    def test_create_with_scope(self):
        b = rl.create_bucket("local", 50, 5.0, scope="node")
        assert b["scope"] == "node"

    def test_create_zero_capacity_raises(self):
        with pytest.raises(ValueError):
            rl.create_bucket("bad", 0, 1.0)

    def test_create_negative_rate_raises(self):
        with pytest.raises(ValueError):
            rl.create_bucket("bad", 10, -1.0)

    def test_create_persists(self, tmp_path):
        rl.create_bucket("persist", 10, 1.0)
        assert (tmp_path / "persist.json").exists()

    def test_create_overwrites(self):
        rl.create_bucket("x", 10, 1.0)
        rl.create_bucket("x", 20, 2.0)
        b = rl.get_bucket("x")
        assert b["capacity"] == 20


# ── consume ────────────────────────────────────────────────────────────────

class TestConsume:
    def test_consume_allowed(self):
        rl.create_bucket("api", 10, 1.0)
        allowed, remaining, retry = rl.consume("api", 1)
        assert allowed is True
        assert remaining == 9
        assert retry is None

    def test_consume_multiple(self):
        rl.create_bucket("api", 10, 1.0)
        rl.consume("api", 3)
        allowed, remaining, _ = rl.consume("api", 3)
        assert allowed is True
        assert 3.9 <= remaining <= 4.1  # allow tiny refill drift

    def test_consume_exact_capacity(self):
        rl.create_bucket("api", 5, 1.0)
        allowed, remaining, _ = rl.consume("api", 5)
        assert allowed is True
        assert remaining == 0

    def test_consume_rejected(self):
        rl.create_bucket("api", 5, 0.0)  # zero refill so no drift
        rl.consume("api", 5)
        allowed, remaining, retry = rl.consume("api", 1)
        assert allowed is False
        assert remaining == 0
        assert retry is not None
        assert retry == float("inf")  # zero rate means infinite retry

    def test_consume_missing_bucket(self):
        with pytest.raises(KeyError):
            rl.consume("nonexistent")

    def test_consume_with_node_id(self, tmp_path):
        rl.create_bucket("api", 10, 1.0)
        rl.consume("api", 1, node_id="node-a")
        log = (tmp_path / "consumption.jsonl").read_text("utf-8").strip().splitlines()
        entry = json.loads(log[0])
        assert entry["node_id"] == "node-a"

    def test_consume_logs_events(self, tmp_path):
        rl.create_bucket("api", 10, 1.0)
        rl.consume("api", 1)
        rl.consume("api", 1)
        log = (tmp_path / "consumption.jsonl").read_text("utf-8").strip().splitlines()
        assert len(log) == 2

    def test_consume_retry_after_zero_rate(self):
        rl.create_bucket("fixed", 2, 0.0)
        rl.consume("fixed", 2)
        allowed, _, retry = rl.consume("fixed", 1)
        assert allowed is False
        assert retry == float("inf")


# ── get_bucket / list_buckets / delete_bucket ──────────────────────────────

class TestBucketOps:
    def test_get_existing(self):
        rl.create_bucket("x", 10, 1.0)
        b = rl.get_bucket("x")
        assert b["name"] == "x"

    def test_get_missing(self):
        assert rl.get_bucket("ghost") is None

    def test_list_all(self):
        rl.create_bucket("a", 10, 1.0)
        rl.create_bucket("b", 20, 2.0)
        buckets = rl.list_buckets()
        names = [b["name"] for b in buckets]
        assert "a" in names
        assert "b" in names

    def test_list_empty(self):
        assert rl.list_buckets() == []

    def test_delete_existing(self):
        rl.create_bucket("rm", 10, 1.0)
        assert rl.delete_bucket("rm") is True
        assert rl.get_bucket("rm") is None

    def test_delete_missing(self):
        assert rl.delete_bucket("nope") is False


# ── sync_bucket ────────────────────────────────────────────────────────────

class TestSyncBucket:
    def test_sync_new_bucket(self):
        remote = {
            "name": "new",
            "capacity": 10,
            "tokens": 7,
            "refill_rate_per_sec": 1.0,
            "last_refill": time.time(),
            "total_consumed": 3,
            "total_rejected": 0,
        }
        merged = rl.sync_bucket("new", remote)
        assert merged["tokens"] == 7

    def test_sync_takes_minimum_tokens(self):
        rl.create_bucket("x", 100, 1.0)
        # Consume locally
        rl.consume("x", 30)
        remote = {
            "name": "x",
            "capacity": 100,
            "tokens": 50,
            "refill_rate_per_sec": 1.0,
            "last_refill": time.time(),
            "total_consumed": 50,
            "total_rejected": 2,
        }
        merged = rl.sync_bucket("x", remote)
        assert merged["tokens"] <= 70  # local had ~70
        assert merged["total_consumed"] == 50  # max of both
        assert "last_sync" in merged

    def test_sync_merges_counters(self):
        rl.create_bucket("x", 100, 1.0)
        remote = {
            "name": "x",
            "capacity": 100,
            "tokens": 90,
            "refill_rate_per_sec": 1.0,
            "last_refill": time.time(),
            "total_consumed": 200,
            "total_rejected": 10,
        }
        merged = rl.sync_bucket("x", remote)
        assert merged["total_consumed"] == 200
        assert merged["total_rejected"] == 10


# ── get_consumption_stats ──────────────────────────────────────────────────

class TestConsumptionStats:
    def test_stats_basic(self):
        rl.create_bucket("api", 10, 1.0)
        rl.consume("api", 1)
        rl.consume("api", 2)
        stats = rl.get_consumption_stats("api")
        assert stats["bucket"] == "api"
        assert stats["requests_allowed"] == 2
        assert stats["tokens_consumed"] == 3

    def test_stats_with_rejections(self):
        rl.create_bucket("api", 3, 0.0)
        rl.consume("api", 3)
        rl.consume("api", 1)  # rejected
        stats = rl.get_consumption_stats("api")
        assert stats["requests_allowed"] == 1
        assert stats["requests_rejected"] == 1

    def test_stats_missing_bucket(self):
        result = rl.get_consumption_stats("ghost")
        assert "error" in result

    def test_stats_empty(self):
        rl.create_bucket("empty", 10, 1.0)
        stats = rl.get_consumption_stats("empty")
        assert stats["requests_allowed"] == 0
        assert stats["tokens_consumed"] == 0
