# 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.

"""Unit tests for model permissions."""
from typing import ClassVar

from django.contrib.auth.models import AnonymousUser
from django.db import models

from debusine.db.context import ContextConsistencyError, context
from debusine.db.models import Group, Scope, Token, User, system_user
from debusine.db.models.permissions import (
    PermissionUser,
    ROLES,
    permission_check,
    permission_filter,
)
from debusine.test.django import TestCase


@permission_filter
def perm_filter(
    queryset: models.QuerySet[User],
    user: PermissionUser,
) -> models.QuerySet[User]:
    """Payload for testing the permission_filter decorator."""
    assert user is not None
    if user.is_authenticated:
        return queryset.filter(pk=user.pk)
    else:
        return queryset.none()


class MockResource(models.Model):
    """Mock resource used to test the permission_check decorator."""

    class Meta:
        managed = False

    def __init__(self, *args, **kwargs) -> None:
        """Make space to store last user set."""
        super().__init__(*args, **kwargs)
        self.last_user: PermissionUser = None

    @permission_check
    def _pred(self, user: PermissionUser) -> bool:  # noqa: U100
        self.last_user = user
        return False


class PermissionsTests(TestCase):
    """Tests for permission infrastructure."""

    scope: ClassVar[Scope]
    scope1: ClassVar[Scope]
    user: ClassVar[User]
    user1: ClassVar[User]
    group: ClassVar[Group]
    token: ClassVar[Token]

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up common test data."""
        super().setUpTestData()
        cls.scope = cls.playground.default_scope
        cls.scope1 = cls.playground.get_or_create_scope("scope")
        cls.user = cls.playground.get_default_user()
        cls.user1 = cls.playground.create_user("user1")
        cls.group = cls.playground.create_group_role(
            cls.scope, Scope.Roles.OWNER
        )
        cls.token = cls.playground.create_token_enabled()

    def test_ROLES(self) -> None:
        """Test the ROLES Q object."""
        self.assertQuerySetEqual(
            Scope.objects.filter(ROLES(self.user, Scope.Roles.OWNER)), []
        )
        self.group.users.add(self.user)
        self.assertQuerySetEqual(
            Scope.objects.filter(ROLES(self.user, Scope.Roles.OWNER)),
            [self.scope],
        )

    def test_permission_check_user_unset(self) -> None:
        """Test the permission_check decorator with no user in context."""
        res = MockResource()

        with self.assertRaisesRegex(
            ContextConsistencyError, "user was not set in context"
        ):
            res._pred(context.user)

        with context.disable_permission_checks():
            self.assertTrue(res._pred(context.user))

    def test_permission_check_user_unset_with_token(self) -> None:
        """Test the permission_check decorator with no user but a token."""
        context.set_worker_token(self.token)

        res = MockResource()
        self.assertFalse(res._pred(None))
        # User was defaulted to AnonymousUser()
        self.assertEqual(res.last_user, AnonymousUser())

        for user in (AnonymousUser(), self.user):
            with self.subTest(user=user):
                self.assertFalse(res._pred(user))
                with context.disable_permission_checks():
                    self.assertTrue(res._pred(user))

    def test_permission_check_user_context(self) -> None:
        """Test the permission_check decorator with user from context."""
        res = MockResource()
        context.set_scope(self.scope)
        context.set_user(self.user)
        self.assertFalse(res._pred(context.user))
        with context.disable_permission_checks():
            self.assertTrue(res._pred(context.user))

    def test_permission_check_user_explicit(self) -> None:
        """Test the permission_check decorator with explicitly given user."""
        res = MockResource()
        for user in (AnonymousUser(), self.user):
            with self.subTest(user=user):
                self.assertFalse(res._pred(user))
                with context.disable_permission_checks():
                    self.assertTrue(res._pred(user))

    def test_permission_filter_user_unset(self) -> None:
        """Test permission_filter with but no user in context."""
        # Default user without context raises ContextConsistencyError
        with self.assertRaisesRegex(
            ContextConsistencyError, r"user was not set in context"
        ):
            perm_filter(User.objects.all(), context.user)

        # But it works if permission checks are disabled
        with context.disable_permission_checks():
            self.assertQuerySetEqual(
                perm_filter(User.objects.all(), context.user),
                [system_user(), self.user, self.user1],
                ordered=False,
            )

    def test_permission_filter_user_unset_with_token(self) -> None:
        """Test permission_filter with the no user in context but a token."""
        context.set_worker_token(self.token)

        # User unset defaults to AnonymousUser()
        self.assertQuerySetEqual(perm_filter(User.objects.all(), None), [])

    def test_permission_filter_user_context(self) -> None:
        """Test permission_filter with the context user."""
        context.set_scope(self.scope)
        context.set_user(self.user)

        self.assertQuerySetEqual(
            perm_filter(User.objects.all(), self.user), [self.user]
        )

        # If user is not passed, take it from context
        self.assertQuerySetEqual(
            perm_filter(User.objects.all(), context.user), [self.user]
        )

        # Disabling permission checks works for any user
        with context.disable_permission_checks():
            for user in (AnonymousUser(), self.user, self.user1):
                self.assertQuerySetEqual(
                    perm_filter(User.objects.all(), user),
                    [system_user(), self.user, self.user1],
                    ordered=False,
                )

    def test_permission_filter_user_explicit(self) -> None:
        """Test permission_filter with an explicit user."""
        for user, expected in (
            (self.user, [self.user]),
            (AnonymousUser(), []),
            (self.user1, [self.user1]),
        ):
            with self.subTest(user=user):
                self.assertQuerySetEqual(
                    perm_filter(User.objects.all(), user),
                    expected,
                    ordered=False,
                )
