"""Tests for app.core.csrf — CSRF protection middleware and constant-time compare"""

import pytest
from unittest.mock import AsyncMock, MagicMock


def _mock_response():
    """Helper: create a mock response with set_cookie method."""
    resp = MagicMock()
    resp.set_cookie = MagicMock()
    return resp


@pytest.mark.asyncio
async def test_csrf_non_api_bypass():
    """Non-api/v1/ paths pass through without CSRF check"""
    from app.core.csrf import CSRFProtectionMiddleware

    mock_request = MagicMock()
    mock_request.url.path = "/health"
    mock_request.method = "POST"

    mock_call_next = AsyncMock()
    mock_call_next.return_value = "response"

    mw = CSRFProtectionMiddleware(object())
    result = await mw.dispatch(mock_request, mock_call_next)
    assert result == "response"


@pytest.mark.asyncio
async def test_csrf_safe_methods_bypass():
    """GET/HEAD/OPTIONS pass through and cookie is NOT set when it already exists"""
    from app.core.csrf import CSRFProtectionMiddleware

    for method in ("GET", "HEAD", "OPTIONS"):
        mock_request = MagicMock()
        mock_request.url.path = "/api/v1/some/route"
        mock_request.method = method
        mock_request.cookies = {"csrf_token": "existing"}

        mock_call_next = AsyncMock()
        mock_call_next.return_value = "response"

        mw = CSRFProtectionMiddleware(object())
        result = await mw.dispatch(mock_request, mock_call_next)
        assert result == "response"


@pytest.mark.asyncio
async def test_csrf_safe_methods_sets_cookie_when_missing():
    """GET without csrf_token cookie triggers set_cookie on response"""
    from app.core.csrf import CSRFProtectionMiddleware

    mock_request = MagicMock()
    mock_request.url.path = "/api/v1/some/route"
    mock_request.method = "GET"
    mock_request.cookies = {}

    resp = _mock_response()
    mock_call_next = AsyncMock()
    mock_call_next.return_value = resp

    mw = CSRFProtectionMiddleware(object())
    result = await mw.dispatch(mock_request, mock_call_next)
    assert result is resp
    resp.set_cookie.assert_called_once()
    args, kwargs = resp.set_cookie.call_args
    assert kwargs["key"] == "csrf_token"
    assert kwargs["httponly"] is False  # JS must read it
    assert kwargs["samesite"] == "lax"


@pytest.mark.asyncio
async def test_csrf_exempt_prefixes():
    """Exempt prefix paths bypass CSRF (no 403) and set csrf_token cookie"""
    from app.core.csrf import CSRFProtectionMiddleware

    for path in ("/api/v1/auth/login", "/api/v1/public/tags", "/api/v1/captcha/new", "/api/v1/ws/feed"):
        mock_request = MagicMock()
        mock_request.url.path = path
        mock_request.method = "POST"
        mock_request.headers = {}
        mock_request.cookies = {}

        resp = _mock_response()
        mock_call_next = AsyncMock()
        mock_call_next.return_value = resp

        mw = CSRFProtectionMiddleware(object())
        result = await mw.dispatch(mock_request, mock_call_next)
        assert result is resp
        resp.set_cookie.assert_called_once()


@pytest.mark.asyncio
async def test_csrf_bearer_token_exempt():
    """Bearer token requests bypass CSRF"""
    from app.core.csrf import CSRFProtectionMiddleware

    mock_request = MagicMock()
    mock_request.url.path = "/api/v1/some/post"
    mock_request.method = "POST"
    mock_request.headers = {"Authorization": "Bearer some.jwt.token"}
    mock_request.cookies = {}

    mock_call_next = AsyncMock()
    mock_call_next.return_value = "response"

    mw = CSRFProtectionMiddleware(object())
    result = await mw.dispatch(mock_request, mock_call_next)
    assert result == "response"


@pytest.mark.asyncio
async def test_csrf_missing_tokens_403():
    """No CSRF tokens returns 403"""
    from app.core.csrf import CSRFProtectionMiddleware

    mock_request = MagicMock()
    mock_request.url.path = "/api/v1/some/post"
    mock_request.method = "POST"
    mock_request.headers = {}
    mock_request.cookies = {}

    mock_call_next = AsyncMock()
    mock_call_next.return_value = "response"

    mw = CSRFProtectionMiddleware(object())
    result = await mw.dispatch(mock_request, mock_call_next)
    assert result.status_code == 403


@pytest.mark.asyncio
async def test_csrf_valid_tokens_pass():
    """Matching CSRF tokens pass through"""
    from app.core.csrf import CSRFProtectionMiddleware

    mock_request = MagicMock()
    mock_request.url.path = "/api/v1/some/post"
    mock_request.method = "POST"
    mock_request.headers = {"X-CSRF-Token": "abc123"}
    mock_request.cookies = {"csrf_token": "abc123"}

    mock_call_next = AsyncMock()
    mock_call_next.return_value = "response"

    mw = CSRFProtectionMiddleware(object())
    result = await mw.dispatch(mock_request, mock_call_next)
    assert result == "response"


@pytest.mark.asyncio
async def test_csrf_mismatched_tokens_403():
    """Mismatched CSRF tokens return 403"""
    from app.core.csrf import CSRFProtectionMiddleware

    mock_request = MagicMock()
    mock_request.url.path = "/api/v1/some/post"
    mock_request.method = "POST"
    mock_request.headers = {"X-CSRF-Token": "token_a"}
    mock_request.cookies = {"csrf_token": "token_b"}

    mock_call_next = AsyncMock()
    mock_call_next.return_value = "response"

    mw = CSRFProtectionMiddleware(object())
    result = await mw.dispatch(mock_request, mock_call_next)
    assert result.status_code == 403


def test_constant_time_compare_equal():
    from app.core.csrf import _constant_time_compare
    assert _constant_time_compare("abc123", "abc123") is True


def test_constant_time_compare_not_equal():
    from app.core.csrf import _constant_time_compare
    assert _constant_time_compare("abc123", "xyz789") is False


def test_constant_time_compare_diff_length():
    from app.core.csrf import _constant_time_compare
    assert _constant_time_compare("abc", "abcdef") is False


def test_constant_time_compare_empty():
    from app.core.csrf import _constant_time_compare
    assert _constant_time_compare("", "") is True
    assert _constant_time_compare("", "a") is False
