# 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."""

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

import yaml

from debusine.artifacts.models import CollectionCategory
from debusine.client.commands.base import Command, DebusineCommand
from debusine.client.commands.collections import (
    CollectionCommand,
    SingleCollectionCommand,
)
from debusine.client.commands.tests.base import BaseCliTests
from debusine.client.exceptions import DebusineError
from debusine.client.models import (
    CollectionData,
    CollectionDataNew,
    CollectionItemResponse,
    LookupChildType,
    LookupMultipleResponse,
    LookupResultType,
    LookupSingleResponse,
)


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

    @override
    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},
            links={
                "webui_self": "/c/42",
                "webui_category": "/c/suite",
            },
        )

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

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

        return self.command(ConcreteCollectionCommand, workspace="workspace")

    def test_markup_category(self) -> None:
        command = self._command()
        self.assertEqual(
            command.markup_category(self.data),
            "[link=https://debusine.debian.org/c/suite]debian:suite[/]",
        )
        self.data.category = CollectionCategory.WORKFLOW_INTERNAL
        self.assertEqual(
            command.markup_category(self.data), "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/c/42"
        category_url = "https://debusine.debian.org/c/suite"
        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,
            mock.patch("rich.json.JSON.from_data", side_effect=lambda x: x),
        ):
            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, 7)
        url = "https://debusine.debian.org/c/42"
        category_url = "https://debusine.debian.org/c/suite"
        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], ("Web URL:", f"[link={url}]{url}[/]"))
        self.assertEqual(rows[2], ("Name:", "sid"))
        self.assertEqual(
            rows[3], ("Category:", f"[link={category_url}]debian:suite[/]")
        )
        self.assertEqual(rows[4], ("Full history retention:", "7 days"))
        self.assertEqual(rows[5], ("Metadata only retention:", "14 days"))
        self.assertEqual(rows[6], ("Data:", self.data.data))

    def test_show_rich_stats(self) -> None:
        self.data.stats = [
            {"type": "test", "category": "test", "active": True, "count": 2}
        ]
        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,
            mock.patch("rich.json.JSON.from_data", side_effect=lambda x: x),
        ):
            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, 8)
        url = "https://debusine.debian.org/c/42"
        category_url = "https://debusine.debian.org/c/suite"
        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], ("Web URL:", f"[link={url}]{url}[/]"))
        self.assertEqual(rows[2], ("Name:", "sid"))
        self.assertEqual(
            rows[3], ("Category:", f"[link={category_url}]debian:suite[/]")
        )
        self.assertEqual(rows[4], ("Full history retention:", "7 days"))
        self.assertEqual(rows[5], ("Metadata only retention:", "14 days"))
        self.assertEqual(rows[6], ("Data:", self.data.data))
        self.assertEqual(rows[7], ("Stats:", self.data.stats))


class SingleCollectionCommandTests(BaseCliTests):
    """Tests for the :py:class:`SingleCollectionCommand` class."""

    @Command.preserve_registry()
    def _command(
        self, category_or_fullname: str, name: str | None = None
    ) -> SingleCollectionCommand:
        class ConcreteCollectionCommand(SingleCollectionCommand):
            """Version of SingleCollectionCommand that can be instantiated."""

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

        return self.command(
            ConcreteCollectionCommand,
            workspace="workspace",
            category_or_fullname=category_or_fullname,
            name=name,
        )

    def test_missing_name(self) -> None:
        for arg in ("debian:suite", "debian:archive", "invalid"):
            with self.subTest(arg=arg):
                command = self._command(arg)
                self.assertEqual(command.collection_category, arg)
                self.assertEqual(command.collection_name, "_")

    def test_full_name(self) -> None:
        for arg in (
            "sid@debian:suite",
            "_@debian:suite",
            "sid@debian:archive",
            "_@debian:archive",
        ):
            with self.subTest(arg=arg):
                command = self._command(arg)
                name, category = arg.split("@")
                self.assertEqual(command.collection_category, category)
                self.assertEqual(command.collection_name, name)

    def test_separate_name(self) -> None:
        for category, name in (
            ("debian:archive", "_"),
            ("debian:suite", "_"),
            ("debian:archive", "sid"),
            ("debian:suite", "sid"),
        ):
            with self.subTest(category=category, name=name):
                command = self._command(category, name)
                self.assertEqual(command.collection_category, category)
                self.assertEqual(command.collection_name, name)

    def test_name_given_twice(self) -> None:
        for category, name in (
            ("_@debian:archive", "_"),
            ("_@debian:suite", "_"),
            ("sid@debian:archive", "sid"),
            ("sid@debian:suite", "sid"),
            ("a@debian:suite", "b"),
            ("a@debian:suite", "b"),
        ):
            with self.subTest(category=category, name=name):
                stderr, _ = self.capture_output(
                    partial(self._command, category, name),
                    assert_system_exit_code=2,
                )
                self.assertRegex(
                    stderr,
                    "collection name is given"
                    f" both in `{re.escape(category)}`"
                    " and in the name argument",
                )


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

    @override
    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", with_stats=True
        )
        show.assert_called_once_with(data)


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

    @override
    def setUp(self) -> None:
        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.model_dump())
        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, _ = 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."""

    @override
    def setUp(self) -> None:
        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.model_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, _ = 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 '-'",
        )


class DeleteTests(BaseCliTests):
    """Tests for the :py:class:`collections.Delete` class."""

    @override
    def setUp(self) -> None:
        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},
            stats=[],
        )

    def test_delete(self, *args: str) -> None:
        command = self.create_command(
            [
                "collection",
                "delete",
                "--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_delete",
            ) as collection_delete,
            mock.patch.object(command, "feedback") as feedback,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        collection_delete.assert_called_once_with("workspace", self.sample)
        feedback.assert_called_once_with("Collection deleted.")

    def test_delete_nonempty(self, *args: str) -> None:
        assert self.sample.stats is not None
        self.sample.stats = [
            {
                "type": "artifact",
                "category": "debusine:test",
                "active": True,
                "count": 1,
            },
            {
                "type": "artifact",
                "category": "debusine:test",
                "active": False,
                "count": 15,
            },
        ]
        command = self.create_command(
            [
                "collection",
                "delete",
                "--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_delete",
            ) as collection_delete,
            mock.patch.object(command, "feedback") as feedback,
        ):
            stderr, stdout = self.capture_output(
                command.run, assert_system_exit_code=3
            )

        self.assertEqual(stdout, "")
        self.assertEqual(
            stderr,
            "Collection is not empty"
            " (active debusine:test artifact: 1,"
            " deleted debusine:test artifact: 15):"
            " use --force to delete it.\n",
        )
        collection_delete.assert_not_called()
        feedback.assert_not_called()

    def test_delete_nonempty_force(self, *args: str) -> None:
        assert self.sample.stats is not None
        self.sample.stats += [
            {
                "type": "artifact",
                "category": "test",
                "active": True,
                "count": 1,
            },
            {
                "type": "artifact",
                "category": "test",
                "active": False,
                "count": 15,
            },
        ]
        command = self.create_command(
            [
                "collection",
                "delete",
                "--workspace=workspace",
                "category",
                "name",
                "--force",
                *args,
            ]
        )
        assert isinstance(command, DebusineCommand)

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

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        collection_delete.assert_called_once_with("workspace", self.sample)
        feedback.assert_called_once_with("Collection deleted.")


class LookupSingleTests(BaseCliTests):
    """Tests for the :py:class:`collections.LookupSingle` class."""

    def test_no_result(self) -> None:
        command = self.create_command(
            [
                "collection",
                "lookup-single",
                "--workspace",
                "workspace",
                "sid@debian:suite/doesntexist",
            ]
        )
        assert isinstance(command, DebusineCommand)
        detail = "'sid@debian:suite' has no item 'name:doesntexist'"
        with mock.patch.object(
            command.debusine,
            "lookup_single",
            side_effect=DebusineError(
                title="No matches",
                detail=detail,
            ),
        ) as lookup_single:
            stderr, stdout = self.capture_output(
                command.run, assert_system_exit_code=3
            )

        self.assertEqual(stdout, "")
        self.assertEqual(
            yaml.safe_load(stderr),
            {
                "result": "failure",
                "error": {
                    "title": "No matches",
                    "detail": detail,
                },
            },
        )
        lookup_single.assert_called_once_with(
            "sid@debian:suite/doesntexist",
            'workspace',
            LookupChildType.ANY,
            None,
            None,
        )

    def test_success(self) -> None:
        command = self.create_command(
            [
                "collection",
                "lookup-single",
                "--workspace",
                "workspace",
                "sid@debian:suite/libfoo_1.0.0-1",
            ]
        )
        assert isinstance(command, DebusineCommand)
        collection_item = CollectionItemResponse(
            name="libfoo_1.0.0-1",
            data={
                "package": "libfoo",
                "version": "1.0.0-1",
                "component": "main",
                "section": "libs",
            },
        )
        with (
            mock.patch.object(
                command.debusine,
                "lookup_single",
                return_value=LookupSingleResponse(
                    result_type=LookupResultType.ARTIFACT,
                    collection_item=collection_item,
                    artifact=42,
                    collection=None,
                    parent_collection_lookup="sid@debian:suite",
                    links={
                        "self": "/api/a/42",
                        "webui_self": "/a/42",
                        "parent_collection": "/api/c/sid",
                        "webui_parent_collection": "/c/sid",
                    },
                ),
            ) as lookup_single,
            mock.patch(
                "debusine.client.commands.collections.Table",
                return_value=(table := mock.MagicMock()),
            ),
            mock.patch(
                "debusine.client.commands.collections.rich.print"
            ) as rich_print,
            mock.patch("rich.json.JSON.from_data", side_effect=lambda x: x),
        ):
            stderr, stdout = self.capture_output(command.run)

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

        lookup_single.assert_called_once_with(
            "sid@debian:suite/libfoo_1.0.0-1",
            'workspace',
            LookupChildType.ANY,
            None,
            None,
        )
        rich_print.assert_called_once()
        self.assertEqual(table.add_row.call_count, 1)
        call = table.add_row.call_args_list[0]
        self.assertEqual(call.args[0], "ARTIFACT")
        self.assertEqual(
            call.args[1], "[link=https://debusine.debian.org/a/42]42[/]"
        )
        self.assertEqual(call.args[2], "")
        self.assertEqual(
            call.args[3],
            "[link=https://debusine.debian.org/c/sid]sid@debian:suite[/]",
        )
        self.assertEqual(call.args[4], collection_item.model_dump())

    def test_success_yaml(self) -> None:
        command = self.create_command(
            [
                "collection",
                "lookup-single",
                "--workspace",
                "workspace",
                "sid@debian:suite/libfoo_1.0.0-1",
                "--yaml",
            ]
        )
        assert isinstance(command, DebusineCommand)
        response = LookupSingleResponse(
            result_type=LookupResultType.ARTIFACT,
            collection_item=CollectionItemResponse(
                name="libfoo_1.0.0-1",
                data={
                    "package": "libfoo",
                    "version": "1.0.0-1",
                    "component": "main",
                    "section": "libs",
                },
            ),
            artifact=42,
            collection=None,
            parent_collection_lookup="sid@debian:suite",
        )
        with mock.patch.object(
            command.debusine, "lookup_single", return_value=response
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stderr, "")
        self.assertEqual(
            yaml.safe_load(stdout),
            response.model_dump(exclude_none=True, exclude_unset=True),
        )

    def test_collection_result(self) -> None:
        command = self.create_command(
            [
                "collection",
                "lookup-single",
                "--workspace",
                "workspace",
                "sid@debian:suite",
            ]
        )
        assert isinstance(command, DebusineCommand)
        with (
            # On trixie: https://github.com/coveragepy/coveragepy/issues/1880
            # pragma: no cover
            mock.patch.object(
                command.debusine,
                "lookup_single",
                return_value=LookupSingleResponse(
                    result_type=LookupResultType.COLLECTION,
                    collection_item=None,
                    artifact=None,
                    collection=42,
                    parent_collection_lookup=None,
                    links={
                        "self": "/api/c/42",
                        "webui_self": "/c/42",
                    },
                ),
            ) as lookup_single,
            mock.patch(
                "debusine.client.commands.collections.Table",
                return_value=(table := mock.MagicMock()),
            ),
            mock.patch(
                "debusine.client.commands.collections.rich.print"
            ) as rich_print,
            mock.patch("rich.json.JSON.from_data", side_effect=lambda x: x),
        ):
            stderr, stdout = self.capture_output(command.run)

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

        lookup_single.assert_called_once_with(
            "sid@debian:suite",
            'workspace',
            LookupChildType.ANY,
            None,
            None,
        )
        rich_print.assert_called_once()
        self.assertEqual(table.add_row.call_count, 1)
        call = table.add_row.call_args_list[0]
        self.assertEqual(call.args[0], "COLLECTION")
        self.assertEqual(call.args[1], "")
        self.assertEqual(
            call.args[2], "[link=https://debusine.debian.org/c/42]42[/]"
        )
        self.assertEqual(call.args[3], "")
        self.assertEqual(call.args[4], "")


class LookupMultipleTests(BaseCliTests):
    """Tests for the :py:class:`collections.LookupMultiple` class."""

    def test_no_result(self) -> None:
        command = self.create_command(
            [
                "collection",
                "lookup-multiple",
                "--workspace",
                "workspace",
                "sid@debian:suite/doesntexist",
            ]
        )
        assert isinstance(command, DebusineCommand)
        detail = "'sid@debian:suite' has no item 'name:doesntexist'"
        with mock.patch.object(
            command.debusine,
            "lookup_multiple",
            side_effect=DebusineError(
                title="No matches",
                detail=detail,
            ),
        ) as lookup_multiple:
            stderr, stdout = self.capture_output(
                command.run, assert_system_exit_code=3
            )

        self.assertEqual(stdout, "")
        self.assertEqual(
            yaml.safe_load(stderr),
            {
                "result": "failure",
                "error": {
                    "title": "No matches",
                    "detail": detail,
                },
            },
        )
        lookup_multiple.assert_called_once_with(
            ["sid@debian:suite/doesntexist"],
            'workspace',
            LookupChildType.ANY,
            None,
            None,
        )

    def test_success(self) -> None:
        command = self.create_command(
            [
                "collection",
                "lookup-multiple",
                "--workspace",
                "workspace",
                "sid@debian:suite/libfoo_1.0.0-1",
                "sid@debian:suite/libbar_1.0.0-1",
            ]
        )
        assert isinstance(command, DebusineCommand)
        collection_items = [
            CollectionItemResponse(
                name="libfoo_1.0.0-1",
                data={
                    "package": "libfoo",
                    "version": "1.0.0-1",
                    "component": "main",
                    "section": "libs",
                },
            ),
            CollectionItemResponse(
                name="libbar_1.0.0-1",
                data={
                    "package": "libbar",
                    "version": "1.0.0-1",
                    "component": "main",
                    "section": "libs",
                },
            ),
        ]
        response = LookupMultipleResponse(
            [
                LookupSingleResponse(
                    result_type=LookupResultType.ARTIFACT,
                    collection_item=collection_item,
                    artifact=artifact,
                    collection=None,
                    parent_collection_lookup="sid@debian:suite",
                    links={
                        "self": "/api/a/42",
                        "webui_self": "/a/42",
                        "parent_collection": "/api/c/sid",
                        "webui_parent_collection": "/c/sid",
                    },
                )
                for (artifact, collection_item) in enumerate(
                    collection_items, start=42
                )
            ]
        )
        with (
            mock.patch.object(
                command.debusine, "lookup_multiple", return_value=response
            ) as lookup_multiple,
            mock.patch(
                "debusine.client.commands.collections.Table",
                return_value=(table := mock.MagicMock()),
            ),
            mock.patch(
                "debusine.client.commands.collections.rich.print"
            ) as rich_print,
            mock.patch("rich.json.JSON.from_data", side_effect=lambda x: x),
        ):
            stderr, stdout = self.capture_output(command.run)

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

        lookup_multiple.assert_called_once_with(
            [
                "sid@debian:suite/libfoo_1.0.0-1",
                "sid@debian:suite/libbar_1.0.0-1",
            ],
            'workspace',
            LookupChildType.ANY,
            None,
            None,
        )
        rich_print.assert_called_once()
        self.assertEqual(table.add_row.call_count, 2)
        call = table.add_row.call_args_list[0]
        self.assertEqual(call.args[0], "ARTIFACT")
        self.assertEqual(
            call.args[1], "[link=https://debusine.debian.org/a/42]42[/]"
        )
        self.assertEqual(call.args[2], "")
        self.assertEqual(
            call.args[3],
            "[link=https://debusine.debian.org/c/sid]sid@debian:suite[/]",
        )
        self.assertEqual(call.args[4], collection_items[0].model_dump())

    def test_success_yaml(self) -> None:
        command = self.create_command(
            [
                "collection",
                "lookup-multiple",
                "--workspace",
                "workspace",
                "sid@debian:suite/libfoo_1.0.0-1",
                "--yaml",
            ]
        )
        assert isinstance(command, DebusineCommand)
        response = LookupSingleResponse(
            result_type=LookupResultType.ARTIFACT,
            collection_item=CollectionItemResponse(
                name="libfoo_1.0.0-1",
                data={
                    "package": "libfoo",
                    "version": "1.0.0-1",
                    "component": "main",
                    "section": "libs",
                },
            ),
            artifact=42,
            collection=None,
            parent_collection_lookup="sid@debian:suite",
        )
        with mock.patch.object(
            command.debusine, "lookup_multiple", return_value=response
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stderr, "")
        self.assertEqual(
            yaml.safe_load(stdout),
            response.model_dump(exclude_none=True, exclude_unset=True),
        )

    def test_invalid_yaml_lookup_without_key(self) -> None:
        self.enterContext(self.patch_sys_stdin_read(""))
        stderr_writer = self.patch_sys_stderr_write()
        with self.assertRaises(SystemExit):
            self.create_command(
                [
                    "collection",
                    "lookup-multiple",
                    "--workspace",
                    "workspace",
                ]
            )
        stderr_writer.assert_called_once_with(
            "Key 'lookups' not found in YAML input.\n"
        )

    def test_invalid_yaml_lookup_wrong_type(self) -> None:
        self.enterContext(self.patch_sys_stdin_read("lookups: {}"))
        stderr_writer = self.patch_sys_stderr_write()
        with self.assertRaises(SystemExit):
            self.create_command(
                [
                    "collection",
                    "lookup-multiple",
                    "--workspace",
                    "workspace",
                ]
            )
        stderr_writer.assert_called_once_with(
            "Key 'lookups' must be a list of lookups.\n"
        )

    def test_lookups_on_stdin(self) -> None:
        self.enterContext(
            self.patch_sys_stdin_read("lookups: [sid@debian:suite/doesntexist]")
        )
        command = self.create_command(
            [
                "collection",
                "lookup-multiple",
                "--workspace",
                "workspace",
            ]
        )
        assert isinstance(command, DebusineCommand)
        with mock.patch.object(
            command.debusine,
            "lookup_multiple",
            side_effect=DebusineError(
                title="No matches",
            ),
        ) as lookup_multiple:
            self.capture_output(command.run, assert_system_exit_code=3)

        lookup_multiple.assert_called_once_with(
            ["sid@debian:suite/doesntexist"],
            'workspace',
            LookupChildType.ANY,
            None,
            None,
        )

    def test_lookups_from_file(self) -> None:
        infile = self.create_temporary_file()
        infile.write_text("lookups: [sid@debian:suite/doesntexist]")
        command = self.create_command(
            [
                "collection",
                "lookup-multiple",
                "--workspace",
                "workspace",
                "--data",
                str(infile),
            ]
        )
        assert isinstance(command, DebusineCommand)
        with mock.patch.object(
            command.debusine,
            "lookup_multiple",
            side_effect=DebusineError(
                title="No matches",
            ),
        ) as lookup_multiple:
            self.capture_output(command.run, assert_system_exit_code=3)

        lookup_multiple.assert_called_once_with(
            ["sid@debian:suite/doesntexist"],
            'workspace',
            LookupChildType.ANY,
            None,
            None,
        )
