# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""Tests for the TokenLastSeenAtMiddleware."""

from collections.abc import Mapping
from typing import Any, TYPE_CHECKING, cast

from asgiref.typing import (
    ASGIReceiveCallable,
    ASGISendCallable,
    WebSocketReceiveEvent,
)
from channels.db import database_sync_to_async
from django.utils import timezone

from debusine.db.context import context
from debusine.db.models import WorkRequest
from debusine.db.playground import scenarios
from debusine.server.consumers import ChannelScopeWithToken
from debusine.server.middlewares.token_last_seen_at import (
    TokenLastSeenAtMiddlewareChannels,
)
from debusine.test.django import TransactionTestCase

if TYPE_CHECKING:
    from channels.consumer import _ChannelScope


class TokenLastSeenAtMiddlewareChannelsTests(TransactionTestCase):
    """Test TokenLastSeenAtMiddlewareChannels."""

    scenario = scenarios.DefaultScopeUser()

    @classmethod
    async def _noop_receive(cls) -> WebSocketReceiveEvent:  # pragma: no cover
        """No-op receive callback for testing."""
        return WebSocketReceiveEvent(
            type="websocket.receive", bytes=None, text=""
        )

    @classmethod
    async def _noop_send(cls, message: Mapping[str, Any]) -> None:
        """No-op send callback for testing."""

    async def test_update_last_seen_at_store_user_token(self) -> None:
        """Request with a token updates token's last_seen_at."""
        token = await database_sync_to_async(
            self.playground.create_user_token
        )()
        assert token is not None

        self.assertIsNone(token.last_seen_at)

        before = timezone.now()

        scope_upstream = cast(
            "_ChannelScope",
            {
                "type": "websocket",
                "headers": [(b"token", token.key.encode("utf-8"))],
            },
        )

        async def mock_app(
            scope_downstream: ChannelScopeWithToken,
            receive: ASGIReceiveCallable,  # noqa: ARG001, U100
            send: ASGISendCallable,  # noqa: ARG001, U100
        ) -> None:
            """Mock ASGI app to inspect the modified scope."""
            await token.arefresh_from_db()
            self.assertEqual(scope_downstream["token"], token)
            self.assertIsNone(context._permission_context.get())
            # self.assertEqual(context.worker_token, token)
            assert token.last_seen_at is not None
            self.assertGreaterEqual(token.last_seen_at, before)
            self.assertLess(token.last_seen_at, timezone.now())

        middleware = TokenLastSeenAtMiddlewareChannels(app=mock_app)

        await middleware(scope_upstream, self._noop_receive, self._noop_send)

    async def test_update_last_seen_at_store_worker_token(self) -> None:
        """Request with a token updates token's last_seen_at."""
        token = await database_sync_to_async(
            self.playground.create_worker_token
        )()
        assert token is not None

        self.assertIsNone(token.last_seen_at)

        before = timezone.now()

        scope_upstream = cast(
            "_ChannelScope",
            {
                "type": "websocket",
                "headers": [(b"token", token.key.encode("utf-8"))],
            },
        )

        async def mock_app(
            scope_downstream: ChannelScopeWithToken,
            receive: ASGIReceiveCallable,  # noqa: ARG001, U100
            send: ASGISendCallable,  # noqa: ARG001, U100
        ) -> None:
            """Mock ASGI app to inspect the modified scope."""
            await token.arefresh_from_db()
            self.assertEqual(scope_downstream["token"], token)
            self.assertEqual(context.pc.worker_token, token)
            assert token.last_seen_at is not None
            self.assertGreaterEqual(token.last_seen_at, before)
            self.assertLess(token.last_seen_at, timezone.now())

        middleware = TokenLastSeenAtMiddlewareChannels(app=mock_app)

        await middleware(scope_upstream, self._noop_receive, self._noop_send)

    async def test_update_last_seen_at_store_work_request_token(self) -> None:
        """Request with a work request token updates token's last_seen_at."""
        token = await database_sync_to_async(
            self.playground.create_work_request_token
        )()
        assert token is not None

        self.assertIsNone(token.last_seen_at)

        before = timezone.now()

        scope_upstream = cast(
            "_ChannelScope",
            {
                "type": "websocket",
                "headers": [(b"token", token.key.encode("utf-8"))],
            },
        )

        async def mock_app(
            scope_downstream: ChannelScopeWithToken,
            receive: ASGIReceiveCallable,  # noqa: ARG001, U100
            send: ASGISendCallable,  # noqa: ARG001, U100
        ) -> None:
            """Mock ASGI app to inspect the modified scope."""
            await token.arefresh_from_db()
            self.assertEqual(scope_downstream["token"], token)
            self.assertEqual(context.pc.work_request_token, token)
            assert token.last_seen_at is not None
            self.assertGreaterEqual(token.last_seen_at, before)
            self.assertLess(token.last_seen_at, timezone.now())

        middleware = TokenLastSeenAtMiddlewareChannels(app=mock_app)

        await middleware(scope_upstream, self._noop_receive, self._noop_send)

    async def test_request_without_token(self) -> None:
        """Request without a token."""
        scope_upstream = cast(
            "_ChannelScope",
            {"type": "websocket", "headers": [(b"user-agent", b"foo")]},
        )

        async def mock_app(
            scope_downstream: ChannelScopeWithToken,
            receive: ASGIReceiveCallable,  # noqa: ARG001, U100
            send: ASGISendCallable,  # noqa: ARG001, U100
        ) -> None:
            """Mock ASGI app to inspect the modified scope."""
            self.assertIsNone(scope_downstream["token"])
            self.assertIsNone(context._permission_context.get())

        middleware = TokenLastSeenAtMiddlewareChannels(app=mock_app)

        await middleware(scope_upstream, self._noop_receive, self._noop_send)

    async def test_request_token_key_does_not_exist(self) -> None:
        """Request where the Token key in the header does not exist."""
        scope_upstream = cast(
            "_ChannelScope",
            {"type": "websocket", "headers": [(b"token", b"DOES-NOT-EXIST")]},
        )

        async def mock_app(
            scope_downstream: ChannelScopeWithToken,
            receive: ASGIReceiveCallable,  # noqa: ARG001, U100
            send: ASGISendCallable,  # noqa: ARG001, U100
        ) -> None:
            """Mock ASGI app to inspect the modified scope."""
            self.assertFalse(hasattr(scope_downstream, "token"))
            self.assertIsNone(context._permission_context.get())

        middleware = TokenLastSeenAtMiddlewareChannels(app=mock_app)
        await middleware(scope_upstream, self._noop_receive, self._noop_send)

    async def test_request_token_disabled(self) -> None:
        """Request with a disabled Token."""
        token = await database_sync_to_async(
            self.playground.create_worker_token
        )(enabled=False)

        scope_upstream = cast(
            "_ChannelScope",
            {
                "type": "websocket",
                "headers": [(b"token", token.key.encode("utf-8"))],
            },
        )

        async def mock_app(
            scope_downstream: ChannelScopeWithToken,
            receive: ASGIReceiveCallable,  # noqa: ARG001, U100
            send: ASGISendCallable,  # noqa: ARG001, U100
        ) -> None:
            """Mock ASGI app to inspect the modified scope."""
            self.assertEqual(scope_downstream["token"], token)
            self.assertIsNone(context._permission_context.get())

        middleware = TokenLastSeenAtMiddlewareChannels(app=mock_app)
        await middleware(scope_upstream, self._noop_receive, self._noop_send)

    async def test_work_request_token_invalid_state(self) -> None:
        token = await database_sync_to_async(
            self.playground.create_work_request_token
        )()
        assert token is not None

        def set_blocked() -> None:
            assert token.work_request is not None
            token.work_request.status = WorkRequest.Statuses.BLOCKED
            token.work_request.save()

        await database_sync_to_async(set_blocked)()

        self.assertIsNone(token.last_seen_at)

        before = timezone.now()

        scope_upstream = cast(
            "_ChannelScope",
            {
                "type": "websocket",
                "headers": [(b"token", token.key.encode("utf-8"))],
            },
        )

        async def mock_app(
            scope_downstream: ChannelScopeWithToken,
            receive: ASGIReceiveCallable,  # noqa: ARG001, U100
            send: ASGISendCallable,  # noqa: ARG001, U100
        ) -> None:
            """Mock ASGI app to inspect the modified scope."""
            await token.arefresh_from_db()
            self.assertEqual(scope_downstream["token"], token)
            self.assertIsNone(context._permission_context.get())
            assert token.last_seen_at is not None
            self.assertGreaterEqual(token.last_seen_at, before)
            self.assertLess(token.last_seen_at, timezone.now())

        middleware = TokenLastSeenAtMiddlewareChannels(app=mock_app)

        await middleware(scope_upstream, self._noop_receive, self._noop_send)
