# 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.django.management.tests import call_command
from debusine.server.management.commands.group import Command
from debusine.test.django import TestCase


class GroupCommandTests(TestCase):
    """Tests for group create management command."""

    scope: ClassVar[Scope]
    group: ClassVar[Group]

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up common test data."""
        super().setUpTestData()
        cls.scope = cls.playground.get_or_create_scope(name="scope")
        cls.group = Group.objects.create(scope=cls.scope, name="group")

    def assertGroup(
        self, scope: Scope, name: str, users: list[User] | None = None
    ) -> Group:
        """Check that the given scope exists, with its Admin group."""
        if users is None:
            users = []
        group = Group.objects.get(scope=scope, name=name)
        self.assertQuerySetEqual(group.users.all(), users, ordered=False)
        return group

    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("group", "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)

    def test_get_scope_and_group_name(self) -> None:
        """Test get_scope_and_group_name."""
        command = Command()
        self.assertEqual(
            command.get_scope_and_group_name("scope/group"),
            (self.scope, "group"),
        )
        self.assertEqual(
            command.get_scope_and_group_name("scope/test"), (self.scope, "test")
        )

        with self.assertRaisesRegex(
            CommandError, r"Scope 'test' not found"
        ) as exc:
            command.get_scope_and_group_name("test/group")
        self.assertEqual(getattr(exc.exception, "returncode"), 3)

    def test_get_group(self) -> None:
        """Test get_group."""
        command = Command()
        self.assertEqual(command.get_group(self.scope, "group"), self.group)

        with self.assertRaisesRegex(
            CommandError, r"Group 'test' not found in scope 'scope'"
        ) as exc:
            command.get_group(self.scope, "test")
        self.assertEqual(getattr(exc.exception, "returncode"), 3)

    def test_get_users(self) -> None:
        """Test get_users."""
        user1 = self.playground.create_user("user1")
        user2 = self.playground.create_user("user2")

        command = Command()
        self.assertEqual(command.get_users(["user1", "user2"]), [user1, user2])

    def test_get_users_empty(self) -> None:
        """Test get_users with an empty username list."""
        command = Command()
        self.assertEqual(command.get_users([]), [])

    def test_get_users_nonexisting(self) -> None:
        """Test get_users with nonexisting usernames."""
        command = Command()
        with self.assertRaisesRegex(
            CommandError,
            r"User 'missing1' does not exist\n"
            r"User 'missing2' does not exist",
        ) as exc:
            command.get_users(["missing1", "missing2"])
        self.assertEqual(getattr(exc.exception, "returncode"), 3)

    def test_create(self) -> None:
        """Test a successful create."""
        stdout, stderr, exit_code = call_command(
            "group", "create", "scope/group1"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)
        self.assertGroup(self.scope, "group1")

    def test_create_idempotent(self) -> None:
        """Test a idempotence in create twice."""
        stdout, stderr, exit_code = call_command(
            "group", "create", "scope/group1"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)
        group = self.assertGroup(self.scope, "group1")
        group.users.add(self.playground.get_default_user())

        stdout, stderr, exit_code = call_command(
            "group", "create", "scope/group1"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)
        group1 = self.assertGroup(
            self.scope, "group1", [self.playground.get_default_user()]
        )

        self.assertEqual(group.pk, group1.pk)

    def test_create_invalid_name(self) -> None:
        """Test a successful create."""
        stdout, stderr, exit_code = call_command(
            "group", "create", "scope/group/name"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(
            stderr,
            "Created group would be invalid:\n"
            "* name: 'group/name' is not a valid group name\n",
        )
        self.assertEqual(exit_code, 3)
        self.assertQuerySetEqual(Group.objects.filter(name="group/name"), [])

    def test_rename(self) -> None:
        """Test a successful rename."""
        self.group.users.add(self.playground.get_default_user())
        stdout, stderr, exit_code = call_command(
            "group", "rename", "scope/group", "group1"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

        self.assertGroup(
            self.scope, "group1", [self.playground.get_default_user()]
        )

    def test_rename_noop(self) -> None:
        """Test renaming to current name."""
        self.group.users.add(self.playground.get_default_user())

        with mock.patch("debusine.db.models.Group.save") as save:
            stdout, stderr, exit_code = call_command(
                "group", "rename", "scope/group", "group"
            )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

        self.assertGroup(
            self.scope, "group", [self.playground.get_default_user()]
        )

        save.assert_not_called()

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

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

    def test_rename_target_exists(self) -> None:
        """Test renaming with a name already in use."""
        group = Group.objects.create(scope=self.scope, name="test")
        stdout, stderr, exit_code = call_command(
            "group", "rename", "scope/test", "group"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(
            stderr.splitlines(),
            [
                "Renamed group would be invalid:",
                "* Group with this Name and Scope already exists.",
            ],
        )

        self.assertEqual(exit_code, 3)
        group.refresh_from_db()
        self.assertEqual(group.name, "test")

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

    def test_delete(self) -> None:
        """Test a successful delete."""
        self.group.users.add(self.playground.get_default_user())
        stdout, stderr, exit_code = call_command(
            "group",
            "delete",
            "scope/group",
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

        self.assertQuerySetEqual(Group.objects.filter(scope=self.scope), [])

    def test_delete_missing(self) -> None:
        """Test idempotence in deleting a missing group."""
        self.group.users.add(self.playground.get_default_user())
        stdout, stderr, exit_code = call_command(
            "group",
            "delete",
            "scope/missing",
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

        self.assertQuerySetEqual(
            Group.objects.filter(scope=self.scope), [self.group]
        )

    def test_members_set(self) -> None:
        """Test members --set."""
        user1 = self.playground.create_user("user1")
        user2 = self.playground.create_user("user2")
        self.group.users.add(self.playground.get_default_user())

        stdout, stderr, exit_code = call_command(
            "group", "members", "scope/group", "--set", "user1", "user2"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

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

    def test_members_set_empty(self) -> None:
        """Test members --set to the empty set."""
        self.group.users.add(self.playground.get_default_user())

        stdout, stderr, exit_code = call_command(
            "group", "members", "scope/group", "--set"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

        self.assertQuerySetEqual(self.group.users.all(), [])

    def test_members_add(self) -> None:
        """Test members --add."""
        user1 = self.playground.create_user("user1")
        self.group.users.add(self.playground.get_default_user())

        stdout, stderr, exit_code = call_command(
            "group",
            "members",
            "scope/group",
            "--add",
            "user1",
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

        self.assertQuerySetEqual(
            self.group.users.all(),
            [self.playground.get_default_user(), user1],
            ordered=False,
        )

    def test_members_remove(self) -> None:
        """Test members --remove."""
        user1 = self.playground.create_user("user1")
        self.group.users.add(self.playground.get_default_user())
        self.group.users.add(user1)

        stdout, stderr, exit_code = call_command(
            "group",
            "members",
            "scope/group",
            "--remove",
            self.playground.get_default_user().username,
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

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

    def test_members_mix(self) -> None:
        """Test running multiple members actions."""
        with self.assertRaisesRegex(
            CommandError,
            r"Error: argument --add: not allowed with argument --remove",
        ) as exc:
            call_command(
                "group",
                "members",
                "scope/group",
                "--remove",
                "1",
                "--add",
                "2",
            )
        self.assertEqual(getattr(exc.exception, "returncode"), 1)
