# Copyright © 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 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,
        label: str | None = None,
        icon: str | 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)
        if label is None:
            self.assertEqual(scope.label, scope.name.capitalize())
        else:
            self.assertEqual(scope.label, label)
        if icon is None:
            self.assertEqual(scope.icon, "")
        else:
            self.assertEqual(scope.icon, icon)

    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_with_label(self) -> None:
        """Test setting label on create."""
        stdout, stderr, exit_code = call_command(
            "scope", "create", "scope", "--label=Scope Name"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)
        self.assertHasScope(label="Scope Name")

    def test_update_label(self) -> None:
        """Test updating a label with create."""
        stdout, stderr, exit_code = call_command("scope", "create", "scope")
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)
        self.assertHasScope(label="Scope")

        stdout, stderr, exit_code = call_command(
            "scope", "create", "scope", "--label=Changed"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)
        self.assertHasScope(label="Changed")

    def test_create_with_icon(self) -> None:
        """Test setting icon on create."""
        stdout, stderr, exit_code = call_command(
            "scope", "create", "scope", "--icon=web/icons/scope.svg"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)
        self.assertHasScope(icon="web/icons/scope.svg")

    def test_update_icon(self) -> None:
        """Test updating a label with create."""
        stdout, stderr, exit_code = call_command("scope", "create", "scope")
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)
        self.assertHasScope()

        stdout, stderr, exit_code = call_command(
            "scope", "create", "scope", "--icon=web/icons/scope.svg"
        )
        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)
        self.assertHasScope(icon="web/icons/scope.svg")

    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())

    def test_new_name_invalid(self) -> None:
        """Test creating with an invalid name."""
        stdout, stderr, exit_code = call_command("scope", "create", "a/b")
        self.assertEqual(stdout, "")
        self.assertEqual(
            stderr.splitlines(),
            [
                "New scope would be invalid:",
                "* name: 'a/b' is not a valid scope name",
            ],
        )
        self.assertEqual(exit_code, 3)
        self.assertFalse(Scope.objects.filter(name="a/b").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)
