"""Tests for security hardening features.

Tests IP-based rate limiting, input sanitization, and PII redaction.
"""

from __future__ import annotations

import pytest

from llmhosts.middleware.rate_limit import IPRateLimitMiddleware, SlidingWindowBucket
from llmhosts.middleware.redact import redact_email, redact_text, safe_log_api_key, safe_log_email
from llmhosts.middleware.sanitize import InputSanitizer

# ---------------------------------------------------------------------------
# IP Rate Limiting
# ---------------------------------------------------------------------------


class TestSlidingWindowBucket:
    """Test the sliding window rate limiter."""

    def test_allows_requests_within_limit(self) -> None:
        bucket = SlidingWindowBucket(limit=5)
        for _ in range(5):
            allowed, retry_after = bucket.is_allowed()
            assert allowed is True
            assert retry_after == 0

    def test_blocks_requests_over_limit(self) -> None:
        bucket = SlidingWindowBucket(limit=3)
        # Allow first 3
        for _ in range(3):
            allowed, _ = bucket.is_allowed()
            assert allowed is True
        # Block 4th
        allowed, retry_after = bucket.is_allowed()
        assert allowed is False
        assert retry_after > 0

    def test_retry_after_is_reasonable(self) -> None:
        bucket = SlidingWindowBucket(limit=2)
        for _ in range(2):
            bucket.is_allowed()
        allowed, retry_after = bucket.is_allowed()
        assert allowed is False
        assert 1 <= retry_after <= 61  # Should be within window


# ---------------------------------------------------------------------------
# Input Sanitization
# ---------------------------------------------------------------------------


class TestInputSanitizer:
    """Test input sanitization utilities."""

    # Email validation
    def test_sanitize_email_valid(self) -> None:
        email = InputSanitizer.sanitize_email("User@Example.COM")
        assert email == "user@example.com"

    def test_sanitize_email_invalid_format(self) -> None:
        with pytest.raises(ValueError, match="Invalid email format"):
            InputSanitizer.sanitize_email("not-an-email")

    def test_sanitize_email_too_long(self) -> None:
        email = "a" * 250 + "@example.com"
        with pytest.raises(ValueError, match="too long"):
            InputSanitizer.sanitize_email(email)

    def test_sanitize_email_empty(self) -> None:
        with pytest.raises(ValueError, match="cannot be empty"):
            InputSanitizer.sanitize_email("")

    # Slug validation
    def test_sanitize_slug_valid(self) -> None:
        slug = InputSanitizer.sanitize_slug("my-cool-slug")
        assert slug == "my-cool-slug"

    def test_sanitize_slug_uppercase_converted(self) -> None:
        slug = InputSanitizer.sanitize_slug("My-Cool-Slug")
        assert slug == "my-cool-slug"

    def test_sanitize_slug_invalid_chars(self) -> None:
        with pytest.raises(ValueError, match="Invalid slug format"):
            InputSanitizer.sanitize_slug("slug_with_underscores")

    def test_sanitize_slug_too_long(self) -> None:
        with pytest.raises(ValueError, match="too long"):
            InputSanitizer.sanitize_slug("a" * 101)

    # UUID validation
    def test_sanitize_uuid_valid(self) -> None:
        uuid_str = "550e8400-e29b-41d4-a716-446655440000"
        result = InputSanitizer.sanitize_uuid(uuid_str)
        assert result == uuid_str.lower()

    def test_sanitize_uuid_uppercase(self) -> None:
        uuid_str = "550E8400-E29B-41D4-A716-446655440000"
        result = InputSanitizer.sanitize_uuid(uuid_str)
        assert result == uuid_str.lower()

    def test_sanitize_uuid_invalid_format(self) -> None:
        with pytest.raises(ValueError, match="Invalid UUID"):
            InputSanitizer.sanitize_uuid("not-a-uuid")

    # String validation
    def test_sanitize_string_valid(self) -> None:
        result = InputSanitizer.sanitize_string("  hello world  ")
        assert result == "hello world"

    def test_sanitize_string_too_short(self) -> None:
        with pytest.raises(ValueError, match="too short"):
            InputSanitizer.sanitize_string("hi", min_length=5)

    def test_sanitize_string_too_long(self) -> None:
        with pytest.raises(ValueError, match="too long"):
            InputSanitizer.sanitize_string("a" * 101, max_length=100)

    def test_sanitize_string_newlines_rejected(self) -> None:
        with pytest.raises(ValueError, match="Newlines not allowed"):
            InputSanitizer.sanitize_string("hello\nworld", allow_newlines=False)

    def test_sanitize_string_newlines_allowed(self) -> None:
        result = InputSanitizer.sanitize_string("hello\nworld", allow_newlines=True)
        assert result == "hello\nworld"

    # Dict sanitization
    def test_sanitize_dict_filters_keys(self) -> None:
        data = {"a": 1, "b": 2, "c": 3}
        result = InputSanitizer.sanitize_dict(data, allowed_keys={"a", "c"})
        assert result == {"a": 1, "c": 3}

    def test_sanitize_dict_no_filter(self) -> None:
        data = {"a": 1, "b": 2}
        result = InputSanitizer.sanitize_dict(data)
        assert result == data


# ---------------------------------------------------------------------------
# PII Redaction
# ---------------------------------------------------------------------------


class TestPIIRedaction:
    """Test PII redaction for logging."""

    def test_redact_email_short_local(self) -> None:
        assert redact_email("a@example.com") == "a***@example.com"

    def test_redact_email_long_local(self) -> None:
        assert redact_email("user@example.com") == "u***@example.com"

    def test_safe_log_email(self) -> None:
        assert safe_log_email("admin@llmhosts.com") == "a***@llmhosts.com"

    def test_redact_api_key(self) -> None:
        key = "sk-abc123xyz789012345678901234567890"
        redacted = safe_log_api_key(key)
        assert redacted.startswith("sk-")
        assert "***" in redacted
        assert redacted.endswith(key[-6:])

    def test_redact_text_multiple_pii(self) -> None:
        text = "User user@example.com has key sk-abc123xyz789012345678901234567890"
        redacted = redact_text(text)
        assert "user@example.com" not in redacted
        assert "u***@example.com" in redacted
        assert "sk-abc123xyz789012345678901234567890" not in redacted
        assert "sk-***" in redacted

    def test_redact_text_no_pii(self) -> None:
        text = "This is a safe log message"
        assert redact_text(text) == text


# ---------------------------------------------------------------------------
# Integration Tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_ip_rate_limit_middleware_integration() -> None:
    """Test IP rate limiting in FastAPI integration."""
    from fastapi import FastAPI
    from httpx import ASGITransport, AsyncClient

    app = FastAPI()
    app.add_middleware(IPRateLimitMiddleware)

    @app.get("/api/auth/test")
    async def test_route() -> dict[str, str]:
        return {"status": "ok"}

    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
        # First 10 should succeed (limit is 10/min for /api/auth/*)
        for _ in range(10):
            response = await client.get("/api/auth/test")
            assert response.status_code == 200

        # 11th should be rate limited
        response = await client.get("/api/auth/test")
        assert response.status_code == 429
        assert "Retry-After" in response.headers
