# 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 Debusine CLI archive token management commands."""

from functools import partial
from typing import override
from unittest import mock

from debusine.artifacts.models import CollectionCategory
from debusine.client.commands.archives.tokens import TokenCommand
from debusine.client.commands.base import Command, DebusineCommand
from debusine.client.commands.tests.base import BaseCliTests
from debusine.client.models import (
    CollectionRoles,
    CollectionTokenGrantRequest,
    CollectionTokenGrantResponse,
)


class TokenCommandTests(BaseCliTests):
    """Tests for the :py:class:`TokenCommand` class."""

    @override
    def setUp(self) -> None:
        super().setUp()
        self.grants = [
            CollectionTokenGrantResponse(
                token_type="external-user",
                token_hash=token_hash,
                username=username,
                comment="a comment",
                role=CollectionRoles.VIEWER,
            )
            for token_hash, username in (
                ("0" * 64, "test1"),
                ("1" * 64, "test2"),
            )
        ]

    @Command.preserve_registry()
    def _command(self) -> TokenCommand:
        class ConcreteTokenCommand(TokenCommand):
            """Version of TokenCommand that can be instantiated."""

            @override
            def run(self) -> None:
                raise NotImplementedError()

        return self.command(
            ConcreteTokenCommand, workspace="workspace", suite=None
        )

    def test_list_rich(self) -> None:
        command = self._command()
        with (
            mock.patch(
                "debusine.client.commands.archives.tokens.Table",
                return_value=(table := mock.MagicMock()),
            ),
            mock.patch(
                "debusine.client.commands.archives.tokens.rich.print"
            ) as rich_print,
        ):
            stderr, stdout = self.capture_output(
                partial(command._list_rich, self.grants)
            )

        self.assertEqual(stderr, "")
        self.assertEqual(stdout, "")
        rich_print.assert_called_once()

        self.assertEqual(table.add_row.call_count, 2)
        rows = [
            call_args_list.args
            for call_args_list in table.add_row.call_args_list
        ]
        self.assertEqual(
            rows[0], ("external-user", "0" * 64, "test1", "a comment", "viewer")
        )
        self.assertEqual(
            rows[1], ("external-user", "1" * 64, "test2", "a comment", "viewer")
        )

    def test_show_rich(self) -> None:
        command = self._command()
        grant = self.grants[0].model_copy(update={"token_key": "9" * 64})
        with (
            mock.patch(
                "debusine.client.commands.archives.tokens.Table",
                return_value=(table := mock.MagicMock()),
            ),
            mock.patch(
                "debusine.client.commands.archives.tokens.rich.print"
            ) as rich_print,
        ):
            stderr, stdout = self.capture_output(
                partial(command._show_rich, grant)
            )

        self.assertEqual(stderr, "")
        self.assertEqual(stdout, "")
        rich_print.assert_called_once()

        self.assertEqual(table.add_row.call_count, 6)
        rows = [
            call_args_list.args
            for call_args_list in table.add_row.call_args_list
        ]
        self.assertEqual(rows[0], ("Token type:", "external-user"))
        self.assertEqual(rows[1], ("Token hash:", "0" * 64))
        self.assertEqual(rows[2], ("Token key (only shown once):", "9" * 64))
        self.assertEqual(rows[3], ("Username:", "test1"))
        self.assertEqual(rows[4], ("Comment:", "a comment"))
        self.assertEqual(rows[5], ("Role:", "viewer"))


class TokenGrantTests(BaseCliTests):
    """Tests for the :py:class:`TokenGrant` class."""

    def assertGrants(
        self,
        *args: str,
        collection_category: CollectionCategory,
        collection_name: str,
        request: CollectionTokenGrantRequest,
        grant: CollectionTokenGrantResponse,
    ) -> None:
        command = self.create_command(
            ["archive", "token", "grant", "--workspace=workspace", *args]
        )
        assert isinstance(command, DebusineCommand)
        with (
            mock.patch.object(
                command.debusine,
                "collection_token_grant_create",
                return_value=grant,
            ) as collection_token_grant_create,
            mock.patch.object(command, "show") as show,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        collection_token_grant_create.assert_called_once_with(
            workspace="workspace",
            collection_category=collection_category,
            collection_name=collection_name,
            request=request,
        )
        show.assert_called_once_with(grant)

    def test_archive_token_hash(self) -> None:
        self.assertGrants(
            f"--token={'0' * 64}",
            CollectionRoles.VIEWER,
            collection_category=CollectionCategory.ARCHIVE,
            collection_name="_",
            request=CollectionTokenGrantRequest(
                token_hash="0" * 64, role=CollectionRoles.VIEWER
            ),
            grant=CollectionTokenGrantResponse(
                token_type="external-user",
                token_hash="0" * 64,
                token_key="9" * 64,
                username="test",
                comment="",
                role=CollectionRoles.VIEWER,
            ),
        )

    def test_archive_username(self) -> None:
        self.assertGrants(
            "--external-username=test",
            "--comment=a comment",
            CollectionRoles.VIEWER,
            collection_category=CollectionCategory.ARCHIVE,
            collection_name="_",
            request=CollectionTokenGrantRequest(
                username="test",
                comment="a comment",
                role=CollectionRoles.VIEWER,
            ),
            grant=CollectionTokenGrantResponse(
                token_type="external-user",
                token_hash="0" * 64,
                token_key="9" * 64,
                username="test",
                comment="a comment",
                role=CollectionRoles.VIEWER,
            ),
        )

    def test_suite_token_hash(self) -> None:
        self.assertGrants(
            "--suite=sid",
            f"--token={'0' * 64}",
            CollectionRoles.VIEWER,
            collection_category=CollectionCategory.SUITE,
            collection_name="sid",
            request=CollectionTokenGrantRequest(
                token_hash="0" * 64, role=CollectionRoles.VIEWER
            ),
            grant=CollectionTokenGrantResponse(
                token_type="external-user",
                token_hash="0" * 64,
                token_key="9" * 64,
                username="test",
                comment="",
                role=CollectionRoles.VIEWER,
            ),
        )

    def test_suite_username(self) -> None:
        self.assertGrants(
            "--suite=sid",
            "--external-username=test",
            "--comment=a comment",
            CollectionRoles.VIEWER,
            collection_category=CollectionCategory.SUITE,
            collection_name="sid",
            request=CollectionTokenGrantRequest(
                username="test",
                comment="a comment",
                role=CollectionRoles.VIEWER,
            ),
            grant=CollectionTokenGrantResponse(
                token_type="external-user",
                token_hash="0" * 64,
                token_key="9" * 64,
                username="test",
                comment="a comment",
                role=CollectionRoles.VIEWER,
            ),
        )


class TokenListTests(BaseCliTests):
    """Tests for the :py:class:`TokenList` class."""

    def assertLists(
        self,
        *args: str,
        collection_category: CollectionCategory,
        collection_name: str,
    ) -> None:
        grants = [
            CollectionTokenGrantResponse(
                token_type="external-user",
                token_hash=token_hash,
                username=username,
                comment="a comment",
                role=CollectionRoles.VIEWER,
            )
            for token_hash, username in (
                ("0" * 64, "test1"),
                ("1" * 64, "test2"),
            )
        ]
        command = self.create_command(
            ["archive", "token", "list", "--workspace=workspace", *args]
        )
        assert isinstance(command, DebusineCommand)
        with (
            mock.patch.object(
                command.debusine,
                "collection_token_grant_iter",
                return_value=grants,
            ) as collection_token_grant_iter,
            mock.patch.object(command, "list") as list_,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        collection_token_grant_iter.assert_called_once_with(
            workspace="workspace",
            collection_category=collection_category,
            collection_name=collection_name,
        )
        list_.assert_called_once_with(grants)

    def test_archive(self) -> None:
        self.assertLists(
            collection_category=CollectionCategory.ARCHIVE, collection_name="_"
        )

    def test_suite(self) -> None:
        self.assertLists(
            "--suite=sid",
            collection_category=CollectionCategory.SUITE,
            collection_name="sid",
        )


class TokenRevokeTests(BaseCliTests):
    """Tests for the :py:class:`TokenRevoke` class."""

    def assertRevokes(
        self,
        *args: str,
        token_hash: str,
        role: CollectionRoles,
        collection_category: CollectionCategory,
        collection_name: str,
    ) -> None:
        command = self.create_command(
            [
                "archive",
                "token",
                "revoke",
                "--workspace=workspace",
                *args,
                token_hash,
                role,
            ]
        )
        assert isinstance(command, DebusineCommand)
        with (
            mock.patch.object(
                command.debusine, "collection_token_grant_delete"
            ) as collection_token_grant_delete,
            mock.patch.object(command, "feedback") as feedback,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        collection_token_grant_delete.assert_called_once_with(
            workspace="workspace",
            collection_category=collection_category,
            collection_name=collection_name,
            token_hash=token_hash,
            role=role,
        )
        feedback.assert_called_once_with(
            f"Role 'viewer' revoked from {token_hash}."
        )

    def test_archive(self) -> None:
        self.assertRevokes(
            token_hash="0" * 64,
            role=CollectionRoles.VIEWER,
            collection_category=CollectionCategory.ARCHIVE,
            collection_name="_",
        )

    def test_suite(self) -> None:
        self.assertRevokes(
            "--suite=sid",
            token_hash="0" * 64,
            role=CollectionRoles.VIEWER,
            collection_category=CollectionCategory.SUITE,
            collection_name="sid",
        )
