"""Tests for ``mgf.django._throttle`` — D-1 brute-force throttle
middleware (v0.4.0)."""

from __future__ import annotations

import json
import time

import pytest
from django.http import HttpResponse
from django.test import RequestFactory, override_settings

from mgf.django._errors import ThrottleConfigError
from mgf.django._throttle import (
    SENSITIVE_PATHS_DEFAULT,
    ThrottleMiddleware,
    TokenBucket,
)

# ---------------------------------------------------------------------------
# TokenBucket — same shape as mgf-fastapi
# ---------------------------------------------------------------------------


class TestTokenBucket:
    def test_starts_full(self) -> None:
        b = TokenBucket(capacity=3, refill_per_second=1.0)
        for _ in range(3):
            ok, _ = b.try_consume("k")
            assert ok is True

    def test_denies_when_empty(self) -> None:
        b = TokenBucket(capacity=2, refill_per_second=1.0)
        b.try_consume("k")
        b.try_consume("k")
        ok, retry_after = b.try_consume("k")
        assert ok is False
        assert 0.9 <= retry_after <= 1.1

    def test_refills(self) -> None:
        b = TokenBucket(capacity=1, refill_per_second=100.0)
        b.try_consume("k")
        time.sleep(0.02)
        ok, _ = b.try_consume("k")
        assert ok is True

    def test_per_key_isolation(self) -> None:
        b = TokenBucket(capacity=1, refill_per_second=0.001)
        b.try_consume("a")
        ok_a, _ = b.try_consume("a")
        ok_b, _ = b.try_consume("b")
        assert ok_a is False
        assert ok_b is True

    def test_invalid_capacity(self) -> None:
        with pytest.raises(ThrottleConfigError, match="capacity must be > 0"):
            TokenBucket(capacity=0, refill_per_second=1.0)

    def test_invalid_refill(self) -> None:
        with pytest.raises(ThrottleConfigError, match="refill_per_second must be > 0"):
            TokenBucket(capacity=1, refill_per_second=0)


# ---------------------------------------------------------------------------
# ThrottleMiddleware — Django-shape
# ---------------------------------------------------------------------------


def _make_get_response():
    def _get_response(request) -> HttpResponse:
        return HttpResponse("ok")

    return _get_response


class TestThrottleMiddleware:
    def test_passes_non_sensitive_paths(self) -> None:
        rf = RequestFactory()
        mw = ThrottleMiddleware(_make_get_response())

        request = rf.get("/some/unrelated/path/")
        request.META["REMOTE_ADDR"] = "1.2.3.4"
        response = mw(request)
        assert response.status_code == 200

    def test_allows_under_default_limit(self) -> None:
        rf = RequestFactory()
        # Override settings to give plenty of room
        with override_settings(
            MGF_DJANGO_THROTTLE={
                "bucket_capacity": 5,
                "bucket_refill_per_second": 1.0,
            }
        ):
            mw = ThrottleMiddleware(_make_get_response())
            for _ in range(5):
                request = rf.get("/login/")
                request.META["REMOTE_ADDR"] = "1.2.3.4"
                response = mw(request)
                assert response.status_code == 200

    def test_429_after_capacity_exhausted(self) -> None:
        rf = RequestFactory()
        with override_settings(
            MGF_DJANGO_THROTTLE={
                "bucket_capacity": 2,
                "bucket_refill_per_second": 0.001,
            }
        ):
            mw = ThrottleMiddleware(_make_get_response())
            for _ in range(2):
                request = rf.get("/login/")
                request.META["REMOTE_ADDR"] = "1.2.3.4"
                assert mw(request).status_code == 200

            request = rf.get("/login/")
            request.META["REMOTE_ADDR"] = "1.2.3.4"
            response = mw(request)
            assert response.status_code == 429
            assert "Retry-After" in response.headers
            assert response.headers["Retry-After"].isdigit()
            body = json.loads(response.content)
            assert body["error"] == "rate-limit-exceeded"
            assert body["retry_after"] >= 1

    def test_per_ip_isolation(self) -> None:
        rf = RequestFactory()
        with override_settings(
            MGF_DJANGO_THROTTLE={
                "bucket_capacity": 1,
                "bucket_refill_per_second": 0.001,
            }
        ):
            mw = ThrottleMiddleware(_make_get_response())

            req_a = rf.get("/login/")
            req_a.META["REMOTE_ADDR"] = "1.1.1.1"
            assert mw(req_a).status_code == 200

            # Different IP — independent bucket
            req_b = rf.get("/login/")
            req_b.META["REMOTE_ADDR"] = "2.2.2.2"
            assert mw(req_b).status_code == 200

            # First IP exhausted now
            req_a2 = rf.get("/login/")
            req_a2.META["REMOTE_ADDR"] = "1.1.1.1"
            assert mw(req_a2).status_code == 429

    def test_no_remote_addr_passes_through(self) -> None:
        # If REMOTE_ADDR is missing, the default key fn returns None →
        # throttle disabled for that request.
        rf = RequestFactory()
        with override_settings(
            MGF_DJANGO_THROTTLE={
                "bucket_capacity": 1,
                "bucket_refill_per_second": 0.001,
            }
        ):
            mw = ThrottleMiddleware(_make_get_response())
            request = rf.get("/login/")
            request.META.pop("REMOTE_ADDR", None)
            # Multiple requests — none throttled.
            for _ in range(5):
                assert mw(request).status_code == 200

    def test_custom_sensitive_paths(self) -> None:
        rf = RequestFactory()
        with override_settings(
            MGF_DJANGO_THROTTLE={
                "bucket_capacity": 1,
                "bucket_refill_per_second": 0.001,
                "sensitive_paths": ("/admin/login/",),
            }
        ):
            mw = ThrottleMiddleware(_make_get_response())

            # Default-sensitive /login/ now NOT throttled (overridden).
            for _ in range(5):
                request = rf.get("/login/")
                request.META["REMOTE_ADDR"] = "1.1.1.1"
                assert mw(request).status_code == 200

            # Custom-sensitive /admin/login/ IS throttled.
            req1 = rf.get("/admin/login/")
            req1.META["REMOTE_ADDR"] = "1.1.1.1"
            assert mw(req1).status_code == 200
            req2 = rf.get("/admin/login/")
            req2.META["REMOTE_ADDR"] = "1.1.1.1"
            assert mw(req2).status_code == 429


class TestSensitivePaths:
    def test_default_includes_login(self) -> None:
        assert "/login/" in SENSITIVE_PATHS_DEFAULT

    def test_default_includes_password_reset(self) -> None:
        assert "/password-reset/" in SENSITIVE_PATHS_DEFAULT

    def test_default_includes_api_auth(self) -> None:
        assert "/api/auth/" in SENSITIVE_PATHS_DEFAULT
