"""Tests for RateLimitMiddleware — per-user / per-IP sliding window (#655, #2213).

Тестируем middleware в изоляции на минимальном FastAPI-приложении, чтобы не
тянуть тяжёлый app.main (DB / scheduler / sentry). Лимит/окно/множитель читаются
из settings на каждый dispatch → monkeypatch'им их в маленькие значения.

#2213: аутентифицированный трафик больше НЕ освобождается целиком — он лимитируется
per-user с щедрым порогом (rate_limit × multiplier). Анонимный ключ = client IP,
взятый из RIGHTMOST элемента X-Forwarded-For (ровно один доверенный прокси Caddy),
поэтому подделка leftmost XFF не меняет ключ.
"""

import os

os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

from app.core import config
from app.core.ratelimit import RateLimitMiddleware


@pytest.fixture
def client(monkeypatch):
    """Минимальное приложение: анонимный лимит 3 / 60 с, auth-множитель ×2 (→6)."""
    monkeypatch.setattr(config.settings, "rate_limit", 3)
    monkeypatch.setattr(config.settings, "rate_limit_window_s", 60.0)
    monkeypatch.setattr(config.settings, "rate_limit_authenticated_multiplier", 2)

    app = FastAPI()
    app.add_middleware(RateLimitMiddleware)

    @app.get("/api/v1/ping")
    def ping() -> dict[str, bool]:
        return {"ok": True}

    return TestClient(app)


def test_anonymous_throttled_past_limit(client):
    # Первые rate_limit запросов проходят, следующий — 429.
    for _ in range(3):
        assert client.get("/api/v1/ping").status_code == 200
    resp = client.get("/api/v1/ping")
    assert resp.status_code == 429
    assert resp.json() == {"detail": "Слишком много запросов. Попробуйте позже."}
    assert "Retry-After" in resp.headers


def test_authenticated_user_is_limited_per_user(client):
    # #2213: authenticated НЕ освобождается — лимит = rate_limit × multiplier = 6.
    headers = {"X-Authenticated-User": "alice"}
    for _ in range(6):
        assert client.get("/api/v1/ping", headers=headers).status_code == 200
    # 7-й запрос того же юзера — 429.
    assert client.get("/api/v1/ping", headers=headers).status_code == 429


def test_authenticated_limit_higher_than_anonymous(client):
    # Auth-порог (6) строго выше анонимного (3): 4-й запрос юзера ещё проходит,
    # тогда как анонимный на 4-м уже режется (см. test_anonymous_throttled_past_limit).
    headers = {"X-Authenticated-User": "alice"}
    for _ in range(4):
        assert client.get("/api/v1/ping", headers=headers).status_code == 200


def test_per_user_key_isolation(client):
    # Разные юзеры — независимые корзины: alice упирается, bob не затронут.
    alice = {"X-Authenticated-User": "alice"}
    bob = {"X-Authenticated-User": "bob"}
    for _ in range(6):
        assert client.get("/api/v1/ping", headers=alice).status_code == 200
    assert client.get("/api/v1/ping", headers=alice).status_code == 429
    # bob со своим ключом проходит свободно.
    assert client.get("/api/v1/ping", headers=bob).status_code == 200


def test_empty_auth_header_does_not_bypass(client):
    # Пустой заголовок не должен no-op'ить лимит (header present но falsy) → per-IP.
    headers = {"X-Authenticated-User": ""}
    for _ in range(3):
        assert client.get("/api/v1/ping", headers=headers).status_code == 200
    assert client.get("/api/v1/ping", headers=headers).status_code == 429


def test_leftmost_xff_does_not_affect_anon_key(client):
    # Клиент подделывает leftmost XFF, но rightmost (добавленный Caddy) одинаков →
    # один ключ ip:9.9.9.9 → лимит общий, подделкой его не обойти.
    spoofed = [
        {"X-Forwarded-For": "1.1.1.1, 9.9.9.9"},
        {"X-Forwarded-For": "2.2.2.2, 9.9.9.9"},
        {"X-Forwarded-For": "3.3.3.3, 9.9.9.9"},
    ]
    for h in spoofed:
        assert client.get("/api/v1/ping", headers=h).status_code == 200
    # 4-й (снова другой leftmost, тот же rightmost) — уже за лимитом.
    resp = client.get("/api/v1/ping", headers={"X-Forwarded-For": "4.4.4.4, 9.9.9.9"})
    assert resp.status_code == 429


def test_anonymous_key_from_rightmost_xff(client):
    # Разный rightmost = разные реальные клиенты = независимые корзины.
    for _ in range(3):
        assert (
            client.get("/api/v1/ping", headers={"X-Forwarded-For": "8.8.8.8, 1.1.1.1"}).status_code
            == 200
        )
    # Другой rightmost — своя корзина, не затронут лимитом первого.
    assert (
        client.get("/api/v1/ping", headers={"X-Forwarded-For": "8.8.8.8, 2.2.2.2"}).status_code
        == 200
    )
