# 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 base view code."""

import json
from typing import Any, override

from django.http import HttpResponse
from django.test import Client
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIRequestFactory

from debusine.db.context import context
from debusine.db.models import Scope, Token, Workspace
from debusine.db.permissioncontext import PermissionContext
from debusine.server.exceptions import DebusineAPIException
from debusine.server.views.base import BaseAPIView, SupersededAPIView
from debusine.test.django import (
    AllowAll,
    BaseDjangoTestCase,
    DenyAll,
    TestCase,
    TestResponseType,
    override_permission,
)

TestResponseType  # Fake use for vulture


class TestBaseAPIView(TestCase):
    """Test the BaseAPIView view."""

    def test_enforce(self) -> None:
        """Test the enforce method."""
        scope = self.playground.get_default_scope()
        user = self.playground.get_default_user()
        context.set_permission_context_scoped(
            scope, PermissionContext.for_ui_user(user)
        )
        view = BaseAPIView()
        with override_permission(Scope, "can_display", AllowAll):
            view.enforce(scope.can_display)
        with (
            override_permission(Scope, "can_display", DenyAll),
            self.assertRaises(DebusineAPIException) as exc,
        ):
            view.enforce(scope.can_display)
        e = exc.exception
        self.assertEqual(
            e.debusine_title, "playground cannot display scope debusine"
        )
        self.assertIsNone(e.debusine_detail)
        self.assertIsNone(e.debusine_validation_errors)
        self.assertEqual(e.debusine_status_code, status.HTTP_403_FORBIDDEN)

    def test_set_current_workspace(self) -> None:
        """Test the set_current_workspace method."""
        workspace: Workspace | str
        error: str | None
        workspace = self.playground.get_default_workspace()
        private_workspace = self.playground.create_workspace(name="private")
        scope = self.playground.get_default_scope()
        user = self.playground.get_default_user()
        context.set_permission_context_scoped(
            scope, PermissionContext.for_ui_user(user)
        )
        view = BaseAPIView()
        for workspace, error in (
            (workspace, None),
            (workspace.name, None),
            (
                private_workspace,
                "Workspace private not found in scope debusine",
            ),
            (
                private_workspace.name,
                "Workspace private not found in scope debusine",
            ),
            (
                "does-not-exist",
                "Workspace does-not-exist not found in scope debusine",
            ),
        ):
            with self.subTest(workspace=repr(workspace)), context.local():
                if not error:
                    view.set_current_workspace(workspace)
                else:
                    with self.assertRaises(DebusineAPIException) as exc:
                        view.set_current_workspace(workspace)
                    self.assertEqual(exc.exception.debusine_detail, error)


class TestWhoami(TestCase):
    """Test authentication using the whoami view."""

    def whoami(
        self, token: Token | str | None = None, client: Client | None = None
    ) -> dict[str, Any]:
        """Call the whoami API view."""
        if client is None:
            client = self.client

        headers = {}
        match token:
            case None:
                pass
            case str():
                headers["token"] = token
            case _:
                headers["token"] = token.key

        response = client.get(reverse("api:whoami"), headers=headers)
        data = getattr(response, "data")
        assert isinstance(data, dict)
        return data

    def test_worker_token(self) -> None:
        """Try a worker token."""
        self.assertEqual(
            self.whoami(self.playground.create_worker_token()),
            {
                "auth": True,
                "user": "<anonymous>",
                "work_request": None,
                "worker": True,
            },
        )

    def test_work_request_token(self) -> None:
        """Try a work request token."""
        token = self.playground.create_work_request_token()
        assert token.work_request is not None
        self.assertEqual(
            self.whoami(token),
            {
                "auth": True,
                "user": "<anonymous>",
                "work_request": token.work_request.id,
                "worker": False,
            },
        )

    def test_user_token(self) -> None:
        """Try a user token."""
        self.assertEqual(
            self.whoami(self.playground.create_user_token()),
            {
                "auth": True,
                "user": "playground",
                "work_request": None,
                "worker": False,
            },
        )

    def test_no_token(self) -> None:
        """Try without a token."""
        self.assertEqual(
            self.whoami(),
            {
                "auth": False,
                "user": "<anonymous>",
                "work_request": None,
                "worker": False,
            },
        )

    def test_session_auth(self) -> None:
        """Try django auth."""
        user = self.playground.get_default_user()
        client = Client()
        client.force_login(user)
        self.assertEqual(
            self.whoami(client=client),
            {
                "auth": False,
                "user": user.username,
                "work_request": None,
                "worker": False,
            },
        )

    def test_token_key_does_not_exist(self) -> None:
        """Try a Token key that does not exist."""
        self.assertEqual(
            self.whoami("does-not-exist"),
            {
                "auth": False,
                "user": "<anonymous>",
                "work_request": None,
                "worker": False,
            },
        )

    def test_token_disabled(self) -> None:
        """Try a disabled Token."""
        # Worker and user token
        token = self.playground.create_user_token(enabled=False)
        self.assertEqual(
            self.whoami(token),
            {
                'auth': False,
                'user': "<anonymous>",
                'worker': False,
                "work_request": None,
            },
        )


class TestSupersededEndpoint(BaseDjangoTestCase):
    """Test the superseded API endpoint."""

    @override
    def setUp(self) -> None:
        super().setUp()
        self.factory = APIRequestFactory()

    def request(self, method: str = "GET", **kwargs: str) -> HttpResponse:
        """
        Assert that a method request to view returns an error with detail.

        We're not mounted at URL, so we skip django's test client and just put
        the pieces together ourselves.
        """
        match method:
            case "GET":
                request = self.factory.get("/some-api")
            case "POST":
                request = self.factory.post("/some-api", {})
            case "PATCH":
                request = self.factory.patch("/some-api", {})
            case "DELETE":
                request = self.factory.delete("/some-api")
            case "OPTIONS":
                request = self.factory.options("/some-api")
            case _:  # pragma: no cover
                raise NotImplementedError()
        request._debusine_token = None  # type: ignore[attr-defined]
        response = SupersededAPIView.as_view()(request, **kwargs)
        response.json = lambda: json.loads(response.content)  # type: ignore[attr-defined]
        return response

    def test_unconfigured(self) -> None:
        detail = r"This endpoint is no longer supported\."
        for method in ("GET", "POST", "PATCH", "DELETE"):
            with self.subTest(method=method):
                response = self.request(method=method)
                self.assertResponseProblem(
                    response,
                    title="Superseded API Endpoint",
                    detail_pattern=detail,
                )

    def test_options(self) -> None:
        response = self.request(method="OPTIONS")
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_with_replacement(self) -> None:
        response = self.request(new_endpoint="/new/")
        self.assertResponseProblem(
            response,
            title="Superseded API Endpoint",
            detail_pattern=(
                r"This endpoint is no longer supported\. "
                r"It has been replaced by /new/\."
            ),
        )

    def test_with_replacement_and_version(self) -> None:
        response = self.request(new_endpoint="/new/", client_version="1.0")
        self.assertResponseProblem(
            response,
            title="Superseded API Endpoint",
            detail_pattern=(
                r"This endpoint is no longer supported\. "
                r"It has been replaced by /new/, which is supported by "
                r"debusine-client \(>= 1\.0\)\. Please upgrade your client\."
            ),
        )
