# Copyright 2024 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.

"""Unit tests for lookups of items in collections."""

from collections.abc import Iterable, Sequence
from typing import Any, ClassVar, assert_never

from django.contrib.auth import get_user_model
from django.test import TestCase

from debusine.artifacts.models import ArtifactCategory, CollectionCategory
from debusine.db.models import (
    Artifact,
    Collection,
    CollectionItem,
    User,
    WorkRequest,
    WorkflowTemplate,
    Workspace,
    default_workspace,
)
from debusine.server.collections.debian_suite import DebianSuiteManager
from debusine.server.collections.lookup import (
    LookupResult,
    lookup_multiple,
    lookup_single,
)
from debusine.tasks.models import LookupMultiple
from debusine.test import TestHelpersMixin


class LookupMixin(TestHelpersMixin):
    """Helpers for lookup tests."""

    user: ClassVar[User]

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up common data for tests."""
        super().setUpTestData()
        cls.user = get_user_model().objects.create_user(
            username="John", email="john@example.org"
        )

    def create_bare_item(
        self,
        parent_collection: Collection,
        name: str,
        *,
        category: str = "test",
        data: dict[str, Any] | None = None,
    ) -> CollectionItem:
        """Create a collection item holding bare data."""
        return CollectionItem.objects.create(
            parent_collection=parent_collection,
            name=name,
            child_type=CollectionItem.Types.BARE,
            category=category,
            data=data or {},
            created_by_user=self.user,
        )

    def create_artifact_item(
        self,
        parent_collection: Collection,
        name: str,
        *,
        category: ArtifactCategory = ArtifactCategory.TEST,
        artifact: Artifact | None = None,
        data: dict[str, Any] | None = None,
    ) -> CollectionItem:
        """Create a collection item holding an artifact."""
        return CollectionItem.objects.create(
            parent_collection=parent_collection,
            name=name,
            child_type=CollectionItem.Types.ARTIFACT,
            category=category,
            artifact=artifact or self.create_artifact(category=category)[0],
            data=data or {},
            created_by_user=self.user,
        )

    def create_collection_item(
        self,
        parent_collection: Collection,
        name: str,
        *,
        category: CollectionCategory = CollectionCategory.TEST,
        collection: Collection | None = None,
        data: dict[str, Any] | None = None,
    ) -> CollectionItem:
        """Create a collection item holding a collection."""
        return CollectionItem.objects.create(
            parent_collection=parent_collection,
            name=name,
            child_type=CollectionItem.Types.COLLECTION,
            category=category,
            collection=collection or self.create_collection(name, category),
            data=data or {},
            created_by_user=self.user,
        )

    def create_source_package(self, name: str, version: str) -> Artifact:
        """Create a minimal `debian:source-package` artifact."""
        artifact, _ = self.create_artifact(
            category=ArtifactCategory.SOURCE_PACKAGE,
            data={
                "name": name,
                "version": version,
                "type": "dpkg",
                "dsc_fields": {},
            },
        )
        return artifact

    def create_binary_package(
        self, name: str, version: str, architecture: str
    ) -> Artifact:
        """Create a minimal `debian:binary-package` artifact."""
        artifact, _ = self.create_artifact(
            category=ArtifactCategory.BINARY_PACKAGE,
            data={
                "srcpkg_name": name,
                "srcpkg_version": version,
                "deb_fields": {
                    "Package": name,
                    "Version": version,
                    "Architecture": architecture,
                },
                "deb_control_files": [],
            },
        )
        return artifact

    @staticmethod
    def make_result(
        obj: Artifact | Collection | CollectionItem,
    ) -> LookupResult:
        """Make a LookupResult from an underlying object."""
        match obj:
            case Artifact():
                return LookupResult(
                    result_type=CollectionItem.Types.ARTIFACT, artifact=obj
                )
            case Collection():
                return LookupResult(
                    result_type=CollectionItem.Types.COLLECTION, collection=obj
                )
            case CollectionItem():
                return LookupResult(
                    result_type=CollectionItem.Types(obj.child_type),
                    collection_item=obj,
                    artifact=obj.artifact,
                    collection=obj.collection,
                )
            case _ as unreachable:
                assert_never(unreachable)


class LookupSingleTests(LookupMixin, TestCase):
    """Tests for lookup_single."""

    def assert_lookup_equal(
        self,
        lookup: int | str,
        expected: Artifact | Collection | CollectionItem,
        *,
        user: User | bool | None = True,
        workspace: Workspace | None = None,
        **kwargs: Any,
    ):
        """Assert that a lookup's result is as expected."""
        if user is True:
            user = self.user
        elif user is False:
            user = None

        self.assertEqual(
            lookup_single(
                lookup, workspace or default_workspace(), user=user, **kwargs
            ),
            self.make_result(expected),
        )

    def assert_lookup_fails(
        self,
        lookup: int | str,
        *,
        user: User | bool | None = True,
        workspace: Workspace | None = None,
        **kwargs: Any,
    ):
        """Assert that a lookup fails."""
        if user is True:
            user = self.user
        elif user is False:
            user = None

        with self.assertRaisesRegex(
            KeyError, f"'{lookup}' does not exist or is hidden"
        ):
            lookup_single(
                lookup, workspace or default_workspace(), user=user, **kwargs
            )

    def test_empty_lookup(self) -> None:
        """An empty lookup raises LookupError."""
        with self.assertRaisesRegex(LookupError, "Empty lookup"):
            lookup_single("", default_workspace(), user=None)

    def test_specific_artifact(self) -> None:
        """`nnn@artifacts` finds a specific artifact."""
        artifacts = [self.create_artifact()[0] for _ in range(3)]

        for i, artifact in enumerate(artifacts):
            lookup = f"{artifact.id}@artifacts"
            with self.subTest(lookup=lookup):
                self.assert_lookup_equal(lookup, artifact)

    def test_specific_artifact_integer(self) -> None:
        """Integer lookups find a specific artifact."""
        artifacts = [self.create_artifact()[0] for _ in range(3)]

        for i, artifact in enumerate(artifacts):
            with self.subTest(artifact=artifact):
                self.assert_lookup_equal(
                    artifact.id,
                    artifact,
                    expect_type=CollectionItem.Types.ARTIFACT,
                )
                with self.assertRaisesRegex(
                    LookupError,
                    "Integer lookups only work in contexts that expect an "
                    "artifact or a collection",
                ):
                    lookup_single(artifact.id, default_workspace(), user=None)

    def test_specific_artifact_restricts_workspace(self) -> None:
        """`nnn@artifacts` requires the given workspace or a public one."""
        private_workspaces = [
            self.create_workspace(name=f"private{i}") for i in range(2)
        ]
        public_workspace = self.create_workspace(name="public", public=True)
        artifacts = [
            self.create_artifact(workspace=workspace)[0]
            for workspace in private_workspaces + [public_workspace]
        ]

        for artifact, workspace, visible in (
            (artifacts[0], private_workspaces[0], True),
            (artifacts[0], private_workspaces[1], False),
            (artifacts[0], public_workspace, False),
            (artifacts[1], private_workspaces[0], False),
            (artifacts[1], private_workspaces[1], True),
            (artifacts[1], public_workspace, False),
            (artifacts[2], private_workspaces[0], True),
            (artifacts[2], private_workspaces[1], True),
            (artifacts[2], public_workspace, True),
        ):
            lookup = f"{artifact.id}@artifacts"
            with self.subTest(lookup=lookup, workspace=workspace):
                if visible:
                    self.assert_lookup_equal(
                        lookup, artifact, workspace=workspace
                    )
                else:
                    with self.assertRaisesRegex(
                        KeyError, f"'{lookup}' does not exist or is hidden"
                    ):
                        lookup_single(lookup, workspace, user=None)

    def test_specific_collection(self) -> None:
        """`nnn@collections` finds a specific collection."""
        collections = [
            self.create_collection(str(i), CollectionCategory.TEST)
            for i in range(3)
        ]

        for i, collection in enumerate(collections):
            lookup = f"{collection.id}@collections"
            with self.subTest(lookup=lookup):
                self.assert_lookup_equal(lookup, collection)

    def test_specific_collection_integer(self) -> None:
        """Integer lookups find a specific collection."""
        collections = [
            self.create_collection(str(i), CollectionCategory.TEST)
            for i in range(3)
        ]

        for i, collection in enumerate(collections):
            with self.subTest(collection=collection):
                self.assert_lookup_equal(
                    collection.id,
                    collection,
                    expect_type=CollectionItem.Types.COLLECTION,
                )
                with self.assertRaisesRegex(
                    LookupError,
                    "Integer lookups only work in contexts that expect an "
                    "artifact or a collection",
                ):
                    lookup_single(collection.id, default_workspace(), user=None)

    def test_specific_collection_restricts_workspace(self) -> None:
        """`nnn@collections` requires the given workspace or a public one."""
        private_workspaces = [
            self.create_workspace(name=f"private{i}") for i in range(2)
        ]
        public_workspace = self.create_workspace(name="public", public=True)
        collections = [
            self.create_collection(
                str(i), CollectionCategory.TEST, workspace=workspace
            )
            for i, workspace in enumerate(
                private_workspaces + [public_workspace]
            )
        ]

        for collection, workspace, visible in (
            (collections[0], private_workspaces[0], True),
            (collections[0], private_workspaces[1], False),
            (collections[0], public_workspace, False),
            (collections[1], private_workspaces[0], False),
            (collections[1], private_workspaces[1], True),
            (collections[1], public_workspace, False),
            (collections[2], private_workspaces[0], True),
            (collections[2], private_workspaces[1], True),
            (collections[2], public_workspace, True),
        ):
            lookup = f"{collection.id}@collections"
            with self.subTest(lookup=lookup, workspace=workspace):
                if visible:
                    self.assert_lookup_equal(
                        lookup, collection, workspace=workspace
                    )
                else:
                    with self.assertRaisesRegex(
                        KeyError, f"'{lookup}' does not exist or is hidden"
                    ):
                        lookup_single(lookup, workspace, user=None)

    def test_user_restrictions(self) -> None:
        """`name@collections` respects user restrictions."""
        wpublic = self.create_workspace(name="public", public=True)
        cpublic = self.create_collection(
            "test", CollectionCategory.TEST, workspace=wpublic
        )
        wprivate = self.create_workspace(name="private", public=False)
        cprivate = self.create_collection(
            "test", CollectionCategory.TEST, workspace=wprivate
        )
        wstart = self.create_workspace(name="start", public=True)

        lookup = f"test@{CollectionCategory.TEST}"

        self.assert_lookup_fails(lookup, workspace=wstart)

        # Lookups in the workspace itself do check restrictions
        self.assert_lookup_equal(lookup, cpublic, user=False, workspace=wpublic)
        self.assert_lookup_equal(
            lookup, cpublic, user=self.user, workspace=wpublic
        )
        self.assert_lookup_fails(lookup, user=False, workspace=wprivate)
        self.assert_lookup_equal(
            lookup, cprivate, user=self.user, workspace=wprivate
        )

        # Inheritance chain is always followed for public datasets
        wstart.set_inheritance([wpublic])
        self.assert_lookup_equal(lookup, cpublic, user=None, workspace=wstart)
        self.assert_lookup_equal(
            lookup, cpublic, user=self.user, workspace=wstart
        )

        # Inheritance chain on private datasets is followed only if logged in
        wstart.set_inheritance([wprivate])
        self.assert_lookup_fails(lookup, user=None, workspace=wstart)
        self.assert_lookup_equal(
            lookup, cprivate, user=self.user, workspace=wstart
        )

        # Inheritance chain skips private datasets but can see public ones
        wstart.set_inheritance([wprivate, wpublic])
        self.assert_lookup_equal(lookup, cpublic, user=None, workspace=wstart)
        self.assert_lookup_equal(
            lookup, cprivate, user=self.user, workspace=wstart
        )

    def test_internal_collection_not_in_workflow(self) -> None:
        """`internal@collections` is only valid in a workflow context."""
        with self.assertRaisesRegex(
            LookupError,
            "internal@collections is only valid in the context of a workflow",
        ):
            lookup_single(
                "internal@collections", default_workspace(), user=None
            )

    def test_internal_collection(self) -> None:
        """`internal@collections` works in a workflow context."""
        template = WorkflowTemplate.objects.create(
            name="test", workspace=default_workspace(), task_name="noop"
        )
        workflow_root = WorkRequest.objects.create_workflow(
            template=template, created_by=self.user, data={}
        )
        self.create_collection("test", CollectionCategory.TEST)

        assert workflow_root.internal_collection is not None
        self.assert_lookup_equal(
            "internal@collections",
            workflow_root.internal_collection,
            workflow_root=workflow_root,
        )

    def test_explicit_category(self) -> None:
        """Collections can be looked up using an explicit category."""
        collection = self.create_collection(
            "bookworm", CollectionCategory.SUITE
        )
        self.create_collection("trixie", CollectionCategory.SUITE)
        self.create_collection("debian", CollectionCategory.TEST)

        self.assert_lookup_equal(
            f"bookworm@{CollectionCategory.SUITE}", collection
        )

    def test_explicit_category_restricts_workspace(self) -> None:
        """Collection lookups require the given workspace or a public one."""
        private_workspaces = [
            self.create_workspace(name=f"private{i}") for i in range(2)
        ]
        public_workspace = self.create_workspace(name="public", public=True)
        collections = [
            self.create_collection(
                workspace.name, CollectionCategory.TEST, workspace=workspace
            )
            for workspace in private_workspaces + [public_workspace]
        ]
        for workspace in private_workspaces:
            workspace.set_inheritance([public_workspace])

        for collection, workspace, visible in (
            (collections[0], private_workspaces[0], True),
            (collections[0], private_workspaces[1], False),
            (collections[0], public_workspace, False),
            (collections[1], private_workspaces[0], False),
            (collections[1], private_workspaces[1], True),
            (collections[1], public_workspace, False),
            (collections[2], private_workspaces[0], True),
            (collections[2], private_workspaces[1], True),
            (collections[2], public_workspace, True),
        ):
            lookup = f"{collection.name}@{CollectionCategory.TEST}"
            with self.subTest(lookup=lookup, workspace=workspace):
                if visible:
                    self.assert_lookup_equal(
                        lookup, collection, workspace=workspace
                    )
                else:
                    with self.assertRaisesRegex(
                        KeyError, f"'{lookup}' does not exist or is hidden"
                    ):
                        lookup_single(lookup, workspace, user=None)

    def test_implicit_category(self) -> None:
        """Collections can be looked up using an implicit category."""
        collection = self.create_collection(
            "bookworm", CollectionCategory.SUITE
        )
        self.create_collection("trixie", CollectionCategory.SUITE)
        self.create_collection("bookworm", CollectionCategory.TEST)

        self.assert_lookup_equal(
            "bookworm", collection, default_category=CollectionCategory.SUITE
        )

    def test_no_default_category(self) -> None:
        """An initial segment with no `@` requires a default category."""
        with self.assertRaisesRegex(
            LookupError,
            "'debian' does not specify a category and the context does not "
            "supply a default",
        ):
            lookup_single("debian", default_workspace(), user=self.user)

    def test_collection_member(self) -> None:
        """Looking up collection members works."""
        self.create_collection("bullseye", CollectionCategory.SUITE)
        bookworm = self.create_collection("bookworm", CollectionCategory.SUITE)
        trixie = self.create_collection("trixie", CollectionCategory.SUITE)
        src1 = self.create_source_package("src1", "1.0")
        src2 = self.create_source_package("src2", "2.0")
        items = []
        for suite, source_package_artifact in (
            (bookworm, src1),
            (bookworm, src2),
            (trixie, src2),
        ):
            items.append(
                DebianSuiteManager(suite).add_source_package(
                    source_package_artifact,
                    user=self.user,
                    component="main",
                    section="devel",
                )
            )

        self.assert_lookup_equal("bookworm@debian:suite/source:src1", items[0])
        self.assert_lookup_equal("bookworm@debian:suite/src2_2.0", items[1])
        with self.assertRaisesRegex(
            KeyError, "'bullseye@debian:suite' has no item 'source:src1'"
        ):
            lookup_single(
                "bullseye@debian:suite/source:src1",
                default_workspace(),
                user=self.user,
            )

    def test_no_lookup_through_artifact(self) -> None:
        """Lookup segments cannot continue after an artifact."""
        suite = self.create_collection("bookworm", CollectionCategory.SUITE)
        source_package_artifact = self.create_source_package("src1", "1.0")
        DebianSuiteManager(suite).add_source_package(
            source_package_artifact,
            user=self.user,
            component="main",
            section="devel",
        )

        with self.assertRaisesRegex(
            LookupError,
            "'bookworm@debian:suite/src1_1.0' is not of type 'collection'",
        ):
            lookup_single(
                "bookworm@debian:suite/src1_1.0/foo",
                default_workspace(),
                user=self.user,
            )

    def test_expect_type(self) -> None:
        """`expect_type` requires the result to be of that type."""
        collection = self.create_collection(
            "collection", CollectionCategory.WORKFLOW_INTERNAL
        )
        bare_item = self.create_bare_item(collection, "bare")
        artifact_item = self.create_artifact_item(collection, "artifact")
        collection_item = self.create_collection_item(
            collection, "sub-collection"
        )

        for name, expect_type, expected in (
            ("bare", CollectionItem.Types.BARE, bare_item),
            ("bare", CollectionItem.Types.ARTIFACT, None),
            ("bare", CollectionItem.Types.COLLECTION, None),
            ("artifact", CollectionItem.Types.BARE, None),
            ("artifact", CollectionItem.Types.ARTIFACT, artifact_item),
            ("artifact", CollectionItem.Types.COLLECTION, None),
            ("sub-collection", CollectionItem.Types.BARE, None),
            ("sub-collection", CollectionItem.Types.ARTIFACT, None),
            (
                "sub-collection",
                CollectionItem.Types.COLLECTION,
                collection_item,
            ),
        ):
            lookup = f"collection@{CollectionCategory.WORKFLOW_INTERNAL}/{name}"
            with self.subTest(lookup=lookup, expect_type=expect_type):
                if expected is not None:
                    self.assert_lookup_equal(
                        lookup, expected, expect_type=expect_type
                    )
                else:
                    with self.assertRaisesRegex(
                        LookupError,
                        f"{lookup!r} is not of type "
                        f"{expect_type.name.lower()!r}",
                    ):
                        lookup_single(
                            lookup,
                            default_workspace(),
                            user=self.user,
                            expect_type=expect_type,
                        )


class LookupMultipleTests(LookupMixin, TestCase):
    """Tests for lookup_multiple."""

    def parse_lookup(
        self, data: dict[str, Any] | Sequence[int | str | dict[str, Any]]
    ) -> LookupMultiple:
        """Parse the lookup syntax."""
        return LookupMultiple.parse_obj(data)

    def assert_lookup_equal(
        self,
        lookup: dict[str, Any] | Sequence[int | str | dict[str, Any]],
        expected: Iterable[Artifact | Collection | CollectionItem],
        *,
        workspace: Workspace | None = None,
        **kwargs: Any,
    ) -> None:
        """Assert that a lookup's result is as expected."""
        self.assertCountEqual(
            lookup_multiple(
                self.parse_lookup(lookup),
                workspace or default_workspace(),
                user=self.user,
                **kwargs,
            ),
            [self.make_result(item) for item in expected],
        )

    def test_not_collection(self) -> None:
        """The `collection` key does not identify a collection."""
        artifact = self.create_artifact()[0]
        lookup = {"collection": f"{artifact.id}@artifacts"}

        with self.assertRaisesRegex(
            LookupError,
            f"'{artifact.id}@artifacts' is not of type 'collection'",
        ):
            lookup_multiple(
                self.parse_lookup(lookup), default_workspace(), user=None
            )

    def test_child_type_bare(self) -> None:
        """Restrict to bare items."""
        collection = self.create_collection(
            "collection", CollectionCategory.TEST
        )
        other_collection = self.create_collection(
            "other-collection", CollectionCategory.TEST
        )
        bare1 = self.create_bare_item(collection, "bare1")
        bare2 = self.create_bare_item(collection, "bare2")
        self.create_artifact_item(collection, "artifact")
        self.create_collection_item(collection, "sub-collection")
        self.create_bare_item(other_collection, "bare3")

        self.assert_lookup_equal(
            {
                "collection": f"collection@{CollectionCategory.TEST}",
                "child_type": "bare",
            },
            (bare1, bare2),
        )

    def test_child_type_artifact(self) -> None:
        """Restrict to artifacts."""
        bookworm = self.create_collection("bookworm", CollectionCategory.SUITE)
        trixie = self.create_collection("trixie", CollectionCategory.SUITE)
        src1 = self.create_source_package("src1", "1.0")
        src2 = self.create_source_package("src2", "2.0")
        self.create_bare_item(bookworm, "bare")
        self.create_collection_item(bookworm, "collection")
        items = []
        for suite, source_package_artifact in (
            (bookworm, src1),
            (bookworm, src2),
            (trixie, src2),
        ):
            items.append(
                DebianSuiteManager(suite).add_source_package(
                    source_package_artifact,
                    user=self.user,
                    component="main",
                    section="devel",
                )
            )

        self.assert_lookup_equal(
            {"collection": "bookworm@debian:suite", "child_type": "artifact"},
            items[:2],
        )
        self.assert_lookup_equal(
            {"collection": "bookworm@debian:suite"}, items[:2]
        )

    def test_child_type_collection(self) -> None:
        """Restrict to collections."""
        collection = self.create_collection(
            "collection", CollectionCategory.TEST
        )
        other_collection = self.create_collection(
            "other-collection", CollectionCategory.TEST
        )
        self.create_bare_item(collection, "bare1")
        self.create_artifact_item(collection, "artifact")
        collection1 = self.create_collection_item(collection, "collection1")
        collection2 = self.create_collection_item(collection, "collection2")
        self.create_collection_item(other_collection, "collection3")

        self.assert_lookup_equal(
            {
                "collection": f"collection@{CollectionCategory.TEST}",
                "child_type": "collection",
            },
            (collection1, collection2),
        )

    def test_expect_type(self) -> None:
        """If `expect_type` is given, the child type must match it."""
        self.create_collection("collection", CollectionCategory.TEST)

        for child_type, expect_type, allowed in (
            ("bare", CollectionItem.Types.BARE, True),
            ("bare", CollectionItem.Types.ARTIFACT, False),
            ("bare", CollectionItem.Types.COLLECTION, False),
            ("artifact", CollectionItem.Types.BARE, False),
            ("artifact", CollectionItem.Types.ARTIFACT, True),
            ("artifact", CollectionItem.Types.COLLECTION, False),
            ("collection", CollectionItem.Types.BARE, False),
            ("collection", CollectionItem.Types.ARTIFACT, False),
            ("collection", CollectionItem.Types.COLLECTION, True),
        ):
            lookup = {
                "collection": f"collection@{CollectionCategory.TEST}",
                "child_type": child_type,
            }
            with self.subTest(lookup=lookup, expect_type=expect_type):
                if allowed:
                    self.assert_lookup_equal(
                        lookup, (), expect_type=expect_type
                    )
                else:
                    with self.assertRaisesRegex(
                        LookupError,
                        f"Only lookups for type {expect_type.name.lower()!r} "
                        f"are allowed here",
                    ):
                        lookup_multiple(
                            self.parse_lookup(lookup),
                            default_workspace(),
                            user=self.user,
                            expect_type=expect_type,
                        )

    def test_category(self) -> None:
        """Restrict by category."""
        bookworm = self.create_collection("bookworm", CollectionCategory.SUITE)
        src1 = self.create_source_package("src1", "1.0")
        src2 = self.create_source_package("src2", "2.0")
        bin1 = self.create_binary_package("bin1", "1.0", "amd64")
        bin2 = self.create_binary_package("bin2", "2.0", "s390x")
        items = []
        for source_package_artifact in (src1, src2):
            items.append(
                DebianSuiteManager(bookworm).add_source_package(
                    source_package_artifact,
                    user=self.user,
                    component="main",
                    section="devel",
                )
            )
        for binary_package_artifact in (bin1, bin2):
            items.append(
                DebianSuiteManager(bookworm).add_binary_package(
                    binary_package_artifact,
                    user=self.user,
                    component="main",
                    section="devel",
                    priority="optional",
                )
            )

        self.assert_lookup_equal(
            {
                "collection": "bookworm@debian:suite",
                "category": ArtifactCategory.SOURCE_PACKAGE,
            },
            items[:2],
        )
        self.assert_lookup_equal(
            {
                "collection": "bookworm@debian:suite",
                "category": ArtifactCategory.BINARY_PACKAGE,
            },
            items[2:],
        )

    def test_name_matcher(self) -> None:
        """Restrict by name."""
        collection = self.create_collection(
            "collection", CollectionCategory.TEST
        )
        items = [
            self.create_artifact_item(collection, name)
            for name in ("foo", "foobar", "rebar")
        ]

        self.assert_lookup_equal(
            {
                "collection": f"collection@{CollectionCategory.TEST}",
                "name": "foo",
            },
            (items[0],),
        )
        self.assert_lookup_equal(
            {
                "collection": f"collection@{CollectionCategory.TEST}",
                "name__startswith": "foo",
            },
            items[:2],
        )
        self.assert_lookup_equal(
            {
                "collection": f"collection@{CollectionCategory.TEST}",
                "name__endswith": "bar",
            },
            items[1:],
        )
        self.assert_lookup_equal(
            {
                "collection": f"collection@{CollectionCategory.TEST}",
                "name__contains": "oo",
            },
            items[:2],
        )

    def test_data_matchers(self) -> None:
        """Restrict by data."""
        collection = self.create_collection(
            "collection", CollectionCategory.TEST
        )
        items = [
            self.create_artifact_item(collection, name, data=data)
            for name, data in (
                ("foo", {"package": "foo"}),
                ("foobar_1.0", {"package": "foobar", "version": "1.0"}),
                ("rebar", {"package": "rebar"}),
            )
        ]

        self.assert_lookup_equal(
            {
                "collection": f"collection@{CollectionCategory.TEST}",
                "data__package": "foo",
            },
            (items[0],),
        )
        self.assert_lookup_equal(
            {
                "collection": f"collection@{CollectionCategory.TEST}",
                "data__package__startswith": "foo",
            },
            items[:2],
        )
        self.assert_lookup_equal(
            {
                "collection": f"collection@{CollectionCategory.TEST}",
                "data__package__endswith": "bar",
            },
            items[1:],
        )
        self.assert_lookup_equal(
            {
                "collection": f"collection@{CollectionCategory.TEST}",
                "data__package__contains": "oo",
            },
            items[:2],
        )
        self.assert_lookup_equal(
            {
                "collection": f"collection@{CollectionCategory.TEST}",
                "data__version": "1.0",
            },
            (items[1],),
        )

    def test_alternatives(self) -> None:
        """The list syntax allows specifying several lookups at once."""
        trixie = self.create_collection("trixie", CollectionCategory.SUITE)
        items = [
            self.create_artifact_item(trixie, name, data=data)
            for name, data in (
                (
                    "libc6-dev_2.37-15",
                    {"package": "libc6-dev", "version": "2.37-15"},
                ),
                (
                    "libc6-dev_2.37-16",
                    {"package": "libc6-dev", "version": "2.37-16"},
                ),
                (
                    "debhelper_13.15.3_amd64",
                    {
                        "package": "debhelper",
                        "version": "13.15.3",
                        "architecture": "amd64",
                    },
                ),
                (
                    "debhelper_13.15.3_s390x",
                    {
                        "package": "debhelper",
                        "version": "13.15.3",
                        "architecture": "s390x",
                    },
                ),
                ("hello_1.0", {"package": "hello", "version": "1.0"}),
            )
        ]
        loose_artifacts = [self.create_artifact()[0] for _ in range(3)]

        self.assert_lookup_equal(
            [
                {
                    "collection": "trixie",
                    "data__package": "libc6-dev",
                    "data__version": "2.37-15",
                },
                {
                    "collection": "trixie",
                    "name__startswith": "debhelper_13.15.3_",
                },
                f"{loose_artifacts[0].id}@artifacts",
                loose_artifacts[1].id,
            ],
            [
                items[0],
                items[2],
                items[3],
                loose_artifacts[0],
                loose_artifacts[1],
            ],
            default_category=CollectionCategory.SUITE,
            expect_type=CollectionItem.Types.ARTIFACT,
        )

    def test_artifact_expired(self) -> None:
        """If one artifact has expired, `lookup_multiple` returns others."""
        artifacts = [self.create_artifact()[0] for _ in range(3)]
        artifact_ids = [artifact.id for artifact in artifacts]
        artifacts[0].delete()

        self.assert_lookup_equal(
            artifact_ids,
            artifacts[1:],
            expect_type=CollectionItem.Types.ARTIFACT,
        )

    def test_collection_removed(self) -> None:
        """If one collection is gone, `lookup_multiple` handles others."""
        collections = [
            self.create_collection(f"collection{i}", CollectionCategory.TEST)
            for i in range(3)
        ]
        items = [
            self.create_artifact_item(collection, "artifact")
            for collection in collections
        ]
        items[0].delete()
        collections[0].delete()

        self.assert_lookup_equal(
            [
                {
                    "collection": f"collection{i}@{CollectionCategory.TEST}",
                    "name": "artifact",
                }
                for i in range(3)
            ],
            items[1:],
        )
