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

"""Unit tests for the workflow_utils functions."""

import re

from debusine.artifacts.models import (
    ArtifactCategory,
    BareDataCategory,
    CollectionCategory,
    DebianSourcePackage,
    DebusinePromise,
)
from debusine.client.models import LookupChildType, LookupResultType
from debusine.db.models import ArtifactRelation, CollectionItem, WorkRequest
from debusine.db.models.lookups import LookupResult
from debusine.server.workflows import (
    LintianWorkflow,
    NoopWorkflow,
    SbuildWorkflow,
    workflow_utils,
)
from debusine.server.workflows.models import (
    LintianWorkflowData,
    SbuildWorkflowData,
)
from debusine.tasks import TaskConfigError
from debusine.tasks.models import (
    BackendType,
    ExtraExternalRepository,
    ExtraRepository,
    LookupMultiple,
    SbuildInput,
)
from debusine.test.django import TestCase


class WorkflowUtilsTests(TestCase):
    """Tests for functions in workflows.workflow_utils."""

    @staticmethod
    def get_noop_workflow(work_request: WorkRequest) -> NoopWorkflow:
        """Return NoopWorkflow from work_request."""
        workflow = work_request.get_task()
        assert isinstance(workflow, NoopWorkflow)
        return workflow

    def test_source_package_input_and_source_package_data(self) -> None:
        source_package = self.playground.create_source_artifact(
            name="hello", version="1.0"
        )
        self.playground.create_debian_environment(
            vendor="debian", codename="trixie"
        )

        work_request = self.playground.create_workflow(
            "sbuild",
            SbuildWorkflowData(
                input=SbuildInput(source_artifact=source_package.id),
                target_distribution="debian:trixie",
                architectures=["amd64"],
            ),
        )

        workflow = work_request.get_task()
        assert isinstance(workflow, SbuildWorkflow)

        artifact = workflow_utils.source_package(
            workflow, configuration_key="input.source_artifact"
        )
        self.assertEqual(artifact, source_package)

        source_package_data = workflow_utils.source_package_data(
            workflow, configuration_key="input.source_artifact"
        )
        self.assertEqual(source_package_data.name, "hello")
        self.assertEqual(source_package_data.version, "1.0")

    def test_source_package_source_artifact(self) -> None:
        source_package = self.playground.create_source_artifact(
            name="hello", version="1.0"
        )
        binary_artifact = (
            self.playground.create_minimal_binary_package_artifact()
        )

        self.playground.create_debian_environment(
            vendor="debian", codename="trixie"
        )

        work_request = self.playground.create_workflow(
            "lintian",
            LintianWorkflowData(
                source_artifact=source_package.id,
                binary_artifacts=LookupMultiple((binary_artifact.id,)),
                vendor="debian",
                codename="trixie",
            ),
        )

        workflow = work_request.get_task()
        assert isinstance(workflow, LintianWorkflow)

        source_package_data = workflow_utils.source_package_data(workflow)

        self.assertEqual(source_package_data.name, "hello")
        self.assertEqual(source_package_data.version, "1.0")

    def test_lookup_result_artifact_category_artifact(self) -> None:
        """lookup_result_artifact_category with an artifact."""
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.SOURCE_PACKAGE,
            data={
                "name": "hello",
                "version": "1.0-1",
                "type": "dpkg",
                "dsc_fields": {"Architecture": "any"},
            },
        )
        result = LookupResult(
            result_type=LookupResultType.ARTIFACT,
            lookup=artifact.id,
            artifact=artifact,
        )

        self.assertEqual(
            workflow_utils.lookup_result_artifact_category(result),
            ArtifactCategory.SOURCE_PACKAGE,
        )

    def test_lookup_result_artifact_category_promise(self) -> None:
        """lookup_result_artifact_category with a promise."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        promise = self.playground.create_bare_data_item(
            collection,
            "test",
            category=BareDataCategory.PROMISE,
            data={
                "promise_work_request_id": 2,
                "promise_workflow_id": 1,
                "promise_category": ArtifactCategory.TEST,
            },
        )
        result = LookupResult(
            result_type=LookupResultType.BARE,
            lookup=f"test@{CollectionCategory.TEST}/test",
            collection_item=promise,
        )

        self.assertEqual(
            workflow_utils.lookup_result_artifact_category(result),
            ArtifactCategory.TEST,
        )

    def test_lookup_result_artifact_category_other_bare_item(self) -> None:
        """lookup_result_artifact_category with a non-promise bare item."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        promise = self.playground.create_bare_data_item(
            collection, "test", category=BareDataCategory.TEST
        )
        result = LookupResult(
            result_type=LookupResultType.BARE,
            lookup=f"test@{CollectionCategory.TEST}/test",
            collection_item=promise,
        )

        with self.assertRaisesRegex(
            ValueError,
            re.escape(
                f"Cannot determine artifact category for lookup result: "
                f"{result}"
            ),
        ):
            workflow_utils.lookup_result_artifact_category(result)

    def test_lookup_result_artifact_category_collection(self) -> None:
        """lookup_result_artifact_category raises ValueError for collections."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        result = LookupResult(
            result_type=LookupResultType.COLLECTION,
            lookup=f"test@{CollectionCategory.TEST}",
            collection=collection,
        )

        with self.assertRaisesRegex(
            ValueError,
            re.escape(
                f"Cannot determine artifact category for lookup result: "
                f"{result}"
            ),
        ):
            workflow_utils.lookup_result_artifact_category(result)

    def test_lookup_result_architectures_promise_with_architecture(
        self,
    ) -> None:
        """lookup_result_architectures with a binary promise."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        promise = self.playground.create_bare_data_item(
            collection,
            "test",
            category=BareDataCategory.PROMISE,
            data={
                "promise_work_request_id": 2,
                "promise_workflow_id": 1,
                "promise_category": ArtifactCategory.TEST,
                "architecture": "amd64",
            },
        )
        result = LookupResult(
            result_type=LookupResultType.BARE,
            lookup=f"test@{CollectionCategory.TEST}/test",
            collection_item=promise,
        )

        self.assertEqual(
            workflow_utils.lookup_result_architectures(result), {"amd64"}
        )

    def test_lookup_result_architectures_promise_no_architecture(self) -> None:
        """lookup_result_architectures rejects promise without architecture."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        promise = self.playground.create_bare_data_item(
            collection,
            "test",
            category=BareDataCategory.PROMISE,
            data=DebusinePromise(
                promise_work_request_id=2,
                promise_workflow_id=1,
                promise_category=ArtifactCategory.TEST,
            ),
        )
        result = LookupResult(
            result_type=LookupResultType.BARE,
            lookup=f"test@{CollectionCategory.TEST}/test",
            collection_item=promise,
        )

        with self.assertRaisesRegex(
            ValueError,
            re.escape(
                f"Cannot determine architecture for lookup result: {result}"
            ),
        ):
            workflow_utils.lookup_result_architectures(result)

    def test_lookup_result_architectures_binary_packages_artifact(self) -> None:
        """lookup_result_architectures with a binary-packages artifact."""
        artifact = self.playground.create_minimal_binary_package_artifact(
            architecture="i386",
        )
        result = LookupResult(
            result_type=LookupResultType.ARTIFACT,
            lookup=artifact.id,
            artifact=artifact,
        )

        self.assertEqual(
            workflow_utils.lookup_result_architectures(result), {"i386"}
        )

    def test_lookup_result_architectures_binary_packages_artifact_item(
        self,
    ) -> None:
        """lookup_result_architectures with a binary-packages artifact item."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        artifact = self.playground.create_minimal_binary_package_artifact(
            architecture="i386",
        )
        item = CollectionItem.objects.create_from_artifact(
            artifact,
            parent_collection=collection,
            name="hello",
            data={},
            created_by_user=self.playground.get_default_user(),
        )
        result = LookupResult(
            result_type=LookupResultType.ARTIFACT,
            lookup=f"test@{CollectionCategory.TEST}/hello",
            collection_item=item,
            artifact=artifact,
        )

        self.assertEqual(
            workflow_utils.lookup_result_architectures(result), {"i386"}
        )

    def test_lookup_result_architectures_debian_binary_package(self) -> None:
        """lookup_result_architectures return arch from a binary package."""
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.BINARY_PACKAGE,
            data={
                "srcpkg_name": "hello",
                "srcpkg_version": "1.0.0",
                "deb_fields": {"Architecture": "amd64"},
                "deb_control_files": [],
            },
        )

        result = LookupResult(
            result_type=LookupResultType.ARTIFACT,
            lookup=artifact.id,
            artifact=artifact,
        )

        self.assertEqual(
            workflow_utils.lookup_result_architectures(result),
            {"amd64"},
        )

    def test_lookup_result_architectures_debian_binary_package_item(
        self,
    ) -> None:
        """lookup_result_architectures returns arch from a binpkg item."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.BINARY_PACKAGE,
            data={
                "srcpkg_name": "hello",
                "srcpkg_version": "1.0.0",
                "deb_fields": {"Architecture": "amd64"},
                "deb_control_files": [],
            },
        )
        item = CollectionItem.objects.create_from_artifact(
            artifact,
            parent_collection=collection,
            name="hello",
            data={},
            created_by_user=self.playground.get_default_user(),
        )
        result = LookupResult(
            result_type=LookupResultType.ARTIFACT,
            lookup=f"test@{CollectionCategory.TEST}/hello",
            collection_item=item,
            artifact=artifact,
        )

        self.assertEqual(
            workflow_utils.lookup_result_architectures(result),
            {"amd64"},
        )

    def test_lookup_result_architectures_debian_upload(self) -> None:
        """lookup_result_architectures return arch from an upload artifact."""
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.UPLOAD,
            data={
                "type": "dpkg",
                "changes_fields": {
                    "Architecture": "amd64",
                    "Files": [{"name": "test.deb"}],
                },
            },
        )

        result = LookupResult(
            result_type=LookupResultType.ARTIFACT,
            lookup=artifact.id,
            artifact=artifact,
        )

        self.assertEqual(
            workflow_utils.lookup_result_architectures(result),
            {"amd64"},
        )

    def test_lookup_result_architectures_mixed_upload(self) -> None:
        """lookup_result_architectures return arch from a mixed upload."""
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.UPLOAD,
            data={
                "type": "dpkg",
                "changes_fields": {
                    "Architecture": "amd64 i386 source",
                    "Files": [
                        {"name": "test.dsc"},
                        {"name": "test_amd64.deb"},
                        {"name": "test_i386.deb"},
                    ],
                },
            },
        )

        result = LookupResult(
            result_type=LookupResultType.ARTIFACT,
            lookup=artifact.id,
            artifact=artifact,
        )

        self.assertEqual(
            workflow_utils.lookup_result_architectures(result),
            {"amd64", "i386"},
        )

    def test_lookup_result_architectures_debian_package_build_log(self) -> None:
        """lookup_result_architectures return arch from a build log."""
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.PACKAGE_BUILD_LOG,
            data={
                "source": "hello",
                "version": "1.0.0-1",
                "architecture": "amd64",
                "filename": "hello.build",
            },
        )

        result = LookupResult(
            result_type=LookupResultType.ARTIFACT,
            lookup=artifact.id,
            artifact=artifact,
        )

        self.assertEqual(
            workflow_utils.lookup_result_architectures(result),
            {"amd64"},
        )

    def test_lookup_result_architectures_debian_upload_item(self) -> None:
        """lookup_result_architectures return arch from an upload item."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.UPLOAD,
            data={
                "type": "dpkg",
                "changes_fields": {
                    "Architecture": "amd64",
                    "Files": [{"name": "test.deb"}],
                },
            },
        )
        item = CollectionItem.objects.create_from_artifact(
            artifact,
            parent_collection=collection,
            name="hello",
            data={},
            created_by_user=self.playground.get_default_user(),
        )
        result = LookupResult(
            result_type=LookupResultType.ARTIFACT,
            lookup=f"test@{CollectionCategory.TEST}/hello",
            collection_item=item,
            artifact=artifact,
        )

        self.assertEqual(
            workflow_utils.lookup_result_architectures(result),
            {"amd64"},
        )

    def test_lookup_result_architectures_other_artifacts(self) -> None:
        """lookup_result_architectures with source package artifact."""
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.SOURCE_PACKAGE,
            data={
                "name": "hello",
                "version": "1.0-1",
                "type": "dpkg",
                "dsc_fields": {"Architecture": "any"},
            },
        )
        result = LookupResult(
            result_type=LookupResultType.ARTIFACT,
            lookup=artifact.id,
            artifact=artifact,
        )

        with self.assertRaisesRegex(
            ValueError, f"Unexpected type: {DebianSourcePackage.__name__}"
        ):
            workflow_utils.lookup_result_architectures(result)

    def test_lookup_result_architectures_collection(self) -> None:
        """lookup_result_architectures with a collection."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        result = LookupResult(
            result_type=LookupResultType.COLLECTION,
            lookup=f"test@{CollectionCategory.TEST}",
            collection=collection,
        )

        with self.assertRaisesRegex(
            ValueError,
            re.escape(
                "Unexpected result: must have collection_item or artifact"
            ),
        ):
            workflow_utils.lookup_result_architectures(result)

    def test_lookup_result_architectures_collection_item_invalid(self) -> None:
        """lookup_result_architectures raises invalid architecture type."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        promise = self.playground.create_bare_data_item(
            collection,
            "test",
            category=BareDataCategory.PROMISE,
            data={
                "promise_work_request_id": 2,
                "promise_workflow_id": 1,
                "promise_category": ArtifactCategory.TEST,
                "architecture": 000,
            },
        )
        result = LookupResult(
            result_type=LookupResultType.BARE,
            lookup=f"test@{CollectionCategory.TEST}/test",
            collection_item=promise,
        )

        with self.assertRaisesRegex(
            ValueError,
            "Cannot determine architecture for lookup result:",
        ):
            workflow_utils.lookup_result_architectures(result)

    def test_lookup_result_architectures_unexpected_result(self) -> None:
        """lookup_result_architectures raise error: unexpected result."""
        result = LookupResult(result_type=LookupResultType.BARE, lookup="")

        with self.assertRaisesRegex(ValueError, "^Unexpected result: .*"):
            workflow_utils.lookup_result_architectures(result)

    def test_lookup_result_binary_package_name_promise_with_bpn(self) -> None:
        """lookup_result_binary_package_name with a binary promise."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        promise = self.playground.create_bare_data_item(
            collection,
            "test",
            category=BareDataCategory.PROMISE,
            data={
                "promise_work_request_id": 2,
                "promise_workflow_id": 1,
                "promise_category": ArtifactCategory.TEST,
                "binary_package_name": "hello",
            },
        )
        result = LookupResult(
            result_type=LookupResultType.BARE,
            lookup=f"test@{CollectionCategory.TEST}/test",
            collection_item=promise,
        )

        self.assertEqual(
            workflow_utils.lookup_result_binary_package_name(result), "hello"
        )

    def test_lookup_result_binary_package_name_promise_no_bpn(self) -> None:
        """lookup_result_binary_package_name rejects promise without BPN."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        promise = self.playground.create_bare_data_item(
            collection,
            "test",
            category=BareDataCategory.PROMISE,
            data=DebusinePromise(
                promise_work_request_id=2,
                promise_workflow_id=1,
                promise_category=ArtifactCategory.TEST,
            ),
        )
        result = LookupResult(
            result_type=LookupResultType.BARE,
            lookup=f"test@{CollectionCategory.TEST}/test",
            collection_item=promise,
        )

        with self.assertRaisesRegex(
            ValueError,
            re.escape(
                f"Cannot determine binary package name for lookup result: "
                f"{result}"
            ),
        ):
            workflow_utils.lookup_result_binary_package_name(result)

    def test_lookup_result_binary_package_name_debian_binary_package(
        self,
    ) -> None:
        """lookup_result_binary_package_name return BPN from a binary."""
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.BINARY_PACKAGE,
            data={
                "srcpkg_name": "hello",
                "srcpkg_version": "1.0.0",
                "deb_fields": {"Package": "hello-bin"},
                "deb_control_files": [],
            },
        )

        result = LookupResult(
            result_type=LookupResultType.ARTIFACT,
            lookup=artifact.id,
            artifact=artifact,
        )

        self.assertEqual(
            workflow_utils.lookup_result_binary_package_name(result),
            "hello-bin",
        )

    def test_lookup_result_binary_package_name_debian_binary_package_item(
        self,
    ) -> None:
        """lookup_result_binary_package_name return BPN from a binary item."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.BINARY_PACKAGE,
            data={
                "srcpkg_name": "hello",
                "srcpkg_version": "1.0.0",
                "deb_fields": {"Package": "hello-bin"},
                "deb_control_files": [],
            },
        )
        item = CollectionItem.objects.create_from_artifact(
            artifact,
            parent_collection=collection,
            name="hello",
            data={},
            created_by_user=self.playground.get_default_user(),
        )
        result = LookupResult(
            result_type=LookupResultType.ARTIFACT,
            lookup=f"test@{CollectionCategory.TEST}/hello",
            collection_item=item,
            artifact=artifact,
        )

        self.assertEqual(
            workflow_utils.lookup_result_binary_package_name(result),
            "hello-bin",
        )

    def test_lookup_result_binary_package_name_other_artifacts(self) -> None:
        """lookup_result_binary_package_name with source package artifact."""
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.SOURCE_PACKAGE,
            data={
                "name": "hello",
                "version": "1.0-1",
                "type": "dpkg",
                "dsc_fields": {"Package": "hello-bin"},
            },
        )
        result = LookupResult(
            result_type=LookupResultType.ARTIFACT,
            lookup=artifact.id,
            artifact=artifact,
        )

        with self.assertRaisesRegex(
            workflow_utils.ArtifactHasNoBinaryPackageName,
            re.escape(f"{DebianSourcePackage!r}"),
        ):
            workflow_utils.lookup_result_binary_package_name(result)

    def test_lookup_result_binary_package_name_collection(self) -> None:
        """lookup_result_binary_package_name with a collection."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        result = LookupResult(
            result_type=LookupResultType.COLLECTION,
            lookup=f"test@{CollectionCategory.TEST}",
            collection=collection,
        )

        with self.assertRaisesRegex(
            ValueError,
            re.escape(
                "Unexpected result: must have collection_item or artifact"
            ),
        ):
            workflow_utils.lookup_result_binary_package_name(result)

    def test_lookup_result_binary_package_name_collection_item_invalid(
        self,
    ) -> None:
        """lookup_result_binary_package_name raise error: invalid BPN type."""
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        promise = self.playground.create_bare_data_item(
            collection,
            "test",
            category=BareDataCategory.PROMISE,
            data={
                "promise_work_request_id": 2,
                "promise_workflow_id": 1,
                "promise_category": ArtifactCategory.TEST,
                "binary_package_name": 000,
            },
        )
        result = LookupResult(
            result_type=LookupResultType.BARE,
            lookup=f"test@{CollectionCategory.TEST}/test",
            collection_item=promise,
        )

        with self.assertRaisesRegex(
            ValueError,
            "Cannot determine binary package name for lookup result:",
        ):
            workflow_utils.lookup_result_binary_package_name(result)

    def test_lookup_result_binary_package_name_unexpected_result(self) -> None:
        """lookup_result_binary_package_name raise error: unexpected result."""
        result = LookupResult(result_type=LookupResultType.BARE, lookup="")

        with self.assertRaisesRegex(ValueError, "^Unexpected result: .*"):
            workflow_utils.lookup_result_binary_package_name(result)

    def test_filter_artifact_lookup_by_arch(self) -> None:
        wf = self.get_noop_workflow(
            self.playground.create_workflow(task_name="noop")
        )
        assert wf.work_request.internal_collection
        for architecture in ("amd64", "i386"):
            wf.work_request.internal_collection.manager.add_bare_data(
                name=f"hello-{architecture}",
                category=BareDataCategory.PROMISE,
                workflow=wf.work_request,
                data={
                    "promise_category": ArtifactCategory.BINARY_PACKAGE,
                    "promise_work_request_id": 42,
                    "promise_workflow_id": wf.work_request.id,
                    "architecture": architecture,
                    "binary_package_name": "hello",
                },
                user=self.playground.get_default_user(),
            )
        for architecture in ("armel", "armhf"):
            wf.work_request.internal_collection.manager.add_artifact(
                self.playground.create_minimal_binary_package_artifact(
                    architecture=architecture
                ),
                name=f"hello-{architecture}",
                variables={
                    "architecture": architecture,
                    "binary_package_name": "hello",
                },
                workflow=wf.work_request,
                user=self.playground.get_default_user(),
            )
        self.assertEqual(
            workflow_utils.filter_artifact_lookup_by_arch(
                wf,
                LookupMultiple.model_validate(
                    [
                        {
                            "collection": "internal@collections",
                            "data__binary_package_name": "hello",
                            "child_type": LookupChildType.ARTIFACT_OR_PROMISE,
                        }
                    ]
                ),
                {"amd64", "armel", "ppc64el"},
            ),
            LookupMultiple(
                (
                    "internal@collections/name:hello-amd64",
                    "internal@collections/name:hello-armel",
                )
            ),
        )

    def test_filter_artifact_lookup_by_arch_with_upload(self) -> None:
        wf = self.get_noop_workflow(
            self.playground.create_workflow(task_name="noop")
        )
        artifacts = self.playground.create_upload_artifacts()
        architectures = {"amd64", "all"}
        self.assertEqual(
            workflow_utils.filter_artifact_lookup_by_arch(
                wf,
                LookupMultiple((f"{artifacts.upload.id}@artifacts",)),
                architectures,
            ),
            LookupMultiple((f"{artifacts.upload.id}@artifacts",)),
        )

    def test_filter_artifact_lookup_by_arch_with_mixed_upload(self) -> None:
        wf = self.get_noop_workflow(
            self.playground.create_workflow(task_name="noop")
        )
        artifacts = self.playground.create_upload_artifacts(
            binaries=[
                ("hello", "amd64"),
                ("hello", "i386"),
                ("hello-doc", "all"),
            ]
        )
        architectures = {"amd64", "all"}
        self.assertEqual(
            workflow_utils.filter_artifact_lookup_by_arch(
                wf,
                LookupMultiple((f"{artifacts.upload.id}@artifacts",)),
                architectures,
            ),
            LookupMultiple.model_validate(
                [
                    f"{artifact.id}@artifacts"
                    for artifact in artifacts.binaries
                    if artifact.data["deb_fields"]["Architecture"]
                    in architectures
                ]
            ),
        )

    def test_get_lookup_architectures(self) -> None:
        wf = self.get_noop_workflow(
            self.playground.create_workflow(task_name="noop")
        )
        assert wf.work_request.internal_collection
        wf.work_request.internal_collection.manager.add_bare_data(
            name="hello-amd64",
            category=BareDataCategory.PROMISE,
            workflow=wf.work_request,
            data={
                "promise_category": ArtifactCategory.BINARY_PACKAGE,
                "promise_work_request_id": 42,
                "promise_workflow_id": wf.work_request.id,
                "architecture": "amd64",
                "binary_package_name": "hello",
            },
            user=self.playground.get_default_user(),
        )
        wf.work_request.internal_collection.manager.add_artifact(
            self.playground.create_minimal_binary_package_artifact(
                architecture="i386"
            ),
            name="hello-i386",
            variables={
                "architecture": "i386",
                "binary_package_name": "hello",
            },
            workflow=wf.work_request,
            user=self.playground.get_default_user(),
        )
        self.assertEqual(
            workflow_utils.get_lookup_architectures(
                wf,
                LookupMultiple.model_validate(
                    [
                        {
                            "collection": "internal@collections",
                            "data__binary_package_name": "hello",
                            "child_type": LookupChildType.ARTIFACT_OR_PROMISE,
                        }
                    ]
                ),
            ),
            {"amd64", "i386"},
        )

    def test_get_lookup_architectures_mixed_upload(self) -> None:
        wf = self.get_noop_workflow(
            self.playground.create_workflow(task_name="noop")
        )
        artifacts = self.playground.create_upload_artifacts(
            binaries=[
                ("hello", "amd64"),
                ("hello", "i386"),
                ("hello-doc", "all"),
            ],
        )
        self.assertEqual(
            workflow_utils.get_lookup_architectures(
                wf, LookupMultiple((f"{artifacts.upload.id}@artifacts",))
            ),
            {"amd64", "i386", "all"},
        )

    def test_locate_debian_source_package_with_srcpkg(self) -> None:
        """Test with a debian:source-package."""
        source_artifact = self.playground.create_source_artifact()
        result = workflow_utils.locate_debian_source_package(
            "source_artifact", source_artifact
        )
        self.assertEqual(result, source_artifact)

    def test_locate_debian_source_package_with_upload(self) -> None:
        """Test with a debian:source-package."""
        upload_artifacts = self.playground.create_upload_artifacts()
        result = workflow_utils.locate_debian_source_package(
            "upload_artifact", upload_artifacts.upload
        )
        self.assertEqual(result, upload_artifacts.source)

    def test_locate_debian_source_package_with_upload_without_relations(
        self,
    ) -> None:
        """Test with a lone debian:upload (no relations)."""
        upload_artifacts = self.playground.create_upload_artifacts(source=False)

        with self.assertRaisesRegex(
            TaskConfigError,
            r"Unable to find an artifact of category debian:source-package "
            r'with a relationship of type extends from ".* debian:upload .*"',
        ):
            workflow_utils.locate_debian_source_package(
                "upload_artifact",
                upload_artifacts.upload,
            )

    def test_locate_debian_source_package_with_upload_with_multiple_relations(
        self,
    ) -> None:
        """Test with a debian:upload extending multiple source packages."""
        upload_artifacts = self.playground.create_upload_artifacts(source=True)
        source2 = self.playground.create_source_artifact()
        self.playground.create_artifact_relation(
            artifact=upload_artifacts.upload,
            target=source2,
            relation_type=ArtifactRelation.Relations.EXTENDS,
        )

        with self.assertRaisesRegex(
            TaskConfigError,
            r"Multiple artifacts of category debian:source-package with "
            r'a relationship of type extends from ".* debian:upload .*" found',
        ):
            workflow_utils.locate_debian_source_package(
                "upload_artifact",
                upload_artifacts.upload,
            )

    def test_locate_debian_source_package_with_junk(self) -> None:
        """Test with a debian:source-package."""
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.TEST
        )
        with self.assertRaisesRegex(
            TaskConfigError,
            r"^test_artifact: unexpected artifact category: "
            r"'debusine:test'. Valid categories: "
            r"\['debian:source-package', 'debian:upload'\]$",
        ):
            workflow_utils.locate_debian_source_package(
                "test_artifact", artifact
            )

    def test_locate_debian_binary_packages_with_binary_package(self) -> None:
        """Test with debian:binary-package artifacts."""
        binary_artifacts = [
            self.playground.create_minimal_binary_package_artifact()
            for _ in range(2)
        ]
        result = workflow_utils.locate_debian_binary_packages(
            "source_artifact", binary_artifacts
        )
        self.assertCountEqual(result, binary_artifacts)

    def test_locate_debian_binary_packages_with_upload(self) -> None:
        """Test with a debian:upload artifact."""
        upload_artifacts = self.playground.create_upload_artifacts(
            binaries=[("foo", "amd64"), ("foo-common", "all")]
        )
        result = workflow_utils.locate_debian_binary_packages(
            "source_artifact", [upload_artifacts.upload]
        )
        self.assertCountEqual(result, upload_artifacts.binaries)

    def test_locate_debian_binary_packages_with_upload_no_binaries(
        self,
    ) -> None:
        """Test with a debian:upload without binaries."""
        upload_artifacts = self.playground.create_upload_artifacts(binary=False)
        result = workflow_utils.locate_debian_binary_packages(
            "source_artifact", [upload_artifacts.upload]
        )
        self.assertEqual(result, [])

    def test_locate_debian_binary_packages_with_junk(self) -> None:
        """Test with a non-binary-package-related artifact."""
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.TEST
        )
        with self.assertRaisesRegex(
            TaskConfigError,
            r"^test_artifact\[0\]: unexpected artifact category: "
            r"'debusine:test'. Valid categories: "
            r"\['debian:binary-package', 'debian:upload'\]$",
        ):
            workflow_utils.locate_debian_binary_packages(
                "test_artifact", [artifact]
            )

    def test_follow_artifact_relations_multiple_found(self) -> None:
        """Test with a debian:upload extending multiple srcpkgs."""
        upload_artifacts = self.playground.create_upload_artifacts(source=True)
        source2 = self.playground.create_source_artifact()
        self.playground.create_artifact_relation(
            artifact=upload_artifacts.upload,
            target=source2,
            relation_type=ArtifactRelation.Relations.EXTENDS,
        )

        self.assertQuerySetEqual(
            workflow_utils.follow_artifact_relations(
                upload_artifacts.upload,
                ArtifactRelation.Relations.EXTENDS,
                ArtifactCategory.SOURCE_PACKAGE,
            ),
            [upload_artifacts.source, source2],
        )

    def test_follow_artifact_relations_not_found(self) -> None:
        """Test with a lone debian:upload (no relations)."""
        upload_artifacts = self.playground.create_upload_artifacts(source=False)

        self.assertQuerySetEqual(
            workflow_utils.follow_artifact_relations(
                upload_artifacts.upload,
                ArtifactRelation.Relations.EXTENDS,
                ArtifactCategory.SOURCE_PACKAGE,
            ),
            [],
        )

    def test_follow_artifact_relations_upload(self) -> None:
        """Test finding a source package from an upload."""
        upload_artifacts = self.playground.create_upload_artifacts()
        test_artifact, _ = self.playground.create_artifact()

        # Create some extra unrelated relations
        self.playground.create_artifact_relation(
            artifact=upload_artifacts.upload,
            target=upload_artifacts.binaries[0],
            relation_type=ArtifactRelation.Relations.BUILT_USING,
        )
        self.playground.create_artifact_relation(
            artifact=upload_artifacts.upload,
            target=test_artifact,
            relation_type=ArtifactRelation.Relations.EXTENDS,
        )

        self.assertQuerySetEqual(
            workflow_utils.follow_artifact_relations(
                upload_artifacts.upload,
                ArtifactRelation.Relations.EXTENDS,
                ArtifactCategory.SOURCE_PACKAGE,
            ),
            [upload_artifacts.source],
        )

    def test_locate_debian_source_package_lookup_srcpkg(self) -> None:
        """Test locate_debian_source_package_lookup() with a source package."""
        workflow = self.get_noop_workflow(
            self.playground.create_workflow(task_name="noop")
        )
        source_artifact = self.playground.create_source_artifact()
        self.assertEqual(
            workflow_utils.locate_debian_source_package_lookup(
                workflow, "source_artifact", source_artifact.id
            ),
            source_artifact.id,
        )

    def test_locate_debian_source_package_lookup_upload(self) -> None:
        """Test locate_debian_source_package_lookup() with a debian:upload."""
        workflow = self.get_noop_workflow(
            self.playground.create_workflow(task_name="noop")
        )
        artifacts = self.playground.create_upload_artifacts()
        self.assertEqual(
            workflow_utils.locate_debian_source_package_lookup(
                workflow, "source_artifact", artifacts.upload.id
            ),
            f"{artifacts.source.id}@artifacts",
        )

    def test_get_source_package_names_invalid_artifact_category(self) -> None:
        source_artifact = self.playground.create_source_artifact(name="hello")

        lookup_result = [
            LookupResult(
                result_type=LookupResultType.ARTIFACT,
                lookup=source_artifact.id,
                artifact=source_artifact,
            )
        ]

        with self.assertRaisesRegex(
            TaskConfigError,
            r"^testing: unexpected artifact category: 'debian:source-package'. "
            r"Valid categories: \['debian:binary-package'\]$",
        ):
            workflow_utils.get_source_package_names(
                lookup_result,
                configuration_key="testing",
                artifact_expected_categories=(ArtifactCategory.BINARY_PACKAGE,),
            )

    def test_get_source_package_names_invalid_artifact_in_promise_category(
        self,
    ) -> None:
        lookup_result = [
            LookupResult(
                result_type=LookupResultType.BARE,
                lookup=f"test@{CollectionCategory.TEST}/test",
                collection_item=CollectionItem(
                    data={
                        "promise_category": ArtifactCategory.TEST,
                        "source_package_name": "python3",
                    }
                ),
            )
        ]

        with self.assertRaisesRegex(
            TaskConfigError,
            r"^testing: unexpected artifact category: 'debusine:test'. "
            r"Valid categories: \['debian:binary-package'\]$",
        ):
            workflow_utils.get_source_package_names(
                lookup_result,
                configuration_key="testing",
                artifact_expected_categories=(ArtifactCategory.BINARY_PACKAGE,),
            )

    def test_get_source_package_names(self) -> None:
        binary_artifact_1 = (
            self.playground.create_minimal_binary_package_artifact(
                srcpkg_name="hello"
            )
        )
        binary_artifact_2 = (
            self.playground.create_minimal_binary_package_artifact(
                srcpkg_name="hello"
            )
        )
        binaries_artifact = (
            self.playground.create_minimal_binary_package_artifact(
                srcpkg_name="linux-base"
            )
        )
        upload_artifact = self.playground.create_upload_artifacts(
            src_name="firefox"
        ).upload

        lookup_result = [
            LookupResult(
                result_type=LookupResultType.ARTIFACT,
                lookup=binary_artifact_1.id,
                artifact=binary_artifact_1,
            ),
            LookupResult(
                result_type=LookupResultType.ARTIFACT,
                lookup=binary_artifact_2.id,
                artifact=binary_artifact_2,
            ),
            LookupResult(
                result_type=LookupResultType.ARTIFACT,
                lookup=binaries_artifact.id,
                artifact=binaries_artifact,
            ),
            LookupResult(
                result_type=LookupResultType.ARTIFACT,
                lookup=upload_artifact.id,
                artifact=upload_artifact,
            ),
            LookupResult(
                result_type=LookupResultType.BARE,
                lookup=f"test@{CollectionCategory.TEST}/empty-binary",
                collection_item=CollectionItem(
                    # This isn't valid, without a source_package_name, but it
                    # gives coverage another path
                    data={
                        "promise_category": ArtifactCategory.BINARY_PACKAGE,
                    }
                ),
            ),
            LookupResult(
                result_type=LookupResultType.BARE,
                lookup=f"test@{CollectionCategory.TEST}/python3",
                collection_item=CollectionItem(
                    data={
                        "promise_category": ArtifactCategory.BINARY_PACKAGE,
                        "source_package_name": "python3",
                    }
                ),
            ),
        ]

        self.assertEqual(
            workflow_utils.get_source_package_names(
                lookup_result,
                configuration_key="testing",
                artifact_expected_categories=(
                    ArtifactCategory.BINARY_PACKAGE,
                    ArtifactCategory.UPLOAD,
                ),
            ),
            ["firefox", "hello", "linux-base", "python3"],
        )

    def test_get_available_architectures(self) -> None:
        work_request = self.playground.create_workflow()
        workflow = self.get_noop_workflow(work_request)
        self.playground.create_debian_environment(
            codename="bookworm", architecture="amd64"
        )
        self.playground.create_debian_environment(
            codename="bookworm", architecture="i386"
        )
        self.assertEqual(
            workflow_utils.get_available_architectures(
                workflow, vendor="debian", codename="bookworm"
            ),
            {"amd64", "i386", "all"},
        )

    def test_get_available_architectures_no_environment(self) -> None:
        work_request = self.playground.create_workflow()
        workflow = self.get_noop_workflow(work_request)
        self.playground.create_debian_environments_collection()
        with self.assertRaisesRegex(
            TaskConfigError,
            r"Unable to find any environments for debian:bookworm",
        ):
            workflow_utils.get_available_architectures(
                workflow, vendor="debian", codename="bookworm"
            )

    def test_configure_for_overlay_suite_experimental(self) -> None:
        work_request = self.playground.create_workflow()
        workflow = self.get_noop_workflow(work_request)
        self.playground.create_debian_environment(
            codename="sid",
            variant="sbuild",
            mirror="http://ftp.uk.debian.org/debian",
            components=["main", "contrib"],
        )
        extra_repositories: list[ExtraRepository] | None
        for extra_repositories in (None, []):
            with self.subTest(extra_repositories=extra_repositories):
                result = workflow_utils.configure_for_overlay_suite(
                    workflow,
                    extra_repositories=extra_repositories,
                    vendor="debian",
                    codename="experimental",
                    environment="debian/match:codename=sid",
                    backend=BackendType.UNSHARE,
                    architecture="amd64",
                    try_variant="sbuild",
                )
                self.assertEqual(
                    result,
                    [
                        ExtraExternalRepository(
                            url="http://ftp.uk.debian.org/debian",
                            suite="experimental",
                            components=["main", "contrib"],
                        ),
                    ],
                )

    def test_configure_for_overlay_suite_no_overlay(self) -> None:
        work_request = self.playground.create_workflow()
        workflow = self.get_noop_workflow(work_request)
        extra_repositories = [
            ExtraExternalRepository(
                url="http://deb.debian.org/debian",
                suite="foo-backports",
                components=["main"],
            ),
        ]
        for codename in ("sid", "trixie", "bookworm-security"):
            with self.subTest(codename=codename):
                self.playground.create_debian_environment(codename=codename)
                self.assertEqual(
                    workflow_utils.configure_for_overlay_suite(
                        workflow,
                        extra_repositories=extra_repositories,
                        vendor="debian",
                        codename=codename,
                        environment=f"debian/match:codename={codename}",
                        backend=BackendType.UNSHARE,
                        architecture="amd64",
                        try_variant="autopkgtest",
                    ),
                    extra_repositories,
                )
