# 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 the scopes models."""
from typing import ClassVar

from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction

from debusine.db.context import context
from debusine.db.models import Group, Scope
from debusine.db.models.scopes import ScopeRole, is_valid_scope_name
from debusine.test.django import TestCase


class ScopeManagerTests(TestCase):
    """Tests for the ScopeManager class."""

    def test_get_roles_model(self) -> None:
        """Test the get_roles_model method."""
        self.assertIs(Scope.objects.get_roles_model(), ScopeRole)


class ScopeTests(TestCase):
    """Tests for the Scope class."""

    def test_create(self) -> None:
        """Test basic behavior."""
        scope = Scope.objects.create(name="test")
        self.assertEqual(scope.name, "test")
        self.assertEqual(str(scope), "test")

    def test_unique(self) -> None:
        """Check that scopes are unique by name."""
        Scope.objects.create(name="test")
        with transaction.atomic():
            with self.assertRaisesRegex(
                IntegrityError,
                # Only match constraint name to support non-english locales
                # "duplicate key value violates unique constraint"
                "db_scope_name_key",
            ):
                Scope.objects.create(name="test")
        Scope.objects.create(name="test1")

    def test_fallback(self) -> None:
        """The fallback scope for migrations exists."""
        scope = Scope.objects.get(name="debusine")
        self.assertEqual(scope.name, "debusine")

    def test_is_valid_scope_name(self) -> None:
        """Test is_valid_scope_name."""
        valid_names = (
            "debian",
            "debian-lts",
            "debusine",
            "c",
            "c++",
            "foo_bar",
            "foo.bar",
            "foo+bar",
            "tail_",
            "c--",
        )
        invalid_names = (
            "api",
            "admin",
            "user",
            "+tag",
            "-tag",
            ".profile",
            "_reserved",
            "foo:bar",
        )

        for name in valid_names:
            with self.subTest(name=name):
                self.assertTrue(is_valid_scope_name(name))

        for name in invalid_names:
            with self.subTest(name=name):
                self.assertFalse(is_valid_scope_name(name))

    def test_scope_name_validation(self) -> None:
        """Test validation for scope names."""
        Scope(name="foo").full_clean()
        Scope(name="foo_").full_clean()
        with self.assertRaises(ValidationError) as exc:
            Scope(name="_foo").full_clean()
        self.assertEqual(
            exc.exception.message_dict,
            {'name': ["'_foo' is not a valid scope name"]},
        )

    def test_assign_role(self) -> None:
        """Test assigning roles."""
        scope = Scope.objects.create(name="Scope")
        group = Group.objects.create(name="Group", scope=scope)
        sr = scope.assign_role(ScopeRole.Roles.OWNER, group)
        self.assertEqual(sr.resource, scope)
        self.assertEqual(sr.group, group)
        self.assertEqual(sr.role, ScopeRole.Roles.OWNER)

        scope1 = Scope.objects.create(name="Scope1")
        group1 = Group.objects.create(name="Group", scope=scope1)
        with self.assertRaisesRegex(
            ValueError, r"group Scope1/Group is not in scope Scope"
        ):
            scope.assign_role(ScopeRole.Roles.OWNER, group1)

    def test_get_roles_not_authenticated(self) -> None:
        """Test Scope.get_roles when not authenticated."""
        scope = Scope.objects.create(name="Scope")
        user = AnonymousUser()
        self.assertQuerySetEqual(scope.get_roles(user), [])

    def test_get_roles(self) -> None:
        """Test Scope.get_roles."""
        scope = Scope.objects.create(name="Scope")
        group = self.playground.create_group_role(scope, ScopeRole.Roles.OWNER)

        user = self.playground.get_default_user()
        self.assertQuerySetEqual(scope.get_roles(user), [])

        group.users.add(user)
        self.assertEqual(list(scope.get_roles(user)), [ScopeRole.Roles.OWNER])

    def test_get_roles_multiple_groups(self) -> None:
        """Test Scope.get_roles."""
        scope = Scope.objects.create(name="Scope")
        group1 = self.playground.create_group_role(
            scope, ScopeRole.Roles.OWNER, name="Group1"
        )
        group2 = self.playground.create_group_role(
            scope, ScopeRole.Roles.OWNER, name="Group2"
        )

        user = self.playground.get_default_user()
        self.assertQuerySetEqual(scope.get_roles(user), [])

        group1.users.add(user)
        self.assertEqual(list(scope.get_roles(user)), [ScopeRole.Roles.OWNER])

        group2.users.add(user)
        self.assertEqual(list(scope.get_roles(user)), [ScopeRole.Roles.OWNER])

    def test_can_display(self) -> None:
        """Test the can_display predicate."""
        scope = self.playground.default_scope
        scope2 = Scope.objects.create(name="Scope2")
        for user in (AnonymousUser(), self.playground.get_default_user()):
            with self.subTest(user=user):
                self.assertTrue(scope.can_display(user))
                self.assertTrue(scope2.can_display(user))
                self.assertQuerySetEqual(
                    Scope.objects.can_display(user),
                    [scope, scope2],
                    ordered=False,
                )

    def test_can_create_workspace_not_owner(self) -> None:
        """Test can_create_workspace with user not owner."""
        scope = self.playground.default_scope
        for user in (AnonymousUser(), self.playground.get_default_user()):
            with self.subTest(user=user):
                self.assertFalse(scope.can_create_workspace(user))
                self.assertQuerySetEqual(
                    Scope.objects.can_create_workspace(user), []
                )

    def test_can_create_workspace_owner(self) -> None:
        """Test can_create_workspace with user owner of the scope."""
        scope = self.playground.default_scope
        user = self.playground.get_default_user()
        self.playground.create_group_role(scope, Scope.Roles.OWNER, user)
        with self.assertNumQueries(1):
            self.assertTrue(scope.can_create_workspace(user))
        self.assertQuerySetEqual(
            Scope.objects.can_create_workspace(user), [scope]
        )

    def test_can_create_workspace_owner_in_context(self) -> None:
        """Test can_create_workspace with current user owner of the scope."""
        scope = self.playground.default_scope
        user = self.playground.get_default_user()
        self.playground.create_group_role(scope, Scope.Roles.OWNER, user)
        context.set_scope(scope)
        context.set_user(user)
        # Testing the current user can shortcut checking the current roles
        with self.assertNumQueries(0):
            self.assertTrue(scope.can_create_workspace(user))


class ScopeRoleTests(TestCase):
    """Tests for the ScopeRole class."""

    scope1: ClassVar[Scope]
    scope2: ClassVar[Scope]
    group1: ClassVar[Group]
    group2: ClassVar[Group]

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up common test data."""
        super().setUpTestData()
        cls.scope1 = cls.playground.get_or_create_scope(name="Scope1")
        cls.scope2 = cls.playground.get_or_create_scope(name="Scope2")
        cls.group1 = Group.objects.create(name="Group1", scope=cls.scope1)
        cls.group2 = Group.objects.create(name="Group2", scope=cls.scope2)

    def test_str(self) -> None:
        """Test stringification."""
        sr = ScopeRole(
            group=self.group1, resource=self.scope1, role=ScopeRole.Roles.OWNER
        )
        self.assertEqual(str(sr), "Scope1/Group1─owner⟶Scope1")

    def test_assign_multiple_groups(self) -> None:
        """Multiple group can share a role on a scope."""
        ScopeRole.objects.create(
            group=self.group1, resource=self.scope1, role=ScopeRole.Roles.OWNER
        )
        ScopeRole.objects.create(
            group=self.group2, resource=self.scope1, role=ScopeRole.Roles.OWNER
        )

    def test_assign_multiple_scopes(self) -> None:
        """A group can be given a role in different scopes."""
        ScopeRole.objects.create(
            group=self.group1, resource=self.scope1, role=ScopeRole.Roles.OWNER
        )
        ScopeRole.objects.create(
            group=self.group1, resource=self.scope2, role=ScopeRole.Roles.OWNER
        )
