# Copyright 2019-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 auth-related models."""

from typing import ClassVar

from asgiref.sync import sync_to_async
from django.contrib.auth import get_user_model
from django.db import IntegrityError
from django.utils import timezone

from debusine.db.models import Group, Identity, Scope, Token, User, Worker
from debusine.test.django import ChannelsHelpersMixin, TestCase


class TokenTests(ChannelsHelpersMixin, TestCase):
    """Unit tests of the ``Token`` model."""

    user: ClassVar[User]

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up common test data."""
        super().setUpTestData()
        cls.user = get_user_model().objects.create_user(
            username="john", email="john@example.com"
        )

    def test_save(self) -> None:
        """The model creates a Token.key on save or keeps it if it existed."""
        token = Token.objects.create(user=self.user)

        self.assertIsNotNone(token.id)
        self.assertEqual(len(token.key), 64)

        key = token.key
        token.save()
        self.assertEqual(token.key, key)

    def test_str(self) -> None:
        """Test Token.__str__."""
        token = Token.objects.create(user=self.user)
        self.assertEqual(token.__str__(), token.hash)

    def test_user_field(self) -> None:
        """Test User field is None by default."""
        token = Token.objects.create()
        self.assertIsNone(token.user)

    def test_get_token_or_none_found(self) -> None:
        """get_token_or_none looks up a token and returns it."""
        token_hash = Token._generate_hash('some_key')
        token = Token.objects.create(hash=token_hash)

        self.assertEqual(token, Token.objects.get_token_or_none('some_key'))

    def test_get_token_or_none_not_found(self) -> None:
        """get_token_or_none cannot find a token and returns None."""
        self.assertIsNone(Token.objects.get_token_or_none('a_non_existing_key'))

    def test_enable(self) -> None:
        """enable() enables the token."""
        token = Token.objects.create()

        # Assert the default is disabled tokens
        self.assertFalse(token.enabled)

        token.enable()
        token.refresh_from_db()

        self.assertTrue(token.enabled)

    async def test_disable(self) -> None:
        """disable() disables the token."""
        token = await Token.objects.acreate(enabled=True)
        await Worker.objects.acreate(token=token, registered_at=timezone.now())

        channel = await self.create_channel(token.hash)

        await sync_to_async(token.disable)()

        await self.assert_channel_received(channel, {"type": "worker.disabled"})
        await token.arefresh_from_db()

        self.assertFalse(token.enabled)


class TokenManagerTests(TestCase):
    """Unit tests for the ``TokenManager`` class."""

    user_john: ClassVar[User]
    user_bev: ClassVar[User]
    token_john: ClassVar[Token]
    token_bev: ClassVar[Token]

    @classmethod
    def setUpTestData(cls) -> None:
        """Test data used by all the tests."""
        super().setUpTestData()
        cls.user_john = get_user_model().objects.create_user(
            username="John", email="john@example.com"
        )
        cls.user_bev = get_user_model().objects.create_user(
            username="Bev", email="bev@example.com"
        )
        cls.token_john = Token.objects.create(user=cls.user_john)
        cls.token_bev = Token.objects.create(user=cls.user_bev)

    def test_get_tokens_all(self) -> None:
        """get_tokens returns all the tokens if no filter is applied."""
        self.assertQuerySetEqual(
            Token.objects.get_tokens(),
            {self.token_bev, self.token_john},
            ordered=False,
        )

    def test_get_tokens_by_owner(self) -> None:
        """get_tokens returns the correct tokens when filtering by owner."""
        self.assertQuerySetEqual(
            Token.objects.get_tokens(username='John'), [self.token_john]
        )
        self.assertQuerySetEqual(
            Token.objects.get_tokens(username='Bev'), [self.token_bev]
        )
        self.assertQuerySetEqual(
            Token.objects.get_tokens(username='Someone'), []
        )

    def test_get_tokens_by_key(self) -> None:
        """get_tokens returns the correct tokens when filtering by key."""
        self.assertQuerySetEqual(
            Token.objects.get_tokens(key=self.token_john.key),
            [self.token_john],
        )
        self.assertQuerySetEqual(
            Token.objects.get_tokens(key='non-existing-key'), []
        )

    def test_get_tokens_by_key_owner_empty(self) -> None:
        """
        get_tokens returns nothing if using a key and username without matches.

        Key for the key parameter or username for the user parameter exist
        but are for different tokens.
        """
        self.assertQuerySetEqual(
            Token.objects.get_tokens(
                key=self.token_john.key, username=self.user_bev.username
            ),
            [],
        )


class IdentityTests(TestCase):
    """Test for Identity class."""

    def test_str(self) -> None:
        """Stringification should show the unique key."""
        ident = Identity(issuer="salsa", subject="test@debian.org")
        self.assertEqual(str(ident), "salsa:test@debian.org")


class GroupTests(TestCase):
    """Test for Group class."""

    scope1: ClassVar[Scope]
    scope2: ClassVar[Scope]

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up common test data."""
        super().setUpTestData()
        cls.scope1 = cls.playground.get_or_create_scope("scope1")
        cls.scope2 = cls.playground.get_or_create_scope("scope2")

    def test_str(self) -> None:
        """Test stringification."""
        group = Group.objects.create(name="group", scope=self.scope1)
        self.assertEqual(str(group), f"{self.scope1.name}/group")

    def test_unique(self) -> None:
        """Test scoped uniqueness."""
        group1 = Group.objects.create(name="group", scope=self.scope1)
        group2 = Group.objects.create(name="group", scope=self.scope2)
        self.assertNotEqual(group1.pk, group2.pk)

        with self.assertRaisesRegex(
            IntegrityError,
            # Only match constraint name to support non-english locales
            # r"duplicate key value violates unique constraint"
            r"db_group_unique_name_scope",
        ):
            Group.objects.create(name="group", scope=self.scope1)

    def test_assign_user(self) -> None:
        """Test assigning users to groups."""
        group = Group.objects.create(name="group", scope=self.scope1)

        user1 = User.objects.create_user(
            username="user1",
            email="user1@debian.org",
        )
        user2 = User.objects.create_user(
            username="user2",
            email="user2@debian.org",
        )

        group.users.add(user1)

        self.assertQuerySetEqual(group.users.all(), [user1])
        self.assertQuerySetEqual(user1.groups.all(), [])
        self.assertQuerySetEqual(user1.debusine_groups.all(), [group])

        group.users.add(user2)

        self.assertQuerySetEqual(
            group.users.all(), [user1, user2], ordered=False
        )
        self.assertQuerySetEqual(user2.groups.all(), [])
        self.assertQuerySetEqual(user2.debusine_groups.all(), [group])
