# Copyright 2025 Softwell S.r.l.
# Licensed under the Apache License, Version 2.0

"""Tests for CacheMiddleware."""

import tempfile
import time
from pathlib import Path
from typing import Any, MutableMapping

import pytest

from genro_asgi.middleware.cache import CacheMiddleware


# Test helpers
async def dummy_receive() -> MutableMapping[str, Any]:
    return {"type": "http.request", "body": b""}


class ResponseCapture:
    """Captures ASGI responses for testing."""

    def __init__(self) -> None:
        self.messages: list[MutableMapping[str, Any]] = []

    async def send(self, message: MutableMapping[str, Any]) -> None:
        self.messages.append(message)

    @property
    def status(self) -> int:
        for msg in self.messages:
            if msg["type"] == "http.response.start":
                return msg["status"]
        return 0

    @property
    def headers(self) -> dict[str, str]:
        for msg in self.messages:
            if msg["type"] == "http.response.start":
                return {
                    name.decode("latin-1"): value.decode("latin-1")
                    for name, value in msg.get("headers", [])
                }
        return {}


class TestCacheMiddleware:
    """Tests for CacheMiddleware."""

    def test_init_defaults(self) -> None:
        """Test default configuration."""
        async def app(scope: Any, receive: Any, send: Any) -> None:
            pass

        middleware = CacheMiddleware(app)
        assert middleware.max_age == 3600
        assert middleware.immutable is False
        assert middleware.public is True

    def test_init_custom_config(self) -> None:
        """Test custom configuration."""
        async def app(scope: Any, receive: Any, send: Any) -> None:
            pass

        middleware = CacheMiddleware(
            app, max_age=86400, immutable=True, public=False
        )
        assert middleware.max_age == 86400
        assert middleware.immutable is True
        assert middleware.public is False

    def test_build_cache_control(self) -> None:
        """Test Cache-Control header building."""
        async def app(scope: Any, receive: Any, send: Any) -> None:
            pass

        # Default
        mw = CacheMiddleware(app)
        assert mw._build_cache_control() == "public, max-age=3600"

        # With immutable
        mw = CacheMiddleware(app, immutable=True)
        assert mw._build_cache_control() == "public, max-age=3600, immutable"

        # Private
        mw = CacheMiddleware(app, public=False)
        assert mw._build_cache_control() == "max-age=3600"

    def test_compute_etag(self) -> None:
        """Test ETag computation."""
        async def app(scope: Any, receive: Any, send: Any) -> None:
            pass

        middleware = CacheMiddleware(app)

        with tempfile.NamedTemporaryFile(delete=False) as f:
            f.write(b"test content")
            f.flush()
            path = Path(f.name)

        try:
            etag = middleware._compute_etag(path)
            # ETag should be quoted
            assert etag.startswith('"')
            assert etag.endswith('"')
            # Should be deterministic
            assert middleware._compute_etag(path) == etag
        finally:
            path.unlink()

    def test_etag_changes_with_content(self) -> None:
        """Test that ETag changes when file changes."""
        async def app(scope: Any, receive: Any, send: Any) -> None:
            pass

        middleware = CacheMiddleware(app)

        with tempfile.NamedTemporaryFile(delete=False) as f:
            f.write(b"content 1")
            f.flush()
            path = Path(f.name)

        try:
            etag1 = middleware._compute_etag(path)
            # Modify file
            time.sleep(0.01)  # Ensure mtime changes
            path.write_bytes(b"content 2 - longer")
            etag2 = middleware._compute_etag(path)
            assert etag1 != etag2
        finally:
            path.unlink()

    def test_check_not_modified_etag_match(self) -> None:
        """Test 304 detection with matching ETag."""
        async def app(scope: Any, receive: Any, send: Any) -> None:
            pass

        middleware = CacheMiddleware(app)
        etag = '"abc123"'

        # Exact match
        assert middleware._check_not_modified(
            {"if-none-match": '"abc123"'}, etag, 0
        )

        # Multiple ETags
        assert middleware._check_not_modified(
            {"if-none-match": '"xyz", "abc123", "def"'}, etag, 0
        )

        # Wildcard
        assert middleware._check_not_modified(
            {"if-none-match": "*"}, etag, 0
        )

        # No match
        assert not middleware._check_not_modified(
            {"if-none-match": '"different"'}, etag, 0
        )

    def test_check_not_modified_if_modified_since(self) -> None:
        """Test 304 detection with If-Modified-Since."""
        async def app(scope: Any, receive: Any, send: Any) -> None:
            pass

        middleware = CacheMiddleware(app)
        # File modified at specific time
        file_mtime = 1700000000.0

        # Client has newer or same version
        assert middleware._check_not_modified(
            {"if-modified-since": "Tue, 14 Nov 2023 22:13:20 GMT"},
            '"etag"',
            file_mtime,
        )

        # Client has older version
        assert not middleware._check_not_modified(
            {"if-modified-since": "Mon, 13 Nov 2023 00:00:00 GMT"},
            '"etag"',
            file_mtime,
        )

    @pytest.mark.asyncio
    async def test_passthrough_non_http(self) -> None:
        """Test that non-HTTP requests pass through unchanged."""
        called = False

        async def app(scope: Any, receive: Any, send: Any) -> None:
            nonlocal called
            called = True

        middleware = CacheMiddleware(app)
        scope = {"type": "websocket"}
        capture = ResponseCapture()

        await middleware(scope, dummy_receive, capture.send)
        assert called

    @pytest.mark.asyncio
    async def test_passthrough_post_request(self) -> None:
        """Test that POST requests pass through unchanged."""
        called = False

        async def app(scope: Any, receive: Any, send: Any) -> None:
            nonlocal called
            called = True
            await send({"type": "http.response.start", "status": 200, "headers": []})
            await send({"type": "http.response.body", "body": b""})

        middleware = CacheMiddleware(app)
        scope = {"type": "http", "method": "POST", "headers": []}
        capture = ResponseCapture()

        await middleware(scope, dummy_receive, capture.send)
        assert called
        # No cache headers added for POST
        assert "etag" not in capture.headers

    @pytest.mark.asyncio
    async def test_adds_cache_headers(self) -> None:
        """Test that cache headers are added for file responses."""
        with tempfile.NamedTemporaryFile(delete=False) as f:
            f.write(b"test content")
            f.flush()
            file_path = Path(f.name)

        try:
            async def app(scope: Any, receive: Any, send: Any) -> None:
                await send({
                    "type": "http.response.start",
                    "status": 200,
                    "headers": [(b"content-type", b"text/plain")],
                })
                await send({"type": "http.response.body", "body": b"content"})

            middleware = CacheMiddleware(app)
            scope = {
                "type": "http",
                "method": "GET",
                "headers": [],
                "_file_path": file_path,
            }
            capture = ResponseCapture()

            await middleware(scope, dummy_receive, capture.send)

            assert capture.status == 200
            assert "etag" in capture.headers
            assert "last-modified" in capture.headers
            assert "cache-control" in capture.headers
            assert capture.headers["cache-control"] == "public, max-age=3600"
        finally:
            file_path.unlink()

    @pytest.mark.asyncio
    async def test_304_response(self) -> None:
        """Test 304 Not Modified response."""
        with tempfile.NamedTemporaryFile(delete=False) as f:
            f.write(b"test content")
            f.flush()
            file_path = Path(f.name)

        try:
            async def app(scope: Any, receive: Any, send: Any) -> None:
                await send({
                    "type": "http.response.start",
                    "status": 200,
                    "headers": [(b"content-type", b"text/plain")],
                })
                await send({"type": "http.response.body", "body": b"content"})

            middleware = CacheMiddleware(app)

            # First, get the ETag
            etag = middleware._compute_etag(file_path)

            scope = {
                "type": "http",
                "method": "GET",
                "headers": [(b"if-none-match", etag.encode("latin-1"))],
                "_file_path": file_path,
            }
            capture = ResponseCapture()

            await middleware(scope, dummy_receive, capture.send)

            assert capture.status == 304
            assert "etag" in capture.headers
        finally:
            file_path.unlink()

    @pytest.mark.asyncio
    async def test_no_file_path_passthrough(self) -> None:
        """Test that requests without _file_path pass through."""
        async def app(scope: Any, receive: Any, send: Any) -> None:
            await send({
                "type": "http.response.start",
                "status": 200,
                "headers": [(b"content-type", b"application/json")],
            })
            await send({"type": "http.response.body", "body": b'{"ok": true}'})

        middleware = CacheMiddleware(app)
        scope = {"type": "http", "method": "GET", "headers": []}
        capture = ResponseCapture()

        await middleware(scope, dummy_receive, capture.send)

        assert capture.status == 200
        # No cache headers (no file path)
        assert "etag" not in capture.headers


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
