# tests/unit/middleware/test_language.py
"""
Unit tests for LanguageMiddleware, set_language_cookie, and delete_language_cookie.

Tests cover:
- Admin frontend: user pref used, falls back to "en"
- Store dashboard: calls resolve_store_dashboard_language, no-store fallback
- Storefront: calls resolve_storefront_language, no-store fallback
- General: platform frontend, fallback/unknown type, unsupported language
  validation, request.state.language set, request.state.language_info populated,
  cookie language read
- Helpers: _get_user_language_from_token, _get_customer_language_from_token,
  set_language_cookie (valid/invalid), delete_language_cookie
- Private methods: user with pref, user without pref, no user, customer with pref
"""

from unittest.mock import AsyncMock, Mock, patch

import pytest
from starlette.requests import Request
from starlette.responses import Response

from app.modules.enums import FrontendType
from middleware.language import (
    LanguageMiddleware,
    delete_language_cookie,
    set_language_cookie,
)


def _make_request(
    frontend_type=None,
    store=None,
    cookies=None,
    headers=None,
    current_user=None,
    current_customer=None,
):
    """Build a mock Request with the given state attributes."""
    request = Mock(spec=Request)
    request.state = Mock(spec=[])
    request.cookies = cookies or {}
    request.headers = headers or {}

    if frontend_type is not None:
        request.state.frontend_type = frontend_type
    if store is not None:
        request.state.store = store
    if current_user is not None:
        request.state.current_user = current_user
    if current_customer is not None:
        request.state.current_customer = current_customer

    return request


def _make_call_next():
    """Return (call_next, response) pair."""
    response = Mock(status_code=200, headers={})
    call_next = AsyncMock(return_value=response)
    return call_next, response


# ---------------------------------------------------------------------------
# TestLanguageDispatchAdmin
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestLanguageDispatchAdmin:
    """Admin frontend language resolution."""

    @pytest.mark.asyncio
    async def test_admin_user_preferred_language_used(self):
        """Admin: user's preferred_language is used when set."""
        middleware = LanguageMiddleware(app=None)
        user = Mock()
        user.preferred_language = "de"
        request = _make_request(
            frontend_type=FrontendType.ADMIN,
            current_user=user,
        )
        call_next, _ = _make_call_next()

        with patch("middleware.language.parse_accept_language", return_value=None):
            await middleware.dispatch(request, call_next)

        assert request.state.language == "de"

    @pytest.mark.asyncio
    async def test_admin_falls_back_to_en(self):
        """Admin: falls back to 'en' when no user preference."""
        middleware = LanguageMiddleware(app=None)
        request = _make_request(frontend_type=FrontendType.ADMIN)
        call_next, _ = _make_call_next()

        with patch("middleware.language.parse_accept_language", return_value=None):
            await middleware.dispatch(request, call_next)

        assert request.state.language == "en"


# ---------------------------------------------------------------------------
# TestLanguageDispatchStore
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestLanguageDispatchStore:
    """Store dashboard language resolution."""

    @pytest.mark.asyncio
    async def test_store_calls_resolve_store_dashboard_language(self):
        """Store: resolve_store_dashboard_language is called with correct args."""
        middleware = LanguageMiddleware(app=None)
        user = Mock()
        user.preferred_language = "de"
        store = Mock()
        store.dashboard_language = "fr"
        request = _make_request(
            frontend_type=FrontendType.STORE,
            store=store,
            current_user=user,
        )
        call_next, _ = _make_call_next()

        with (
            patch("middleware.language.parse_accept_language", return_value=None),
            patch(
                "middleware.language.resolve_store_dashboard_language",
                return_value="de",
            ) as mock_resolve,
        ):
            await middleware.dispatch(request, call_next)

        mock_resolve.assert_called_once_with(
            user_preferred="de",
            store_dashboard="fr",
            cookie_language=None,
        )
        assert request.state.language == "de"

    @pytest.mark.asyncio
    async def test_store_no_store_passes_none_dashboard(self):
        """Store: store_dashboard is None when request.state has no store."""
        middleware = LanguageMiddleware(app=None)
        request = _make_request(frontend_type=FrontendType.STORE)
        call_next, _ = _make_call_next()

        with (
            patch("middleware.language.parse_accept_language", return_value=None),
            patch(
                "middleware.language.resolve_store_dashboard_language",
                return_value="fr",
            ) as mock_resolve,
        ):
            await middleware.dispatch(request, call_next)

        mock_resolve.assert_called_once_with(
            user_preferred=None,
            store_dashboard=None,
            cookie_language=None,
        )


# ---------------------------------------------------------------------------
# TestLanguageDispatchStorefront
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestLanguageDispatchStorefront:
    """Storefront language resolution."""

    @pytest.mark.asyncio
    async def test_storefront_calls_resolve_storefront_language(self):
        """Storefront: resolve_storefront_language called with correct args."""
        middleware = LanguageMiddleware(app=None)
        customer = Mock()
        customer.preferred_language = "lb"
        store = Mock()
        store.storefront_language = "fr"
        store.storefront_languages = ["fr", "de", "lb"]
        request = _make_request(
            frontend_type=FrontendType.STOREFRONT,
            store=store,
            current_customer=customer,
            cookies={"lang": "de"},
            headers={"accept-language": "en-US,en;q=0.9"},
        )
        call_next, _ = _make_call_next()

        with (
            patch(
                "middleware.language.parse_accept_language", return_value="en"
            ),
            patch(
                "middleware.language.resolve_storefront_language",
                return_value="lb",
            ) as mock_resolve,
        ):
            await middleware.dispatch(request, call_next)

        mock_resolve.assert_called_once_with(
            customer_preferred="lb",
            session_language="de",
            store_storefront="fr",
            browser_language="en",
            enabled_languages=["fr", "de", "lb"],
        )
        assert request.state.language == "lb"

    @pytest.mark.asyncio
    async def test_storefront_no_store_passes_none(self):
        """Storefront: store fields are None when no store in state."""
        middleware = LanguageMiddleware(app=None)
        request = _make_request(frontend_type=FrontendType.STOREFRONT)
        call_next, _ = _make_call_next()

        with (
            patch("middleware.language.parse_accept_language", return_value=None),
            patch(
                "middleware.language.resolve_storefront_language",
                return_value="fr",
            ) as mock_resolve,
        ):
            await middleware.dispatch(request, call_next)

        mock_resolve.assert_called_once_with(
            customer_preferred=None,
            session_language=None,
            store_storefront=None,
            browser_language=None,
            enabled_languages=None,
        )


# ---------------------------------------------------------------------------
# TestLanguageDispatchGeneral
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestLanguageDispatchGeneral:
    """Platform, fallback/unknown, validation, state, and cookie tests."""

    @pytest.mark.asyncio
    async def test_platform_uses_cookie_language(self):
        """Platform: cookie language is used first."""
        middleware = LanguageMiddleware(app=None)
        request = _make_request(
            frontend_type=FrontendType.PLATFORM,
            cookies={"lang": "de"},
        )
        call_next, _ = _make_call_next()

        with patch("middleware.language.parse_accept_language", return_value="en"):
            await middleware.dispatch(request, call_next)

        assert request.state.language == "de"

    @pytest.mark.asyncio
    async def test_platform_falls_back_to_browser_language(self):
        """Platform: browser language used when no cookie."""
        middleware = LanguageMiddleware(app=None)
        request = _make_request(frontend_type=FrontendType.PLATFORM)
        call_next, _ = _make_call_next()

        with patch("middleware.language.parse_accept_language", return_value="de"):
            await middleware.dispatch(request, call_next)

        assert request.state.language == "de"

    @pytest.mark.asyncio
    async def test_fallback_unknown_frontend_type(self):
        """Unknown/None frontend type uses cookie → browser → default."""
        middleware = LanguageMiddleware(app=None)
        request = _make_request()  # no frontend_type
        call_next, _ = _make_call_next()

        with patch("middleware.language.parse_accept_language", return_value=None):
            await middleware.dispatch(request, call_next)

        # DEFAULT_LANGUAGE is "fr"
        assert request.state.language == "fr"

    @pytest.mark.asyncio
    async def test_unsupported_language_falls_back_to_default(self):
        """Unsupported language code is replaced by DEFAULT_LANGUAGE."""
        middleware = LanguageMiddleware(app=None)
        request = _make_request(
            frontend_type=FrontendType.PLATFORM,
            cookies={"lang": "xx"},  # unsupported
        )
        call_next, _ = _make_call_next()

        with patch("middleware.language.parse_accept_language", return_value=None):
            await middleware.dispatch(request, call_next)

        assert request.state.language == "fr"

    @pytest.mark.asyncio
    async def test_request_state_language_is_set(self):
        """request.state.language is always set after dispatch."""
        middleware = LanguageMiddleware(app=None)
        request = _make_request(frontend_type=FrontendType.PLATFORM)
        call_next, _ = _make_call_next()

        with patch("middleware.language.parse_accept_language", return_value="en"):
            await middleware.dispatch(request, call_next)

        assert hasattr(request.state, "language")
        assert request.state.language == "en"

    @pytest.mark.asyncio
    async def test_request_state_language_info_populated(self):
        """request.state.language_info contains code, cookie, browser, frontend_type."""
        middleware = LanguageMiddleware(app=None)
        request = _make_request(
            frontend_type=FrontendType.PLATFORM,
            cookies={"lang": "en"},
            headers={"accept-language": "de"},
        )
        call_next, _ = _make_call_next()

        with patch("middleware.language.parse_accept_language", return_value="de"):
            await middleware.dispatch(request, call_next)

        info = request.state.language_info
        assert info["code"] == "en"
        assert info["cookie"] == "en"
        assert info["browser"] == "de"
        assert info["frontend_type"] == "platform"

    @pytest.mark.asyncio
    async def test_cookie_language_read_from_request(self):
        """The 'lang' cookie is read and considered in resolution."""
        middleware = LanguageMiddleware(app=None)
        request = _make_request(
            frontend_type=FrontendType.PLATFORM,
            cookies={"lang": "lb"},
        )
        call_next, _ = _make_call_next()

        with patch("middleware.language.parse_accept_language", return_value=None):
            await middleware.dispatch(request, call_next)

        assert request.state.language == "lb"


# ---------------------------------------------------------------------------
# TestLanguageHelpers
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestLanguageHelpers:
    """Tests for set_language_cookie and delete_language_cookie."""

    def test_set_language_cookie_valid(self):
        """set_language_cookie sets cookie for supported language."""
        response = Mock(spec=Response)
        result = set_language_cookie(response, "de")

        response.set_cookie.assert_called_once_with(
            key="lang",
            value="de",
            max_age=60 * 60 * 24 * 365,
            httponly=False,
            samesite="lax",
            path="/",
        )
        assert result is response

    def test_set_language_cookie_invalid_language(self):
        """set_language_cookie does NOT call set_cookie for unsupported lang."""
        response = Mock(spec=Response)
        result = set_language_cookie(response, "xx")

        response.set_cookie.assert_not_called()
        assert result is response

    def test_delete_language_cookie(self):
        """delete_language_cookie calls delete_cookie with correct key."""
        response = Mock(spec=Response)
        result = delete_language_cookie(response)

        response.delete_cookie.assert_called_once_with(key="lang", path="/")
        assert result is response

    def test_get_user_language_from_token_with_pref(self):
        """_get_user_language_from_token returns preferred_language."""
        middleware = LanguageMiddleware(app=None)
        request = Mock(spec=Request)
        request.state = Mock()
        request.state.current_user = Mock()
        request.state.current_user.preferred_language = "de"

        assert middleware._get_user_language_from_token(request) == "de"

    def test_get_user_language_from_token_no_user(self):
        """_get_user_language_from_token returns None with no user."""
        middleware = LanguageMiddleware(app=None)
        request = Mock(spec=Request)
        request.state = Mock(spec=[])  # no current_user

        assert middleware._get_user_language_from_token(request) is None

    def test_get_customer_language_from_token_with_pref(self):
        """_get_customer_language_from_token returns preferred_language."""
        middleware = LanguageMiddleware(app=None)
        request = Mock(spec=Request)
        request.state = Mock()
        request.state.current_customer = Mock()
        request.state.current_customer.preferred_language = "lb"

        assert middleware._get_customer_language_from_token(request) == "lb"


# ---------------------------------------------------------------------------
# TestLanguageDispatchPrivateMethods
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestLanguageDispatchPrivateMethods:
    """Integration-style tests exercising private helpers through dispatch."""

    @pytest.mark.asyncio
    async def test_admin_user_with_preferred_language(self):
        """Admin + user with pref → uses that preference."""
        middleware = LanguageMiddleware(app=None)
        user = Mock()
        user.preferred_language = "lb"
        request = _make_request(
            frontend_type=FrontendType.ADMIN,
            current_user=user,
        )
        call_next, _ = _make_call_next()

        with patch("middleware.language.parse_accept_language", return_value=None):
            await middleware.dispatch(request, call_next)

        assert request.state.language == "lb"

    @pytest.mark.asyncio
    async def test_admin_user_without_preferred_language(self):
        """Admin + user without preferred_language → falls back to 'en'."""
        middleware = LanguageMiddleware(app=None)
        user = Mock(spec=[])  # no preferred_language attr
        request = _make_request(
            frontend_type=FrontendType.ADMIN,
            current_user=user,
        )
        call_next, _ = _make_call_next()

        with patch("middleware.language.parse_accept_language", return_value=None):
            await middleware.dispatch(request, call_next)

        assert request.state.language == "en"

    @pytest.mark.asyncio
    async def test_admin_no_user_at_all(self):
        """Admin + no user at all → falls back to 'en'."""
        middleware = LanguageMiddleware(app=None)
        request = _make_request(frontend_type=FrontendType.ADMIN)
        call_next, _ = _make_call_next()

        with patch("middleware.language.parse_accept_language", return_value=None):
            await middleware.dispatch(request, call_next)

        assert request.state.language == "en"

    @pytest.mark.asyncio
    async def test_storefront_customer_with_preferred_language(self):
        """Storefront + customer with pref → passed to resolve function."""
        middleware = LanguageMiddleware(app=None)
        customer = Mock()
        customer.preferred_language = "de"
        store = Mock()
        store.storefront_language = "fr"
        store.storefront_languages = ["fr", "de"]
        request = _make_request(
            frontend_type=FrontendType.STOREFRONT,
            store=store,
            current_customer=customer,
        )
        call_next, _ = _make_call_next()

        with (
            patch("middleware.language.parse_accept_language", return_value=None),
            patch(
                "middleware.language.resolve_storefront_language",
                return_value="de",
            ) as mock_resolve,
        ):
            await middleware.dispatch(request, call_next)

        mock_resolve.assert_called_once_with(
            customer_preferred="de",
            session_language=None,
            store_storefront="fr",
            browser_language=None,
            enabled_languages=["fr", "de"],
        )
        assert request.state.language == "de"
