# #region Test.TraceContextMiddleware [C:3] [TYPE Module] [SEMANTICS test,middleware,trace,asgi]
# @BRIEF Tests for core/middleware/trace.py — TraceContextMiddleware ASGI middleware.
# @RELATION BINDS_TO -> [TraceContextMiddlewareModule]

from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))

import pytest
from unittest.mock import AsyncMock, MagicMock, patch


# #region test_trace_middleware [C:2] [TYPE Function]
# @BRIEF Test TraceContextMiddleware for various ASGI scenarios.
class TestTraceContextMiddleware:
    @pytest.fixture
    def mock_app(self):
        return AsyncMock()

    @pytest.fixture
    def middleware(self, mock_app):
        from src.core.middleware.trace import TraceContextMiddleware
        return TraceContextMiddleware(mock_app)

    @pytest.mark.asyncio
    async def test_non_http_passthrough(self, middleware, mock_app):
        """Non-HTTP scopes pass through without seeding trace."""
        scope = {"type": "websocket"}
        receive = MagicMock()
        send = MagicMock()
        await middleware(scope, receive, send)
        mock_app.assert_called_once_with(scope, receive, send)

    @pytest.mark.asyncio
    async def test_http_seeds_new_trace_id(self, middleware, mock_app):
        """HTTP scope without X-Trace-ID header seeds a new trace."""
        scope = {"type": "http", "headers": []}
        with patch("src.core.middleware.trace.seed_trace_id") as mock_seed:
            await middleware(scope, MagicMock(), MagicMock())
            mock_seed.assert_called_once()
            mock_app.assert_called_once()

    @pytest.mark.asyncio
    async def test_http_accepts_valid_v4_trace_id(self, middleware, mock_app):
        """Valid UUID4 in X-Trace-ID header is used."""
        valid_uuid = "550e8400-e29b-41d4-a716-446655440000"
        scope = {
            "type": "http",
            "headers": [(b"x-trace-id", valid_uuid.encode())],
        }
        with patch("src.core.middleware.trace.set_trace_id") as mock_set:
            await middleware(scope, MagicMock(), MagicMock())
            mock_set.assert_called_once_with(valid_uuid)

    @pytest.mark.asyncio
    async def test_rejects_non_v4_trace_id(self, middleware, mock_app):
        """Non-v4 UUID falls back to seed_trace_id."""
        non_v4_uuid = "550e8400-e29b-11d4-a716-446655440000"  # version 1
        scope = {
            "type": "http",
            "headers": [(b"x-trace-id", non_v4_uuid.encode())],
        }
        with patch("src.core.middleware.trace.set_trace_id") as mock_set, \
             patch("src.core.middleware.trace.seed_trace_id") as mock_seed:
            await middleware(scope, MagicMock(), MagicMock())
            mock_set.assert_not_called()
            mock_seed.assert_called_once()

    @pytest.mark.asyncio
    async def test_rejects_invalid_uuid(self, middleware, mock_app):
        """Invalid UUID string falls back to seed_trace_id."""
        scope = {
            "type": "http",
            "headers": [(b"x-trace-id", b"not-a-uuid")],
        }
        with patch("src.core.middleware.trace.set_trace_id") as mock_set, \
             patch("src.core.middleware.trace.seed_trace_id") as mock_seed:
            await middleware(scope, MagicMock(), MagicMock())
            mock_set.assert_not_called()
            mock_seed.assert_called_once()

    @pytest.mark.asyncio
    async def test_handles_non_utf8_header(self, middleware, mock_app):
        """Non-UTF8 bytes in header do not crash."""
        scope = {
            "type": "http",
            "headers": [(b"x-trace-id", b"\xff\xfe\x00\x01")],
        }
        with patch("src.core.middleware.trace.seed_trace_id") as mock_seed:
            await middleware(scope, MagicMock(), MagicMock())
            mock_seed.assert_called_once()

    @pytest.mark.asyncio
    async def test_forwards_to_app(self, middleware, mock_app):
        """After seeding, the ASGI app is called with scope, receive, and a wrapped send."""
        scope = {"type": "http", "headers": []}
        receive = MagicMock()
        send = MagicMock()
        await middleware(scope, receive, send)
        # The middleware wraps send in send_with_trace, so verify scope+receive match
        mock_app.assert_called_once()
        call_args = mock_app.call_args
        assert call_args[0][0] == scope
        assert call_args[0][1] == receive
        # call_args[0][2] is send_with_trace — a wrapper function, not the original send
        assert callable(call_args[0][2])
# #endregion test_trace_middleware



# #region TestTraceResponseHeaders [C:2] [TYPE Class] [SEMANTICS test,middleware,trace,header]
# @BRIEF Verify X-Trace-Id response header injection — new, preserved, passthrough.
class TestTraceResponseHeaders:
    """TraceContextMiddleware response header injection."""

    @pytest.fixture
    def mock_app(self):
        return AsyncMock()

    @pytest.fixture
    def middleware(self, mock_app):
        from src.core.middleware.trace import TraceContextMiddleware
        return TraceContextMiddleware(mock_app)

    # #region test_sets_x_trace_id_header_on_response [C:2] [TYPE Function] [SEMANTICS test,trace,header]
    # @BRIEF HTTP response receives x-trace-id header with a valid UUID4 value.
    @pytest.mark.asyncio
    async def test_sets_x_trace_id_header_on_response(self, middleware, mock_app):
        """The middleware injects x-trace-id into http.response.start headers."""
        import uuid

        scope = {"type": "http", "headers": []}
        receive = MagicMock()
        send = AsyncMock()

        # The inner app receives send_with_trace as its 3rd positional arg.
        # Use that wrapped send to exercise header injection.
        async def _inner_app(*args):
            _send = args[2]  # send_with_trace from middleware
            await _send({
                "type": "http.response.start",
                "status": 200,
                "headers": [(b"content-type", b"text/plain")],
            })
            await _send({
                "type": "http.response.body",
                "body": b"OK",
            })

        mock_app.side_effect = _inner_app

        await middleware(scope, receive, send)

        # Collect send calls — find http.response.start
        send_calls = [c for c in send.call_args_list
                      if c.args[0]["type"] == "http.response.start"]
        assert len(send_calls) >= 1, "No http.response.start send call"

        headers = dict(send_calls[0].args[0].get("headers", []))
        assert b"x-trace-id" in headers, "Missing x-trace-id header"

        trace_id = headers[b"x-trace-id"].decode("ascii")
        # Valid UUID4
        parsed = uuid.UUID(hex=trace_id)
        assert parsed.version == 4, f"Trace ID is not UUID4: got version {parsed.version}"
    # #endregion test_sets_x_trace_id_header_on_response

    # #region test_preserves_incoming_trace_id [C:2] [TYPE Function] [SEMANTICS test,trace,header,preserve]
    # @BRIEF When request carries X-Trace-ID, the response header uses the same value.
    @pytest.mark.asyncio
    async def test_preserves_incoming_trace_id(self, middleware, mock_app):
        """Incoming X-Trace-ID is echoed back in the response header."""
        incoming_id = "550e8400-e29b-41d4-a716-446655440000"

        scope = {
            "type": "http",
            "headers": [(b"x-trace-id", incoming_id.encode())],
        }
        receive = MagicMock()
        send = AsyncMock()

        async def _inner_app(*args):
            _send = args[2]  # send_with_trace from middleware
            await _send({
                "type": "http.response.start",
                "status": 200,
                "headers": [],
            })
            await _send({
                "type": "http.response.body",
                "body": b"OK",
            })

        mock_app.side_effect = _inner_app

        await middleware(scope, receive, send)

        send_calls = [c for c in send.call_args_list
                      if c.args[0]["type"] == "http.response.start"]
        assert len(send_calls) >= 1

        headers = dict(send_calls[0].args[0].get("headers", []))
        assert b"x-trace-id" in headers

        response_trace_id = headers[b"x-trace-id"].decode("ascii")
        assert response_trace_id == incoming_id, (
            f"Response trace ID {response_trace_id} != incoming {incoming_id}"
        )
    # #endregion test_preserves_incoming_trace_id

    # #region test_non_http_scope_passthrough [C:2] [TYPE Function] [SEMANTICS test,trace,websocket,passthrough]
    # @BRIEF Non-HTTP scopes (websocket, lifespan) do not inject X-Trace-Id header.
    @pytest.mark.asyncio
    async def test_non_http_scope_passthrough(self, middleware, mock_app):
        """Websocket scope passes through without header injection."""
        scope = {"type": "websocket", "headers": []}
        receive = MagicMock()
        send = MagicMock()

        async def _inner_app(scope, receive, send):
            # Ensure the app was called with the original send, not send_with_trace
            pass

        mock_app.side_effect = _inner_app

        await middleware(scope, receive, send)

        # For non-http, the middleware calls self.app(scope, receive, send)
        # (the original send — no wrapper). Verify original send was called.
        mock_app.assert_called_once_with(scope, receive, send)

    @pytest.mark.asyncio
    async def test_lifespan_scope_passthrough(self, middleware, mock_app):
        """Lifespan scope passes through without header injection."""
        scope = {"type": "lifespan", "headers": []}
        receive = MagicMock()
        send = MagicMock()

        await middleware(scope, receive, send)

        # The middleware returns early for non-http, calling self.app directly
        mock_app.assert_called_once_with(scope, receive, send)
    # #endregion test_non_http_scope_passthrough

# #endregion TestTraceResponseHeaders
# #endregion Test.TraceContextMiddleware
