"""
Tests for Phase 35: API Governance — rate limiting, quotas, abuse detection.
"""

import json
import threading
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path

import pytest


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect all governance storage to a temp directory and reset state."""
    governance_dir = tmp_path / "governance"
    suspensions_dir = governance_dir / "suspensions"
    log_path = governance_dir / "request_log.jsonl"
    api_keys_dir = tmp_path / "api_keys"
    for d in (governance_dir, suspensions_dir, api_keys_dir):
        d.mkdir(parents=True, exist_ok=True)

    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)

    import brynq.api_governance as gov
    monkeypatch.setattr(gov, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(gov, "GOVERNANCE_DIR", governance_dir)
    monkeypatch.setattr(gov, "REQUEST_LOG_PATH", log_path)
    monkeypatch.setattr(gov, "SUSPENSIONS_DIR", suspensions_dir)

    # Reset all in-memory state between tests
    gov._token_bucket.reset_all()
    gov._minute_counter.reset_all()
    gov._hour_counter.reset_all()
    gov._day_counter.reset_all()


# ── Helpers ────────────────────────────────────────────────────────────────


def _log(api_key="key-1", endpoint="/api/v1/tasks", method="GET",
         status_code=200, response_time_ms=42.0):
    from brynq.api_governance import log_request
    return log_request(api_key, endpoint, method, status_code, response_time_ms)


def _create_api_key_file(tmp_path, api_key, name="agent-a", tier="free"):
    keys_dir = tmp_path / "api_keys"
    keys_dir.mkdir(parents=True, exist_ok=True)
    data = {
        "api_key": api_key,
        "name": name,
        "tier": tier,
        "created": datetime.now(timezone.utc).isoformat(),
    }
    path = keys_dir / f"{name}.json"
    path.write_text(json.dumps(data), encoding="utf-8")
    return path


# ── 1. Token Bucket Rate Limiter ─────────────────────────────────────────


class TestRateLimiter:
    def test_allows_within_capacity(self):
        from brynq.api_governance import RateLimiter
        rl = RateLimiter(capacity=5, refill_rate=1.0)
        for _ in range(5):
            allowed, remaining, retry = rl.allow("k1")
            assert allowed is True
            assert retry == 0.0

    def test_blocks_when_exhausted(self):
        from brynq.api_governance import RateLimiter
        rl = RateLimiter(capacity=3, refill_rate=0.0)
        for _ in range(3):
            rl.allow("k1")
        allowed, remaining, retry = rl.allow("k1")
        assert allowed is False
        assert remaining == 0
        assert retry == float("inf")

    def test_remaining_decreases(self):
        from brynq.api_governance import RateLimiter
        rl = RateLimiter(capacity=5, refill_rate=0.0)
        _, r1, _ = rl.allow("k1")
        _, r2, _ = rl.allow("k1")
        assert r1 == 4
        assert r2 == 3

    def test_refill_over_time(self):
        from brynq.api_governance import RateLimiter
        rl = RateLimiter(capacity=2, refill_rate=1000.0)  # fast refill
        rl.allow("k1")
        rl.allow("k1")
        # With a very high refill rate, next call should succeed
        time.sleep(0.01)
        allowed, _, _ = rl.allow("k1")
        assert allowed is True

    def test_retry_after_positive(self):
        from brynq.api_governance import RateLimiter
        rl = RateLimiter(capacity=1, refill_rate=2.0)
        rl.allow("k1")  # consume the 1 token
        allowed, _, retry = rl.allow("k1")
        assert allowed is False
        assert retry > 0.0

    def test_separate_keys_independent(self):
        from brynq.api_governance import RateLimiter
        rl = RateLimiter(capacity=1, refill_rate=0.0)
        a1, _, _ = rl.allow("k1")
        a2, _, _ = rl.allow("k2")
        assert a1 is True
        assert a2 is True
        # Both exhausted now
        assert rl.allow("k1")[0] is False
        assert rl.allow("k2")[0] is False

    def test_reset_single_key(self):
        from brynq.api_governance import RateLimiter
        rl = RateLimiter(capacity=1, refill_rate=0.0)
        rl.allow("k1")
        rl.reset("k1")
        allowed, _, _ = rl.allow("k1")
        assert allowed is True

    def test_reset_all(self):
        from brynq.api_governance import RateLimiter
        rl = RateLimiter(capacity=1, refill_rate=0.0)
        rl.allow("k1")
        rl.allow("k2")
        rl.reset_all()
        assert rl.allow("k1")[0] is True
        assert rl.allow("k2")[0] is True

    def test_peek_does_not_consume(self):
        from brynq.api_governance import RateLimiter
        rl = RateLimiter(capacity=5, refill_rate=0.0)
        assert rl.peek("k1") == 5
        assert rl.peek("k1") == 5  # still 5


# ── 2. Tier-based Quotas ─────────────────────────────────────────────────


class TestTierQuotas:
    def test_free_tier_allows_within_limit(self):
        from brynq.api_governance import check_rate_limit
        allowed, headers = check_rate_limit("free-key", tier="free")
        assert allowed is True
        assert "X-RateLimit-Limit" in headers
        assert "X-RateLimit-Remaining" in headers
        assert "X-RateLimit-Reset" in headers

    def test_free_tier_limit_header_value(self):
        from brynq.api_governance import check_rate_limit
        _, headers = check_rate_limit("free-key2", tier="free")
        assert headers["X-RateLimit-Limit"] == "100"

    def test_pro_tier_limit_header_value(self):
        from brynq.api_governance import check_rate_limit
        _, headers = check_rate_limit("pro-key", tier="pro")
        assert headers["X-RateLimit-Limit"] == "500"

    def test_enterprise_tier_limit_header_value(self):
        from brynq.api_governance import check_rate_limit
        _, headers = check_rate_limit("ent-key", tier="enterprise")
        assert headers["X-RateLimit-Limit"] == "2000"

    def test_free_tier_blocks_over_minute_limit(self):
        from brynq.api_governance import check_rate_limit
        key = "flood-free"
        for _ in range(100):
            check_rate_limit(key, tier="free")
        allowed, headers = check_rate_limit(key, tier="free")
        assert allowed is False
        assert "Retry-After" in headers

    def test_no_retry_after_when_allowed(self):
        from brynq.api_governance import check_rate_limit
        allowed, headers = check_rate_limit("ok-key", tier="free")
        assert allowed is True
        assert "Retry-After" not in headers

    def test_unknown_tier_falls_back_to_free(self):
        from brynq.api_governance import check_rate_limit
        _, headers = check_rate_limit("unk-key", tier="nonexistent")
        assert headers["X-RateLimit-Limit"] == "100"


# ── 3. Request Logging & Usage Stats ─────────────────────────────────────


class TestRequestLogging:
    def test_log_creates_record(self):
        record = _log()
        assert record["api_key"] == "key-1"
        assert record["endpoint"] == "/api/v1/tasks"
        assert record["method"] == "GET"
        assert record["status_code"] == 200
        assert "ts" in record

    def test_log_appends_to_file(self):
        from brynq.api_governance import REQUEST_LOG_PATH
        _log(api_key="a1")
        _log(api_key="a2")
        lines = REQUEST_LOG_PATH.read_text("utf-8").strip().splitlines()
        assert len(lines) == 2

    def test_usage_stats_counts(self):
        from brynq.api_governance import get_usage_stats
        for _ in range(5):
            _log(api_key="stats-key", status_code=200)
        _log(api_key="stats-key", status_code=500)
        stats = get_usage_stats("stats-key", hours=1)
        assert stats["total_requests"] == 6
        assert stats["error_rate"] > 0

    def test_usage_stats_avg_response_time(self):
        from brynq.api_governance import get_usage_stats
        _log(api_key="rt-key", response_time_ms=100.0)
        _log(api_key="rt-key", response_time_ms=200.0)
        stats = get_usage_stats("rt-key", hours=1)
        assert stats["avg_response_time_ms"] == 150.0

    def test_usage_stats_top_endpoints(self):
        from brynq.api_governance import get_usage_stats
        for _ in range(3):
            _log(api_key="ep-key", endpoint="/a")
        _log(api_key="ep-key", endpoint="/b")
        stats = get_usage_stats("ep-key", hours=1)
        assert stats["top_endpoints"][0]["endpoint"] == "/a"
        assert stats["top_endpoints"][0]["count"] == 3

    def test_usage_stats_empty_key(self):
        from brynq.api_governance import get_usage_stats
        stats = get_usage_stats("nobody", hours=1)
        assert stats["total_requests"] == 0
        assert stats["error_rate"] == 0.0


# ── 4. Abuse Detection ───────────────────────────────────────────────────


class TestAbuseDetection:
    def test_no_abuse_on_clean_key(self):
        from brynq.api_governance import check_abuse
        for _ in range(5):
            _log(api_key="clean")
        is_abusive, reason, confidence = check_abuse("clean")
        assert is_abusive is False

    def test_rapid_fire_detection(self):
        from brynq.api_governance import check_abuse
        for _ in range(55):
            _log(api_key="spammer")
        is_abusive, reason, confidence = check_abuse("spammer")
        assert is_abusive is True
        assert "Rapid-fire" in reason
        assert confidence > 0.0

    def test_high_error_rate_detection(self):
        from brynq.api_governance import check_abuse
        # Log enough older entries so rapid-fire check doesn't trigger
        # (they'll be >10s old by the time we check because we backdate them)
        from brynq.api_governance import GOVERNANCE_DIR, REQUEST_LOG_PATH
        key = "error-bot"
        now = datetime.now(timezone.utc)
        older = (now - timedelta(seconds=60)).isoformat()
        for i in range(60):
            sc = 500 if i < 40 else 200
            record = {
                "ts": older,
                "api_key": key,
                "endpoint": "/api/tasks",
                "method": "GET",
                "status_code": sc,
                "response_time_ms": 10.0,
            }
            with REQUEST_LOG_PATH.open("a", encoding="utf-8") as f:
                f.write(json.dumps(record) + "\n")

        is_abusive, reason, confidence = check_abuse(key)
        assert is_abusive is True
        assert "error rate" in reason.lower()

    def test_endpoint_scanning_detection(self):
        from brynq.api_governance import check_abuse
        key = "scanner"
        for i in range(25):
            _log(api_key=key, endpoint=f"/api/v1/endpoint-{i}")
        is_abusive, reason, confidence = check_abuse(key)
        assert is_abusive is True
        assert "scanning" in reason.lower()

    def test_no_abuse_unknown_key(self):
        from brynq.api_governance import check_abuse
        is_abusive, reason, confidence = check_abuse("unknown")
        assert is_abusive is False
        assert confidence == 0.0


# ── 5. Key Suspension ────────────────────────────────────────────────────


class TestKeySuspension:
    def test_suspend_creates_file(self):
        from brynq.api_governance import suspend_key, SUSPENSIONS_DIR
        result = suspend_key("key-x", "abuse detected")
        assert result["reason"] == "abuse detected"
        assert any(SUSPENSIONS_DIR.glob("*.json"))

    def test_is_suspended(self):
        from brynq.api_governance import suspend_key, is_suspended
        assert is_suspended("key-y") is False
        suspend_key("key-y", "test")
        assert is_suspended("key-y") is True

    def test_unsuspend(self):
        from brynq.api_governance import suspend_key, unsuspend_key, is_suspended
        suspend_key("key-z", "test")
        assert is_suspended("key-z") is True
        result = unsuspend_key("key-z")
        assert result is True
        assert is_suspended("key-z") is False

    def test_unsuspend_nonexistent(self):
        from brynq.api_governance import unsuspend_key
        result = unsuspend_key("never-suspended")
        assert result is False


# ── 6. Admin Utilities ───────────────────────────────────────────────────


class TestAdminUtilities:
    def test_reset_quotas(self):
        from brynq.api_governance import check_rate_limit, reset_quotas
        key = "reset-me"
        for _ in range(100):
            check_rate_limit(key, tier="free")
        # Now blocked
        assert check_rate_limit(key, tier="free")[0] is False
        reset_quotas()
        # After reset, allowed again
        assert check_rate_limit(key, tier="free")[0] is True

    def test_get_all_usage(self):
        from brynq.api_governance import get_all_usage
        _log(api_key="a1")
        _log(api_key="a2")
        _log(api_key="a1")
        summary = get_all_usage(hours=1)
        assert summary["total_requests"] == 3
        assert summary["unique_keys"] == 2

    def test_get_all_usage_empty(self):
        from brynq.api_governance import get_all_usage
        summary = get_all_usage(hours=1)
        assert summary["total_requests"] == 0
        assert summary["unique_keys"] == 0


# ── 7. Key Info Lookup ───────────────────────────────────────────────────


class TestKeyInfo:
    def test_get_key_info(self, tmp_path):
        from brynq.api_governance import get_key_info
        _create_api_key_file(tmp_path, "brynq_test123", name="bot-1", tier="pro")
        info = get_key_info("brynq_test123")
        assert info is not None
        assert info["tier"] == "pro"
        assert info["owner"] == "bot-1"

    def test_get_key_info_not_found(self):
        from brynq.api_governance import get_key_info
        info = get_key_info("nonexistent_key")
        assert info is None


# ── 8. Thread Safety ─────────────────────────────────────────────────────


class TestThreadSafety:
    def test_concurrent_rate_limit_calls(self):
        """Multiple threads hammering the same key should not raise."""
        from brynq.api_governance import RateLimiter
        rl = RateLimiter(capacity=1000, refill_rate=0.0)
        results: list[bool] = []
        errors: list[Exception] = []

        def worker():
            try:
                for _ in range(100):
                    allowed, _, _ = rl.allow("shared-key")
                    results.append(allowed)
            except Exception as exc:
                errors.append(exc)

        threads = [threading.Thread(target=worker) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert not errors, f"Thread errors: {errors}"
        # Exactly 1000 should be allowed (capacity=1000, refill=0)
        allowed_count = sum(1 for r in results if r)
        assert allowed_count == 1000

    def test_concurrent_log_writes(self):
        """Multiple threads writing logs should not corrupt the file."""
        from brynq.api_governance import log_request, REQUEST_LOG_PATH

        def writer(tid):
            for i in range(20):
                log_request(f"key-{tid}", f"/ep/{i}", "POST", 200, 5.0)

        threads = [threading.Thread(target=writer, args=(i,)) for i in range(5)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        lines = REQUEST_LOG_PATH.read_text("utf-8").strip().splitlines()
        assert len(lines) == 100  # 5 threads * 20 writes
        # Every line should be valid JSON
        for line in lines:
            json.loads(line)
