# 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 management command rename_scope."""
from typing import ClassVar
from unittest import mock

from django.core.management import CommandError

from debusine.db.models import Group, Scope, User
from debusine.db.models.scopes import ScopeRole
from debusine.django.management.tests import call_command
from debusine.server.management.commands.scope import Command
from debusine.test.django import TestCase


class CreateScopeCommandTests(TestCase):
    """Tests for scope create management command."""

    def assertHasScope(
        self,
        name: str = "scope",
        group_name: str | None = None,
        users: list[User] | None = None,
    ) -> None:
        """Check that the given scope exists, with its Owners group."""
        if users is None:
            users = []
        scope = Scope.objects.get(name=name)
        if group_name is not None:
            admin_group = Group.objects.get(scope=scope, name=group_name)
            self.assertQuerySetEqual(admin_group.users.all(), users)
            role = ScopeRole.objects.get(resource=scope, group=admin_group)
            self.assertEqual(role.role, ScopeRole.Roles.OWNER)

    def test_create(self) -> None:
        """Test a successful create."""
        stdout, stderr, exit_code = call_command(
            "scope", "create", "scope", "--with-owners-group"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)
        self.assertHasScope(group_name="Owners")

    def test_create_custom_group_name(self) -> None:
        """Test a successful create with specified group name."""
        stdout, stderr, exit_code = call_command(
            "scope", "create", "scope", "--with-owners-group=Admin"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)
        self.assertHasScope(group_name="Admin")

    def test_idempotent(self) -> None:
        """Test idempotence in creating twice."""
        stdout, stderr, exit_code = call_command(
            "scope", "create", "scope", "--with-owners-group"
        )
        scope = Scope.objects.get(name="scope")
        admin_group = Group.objects.get(scope=scope, name="Owners")
        admin_group.users.set([self.playground.get_default_user()])

        # Recreate: the existing database objects are maintained
        stdout, stderr, exit_code = call_command(
            "scope", "create", "scope", "--with-owners-group"
        )
        new_scope = Scope.objects.get(name="scope")
        self.assertEqual(new_scope.pk, scope.pk)
        new_group = Group.objects.get(scope=new_scope, name="Owners")
        self.assertEqual(new_group.pk, admin_group.pk)

        # The previous group membership is also maintained
        self.assertHasScope(users=[self.playground.get_default_user()])

    def test_no_admin_group(self) -> None:
        """Test a successful create with no admin group."""
        stdout, stderr, exit_code = call_command("scope", "create", "scope")
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)
        self.assertHasScope()
        self.assertFalse(Group.objects.filter(scope__name="scope").exists())


class RenameScopeCommandTests(TestCase):
    """Tests for scope rename management command."""

    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(name="scope1")
        cls.scope2 = cls.playground.get_or_create_scope(name="scope2")

    def test_rename(self) -> None:
        """Test a successful rename."""
        stdout, stderr, exit_code = call_command(
            "scope", "rename", "scope1", "newname"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)
        self.scope1.refresh_from_db()
        self.assertEqual(self.scope1.name, "newname")

    def test_noop(self) -> None:
        """Test renaming to current name."""
        with mock.patch("debusine.db.models.Scope.save") as save:
            stdout, stderr, exit_code = call_command(
                "scope", "rename", "scope1", "scope1"
            )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

        save.assert_not_called()

    def test_source_does_not_exist(self) -> None:
        """Test renaming a nonexisting scope."""
        with self.assertRaisesRegex(
            CommandError, r"Scope 'does-not-exist' not found"
        ) as exc:
            call_command("scope", "rename", "does-not-exist", "newname")

        self.assertEqual(getattr(exc.exception, "returncode"), 3)

    def test_target_exists(self) -> None:
        """Test renaming with a name already in use."""
        stdout, stderr, exit_code = call_command(
            "scope", "rename", "scope1", "scope2"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(
            stderr.splitlines(),
            [
                "Renamed scope would be invalid:",
                "* name: Scope with this Name already exists.",
            ],
        )

        self.assertEqual(exit_code, 3)
        self.scope1.refresh_from_db()
        self.assertEqual(self.scope1.name, "scope1")

    def test_new_name_invalid(self) -> None:
        """Test renaming to an invalid name."""
        stdout, stderr, exit_code = call_command(
            "scope", "rename", "scope1", "api"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(
            stderr.splitlines(),
            [
                "Renamed scope would be invalid:",
                "* name: 'api' is not a valid scope name",
            ],
        )
        self.assertEqual(exit_code, 3)
        self.scope1.refresh_from_db()
        self.assertEqual(self.scope1.name, "scope1")

    def test_invalid_action(self) -> None:
        """Test invoking an invalid subcommand."""
        with self.assertRaisesRegex(
            CommandError, r"invalid choice: 'does-not-exist'"
        ) as exc:
            call_command("scope", "does-not-exist")

        self.assertEqual(getattr(exc.exception, "returncode"), 1)

    def test_unexpected_action(self) -> None:
        """Test a subcommand with no implementation."""
        command = Command()

        with self.assertRaisesRegex(
            CommandError, r"Action 'does_not_exist' not found"
        ) as exc:
            command.handle(action="does_not_exist")

        self.assertEqual(getattr(exc.exception, "returncode"), 3)
