# 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 (
    ArtifactCategory,
    CollectionCategory,
    TaskTypes,
)
from debusine.db.models import (
    Artifact,
    ArtifactRelation,
    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


@dataclass
class ExpectedDebugSymbolsCopy:
    """A summary of an expected debug-symbols copy."""

    artifact: Artifact
    build_id: str

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

    @property
    def variables(self) -> dict[str, str]:
        return {"build_id": self.build_id}


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")

    @override
    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 test_populate_copies_debug_symbols(self) -> None:
        """Publishing a binary also queues its debug-symbols artifact."""
        source_workspace = self.playground.create_workspace(
            name="source", public=True
        )
        self.playground.create_group_role(
            source_workspace, Workspace.Roles.OWNER, users=[self.scenario.user]
        )
        binary = self.playground.create_minimal_binary_package_artifact(
            "hello", workspace=source_workspace
        )
        build_ids = ["a" * 40, "b" * 40]
        debug_artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.DEBUG_SYMBOLS,
            workspace=source_workspace,
            data={"build_ids": build_ids},
            paths=dict.fromkeys(build_ids, b"debug"),
            create_files=True,
        )
        self.playground.create_artifact_relation(
            debug_artifact, binary, ArtifactRelation.Relations.RELATES_TO
        )

        package_publish = self.playground.create_workflow(
            task_name="package_publish",
            task_data=PackagePublishWorkflowData(
                binary_artifacts=LookupMultiple((binary.id,)),
                target_suite=(
                    f"{self.bookworm.name}@{CollectionCategory.SUITE}"
                ),
            ),
        )
        self.playground.orchestrate_workflow(package_publish)

        copy_task = package_publish.children.get(
            task_name="copycollectionitems"
        )
        self.assert_copies(
            copy_task,
            self.bookworm,
            [
                ExpectedCopy(binary, "main", "misc", "optional"),
                ExpectedDebugSymbolsCopy(debug_artifact, build_ids[0]),
                ExpectedDebugSymbolsCopy(debug_artifact, build_ids[1]),
            ],
        )
        self.assert_updates_indexes(package_publish, self.bookworm)

    def test_populate_no_debug_symbols_without_relation(self) -> None:
        """No debug-symbols copy when binary has no related debug artifact."""
        source_workspace = self.playground.create_workspace(
            name="source", public=True
        )
        self.playground.create_group_role(
            source_workspace, Workspace.Roles.OWNER, users=[self.scenario.user]
        )
        binary = self.playground.create_minimal_binary_package_artifact(
            "hello", workspace=source_workspace
        )
        package_publish = self.playground.create_workflow(
            task_name="package_publish",
            task_data=PackagePublishWorkflowData(
                binary_artifacts=LookupMultiple((binary.id,)),
                target_suite=(
                    f"{self.bookworm.name}@{CollectionCategory.SUITE}"
                ),
            ),
        )
        self.playground.orchestrate_workflow(package_publish)

        copy_task = package_publish.children.get(
            task_name="copycollectionitems"
        )
        self.assert_copies(
            copy_task,
            self.bookworm,
            [ExpectedCopy(binary, "main", "misc", "optional")],
        )
        self.assert_updates_indexes(package_publish, self.bookworm)

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

            @override
            def populate(self) -> None:
                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 | ExpectedDebugSymbolsCopy],
    ) -> 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.workflow_callbacks().get(
                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.workflow_callbacks()
            .filter(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]] = [
            {
                "source_items": [f"{binary_artifact.id}@artifacts"],
                "target_collection": target_suite.id,
                "unembargo": True,
                "replace": False,
                "variables": {
                    "component": "main",
                    "section": "misc",
                    "priority": "optional",
                },
            }
            for binary_artifact in binary_artifacts
        ]
        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]] = [
            {
                "source_items": [f"{binary_artifact.id}@artifacts"],
                "target_collection": target_suite.id,
                "unembargo": True,
                "replace": False,
                "variables": {
                    "component": "main",
                    "section": "misc",
                    "priority": "optional",
                },
            }
            for binary_artifact in binary_artifacts
        ]
        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],
                ),
            ],
        )
