# 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 package management commands."""

from collections.abc import Generator, Sequence
from functools import partial
from typing import override
from unittest import mock

from debusine.artifacts.models import ArtifactCategory, CollectionCategory
from debusine.client.commands.archives.packages import PackageCommand
from debusine.client.commands.base import Command, DebusineCommand
from debusine.client.commands.tests.base import BaseCliTests
from debusine.client.models import CollectionItemData


class PackageCommandTests(BaseCliTests):
    """Tests for the :py:class:`PackageCommand` class."""

    @override
    def setUp(self) -> None:
        super().setUp()
        self.items = [
            CollectionItemData(
                name="hello_1.0",
                category=ArtifactCategory.SOURCE_PACKAGE,
                data={
                    "package": "hello",
                    "version": "1.0",
                    "component": "main",
                    "section": "misc",
                },
            ),
            CollectionItemData(
                name="hello_1.0_amd64",
                category=ArtifactCategory.BINARY_PACKAGE,
                data={
                    "srcpkg_name": "hello",
                    "srcpkg_version": "1.0",
                    "package": "hello",
                    "version": "1.0",
                    "architecture": "amd64",
                    "component": "main",
                    "section": "misc",
                    "priority": "optional",
                },
            ),
            CollectionItemData(
                name="index:Release",
                category=ArtifactCategory.REPOSITORY_INDEX,
                data={"path": "Release"},
            ),
        ]

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

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

        return self.command(
            ConcretePackageCommand, workspace="workspace", suite="sid"
        )

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

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

        rows = [
            call_args_list.args
            for call_args_list in table.add_row.call_args_list
        ]
        self.assertEqual(
            rows, [("hello", "1.0", "source"), ("hello", "1.0", "amd64")]
        )

    def test_show_rich_source(self) -> None:
        command = self._command()
        with (
            mock.patch(
                "debusine.client.commands.archives.packages.Table",
                return_value=(table := mock.MagicMock()),
            ),
            mock.patch(
                "debusine.client.commands.archives.packages.rich.print"
            ) as rich_print,
        ):
            stderr, stdout = self.capture_output(
                partial(command._show_rich, self.items[0])
            )

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

        rows = [
            call_args_list.args
            for call_args_list in table.add_row.call_args_list
        ]
        self.assertEqual(
            rows,
            [
                ("Item name:", "hello_1.0"),
                ("Category:", ArtifactCategory.SOURCE_PACKAGE),
                ("Component:", "main"),
                ("Section:", "misc"),
            ],
        )

    def test_show_rich_binary(self) -> None:
        command = self._command()
        with (
            mock.patch(
                "debusine.client.commands.archives.packages.Table",
                return_value=(table := mock.MagicMock()),
            ),
            mock.patch(
                "debusine.client.commands.archives.packages.rich.print"
            ) as rich_print,
        ):
            stderr, stdout = self.capture_output(
                partial(command._show_rich, self.items[1])
            )

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

        rows = [
            call_args_list.args
            for call_args_list in table.add_row.call_args_list
        ]
        self.assertEqual(
            rows,
            [
                ("Item name:", "hello_1.0_amd64"),
                ("Category:", ArtifactCategory.BINARY_PACKAGE),
                ("Source package name:", "hello"),
                ("Source package version:", "1.0"),
                ("Component:", "main"),
                ("Section:", "misc"),
                ("Priority:", "optional"),
            ],
        )

    def test_show_rich_index(self) -> None:
        command = self._command()
        with (
            mock.patch(
                "debusine.client.commands.archives.packages.Table",
                return_value=(table := mock.MagicMock()),
            ),
            mock.patch(
                "debusine.client.commands.archives.packages.rich.print"
            ) as rich_print,
        ):
            stderr, stdout = self.capture_output(
                partial(command._show_rich, self.items[2])
            )

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

        rows = [
            call_args_list.args
            for call_args_list in table.add_row.call_args_list
        ]
        self.assertEqual(
            rows,
            [
                ("Item name:", "index:Release"),
                ("Category:", ArtifactCategory.REPOSITORY_INDEX),
            ],
        )


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

    def assertLists(self, suite: str) -> None:
        items = [
            CollectionItemData(
                name="hello_1.0",
                category=ArtifactCategory.SOURCE_PACKAGE,
                data={
                    "package": "hello",
                    "version": "1.0",
                    "component": "main",
                    "section": "misc",
                },
            ),
            CollectionItemData(
                name="hello_1.0_amd64",
                category=ArtifactCategory.BINARY_PACKAGE,
                data={
                    "srcpkg_name": "hello",
                    "srcpkg_version": "1.0",
                    "package": "hello",
                    "version": "1.0",
                    "architecture": "amd64",
                    "component": "main",
                    "section": "misc",
                    "priority": "optional",
                },
            ),
            CollectionItemData(
                name="index:Release",
                category=ArtifactCategory.REPOSITORY_INDEX,
                data={"path": "Release"},
            ),
        ]
        command = self.create_command(
            [
                "archive",
                "package",
                "list",
                "--workspace=workspace",
                f"--suite={suite}",
            ]
        )
        assert isinstance(command, DebusineCommand)
        with (
            mock.patch.object(
                command.debusine, "collection_item_iter", return_value=items
            ) as collection_item_iter,
            mock.patch.object(command, "list") as list_,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        collection_item_iter.assert_called_once_with(
            workspace="workspace",
            collection_category=CollectionCategory.SUITE,
            collection_name=suite,
        )
        list_.assert_called_once_with(items)

    def test_list(self) -> None:
        self.assertLists(suite="sid")


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

    def assertShows(self, suite: str, name: str) -> None:
        item = CollectionItemData(
            name="hello_1.0",
            category=ArtifactCategory.SOURCE_PACKAGE,
            data={
                "package": "hello",
                "version": "1.0",
                "component": "main",
                "section": "misc",
            },
        )
        command = self.create_command(
            [
                "archive",
                "package",
                "show",
                "--workspace=workspace",
                f"--suite={suite}",
                name,
            ]
        )
        assert isinstance(command, DebusineCommand)
        with (
            mock.patch.object(
                command.debusine, "collection_item_get", return_value=item
            ) as collection_item_get,
            mock.patch.object(command, "show") as show,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        collection_item_get.assert_called_once_with(
            workspace="workspace",
            collection_category=CollectionCategory.SUITE,
            collection_name=suite,
            name=name,
        )
        show.assert_called_once_with(item)

    def test_show(self) -> None:
        self.assertShows("sid", "hello_1.0")


class RemoveTests(BaseCliTests):
    """Tests for the :py:class:`Remove` class."""

    @override
    def setUp(self) -> None:
        super().setUp()
        self.items = [
            CollectionItemData(
                name=f"{name}_{version}",
                category=ArtifactCategory.SOURCE_PACKAGE,
                data={
                    "package": name,
                    "version": version,
                    "component": "main",
                    "section": "misc",
                },
            )
            for name, version in (
                ("hello", "1.0"),
                ("hello", "1.01"),
                ("other", "1.0"),
            )
        ]
        self.items += [
            CollectionItemData(
                name=f"{name}_{version}_{architecture}",
                category=ArtifactCategory.BINARY_PACKAGE,
                data={
                    "srcpkg_name": name,
                    "srcpkg_version": version,
                    "package": name,
                    "version": version,
                    "architecture": architecture,
                    "component": "main",
                    "section": "misc",
                    "priority": "optional",
                },
            )
            for srcpkg_name, srcpkg_version, name, version, architecture in (
                ("hello", "1.0", "hello", "1.0", "amd64"),
                ("hello", "1.0", "hello", "1.0", "s390x"),
                ("hello", "1.0", "hello-common", "1.0", "all"),
                ("hello", "1.01", "hello", "1.01", "amd64"),
                ("hello", "1.01", "hello-common", "1.01", "all"),
                ("other", "1.0", "other", "1.0", "amd64"),
            )
        ]

    def fake_collection_item_iter(
        self,
        workspace: str,  # noqa: ARG002
        collection_category: CollectionCategory,  # noqa: ARG002
        collection_name: str,  # noqa: ARG002
        name__startswith: str | None = None,
        category: ArtifactCategory | None = None,
    ) -> Generator[CollectionItemData]:
        for item in self.items:
            if name__startswith is not None and not item.name.startswith(
                name__startswith
            ):
                continue
            if category is not None and item.category != category:
                continue
            yield item

    def assertRemoves(
        self, *args: str, suite: str, items: Sequence[CollectionItemData]
    ) -> None:
        command = self.create_command(
            [
                "archive",
                "package",
                "remove",
                "--workspace=workspace",
                f"--suite={suite}",
                *args,
            ]
        )
        assert isinstance(command, DebusineCommand)

        with (
            mock.patch.object(
                command.debusine,
                "collection_item_iter",
                side_effect=self.fake_collection_item_iter,
            ),
            mock.patch.object(
                command.debusine, "collection_item_delete"
            ) as collection_item_delete,
            mock.patch.object(command, "feedback") as feedback,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        self.assertEqual(collection_item_delete.call_count, len(items))
        collection_item_delete.assert_has_calls(
            [
                mock.call(
                    workspace="workspace",
                    collection_category=CollectionCategory.SUITE,
                    collection_name=suite,
                    name=item.name,
                )
                for item in items
            ]
        )
        self.assertEqual(feedback.call_count, len(items))
        feedback.assert_has_calls(
            [mock.call(f"Removed: {item.name}") for item in items]
        )

    def assertDryRunRemoves(
        self, *args: str, suite: str, items: Sequence[CollectionItemData]
    ) -> None:
        command = self.create_command(
            [
                "archive",
                "package",
                "remove",
                "--workspace=workspace",
                f"--suite={suite}",
                "--dry-run",
                *args,
            ]
        )
        assert isinstance(command, DebusineCommand)

        with (
            mock.patch.object(
                command.debusine,
                "collection_item_iter",
                side_effect=self.fake_collection_item_iter,
            ),
            mock.patch.object(command, "list") as list_,
            mock.patch.object(
                command.debusine, "collection_item_delete"
            ) as collection_item_delete,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        list_.assert_called_once_with(list(items))
        collection_item_delete.assert_not_called()

    def test_one_package(self) -> None:
        self.assertRemoves("hello", suite="sid", items=self.items[:2])

    def test_one_package_dry_run(self) -> None:
        self.assertDryRunRemoves("hello", suite="sid", items=self.items[:2])

    def test_multiple_packages(self) -> None:
        self.assertRemoves("hello", "other", suite="sid", items=self.items[:3])

    def test_nonexistent_package(self) -> None:
        self.assertRemoves("nonexistent", suite="sid", items=[])

    def test_package_version_source(self) -> None:
        self.assertRemoves(
            "--package-version=1.0",
            "hello",
            suite="sid",
            items=[self.items[0]],
        )

    def test_package_version_binary(self) -> None:
        self.assertRemoves(
            "--package-version=1.0",
            "--binary",
            "hello",
            suite="sid",
            items=[self.items[3], self.items[4]],
        )

    def test_binary(self) -> None:
        self.assertRemoves(
            "--binary",
            "hello",
            suite="sid",
            items=[self.items[3], self.items[4], self.items[6]],
        )

    def test_architecture(self) -> None:
        self.assertRemoves(
            "--architecture=amd64",
            "hello",
            suite="sid",
            items=[self.items[3], self.items[6]],
        )
