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

import logging
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any, ClassVar, override

import debusine.tasks.tags as ttags
from debusine.artifacts.models import CollectionCategory, TaskTypes
from debusine.db.models import Artifact, Collection, WorkRequest, Workspace
from debusine.db.playground import scenarios
from debusine.server.celery import run_server_task
from debusine.server.workflows import PackagePublishWorkflow
from debusine.server.workflows.models import (
    BaseWorkflowData,
    PackagePublishWorkflowData,
    PackagePublishWorkflowDynamicData,
    SbuildWorkflowData,
    WorkRequestWorkflowData,
)
from debusine.server.workflows.tests.helpers import (
    SampleWorkflow,
    WorkflowTestCase,
)
from debusine.tasks import TaskConfigError
from debusine.tasks.models import (
    BaseDynamicTaskData,
    InputArtifactMultiple,
    InputArtifactSingle,
    LookupMultiple,
    OutputData,
    SbuildInput,
)
from debusine.test.django import preserve_db_task_registry


@dataclass
class ExpectedCopy:
    """
    A summary of an expected copy.

    This exists to abbreviate tests, and is only useful for copying
    source/binary artifacts.
    """

    artifact: Artifact
    component: str
    section: str
    priority: str | None = None

    @property
    def source_items(self) -> list[str]:
        return [f"{self.artifact.id}@artifacts"]

    @property
    def variables(self) -> dict[str, str]:
        variables = {"component": self.component, "section": self.section}
        if self.priority is not None:
            variables["priority"] = self.priority
        return variables


class PackagePublishWorkflowTests(WorkflowTestCase[PackagePublishWorkflow]):
    """Unit tests for :py:class:`PackagePublishWorkflow`."""

    SAMPLE_TASK_DATA = {
        "target_suite": (f"bookworm@{CollectionCategory.SUITE}")
    }

    scenario = scenarios.DefaultContext()
    source_artifact: ClassVar[Artifact]
    binary_artifact: ClassVar[Artifact]
    upload_artifact: ClassVar[Artifact]
    bookworm: ClassVar[Collection]

    @override
    @classmethod
    def setUpTestData(cls) -> None:
        super().setUpTestData()
        cls.playground.create_group_role(
            cls.scenario.workspace,
            Workspace.Roles.OWNER,
            users=[cls.scenario.user],
        )
        cls.source_artifact = cls.playground.create_source_artifact(
            name="hello"
        )
        cls.binary_artifact = (
            cls.playground.create_minimal_binary_package_artifact("hello")
        )
        cls.upload_artifact = (
            cls.playground.create_upload_artifacts(src_name="hello")
        ).upload
        cls.bookworm = cls.create_suite_collection("bookworm")

    def setUp(self) -> None:
        super().setUp()
        self.task_data["binary_artifacts"] = LookupMultiple(
            (self.upload_artifact.id,)
        )
        self.task_data["source_artifact"] = self.source_artifact.id

    @classmethod
    def create_suite_collection(
        cls, name: str, *, workspace: Workspace | None = None
    ) -> Collection:
        """Create a `debian:suite` collection."""
        return cls.playground.create_collection(
            name=name, category=CollectionCategory.SUITE, workspace=workspace
        )

    def create_package_publish_workflow(
        self, task_data: PackagePublishWorkflowData
    ) -> PackagePublishWorkflow:
        """Create a package_publish workflow."""
        wr = self.playground.create_workflow(
            task_name="package_publish", task_data=task_data
        )
        return self.get_workflow(wr)

    def test_create_orchestrator(self) -> None:
        """A PackagePublishWorkflow can be instantiated."""
        source_artifact = self.source_artifact.pk
        binary_artifacts = (self.binary_artifact.pk,)
        target_suite = self.bookworm.pk

        workflow = self.create_package_publish_workflow(
            PackagePublishWorkflowData(
                source_artifact=source_artifact,
                binary_artifacts=LookupMultiple(binary_artifacts),
                target_suite=target_suite,
            )
        )

        self.assertEqual(workflow.data.source_artifact, source_artifact)
        self.assertEqual(
            workflow.data.binary_artifacts, LookupMultiple(binary_artifacts)
        )
        self.assertEqual(workflow.data.target_suite, target_suite)

    def orchestrate(
        self,
        task_data: PackagePublishWorkflowData,
        source_artifact: Artifact,
        architectures: Sequence[str],
        *,
        workspace: Workspace | None = None,
    ) -> WorkRequest:
        """Create and orchestrate a PackagePublishWorkflow."""
        playground = self.playground

        class ExamplePipeline(
            SampleWorkflow[BaseWorkflowData, BaseDynamicTaskData]
        ):
            """Example pipeline for package publishing."""

            def populate(self) -> None:
                """Populate the pipeline."""
                sbuild, _ = self.work_request_ensure_child_workflow(
                    task_name="sbuild",
                    task_data=SbuildWorkflowData(
                        input=SbuildInput(source_artifact=source_artifact.id),
                        target_distribution="debian:sid",
                        architectures=list(architectures) or ["all"],
                    ),
                    workflow_data=WorkRequestWorkflowData(
                        display_name="sbuild", step="sbuild"
                    ),
                )
                self.orchestrate_child(sbuild)

                package_publish, _ = self.work_request_ensure_child_workflow(
                    task_name="package_publish",
                    task_data=task_data,
                    workflow_data=WorkRequestWorkflowData(
                        display_name="publish packages", step="package_publish"
                    ),
                )

                if task_data.source_artifact is not None:
                    self.requires_artifact(
                        package_publish, task_data.source_artifact
                    )
                self.requires_artifact(
                    package_publish, task_data.binary_artifacts
                )
                playground.orchestrate_child(self, package_publish)

        template = self.playground.create_workflow_template(
            name="examplepipeline-template",
            task_name="examplepipeline",
            workspace=workspace,
        )
        root = self.playground.create_workflow(task_name="examplepipeline")
        self.assertEqual(root.workspace, template.workspace)
        self.assertTrue(
            self.playground.orchestrate_workflow(root, use_data=False)
        )

        return root

    def assert_copies(
        self,
        work_request: WorkRequest,
        target_collection: Collection,
        expected_copies: Sequence[ExpectedCopy],
    ) -> None:
        """Assert that a task declares the expected copies."""
        self.assertEqual(
            work_request.task_data,
            {
                "copies": [
                    {
                        "source_items": copy.source_items,
                        "target_collection": target_collection.id,
                        "unembargo": False,
                        "replace": False,
                        "variables": copy.variables,
                    }
                    for copy in expected_copies
                ]
            },
        )

    def assert_updates_indexes(
        self, package_publish: WorkRequest, target_suite: Collection
    ) -> None:
        """Assert that the workflow updates indexes after copying."""
        copy_collection_items = package_publish.children.get(
            task_type=TaskTypes.SERVER, task_name="copycollectionitems"
        )
        self.playground.advance_work_request(
            copy_collection_items, assign_new_worker=True
        )
        trigger_update_suites = package_publish.children.get(
            task_type=TaskTypes.INTERNAL,
            task_name="workflow",
            workflow_data_json__step="trigger-update-suites",
        )

        self.assertQuerySetEqual(
            trigger_update_suites.dependencies.all(), [copy_collection_items]
        )

        copy_collection_items_result = run_server_task.apply(
            args=(copy_collection_items.id,)
        )
        self.assertTrue(copy_collection_items_result.get())

        trigger_update_suites.refresh_from_db()
        self.assertEqual(
            trigger_update_suites.status, WorkRequest.Statuses.PENDING
        )

        self.schedule_and_run_workflow_callback(package_publish)
        trigger_update_suites.refresh_from_db()
        self.assertEqual(
            trigger_update_suites.status, WorkRequest.Statuses.COMPLETED
        )
        self.assertEqual(
            trigger_update_suites.result, WorkRequest.Results.SUCCESS
        )

        update_suites = target_suite.workspace.workrequest_set.get(
            task_type=TaskTypes.WORKFLOW, task_name="update_suites"
        )
        self.assertEqual(update_suites.status, WorkRequest.Statuses.PENDING)
        self.assertEqual(
            update_suites.task_data, {"only_suites": [target_suite.name]}
        )

        self.schedule_and_run_workflow(update_suites)
        update_suites.refresh_from_db()
        self.assertEqual(update_suites.status, WorkRequest.Statuses.RUNNING)
        self.assertTrue(update_suites.children.exists())

    def test_target_suite_in_wrong_workspace(self) -> None:
        other_workspace = self.playground.create_workspace(
            name="other", public=True
        )
        self.create_suite_collection("sid", workspace=other_workspace)
        target_suite_lookup = (
            f"//workspace=other/sid@{CollectionCategory.SUITE}"
        )

        with self.assertRaisesRegex(
            TaskConfigError,
            f"{target_suite_lookup} is in workspace {other_workspace},"
            f" not {self.scenario.workspace}",
        ):
            self.create_package_publish_workflow(
                PackagePublishWorkflowData(
                    source_artifact=self.source_artifact.pk,
                    binary_artifacts=LookupMultiple((self.binary_artifact.pk,)),
                    target_suite=target_suite_lookup,
                )
            )

    @preserve_db_task_registry()
    def test_populate_source_and_binary(self) -> None:
        """Test population with both source and binary artifacts."""
        source_artifact = self.playground.create_source_artifact(
            name="hello", architectures={"any", "all"}
        )
        target_suite = self.bookworm
        architectures = ("amd64", "i386", "all")

        root = self.orchestrate(
            task_data=PackagePublishWorkflowData(
                source_artifact=source_artifact.id,
                binary_artifacts=LookupMultiple.model_validate(
                    [
                        f"internal@collections/name:build-{arch}"
                        for arch in architectures
                    ]
                ),
                target_suite=(
                    f"{target_suite.name}@{CollectionCategory.SUITE}"
                ),
            ),
            source_artifact=source_artifact,
            architectures=architectures,
        )
        package_publish = root.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="package_publish"
        )
        self.assertEqual(package_publish.status, WorkRequest.Statuses.BLOCKED)

        sbuild_workflow = root.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="sbuild"
        )
        binaries, _ = self.simulate_sbuild_workflow_completion(
            sbuild_workflow, use_data=False
        )

        package_publish.refresh_from_db()
        self.assertEqual(package_publish.status, WorkRequest.Statuses.PENDING)
        self.schedule_and_run_workflow(root)

        copy_collection_items = package_publish.children.get(
            task_name="copycollectionitems"
        )

        expected = [ExpectedCopy(source_artifact, "main", "misc")]
        for arch in architectures:
            expected += [
                ExpectedCopy(binary_artifact, "main", "misc", "optional")
                for binary_artifact in binaries[arch]
            ]
        self.assert_copies(copy_collection_items, target_suite, expected)
        self.assert_updates_indexes(package_publish, target_suite)

    @preserve_db_task_registry()
    def test_populate_source_only(self) -> None:
        """Test population with only a source artifact."""
        source_artifact = self.playground.create_source_artifact(
            name="hello", version="1.0-1", architectures={"all"}
        )
        target_suite = self.bookworm

        with self.assertLogs(
            logger="debusine.server.workflows.package_publish",
            level=logging.WARNING,
        ) as log:
            root = self.orchestrate(
                task_data=PackagePublishWorkflowData(
                    source_artifact=source_artifact.id,
                    target_suite=(
                        f"{target_suite.name}@{CollectionCategory.SUITE}"
                    ),
                ),
                source_artifact=source_artifact,
                architectures=[],
            )
        self.assertEqual(
            [record.message for record in log.records],
            [
                f"No component specified when adding hello_1.0-1 to "
                f"bookworm@{CollectionCategory.SUITE}; defaulting to 'main'",
                f"No section specified when adding hello_1.0-1 to "
                f"bookworm@{CollectionCategory.SUITE}; defaulting to 'misc'",
            ],
        )

        package_publish = root.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="package_publish"
        )
        copy_collection_items = package_publish.children.get(
            task_name="copycollectionitems"
        )
        self.assert_copies(
            copy_collection_items,
            target_suite,
            [ExpectedCopy(source_artifact, "main", "misc")],
        )
        self.assert_updates_indexes(package_publish, target_suite)

    @preserve_db_task_registry()
    def test_populate_source_upload_only(self) -> None:
        """Test population with only a source upload artifact."""
        upload_artifacts = self.playground.create_upload_artifacts(
            src_name="hello",
            src_architectures={"any", "all"},
            binary=False,
            section="editors",
        )
        target_suite = self.bookworm

        root = self.orchestrate(
            task_data=PackagePublishWorkflowData(
                source_artifact=upload_artifacts.upload.id,
                target_suite=(
                    f"{target_suite.name}@{CollectionCategory.SUITE}"
                ),
            ),
            source_artifact=upload_artifacts.source,
            architectures=[],
        )
        package_publish = root.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="package_publish"
        )
        copy_collection_items = package_publish.children.get(
            task_name="copycollectionitems"
        )

        self.assert_copies(
            copy_collection_items,
            target_suite,
            [ExpectedCopy(upload_artifacts.source, "main", "editors")],
        )
        self.assert_updates_indexes(package_publish, target_suite)

    @preserve_db_task_registry()
    def test_populate_missing_binary_fields(self) -> None:
        """Population warns about missing binary component/section/priority."""
        source_artifact = self.playground.create_source_artifact(
            name="hello", version="1.0-1", architectures={"all"}
        )
        binary_artifact = (
            self.playground.create_minimal_binary_package_artifact(
                srcpkg_name="hello",
                srcpkg_version="1.0-1",
                package="hello",
                version="1.0-1",
                architecture="all",
            )
        )
        target_suite = self.bookworm

        with self.assertLogs(
            logger="debusine.server.workflows.package_publish",
            level=logging.WARNING,
        ) as log:
            root = self.orchestrate(
                task_data=PackagePublishWorkflowData(
                    binary_artifacts=LookupMultiple((binary_artifact.id,)),
                    target_suite=(
                        f"{target_suite.name}@{CollectionCategory.SUITE}"
                    ),
                ),
                source_artifact=source_artifact,
                architectures=[],
            )
        self.assertEqual(
            [record.message for record in log.records],
            [
                f"No component specified when adding hello_1.0-1_all to "
                f"bookworm@{CollectionCategory.SUITE}; defaulting to 'main'",
                f"No section specified when adding hello_1.0-1_all to "
                f"bookworm@{CollectionCategory.SUITE}; defaulting to 'misc'",
                f"No priority specified when adding hello_1.0-1_all to "
                f"bookworm@{CollectionCategory.SUITE}; defaulting to "
                f"'optional'",
            ],
        )

        package_publish = root.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="package_publish"
        )
        copy_collection_items = package_publish.children.get(
            task_name="copycollectionitems"
        )
        self.assert_copies(
            copy_collection_items,
            target_suite,
            [ExpectedCopy(binary_artifact, "main", "misc", "optional")],
        )
        self.assert_updates_indexes(package_publish, target_suite)

    @preserve_db_task_registry()
    def test_populate_non_main_component(self) -> None:
        """Population handles sections with non-main components."""
        upload_artifacts = self.playground.create_upload_artifacts(
            src_name="hello",
            src_architectures={"any", "all"},
            binary=False,
            section="contrib/games",
        )
        target_suite = self.bookworm

        root = self.orchestrate(
            task_data=PackagePublishWorkflowData(
                source_artifact=upload_artifacts.upload.id,
                target_suite=(
                    f"{target_suite.name}@{CollectionCategory.SUITE}"
                ),
            ),
            source_artifact=upload_artifacts.source,
            architectures=[],
        )
        package_publish = root.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="package_publish"
        )
        copy_collection_items = package_publish.children.get(
            task_name="copycollectionitems"
        )

        self.assert_copies(
            copy_collection_items,
            target_suite,
            [ExpectedCopy(upload_artifacts.source, "contrib", "games")],
        )
        self.assert_updates_indexes(package_publish, target_suite)

    @preserve_db_task_registry()
    def test_populate_honours_suite_variables(self) -> None:
        """Population honours ``suite_variables``."""
        source_artifact = self.playground.create_source_artifact(
            name="hello", version="1.0-1", architectures={"all"}
        )
        binary_artifact = (
            self.playground.create_minimal_binary_package_artifact(
                srcpkg_name="hello",
                srcpkg_version="1.0-1",
                package="hello",
                version="1.0-1",
                architecture="all",
            )
        )
        target_suite = self.bookworm

        with self.assertNoLogs(
            logger="debusine.server.workflows.package_publish"
        ):
            root = self.orchestrate(
                task_data=PackagePublishWorkflowData(
                    source_artifact=source_artifact.id,
                    binary_artifacts=LookupMultiple((binary_artifact.id,)),
                    target_suite=(
                        f"{target_suite.name}@{CollectionCategory.SUITE}"
                    ),
                    suite_variables={
                        "component": "non-free",
                        "section": "editors",
                        "priority": "extra",
                    },
                ),
                source_artifact=source_artifact,
                architectures=[],
            )

        package_publish = root.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="package_publish"
        )
        copy_collection_items = package_publish.children.get(
            task_name="copycollectionitems"
        )
        self.assert_copies(
            copy_collection_items,
            target_suite,
            [
                ExpectedCopy(artifact, "non-free", "editors", "extra")
                for artifact in (source_artifact, binary_artifact)
            ],
        )
        self.assert_updates_indexes(package_publish, target_suite)

    @preserve_db_task_registry()
    def test_populate_no_update_indexes(self) -> None:
        """The workflow can be told not to update indexes."""
        upload_artifacts = self.playground.create_upload_artifacts(
            src_name="hello",
            src_architectures={"any", "all"},
            binary=False,
            section="editors",
        )
        target_suite = self.bookworm

        root = self.orchestrate(
            task_data=PackagePublishWorkflowData(
                source_artifact=upload_artifacts.upload.id,
                target_suite=(
                    f"{target_suite.name}@{CollectionCategory.SUITE}"
                ),
                update_indexes=False,
            ),
            source_artifact=upload_artifacts.source,
            architectures=[],
        )
        package_publish = root.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="package_publish"
        )
        copy_collection_items = package_publish.children.get(
            task_name="copycollectionitems"
        )

        self.assert_copies(
            copy_collection_items,
            target_suite,
            [ExpectedCopy(upload_artifacts.source, "main", "editors")],
        )
        self.assertFalse(
            package_publish.children.filter(
                task_type=TaskTypes.INTERNAL,
                task_name="workflow",
                workflow_data_json__step="trigger-update-suites",
            ).exists()
        )

    @preserve_db_task_registry()
    def test_populate_package_build_logs(self) -> None:
        """Test population with binary artifacts and copying build logs."""
        source_workspace = self.playground.create_workspace(name="source")
        self.playground.create_group_role(
            source_workspace, Workspace.Roles.OWNER, users=[self.scenario.user]
        )
        target_workspace = self.playground.create_workspace(name="target")
        self.playground.create_group_role(
            target_workspace, Workspace.Roles.OWNER, users=[self.scenario.user]
        )
        self.playground.create_singleton_collection(
            CollectionCategory.ARCHIVE, workspace=target_workspace
        )
        source_workspace.set_inheritance([target_workspace])
        source_suite = self.create_suite_collection(
            "bookworm", workspace=source_workspace
        )
        target_suite = self.create_suite_collection(
            "bookworm", workspace=target_workspace
        )
        source_build_logs_collection = (
            self.playground.create_singleton_collection(
                CollectionCategory.PACKAGE_BUILD_LOGS,
                workspace=source_workspace,
            )
        )
        target_build_logs_collection = (
            self.playground.create_singleton_collection(
                CollectionCategory.PACKAGE_BUILD_LOGS,
                workspace=target_workspace,
            )
        )
        binary_names_by_architecture = {
            "amd64": ("hello", "hello-dev"),
            "i386": ("hello", "hello-dev"),
            "all": ("hello-doc",),
        }
        binary_artifacts: list[Artifact] = []
        binary_artifacts_lookup: list[str] = []
        for architecture, binary_names in binary_names_by_architecture.items():
            for binary_name in binary_names:
                binary_artifacts.append(
                    self.playground.create_minimal_binary_package_artifact(
                        package=binary_name,
                        version="1.0-1",
                        architecture=architecture,
                        workspace=source_workspace,
                    )
                )
                source_suite.manager.add_artifact(
                    binary_artifacts[-1],
                    user=self.playground.get_default_user(),
                    variables={
                        "component": "main",
                        "section": "misc",
                        "priority": "optional",
                    },
                )
                binary_artifacts_lookup.append(
                    f"//workspace={source_workspace.name}/bookworm@debian:suite"
                    f"/binary:{binary_name}_{architecture}"
                )

        package_publish = self.playground.create_workflow(
            task_name="package_publish",
            task_data=PackagePublishWorkflowData(
                binary_artifacts=LookupMultiple.model_validate(
                    binary_artifacts_lookup
                ),
                target_suite=(
                    f"{target_suite.name}@{CollectionCategory.SUITE}"
                ),
                unembargo=True,
            ),
            workspace=target_workspace,
        )
        self.assertEqual(package_publish.status, WorkRequest.Statuses.PENDING)
        self.playground.orchestrate_workflow(package_publish)

        copy_collection_items = package_publish.children.get(
            task_name="copycollectionitems"
        )

        expected_copies: list[dict[str, Any]] = []
        for binary_artifact in binary_artifacts:
            expected_copies.append(
                {
                    "source_items": [f"{binary_artifact.id}@artifacts"],
                    "target_collection": target_suite.id,
                    "unembargo": True,
                    "replace": False,
                    "variables": {
                        "component": "main",
                        "section": "misc",
                        "priority": "optional",
                    },
                }
            )
        expected_copies.append(
            {
                "source_items": [
                    {
                        "collection": source_build_logs_collection.id,
                        "lookup_filters": [
                            [
                                "same_work_request",
                                binary_artifacts_lookup,
                            ]
                        ],
                    }
                ],
                "target_collection": target_build_logs_collection.id,
                "unembargo": True,
                "replace": False,
            }
        )
        self.assertEqual(
            copy_collection_items.task_data, {"copies": expected_copies}
        )
        self.assert_updates_indexes(package_publish, target_suite)

    @preserve_db_task_registry()
    def test_populate_task_history(self) -> None:
        """Test population with binary artifacts and copying build logs."""
        source_workspace = self.playground.create_workspace(name="source")
        self.playground.create_group_role(
            source_workspace, Workspace.Roles.OWNER, users=[self.scenario.user]
        )
        target_workspace = self.playground.create_workspace(name="target")
        self.playground.create_group_role(
            target_workspace, Workspace.Roles.OWNER, users=[self.scenario.user]
        )
        self.playground.create_singleton_collection(
            CollectionCategory.ARCHIVE, workspace=target_workspace
        )
        source_workspace.set_inheritance([target_workspace])
        source_suite = self.create_suite_collection(
            "bookworm", workspace=source_workspace
        )
        target_suite = self.create_suite_collection(
            "bookworm", workspace=target_workspace
        )
        source_task_history_collection = (
            self.playground.create_singleton_collection(
                CollectionCategory.TASK_HISTORY,
                workspace=source_workspace,
            )
        )
        target_task_history_collection = (
            self.playground.create_singleton_collection(
                CollectionCategory.TASK_HISTORY,
                workspace=target_workspace,
            )
        )
        binary_names_by_architecture = {
            "amd64": ("hello", "hello-dev"),
            "i386": ("hello", "hello-dev"),
            "all": ("hello-doc",),
        }
        binary_artifacts: list[Artifact] = []
        binary_artifacts_lookup: list[str] = []
        for architecture, binary_names in binary_names_by_architecture.items():
            for binary_name in binary_names:
                binary_artifacts.append(
                    self.playground.create_minimal_binary_package_artifact(
                        package=binary_name,
                        version="1.0-1",
                        architecture=architecture,
                        workspace=source_workspace,
                    )
                )
                source_suite.manager.add_artifact(
                    binary_artifacts[-1],
                    user=self.playground.get_default_user(),
                    variables={
                        "component": "main",
                        "section": "misc",
                        "priority": "optional",
                    },
                )
                binary_artifacts_lookup.append(
                    f"//workspace={source_workspace.name}/bookworm@debian:suite"
                    f"/binary:{binary_name}_{architecture}"
                )

        package_publish = self.playground.create_workflow(
            task_name="package_publish",
            task_data=PackagePublishWorkflowData(
                binary_artifacts=LookupMultiple.model_validate(
                    binary_artifacts_lookup
                ),
                target_suite=(
                    f"{target_suite.name}@{CollectionCategory.SUITE}"
                ),
                unembargo=True,
            ),
            workspace=target_workspace,
        )
        self.assertEqual(package_publish.status, WorkRequest.Statuses.PENDING)
        self.playground.orchestrate_workflow(package_publish)

        copy_collection_items = package_publish.children.get(
            task_name="copycollectionitems"
        )

        expected_copies: list[dict[str, Any]] = []
        for binary_artifact in binary_artifacts:
            expected_copies.append(
                {
                    "source_items": [f"{binary_artifact.id}@artifacts"],
                    "target_collection": target_suite.id,
                    "unembargo": True,
                    "replace": False,
                    "variables": {
                        "component": "main",
                        "section": "misc",
                        "priority": "optional",
                    },
                }
            )
        expected_copies.append(
            {
                "source_items": [
                    {
                        "collection": source_task_history_collection.id,
                        "lookup_filters": [
                            ["same_workflow", binary_artifacts_lookup]
                        ],
                    }
                ],
                "target_collection": target_task_history_collection.id,
                "unembargo": True,
                "replace": False,
            }
        )
        self.assertEqual(
            copy_collection_items.task_data, {"copies": expected_copies}
        )
        self.assert_updates_indexes(package_publish, target_suite)

    def test_populate_fails_with_inconsistent_binary_artifact_workspaces(
        self,
    ) -> None:
        workspaces = {
            name: self.playground.create_workspace(name=name)
            for name in ("source1", "source2", "target")
        }
        for workspace in workspaces.values():
            self.playground.create_group_role(
                workspace, Workspace.Roles.OWNER, users=[self.scenario.user]
            )
        self.playground.create_singleton_collection(
            CollectionCategory.ARCHIVE, workspace=workspaces["target"]
        )
        workspaces["source1"].set_inheritance([workspaces["target"]])
        workspaces["source2"].set_inheritance([workspaces["target"]])
        suites = {
            name: self.create_suite_collection("bookworm", workspace=workspace)
            for name, workspace in workspaces.items()
        }
        binary_artifacts: list[Artifact] = []
        binary_artifacts_lookup: list[str] = []
        for architecture, suite in (
            ("amd64", suites["source1"]),
            ("i386", suites["source2"]),
        ):
            binary_artifacts.append(
                self.playground.create_minimal_binary_package_artifact(
                    package="hello",
                    version="1.0-1",
                    architecture=architecture,
                    workspace=suite.workspace,
                )
            )
            suite.manager.add_artifact(
                binary_artifacts[-1],
                user=self.playground.get_default_user(),
                variables={
                    "component": "main",
                    "section": "misc",
                    "priority": "optional",
                },
            )
            binary_artifacts_lookup.append(
                f"//workspace={suite.workspace.name}/bookworm@debian:suite"
                f"/binary:hello_{architecture}"
            )

        package_publish = self.playground.create_workflow(
            task_name="package_publish",
            task_data=PackagePublishWorkflowData(
                binary_artifacts=LookupMultiple.model_validate(
                    binary_artifacts_lookup
                ),
                target_suite=(
                    f"{suites['target'].name}@{CollectionCategory.SUITE}"
                ),
                unembargo=True,
            ),
            workspace=workspaces["target"],
        )
        self.assertEqual(package_publish.status, WorkRequest.Statuses.PENDING)
        self.playground.orchestrate_workflow(package_publish)

        package_publish.refresh_from_db()
        self.assertEqual(package_publish.status, WorkRequest.Statuses.COMPLETED)
        self.assertEqual(package_publish.result, WorkRequest.Results.ERROR)
        assert isinstance(package_publish.output_data, OutputData)
        assert package_publish.output_data.errors is not None
        self.assertEqual(len(package_publish.output_data.errors), 1)
        self.assertTrue(
            package_publish.output_data.errors[0].message.startswith(
                "Orchestrator failed:"
                " Binary artifacts must all be in the same workspace"
            )
        )
        self.assertEqual(
            package_publish.output_data.errors[0].code, "orchestrator-failed"
        )

    def test_compute_scheduler_tags(self) -> None:
        self.assertComputesSchedulerTags(
            provide_system={ttags.GROUP_PREFIX + "debusine:System:owner"}
        )

    def test_compute_dynamic_data_binary_package(self) -> None:
        self.assertEqual(
            self.compute_dynamic_data(self.task, strip_task_inputs=True),
            PackagePublishWorkflowDynamicData(
                subject="hello",
                parameter_summary="hello → "
                f"bookworm@{CollectionCategory.SUITE}",
                source_artifact_id=self.source_artifact.id,
                target_suite_id=self.bookworm.id,
                binary_artifacts_ids=[self.upload_artifact.id],
            ),
        )

    def test_get_input_artifacts(self) -> None:
        workflow = self.task
        workflow.dynamic_data = self.compute_dynamic_data(workflow)

        assert workflow.data.source_artifact is not None

        self.assertEqual(
            workflow.get_input_artifacts(),
            [
                InputArtifactSingle(
                    lookup=workflow.data.source_artifact,
                    label="source_artifact",
                    artifact_id=self.source_artifact.id,
                ),
                InputArtifactMultiple.from_ids(
                    lookup=workflow.data.binary_artifacts,
                    label="binary_artifacts",
                    artifact_ids=[self.upload_artifact.id],
                ),
            ],
        )

    def test_get_input_artifacts_without_source_artifact(self) -> None:
        del self.task_data["source_artifact"]
        workflow = self.task
        workflow.dynamic_data = self.compute_dynamic_data(workflow)

        self.assertEqual(
            workflow.get_input_artifacts(),
            [
                InputArtifactMultiple.from_ids(
                    lookup=workflow.data.binary_artifacts,
                    label="binary_artifacts",
                    artifact_ids=[self.upload_artifact.id],
                ),
            ],
        )
