# -*- coding: utf-8 -*-
"""Location: ./tests/unit/mcpgateway/middleware/test_csrf_middleware.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0
Authors: Mihai Criveti

Integration tests for CSRF middleware.

Tests cover request validation, token checking, exempt paths, and referer validation.
"""

# Standard
from unittest.mock import AsyncMock, MagicMock, Mock, patch

# Third-Party
import pytest
from starlette.requests import Request
from starlette.responses import Response

# First-Party
from mcpgateway.middleware.csrf_middleware import CSRFMiddleware


@pytest.mark.asyncio
async def test_get_request_passes_without_token():
    """Test that GET requests pass without CSRF token."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "GET"
    request.url.path = "/api/data"
    request.headers = {}

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings:
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 200
    call_next.assert_awaited_once_with(request)


@pytest.mark.asyncio
async def test_head_request_passes_without_token():
    """Test that HEAD requests pass without CSRF token."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "HEAD"
    request.url.path = "/api/data"
    request.headers = {}

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings:
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 200
    call_next.assert_awaited_once_with(request)


@pytest.mark.asyncio
async def test_post_without_token_returns_403():
    """Test that POST without CSRF token returns 403 with CSRF_TOKEN_INVALID."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/api/data"
    request.headers = {}

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings:
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 403
    assert response.body == b'{"detail":"CSRF validation failed","code":"CSRF_TOKEN_INVALID"}'
    call_next.assert_not_awaited()


@pytest.mark.asyncio
async def test_post_with_invalid_token_returns_403():
    """Test that POST with invalid CSRF token returns 403 with CSRF_TOKEN_INVALID."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/api/data"
    request.headers = {"X-CSRF-Token": "invalid_token"}
    request.state = MagicMock()
    request.state.user = MagicMock(email="user@example.com")
    request.state.jti = "session123"
    request.cookies = {"session_id": "session123", "csrf_token": "invalid_token"}

    mock_csrf_service = MagicMock()
    mock_csrf_service.validate_csrf_token.return_value = False

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings, patch("mcpgateway.middleware.csrf_middleware.get_csrf_service", return_value=mock_csrf_service):
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"
        mock_settings.csrf_check_referer = False
        mock_settings.csrf_cookie_name = "csrf_token"

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 403
    assert response.body == b'{"detail":"CSRF validation failed","code":"CSRF_TOKEN_INVALID"}'
    call_next.assert_not_awaited()


@pytest.mark.asyncio
async def test_post_with_missing_cookie_returns_403():
    """Test that POST with no CSRF cookie returns 403."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/api/data"
    request.headers = {"X-CSRF-Token": "valid_token"}
    request.state = MagicMock()
    request.state.user = MagicMock(email="user@example.com")
    request.state.jti = "session123"
    request.cookies = {"session_id": "session123"}

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings:
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"
        mock_settings.csrf_check_referer = False
        mock_settings.csrf_cookie_name = "csrf_token"

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 403
    assert response.body == b'{"detail":"CSRF validation failed","code":"CSRF_TOKEN_INVALID"}'
    call_next.assert_not_awaited()


@pytest.mark.asyncio
async def test_post_with_mismatched_cookie_returns_403():
    """Test that POST with mismatched CSRF cookie returns 403."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/api/data"
    request.headers = {"X-CSRF-Token": "valid_token"}
    request.state = MagicMock()
    request.state.user = MagicMock(email="user@example.com")
    request.state.jti = "session123"
    request.cookies = {"session_id": "session123", "csrf_token": "different_token"}

    mock_csrf_service = MagicMock()

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings, patch("mcpgateway.middleware.csrf_middleware.get_csrf_service", return_value=mock_csrf_service):
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"
        mock_settings.csrf_check_referer = False
        mock_settings.csrf_cookie_name = "csrf_token"

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 403
    assert response.body == b'{"detail":"CSRF validation failed","code":"CSRF_TOKEN_INVALID"}'
    call_next.assert_not_awaited()


@pytest.mark.asyncio
async def test_post_with_valid_token_succeeds():
    """Test that POST with valid CSRF token succeeds."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/api/data"
    request.headers = {"X-CSRF-Token": "valid_token"}
    request.state = MagicMock()
    request.state.user = MagicMock(email="user@example.com")
    request.state.jti = "session123"
    request.cookies = {"session_id": "session123", "csrf_token": "valid_token"}

    mock_csrf_service = MagicMock()
    mock_csrf_service.validate_csrf_token.return_value = True

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings, patch("mcpgateway.middleware.csrf_middleware.get_csrf_service", return_value=mock_csrf_service):
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"
        mock_settings.csrf_check_referer = False
        mock_settings.csrf_cookie_name = "csrf_token"

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 200
    call_next.assert_awaited_once_with(request)


@pytest.mark.asyncio
async def test_exempt_path_passes_without_token():
    """Test that requests to exempt paths pass without CSRF token."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/auth/login"
    request.headers = {}

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings:
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = ["/auth/login", "/auth/register"]

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 200
    call_next.assert_awaited_once_with(request)


@pytest.mark.asyncio
async def test_bearer_token_request_passes_without_csrf():
    """Test that requests with Authorization: Bearer header pass without CSRF token."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/api/data"
    request.headers = {"authorization": "Bearer abc123token"}

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings:
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 200
    call_next.assert_awaited_once_with(request)


@pytest.mark.asyncio
async def test_csrf_disabled_allows_all_requests():
    """Test that CSRF_ENABLED=False allows all requests through."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/api/data"
    request.headers = {}

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings:
        mock_settings.csrf_enabled = False

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 200
    call_next.assert_awaited_once_with(request)


@pytest.mark.asyncio
async def test_referer_matches_trusted_origin_passes():
    """Test that request with matching referer passes."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/api/data"
    request.headers = {"X-CSRF-Token": "valid_token", "referer": "https://example.com/page"}
    request.state = MagicMock()
    request.state.user = MagicMock(email="user@example.com")
    request.state.jti = "session123"
    request.cookies = {"session_id": "session123", "csrf_token": "valid_token"}

    mock_csrf_service = MagicMock()
    mock_csrf_service.validate_csrf_token.return_value = True

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings, patch("mcpgateway.middleware.csrf_middleware.get_csrf_service", return_value=mock_csrf_service):
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"
        mock_settings.csrf_check_referer = True
        mock_settings.app_domain = "https://example.com"
        mock_settings.csrf_trusted_origins = set()
        mock_settings.csrf_cookie_name = "csrf_token"

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 200
    call_next.assert_awaited_once_with(request)


@pytest.mark.asyncio
async def test_referer_wrong_domain_returns_403():
    """Test that request with wrong referer domain returns 403."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/api/data"
    request.headers = {"X-CSRF-Token": "valid_token", "referer": "https://evil.com/page"}
    request.state = MagicMock()
    request.state.user = MagicMock(email="user@example.com")
    request.state.jti = "session123"
    request.cookies = {"session_id": "session123", "csrf_token": "valid_token"}

    mock_csrf_service = MagicMock()
    mock_csrf_service.validate_csrf_token.return_value = True

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings, patch("mcpgateway.middleware.csrf_middleware.get_csrf_service", return_value=mock_csrf_service):
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"
        mock_settings.csrf_check_referer = True
        mock_settings.app_domain = "https://example.com"
        mock_settings.csrf_trusted_origins = set()
        mock_settings.csrf_cookie_name = "csrf_token"

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 403
    assert b"CSRF_TOKEN_INVALID" in response.body
    call_next.assert_not_awaited()


@pytest.mark.asyncio
async def test_referer_absent_passes():
    """Test that request without referer header passes (do NOT block on absence)."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/api/data"
    request.headers = {"X-CSRF-Token": "valid_token"}
    request.state = MagicMock()
    request.state.user = MagicMock(email="user@example.com")
    request.state.jti = "session123"
    request.cookies = {"session_id": "session123", "csrf_token": "valid_token"}

    mock_csrf_service = MagicMock()
    mock_csrf_service.validate_csrf_token.return_value = True

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings, patch("mcpgateway.middleware.csrf_middleware.get_csrf_service", return_value=mock_csrf_service):
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"
        mock_settings.csrf_check_referer = True
        mock_settings.app_domain = "https://example.com"
        mock_settings.csrf_trusted_origins = set()
        mock_settings.csrf_cookie_name = "csrf_token"

        response = await middleware.dispatch(request, call_next)

    # Middleware fails closed when referer/origin header is absent
    assert response.status_code == 403
    assert b"CSRF_TOKEN_INVALID" in response.body
    call_next.assert_not_awaited()


@pytest.mark.asyncio
async def test_no_user_context_returns_403():
    """Test that request without user context returns 403."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/api/data"
    request.headers = {"X-CSRF-Token": "some_token"}
    request.state = MagicMock()
    request.state.user = None  # No user context
    request.state.jti = None
    request.cookies = {}

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings:
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 403
    assert response.body == b'{"detail":"CSRF validation failed","code":"CSRF_TOKEN_INVALID"}'
    call_next.assert_not_awaited()


@pytest.mark.asyncio
async def test_put_request_requires_csrf_token():
    """Test that PUT requests require CSRF token."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "PUT"
    request.url.path = "/api/data/123"
    request.headers = {}

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings:
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 403
    assert response.body == b'{"detail":"CSRF validation failed","code":"CSRF_TOKEN_INVALID"}'


@pytest.mark.asyncio
async def test_delete_request_requires_csrf_token():
    """Test that DELETE requests require CSRF token."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "DELETE"
    request.url.path = "/api/data/123"
    request.headers = {}

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings:
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 403
    assert response.body == b'{"detail":"CSRF validation failed","code":"CSRF_TOKEN_INVALID"}'


@pytest.mark.asyncio
async def test_patch_request_requires_csrf_token():
    """Test that PATCH requests require CSRF token."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "PATCH"
    request.url.path = "/api/data/123"
    request.headers = {}

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings:
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 403
    assert response.body == b'{"detail":"CSRF validation failed","code":"CSRF_TOKEN_INVALID"}'


@pytest.mark.asyncio
async def test_options_request_passes_without_token():
    """Test that OPTIONS requests pass without CSRF token."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "OPTIONS"
    request.url.path = "/api/data"
    request.headers = {}

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings:
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 200
    call_next.assert_awaited_once_with(request)


@pytest.mark.asyncio
async def test_session_id_from_header():
    """Test that session_id can be extracted from X-Session-ID header."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/api/data"
    request.headers = {
        "X-CSRF-Token": "valid_token",
    }
    request.state = MagicMock()
    request.state.user = MagicMock(email="user@example.com")
    request.state.jti = "header_session_123"
    request.cookies = {"csrf_token": "valid_token"}

    mock_csrf_service = MagicMock()
    mock_csrf_service.validate_csrf_token.return_value = True

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings, patch("mcpgateway.middleware.csrf_middleware.get_csrf_service", return_value=mock_csrf_service):
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"
        mock_settings.csrf_check_referer = False
        mock_settings.csrf_cookie_name = "csrf_token"

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 200
    # Verify session_id from JWT context was used
    mock_csrf_service.validate_csrf_token.assert_called_once_with("valid_token", "user@example.com", "header_session_123")


@pytest.mark.asyncio
async def test_origin_header_used_when_referer_absent():
    """Test that Origin header is used when Referer is absent."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/api/data"
    request.headers = {"X-CSRF-Token": "valid_token", "origin": "https://example.com"}
    request.state = MagicMock()
    request.state.user = MagicMock(email="user@example.com")
    request.state.jti = "session123"
    request.cookies = {"session_id": "session123", "csrf_token": "valid_token"}

    mock_csrf_service = MagicMock()
    mock_csrf_service.validate_csrf_token.return_value = True

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings, patch("mcpgateway.middleware.csrf_middleware.get_csrf_service", return_value=mock_csrf_service):
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"
        mock_settings.csrf_check_referer = True
        mock_settings.app_domain = "https://example.com"
        mock_settings.csrf_trusted_origins = set()
        mock_settings.csrf_cookie_name = "csrf_token"

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 200
    call_next.assert_awaited_once_with(request)


@pytest.mark.asyncio
async def test_trusted_origins_accepted():
    """Test that requests from trusted origins are accepted."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/api/data"
    request.headers = {"X-CSRF-Token": "valid_token", "referer": "https://trusted.com/page"}
    request.state = MagicMock()
    request.state.user = MagicMock(email="user@example.com")
    request.state.jti = "session123"
    request.cookies = {"session_id": "session123", "csrf_token": "valid_token"}

    mock_csrf_service = MagicMock()
    mock_csrf_service.validate_csrf_token.return_value = True

    with patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings, patch("mcpgateway.middleware.csrf_middleware.get_csrf_service", return_value=mock_csrf_service):
        mock_settings.csrf_enabled = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"
        mock_settings.csrf_check_referer = True
        mock_settings.app_domain = "https://example.com"
        mock_settings.csrf_trusted_origins = {"https://trusted.com"}
        mock_settings.csrf_cookie_name = "csrf_token"

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 200
    call_next.assert_awaited_once_with(request)


@pytest.mark.asyncio
async def test_csrf_fallback_jwt_context_from_cookie_succeeds():
    """When request.state context is missing, middleware can derive user/jti from verified JWT cookie."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/admin/change-password-required"
    request.headers = {"X-CSRF-Token": "valid_token"}
    request.state = MagicMock()
    request.state.user = None
    request.state.jti = None
    request.cookies = {"jwt_token": "jwt_cookie_token", "csrf_token": "valid_token"}

    mock_csrf_service = MagicMock()
    mock_csrf_service.validate_csrf_token.return_value = True

    with (
        patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings,
        patch("mcpgateway.middleware.csrf_middleware.get_csrf_service", return_value=mock_csrf_service),
        patch(
            "mcpgateway.middleware.csrf_middleware.verify_jwt_token_cached",
            AsyncMock(return_value={"sub": "admin@example.com", "jti": "session-jti-1"}),
        ),
    ):
        mock_settings.csrf_enabled = True
        mock_settings.auth_required = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"
        mock_settings.csrf_cookie_name = "csrf_token"
        mock_settings.csrf_check_referer = False

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 200
    mock_csrf_service.validate_csrf_token.assert_called_once_with("valid_token", "admin@example.com", "session-jti-1")
    call_next.assert_awaited_once_with(request)


@pytest.mark.asyncio
async def test_csrf_fallback_jwt_verification_failure_returns_403():
    """Missing state context plus invalid JWT should fail closed."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/admin/change-password-required"
    request.headers = {"X-CSRF-Token": "valid_token"}
    request.state = MagicMock()
    request.state.user = None
    request.state.jti = None
    request.cookies = {"jwt_token": "bad_jwt_token", "csrf_token": "valid_token"}

    with (
        patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings,
        patch("mcpgateway.middleware.csrf_middleware.verify_jwt_token_cached", AsyncMock(side_effect=Exception("invalid token"))),
    ):
        mock_settings.csrf_enabled = True
        mock_settings.auth_required = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"
        mock_settings.csrf_cookie_name = "csrf_token"
        mock_settings.csrf_check_referer = False

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 403
    assert response.body == b'{"detail":"CSRF validation failed","code":"CSRF_TOKEN_INVALID"}'
    call_next.assert_not_awaited()


@pytest.mark.asyncio
async def test_csrf_form_post_without_header_is_rejected_without_consuming_body():
    """Form posts must use the CSRF header so middleware does not consume bodies."""
    middleware = CSRFMiddleware(app=AsyncMock())
    call_next = AsyncMock(return_value=Response("ok", status_code=200))

    request = MagicMock(spec=Request)
    request.method = "POST"
    request.url.path = "/admin/change-password-required"
    request.headers = {"content-type": "application/x-www-form-urlencoded"}
    request.body = AsyncMock(return_value=b"csrf_token=valid_token&current_password=a&new_password=b&confirm_password=b")
    request.state = MagicMock()
    request.state.user = None
    request.state.jti = None
    request.cookies = {"jwt_token": "jwt_cookie_token", "csrf_token": "valid_token"}

    mock_csrf_service = MagicMock()
    mock_csrf_service.validate_csrf_token.return_value = True

    with (
        patch("mcpgateway.middleware.csrf_middleware.settings") as mock_settings,
        patch("mcpgateway.middleware.csrf_middleware.get_csrf_service", return_value=mock_csrf_service),
        patch(
            "mcpgateway.middleware.csrf_middleware.verify_jwt_token_cached",
            AsyncMock(return_value={"sub": "admin@example.com", "jti": "session-jti-2"}),
        ),
    ):
        mock_settings.csrf_enabled = True
        mock_settings.auth_required = True
        mock_settings.csrf_exempt_paths = []
        mock_settings.csrf_token_name = "X-CSRF-Token"
        mock_settings.csrf_cookie_name = "csrf_token"
        mock_settings.csrf_check_referer = False

        response = await middleware.dispatch(request, call_next)

    assert response.status_code == 403
    request.body.assert_not_awaited()
    mock_csrf_service.validate_csrf_token.assert_not_called()
    call_next.assert_not_awaited()
