# 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 debusine Cli Collection commands."""

from functools import partial
from unittest import mock

from debusine.artifacts.models import CollectionCategory
from debusine.client.commands.base import Command, DebusineCommand
from debusine.client.commands.collections import CollectionCommand
from debusine.client.commands.tests.base import BaseCliTests
from debusine.client.models import CollectionData, CollectionDataNew


class CollectionCommandTests(BaseCliTests):
    """Tests for the :py:class:`CollectionCommand` class."""

    def setUp(self) -> None:
        super().setUp()
        self.data = CollectionData(
            id=42,
            name="sid",
            category=CollectionCategory.SUITE,
            full_history_retention_period=7,
            metadata_only_retention_period=14,
            data={"test": 2},
        )

    @Command.preserve_registry()
    def _command(self) -> CollectionCommand:

        class ConcreteCollectionCommand(CollectionCommand):
            """Version of CollectionCommand that can be instantiated."""

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

        return ConcreteCollectionCommand(
            self.build_parsed_namespace(workspace="workspace")
        )

    def test_markup_category(self) -> None:
        command = self._command()
        self.assertEqual(
            command.markup_category("debian:suite"),
            "[link=https://debusine.debian.org/debian/workspace/collection/"
            "debian%3Asuite/]debian:suite[/]",
        )
        self.assertEqual(
            command.markup_category("debusine:workflow-internal"),
            "debusine:workflow-internal",
        )

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

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

        self.assertEqual(table.add_row.call_count, 1)
        url = (
            "https://debusine.debian.org/debian/workspace/"
            "collection/debian%3Asuite/sid/"
        )
        category_url = (
            "https://debusine.debian.org/debian/workspace/"
            "collection/debian%3Asuite/"
        )
        call = table.add_row.call_args_list[0]
        self.assertEqual(call.args[0], "42")
        self.assertEqual(call.args[1], f"[link={url}]sid[/]")
        self.assertEqual(call.args[2], f"[link={category_url}]debian:suite[/]")
        self.assertEqual(call.args[3], "7")
        self.assertEqual(call.args[4], "14")

    def test_show_rich(self) -> None:
        command = self._command()
        with (
            mock.patch(
                "debusine.client.commands.collections.Table",
                return_value=(table := mock.MagicMock()),
            ),
            mock.patch(
                "debusine.client.commands.collections.rich.print"
            ) as rich_print,
        ):
            stderr, stdout = self.capture_output(
                partial(command._show_rich, self.data)
            )

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

        self.assertEqual(table.add_row.call_count, 6)
        url = (
            "https://debusine.debian.org/debian/workspace/"
            "collection/debian%3Asuite/sid/"
        )
        category_url = (
            "https://debusine.debian.org/debian/workspace/"
            "collection/debian%3Asuite/"
        )
        rows = [
            call_args_list.args
            for call_args_list in table.add_row.call_args_list
        ]
        self.assertEqual(rows[0], ("ID:", "#42"))
        self.assertEqual(rows[1], ("Name:", f"[link={url}]sid[/]"))
        self.assertEqual(
            rows[2], ("Category:", f"[link={category_url}]debian:suite[/]")
        )
        self.assertEqual(rows[3], ("Full history retention:", "7 days"))
        self.assertEqual(rows[4], ("Metadata only retention:", "14 days"))


class ListTests(BaseCliTests):
    """Tests for the :py:class:`collections.List` class."""

    def setUp(self) -> None:
        super().setUp()
        self.data = CollectionData(
            id=42,
            name="sid",
            category=CollectionCategory.SUITE,
            full_history_retention_period=7,
            metadata_only_retention_period=14,
            data={"test": 2},
        )

    def test_list(self) -> None:
        command = self.create_command(
            ["collection", "list", "--workspace=workspace"]
        )
        assert isinstance(command, DebusineCommand)
        with (
            mock.patch.object(
                command.debusine, "collection_iter", return_value=[self.data]
            ) as collection_iter,
            mock.patch.object(command, "list") as list_,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        collection_iter.assert_called_once_with("workspace")
        list_.assert_called_once_with([self.data])


class ShowTests(BaseCliTests):
    """Tests for the :py:class:`collections.Show` class."""

    def test_show(self) -> None:
        data = CollectionData(
            id=42,
            name="sid",
            category=CollectionCategory.SUITE,
            full_history_retention_period=7,
            metadata_only_retention_period=14,
            data={"test": 2},
        )

        command = self.create_command(
            ["collection", "show", "--workspace=workspace", "category", "name"]
        )
        assert isinstance(command, DebusineCommand)
        with (
            mock.patch.object(
                command.debusine, "collection_get", return_value=data
            ) as collection_get,
            mock.patch.object(command, "show") as show,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        collection_get.assert_called_once_with("workspace", "category", "name")
        show.assert_called_once_with(data)


class CreateTests(BaseCliTests):
    """Tests for the :py:class:`collections.Create` class."""

    def setUp(self) -> None:
        """Set up the test case."""
        super().setUp()
        self.sent = CollectionDataNew(
            name="sid",
            category=CollectionCategory.SUITE,
            data={},
        )

    def assertCreates(self, *args: str) -> None:
        """Collection_create with the given arguments returns self.sample."""
        command = self.create_command(
            [
                "collection",
                "create",
                "--workspace=workspace",
                CollectionCategory.SUITE,
                "sid",
                *args,
            ]
        )
        assert isinstance(command, DebusineCommand)

        created = CollectionData(id=42, **self.sent.dict())
        with (
            mock.patch.object(
                command.debusine, "collection_create", return_value=created
            ) as collection_create,
            mock.patch.object(command, "show") as show,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        collection_create.assert_called_once_with("workspace", self.sent)
        show.assert_called_once_with(created)

    def test_defaults(self) -> None:
        self.enterContext(self.patch_sys_stdin_read(""))
        self.assertCreates()

    def test_set_full_history_retention_period(self) -> None:
        self.enterContext(self.patch_sys_stdin_read(""))
        self.sent.full_history_retention_period = 1
        self.assertCreates("--full-history-retention-period=1")

    def test_unset_full_history_retention_period(self) -> None:
        self.enterContext(self.patch_sys_stdin_read(""))
        self.sent.full_history_retention_period = None
        self.assertCreates("--full-history-retention-period=-")

    def test_set_metadata_only_retention_period(self) -> None:
        self.enterContext(self.patch_sys_stdin_read(""))
        self.sent.metadata_only_retention_period = 1
        self.assertCreates("--metadata-only-retention-period=1")

    def test_unset_metadata_only_retention_period(self) -> None:
        self.enterContext(self.patch_sys_stdin_read(""))
        self.sent.metadata_only_retention_period = None
        self.assertCreates("--metadata-only-retention-period=-")

    def test_set_data(self) -> None:
        infile = self.create_temporary_file()
        infile.write_text("{'test': 7}")
        self.sent.data = {"test": 7}
        self.assertCreates(f"--data={infile}")

    def test_set_data_stdin(self) -> None:
        self.enterContext(self.patch_sys_stdin_read("{'test': 7}"))
        self.sent.data = {"test": 7}
        self.assertCreates("--data=-")

    def test_invalid_period(self) -> None:
        stderr, stdout = self.capture_output(
            partial(
                self.create_command,
                [
                    "collection",
                    "create",
                    "--workspace=workspace",
                    "category",
                    "name",
                    "--full-history-retention-period=invalid",
                ],
            ),
            assert_system_exit_code=2,
        )
        self.assertRegex(
            stderr,
            r"error: retention period must be a number or '-'",
        )


class ManageTests(BaseCliTests):
    """Tests for the :py:class:`collections.Manage` class."""

    def setUp(self) -> None:
        """Set up the test case."""
        super().setUp()
        self.sample = CollectionData(
            id=42,
            name="sid",
            category=CollectionCategory.SUITE,
            full_history_retention_period=7,
            metadata_only_retention_period=14,
            data={"test": 2},
        )
        self.edited = self.sample.copy()

    def assertManages(self, *args: str) -> None:
        """Call collection_update with the given arguments match self.edited."""
        command = self.create_command(
            [
                "collection",
                "manage",
                "--workspace=workspace",
                "category",
                "name",
                *args,
            ]
        )
        assert isinstance(command, DebusineCommand)

        with (
            mock.patch.object(
                command.debusine, "collection_get", return_value=self.sample
            ),
            mock.patch.object(
                command.debusine, "collection_update", return_value=self.edited
            ) as collection_update,
            mock.patch.object(command, "show") as show,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        collection_update.assert_called_once_with("workspace", self.edited)
        show.assert_called_once_with(self.edited)

    def test_rename(self) -> None:
        self.edited.name = "renamed"
        self.assertManages("--rename=renamed")

    def test_set_full_history_retention_period(self) -> None:
        self.edited.full_history_retention_period = 1
        self.assertManages("--full-history-retention-period=1")

    def test_unset_full_history_retention_period(self) -> None:
        self.edited.full_history_retention_period = None
        self.assertManages("--full-history-retention-period=-")

    def test_set_metadata_only_retention_period(self) -> None:
        self.edited.metadata_only_retention_period = 1
        self.assertManages("--metadata-only-retention-period=1")

    def test_unset_metadata_only_retention_period(self) -> None:
        self.edited.metadata_only_retention_period = None
        self.assertManages("--metadata-only-retention-period=-")

    def test_set_data(self) -> None:
        infile = self.create_temporary_file()
        infile.write_text("{'test': 7}")
        self.edited.data = {"test": 7}
        self.assertManages(f"--data={infile}")

    def test_set_data_stdin(self) -> None:
        self.enterContext(self.patch_sys_stdin_read("{'test': 7}"))
        self.edited.data = {"test": 7}
        self.assertManages("--data=-")

    def test_invalid_period(self) -> None:
        stderr, stdout = self.capture_output(
            partial(
                self.create_command,
                [
                    "collection",
                    "manage",
                    "--workspace=workspace",
                    "category",
                    "name",
                    "--full-history-retention-period=invalid",
                ],
            ),
            assert_system_exit_code=2,
        )
        self.assertRegex(
            stderr,
            r"error: retention period must be a number or '-'",
        )
