# Copyright 2024 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 typing import Any
from unittest.mock import Mock

from channels.db import database_sync_to_async
from django.http import HttpResponse
from django.test import RequestFactory
from django.utils import timezone
from rest_framework import status

from debusine.db.context import context
from debusine.server.middlewares.token_last_seen_at import (
    TokenLastSeenAtMiddleware,
    TokenLastSeenAtMiddlewareChannels,
)
from debusine.test.django import TestCase, TransactionTestCase


class TokenLastSeenAtMiddlewareTests(TestCase):
    """Test TokenLastSeenAtMiddleware."""

    def setUp(self):
        """Set up objects for the tests."""
        super().setUp()
        self.token = self.playground.create_token_enabled()
        self.request_factory = RequestFactory()

    def test_update_last_seen_at(self):
        """Request with a token updates last_seen_at."""
        self.assertIsNone(self.token.last_seen_at)
        before = timezone.now()

        response = self.client.get("/", headers={"token": self.token.key})
        self.assertEqual(response.status_code, status.HTTP_200_OK)

        # token.last_seen_at was updated
        self.token.refresh_from_db()
        self.assertGreaterEqual(self.token.last_seen_at, before)
        self.assertLessEqual(self.token.last_seen_at, timezone.now())

    def test_store_token(self):
        """Token is stored in the request."""
        request = self.request_factory.get("/", HTTP_TOKEN=self.token.key)

        # Use the middleware
        middleware = TokenLastSeenAtMiddleware(
            get_response=Mock(return_value=HttpResponse())
        )
        with self.assertNumQueries(2):
            # two queries: find the token and update it
            middleware(request)

        # Verify that the token is stored
        self.assertEqual(request.token, self.token)
        # Token is not a worker token
        self.assertIsNone(context.worker_token)

    def test_store_worker_token(self):
        """Token is stored in the request."""
        worker = self.playground.create_worker()
        worker.token = self.token
        worker.save()
        request = self.request_factory.get("/", HTTP_TOKEN=self.token.key)

        # Use the middleware
        middleware = TokenLastSeenAtMiddleware(
            get_response=Mock(return_value=HttpResponse())
        )
        with self.assertNumQueries(2):
            # two queries: find the token and update it
            middleware(request)

        # Verify that the token is stored
        self.assertEqual(request.token, self.token)
        # Token is a worker token, so it gets stored in context
        self.assertEqual(context.worker_token, self.token)

    def test_request_token_without_token_key(self):
        """Request without a token works as expected."""
        request = self.request_factory.get("/")

        # Use the middleware
        middleware = TokenLastSeenAtMiddleware(
            get_response=Mock(return_value=HttpResponse())
        )
        middleware(request)

        self.assertIsNone(request.token)
        self.assertIsNone(context.worker_token)

    def test_request_token_key_does_not_exist(self):
        """Request where the Token key in the header does not exist."""
        request = self.request_factory.get("/", HTTP_TOKEN="not-exist")

        middleware = TokenLastSeenAtMiddleware(
            get_response=Mock(return_value=HttpResponse())
        )
        middleware(request)

        # Verity that the token is None: request with a token_key not matching
        # any token sets a token=None in the request
        self.assertIsNone(request.token)
        self.assertIsNone(context.worker_token)

    def test_request_token_disabled(self):
        """Request with a disabled Token."""
        self.token.disable()
        request = self.request_factory.get("/", HTTP_TOKEN=self.token.key)

        # Use the middleware
        middleware = TokenLastSeenAtMiddleware(
            get_response=Mock(return_value=HttpResponse())
        )
        middleware(request)

        self.assertEqual(request.token, self.token)
        self.assertIsNone(context.worker_token)


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

    async def test_update_last_seen_at_store_token(self):
        """Request with a token updates token's last_seen_at."""
        token = await database_sync_to_async(
            self.playground.create_token_enabled
        )()

        self.assertIsNone(token.last_seen_at)

        before = timezone.now()

        scope_upstream = {
            "type": "http",
            "method": "GET",
            "headers": [(b"token", token.key.encode("utf-8"))],
        }

        async def mock_app(scope_downstream, receive, send):  # noqa: U100
            """Mock ASGI app to inspect the modified scope."""
            await token.arefresh_from_db()
            self.assertEqual(scope_downstream["token"], token)
            self.assertIsNone(context.worker_token)
            # self.assertEqual(context.worker_token, token)
            self.assertGreaterEqual(token.last_seen_at, before)
            self.assertLess(token.last_seen_at, timezone.now())

        middleware = TokenLastSeenAtMiddlewareChannels(app=mock_app)

        await middleware(scope_upstream, None, None)

    async def test_update_last_seen_at_store_worker_token(self):
        """Request with a token updates token's last_seen_at."""
        worker = await database_sync_to_async(self.playground.create_worker)()
        token = await database_sync_to_async(
            self.playground.create_token_enabled
        )()
        worker.token = token
        await worker.asave()

        self.assertIsNone(token.last_seen_at)

        before = timezone.now()

        scope_upstream = {
            "type": "http",
            "method": "GET",
            "headers": [(b"token", token.key.encode("utf-8"))],
        }

        async def mock_app(scope_downstream, receive, send):  # noqa: U100
            """Mock ASGI app to inspect the modified scope."""
            await token.arefresh_from_db()
            self.assertEqual(scope_downstream["token"], token)
            self.assertEqual(context.worker_token, token)
            self.assertGreaterEqual(token.last_seen_at, before)
            self.assertLess(token.last_seen_at, timezone.now())

        middleware = TokenLastSeenAtMiddlewareChannels(app=mock_app)

        await middleware(scope_upstream, None, None)

    async def test_request_without_token(self):
        """Request without a token."""
        scope_upstream = {"type": "http", "method": "GET", "headers": {}}

        async def mock_app(
            scope_downstream: dict[str, Any], receive, send  # noqa: U100
        ):
            """Mock ASGI app to inspect the modified scope."""
            self.assertIsNone(scope_downstream["token"])
            self.assertIsNone(context.worker_token)

        middleware = TokenLastSeenAtMiddlewareChannels(app=mock_app)

        await middleware(scope_upstream, None, None)

    async def test_request_token_key_does_not_exist(self):
        """Request where the Token key in the header does not exist."""
        scope_upstream = {
            "type": "http",
            "method": "GET",
            "headers": {"token": "DOES-NOT-EXIST"},
        }

        async def mock_app(
            scope_downstream: dict[str, Any], receive, send  # noqa: U100
        ):
            """Mock ASGI app to inspect the modified scope."""
            self.assertFalse(hasattr(scope_downstream, "token"))
            self.assertIsNone(context.worker_token)

        middleware = TokenLastSeenAtMiddlewareChannels(app=mock_app)
        await middleware(scope_upstream, None, None)

    async def test_request_token_disabled(self):
        """Request with a disabled Token."""
        token = await database_sync_to_async(
            self.playground.create_token_enabled
        )()
        await database_sync_to_async(token.disable)()

        scope_upstream = {
            "type": "http",
            "method": "GET",
            "headers": [(b"token", token.key.encode("utf-8"))],
        }

        async def mock_app(
            scope_downstream: dict[str, Any], receive, send  # noqa: U100
        ):
            """Mock ASGI app to inspect the modified scope."""
            self.assertEqual(scope_downstream["token"], token)
            self.assertIsNone(context.worker_token)

        middleware = TokenLastSeenAtMiddlewareChannels(app=mock_app)
        await middleware(scope_upstream, None, None)
