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

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

from django.utils import timezone

import debusine.worker.tags as wtags
from debusine.artifacts.models import (
    ArtifactCategory,
    CollectionCategory,
    DebianAutopkgtest,
    DebianAutopkgtestResult,
    DebianAutopkgtestResultStatus,
    DebianAutopkgtestSource,
    DebianBinaryPackage,
    DebianSourcePackage,
    TaskTypes,
)
from debusine.db.models import Artifact, TaskDatabase, WorkRequest
from debusine.server.collections import DebianSuiteManager
from debusine.server.workflows import (
    QAWorkflow,
    WorkflowValidationError,
    workflow_utils,
)
from debusine.server.workflows.base import orchestrate_workflow
from debusine.server.workflows.models import (
    BaseWorkflowData,
    QAWorkflowData,
    QAWorkflowDynamicData,
    SbuildWorkflowData,
    WorkRequestWorkflowData,
)
from debusine.server.workflows.tests.helpers import (
    SampleWorkflow,
    WorkflowTestBase,
)
from debusine.tasks import TaskConfigError
from debusine.tasks.models import (
    BackendType,
    BaseDynamicTaskData,
    InputArtifactMultiple,
    InputArtifactSingle,
    LintianFailOnSeverity,
    LookupMultiple,
    RegressionAnalysis,
    RegressionAnalysisStatus,
    SbuildData,
    SbuildInput,
)
from debusine.test.django import preserve_db_task_registry


class QAWorkflowTests(WorkflowTestBase[QAWorkflow]):
    """Unit tests for :py:class:`QAWorkflow`."""

    source_artifact: ClassVar[Artifact]
    binary_artifact: ClassVar[Artifact]
    package_build_log: ClassVar[Artifact]

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up common data."""
        super().setUpTestData()
        cls.source_artifact = cls.playground.create_source_artifact(
            name="hello"
        )
        cls.binary_artifact = (
            cls.playground.create_minimal_binary_package_artifact(
                architecture="amd64"
            )
        )
        cls.package_build_log = cls.playground.create_build_log_artifact()
        cls.playground.create_artifact_relation(
            artifact=cls.package_build_log, target=cls.binary_artifact
        )

    def create_qa_workflow(
        self,
        *,
        extra_task_data: dict[str, Any] | None = None,
        validate: bool = True,
    ) -> QAWorkflow:
        """Create a qa workflow."""
        task_data = {
            "source_artifact": self.source_artifact.pk,
            "binary_artifacts": [self.binary_artifact.pk],
            "vendor": "debian",
            "codename": "bookworm",
        }
        if extra_task_data is not None:
            task_data.update(extra_task_data)
        wr = self.playground.create_workflow(
            task_name="qa", task_data=task_data, validate=validate
        )
        return self.get_workflow(wr)

    def orchestrate(
        self,
        extra_data: dict[str, Any],
        *,
        architectures: Sequence[str] | None = None,
        sbuild_workflow_source_artifact: Artifact,
    ) -> WorkRequest:
        """Create a QAWorkflow and call orchestrate_workflow."""

        class ExamplePipeline(
            SampleWorkflow[BaseWorkflowData, BaseDynamicTaskData]
        ):
            """Example workflow."""

            def populate(self_) -> None:
                """Populate the pipeline."""
                source_artifact_id = (
                    sbuild_workflow_source_artifact.id
                    if sbuild_workflow_source_artifact is not None
                    else self.source_artifact.id
                )
                sbuild = self_.work_request.create_child_workflow(
                    task_name="sbuild",
                    task_data=SbuildWorkflowData(
                        input=SbuildInput(source_artifact=source_artifact_id),
                        target_distribution="debian:sid",
                        architectures=list(architectures or ["all"]),
                    ),
                )
                self.playground.advance_work_request(sbuild, mark_running=True)

                if architectures is not None:
                    for arch in architectures:
                        child = sbuild.create_child_worker(
                            task_name="sbuild",
                            task_data=SbuildData(
                                input=SbuildInput(
                                    source_artifact=source_artifact_id
                                ),
                                build_architecture=arch,
                                environment="debian/match:codename=sid",
                            ),
                            workflow_data=WorkRequestWorkflowData(
                                display_name=f"Build {arch}",
                                step=f"build-{arch}",
                            ),
                        )

                        self_.provides_artifact(
                            child,
                            ArtifactCategory.UPLOAD,
                            f"build-{arch}",
                            data={
                                "binary_names": ["hello"],
                                "architecture": arch,
                                "source_package_name": "hello",
                            },
                        )
                        self_.provides_artifact(
                            child,
                            ArtifactCategory.PACKAGE_BUILD_LOG,
                            f"buildlog-{arch}",
                            data={
                                "architecture": arch,
                                "source_package_name": "hello",
                                "source_package_version": "1.0-1",
                            },
                        )

                sbuild.unblock_workflow_children()

                data = {
                    "source_artifact": source_artifact_id,
                    "binary_artifacts": [self.binary_artifact.id],
                    "enable_blhc": False,
                    "vendor": "debian",
                    "codename": "bookworm",
                }

                qa = self_.work_request_ensure_child_workflow(
                    task_name="qa",
                    task_data=QAWorkflowData(**{**data, **extra_data}),
                    workflow_data=WorkRequestWorkflowData(
                        display_name="DebDiff workflow", step="debdiff-workflow"
                    ),
                )
                self_.orchestrate_child(qa)

        root = self.playground.create_workflow(task_name="examplepipeline")
        self.assertTrue(orchestrate_workflow(root))

        return root.children.get(task_name="qa")

    def test_validate_input(self) -> None:
        """validate_input passes a valid case."""
        workflow = self.create_qa_workflow()

        workflow.validate_input()

    def test_validate_input_bad_qa_suite(self) -> None:
        """validate_input raises errors in looking up a suite."""
        workflow = self.create_qa_workflow(
            extra_task_data={
                "qa_suite": "nonexistent@debian:suite",
                "enable_reverse_dependencies_autopkgtest": True,
            },
            validate=False,
        )

        with self.assertRaisesRegex(
            WorkflowValidationError,
            "'nonexistent@debian:suite' does not exist or is hidden",
        ):
            workflow.validate_input()

    def test_compute_system_required_tags(self) -> None:
        wr = self.playground.create_workflow(
            task_name="qa",
            task_data=QAWorkflowData(
                source_artifact=self.source_artifact.id,
                binary_artifacts=LookupMultiple(
                    ("internal@collections/name:build-amd64",)
                ),
                vendor="debian",
                codename="sid",
            ),
            internal_collection_artifacts=[
                (self.binary_artifact, {"name": "build-amd64"}),
                (self.package_build_log, {"name": "buildlog-amd64"}),
            ],
        )
        workflow = self.get_workflow(wr)

        self.assertCountEqual(
            workflow.compute_system_required_tags(),
            [wtags.WORKER_TYPE_NOT_ASSIGNABLE],
        )

    def test_compute_dynamic_data(self) -> None:
        wr = self.playground.create_workflow(
            task_name="qa",
            task_data=QAWorkflowData(
                source_artifact=self.source_artifact.id,
                binary_artifacts=LookupMultiple(
                    ("internal@collections/name:build-amd64",)
                ),
                vendor="debian",
                codename="sid",
            ),
            internal_collection_artifacts=[
                (self.binary_artifact, {"name": "build-amd64"}),
                (self.package_build_log, {"name": "buildlog-amd64"}),
            ],
        )
        workflow = self.get_workflow(wr)

        self.assertEqual(
            workflow.compute_dynamic_data(TaskDatabase(wr)),
            QAWorkflowDynamicData(
                subject="hello",
                parameter_summary="hello_1.0-1",
                source_artifact_id=self.source_artifact.id,
                binary_artifacts_ids=[self.binary_artifact.id],
            ),
        )

    def test_compute_dynamic_data_reference_source_artifact(self) -> None:
        reference_source_artifact = self.playground.create_source_artifact(
            name="hello", version="1.0-1"
        )
        new_source_artifact = self.playground.create_source_artifact(
            name="hello", version="1.0-2"
        )

        wr = self.playground.create_workflow(
            task_name="qa",
            task_data=QAWorkflowData(
                source_artifact=new_source_artifact.id,
                binary_artifacts=LookupMultiple(
                    ("internal@collections/name:build-amd64",)
                ),
                reference_source_artifact=reference_source_artifact.id,
                vendor="debian",
                codename="sid",
            ),
            internal_collection_artifacts=[
                (self.binary_artifact, {"name": "build-amd64"}),
                (self.package_build_log, {"name": "buildlog-amd64"}),
            ],
        )
        workflow = self.get_workflow(wr)

        self.assertEqual(
            workflow.compute_dynamic_data(TaskDatabase(wr)),
            QAWorkflowDynamicData(
                subject="hello",
                parameter_summary="hello_1.0-2",
                source_artifact_id=new_source_artifact.id,
                reference_source_artifact_id=reference_source_artifact.id,
                binary_artifacts_ids=[self.binary_artifact.id],
            ),
        )

    def test_compute_dynamic_data_source_artifact_wrong_category(self) -> None:
        """`source_artifact` must be a source-package or upload."""
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.TEST
        )
        with self.assertRaisesRegex(
            TaskConfigError,
            r"^source_artifact: unexpected artifact category: 'debusine:test'. "
            r"Valid categories: \['debian:source-package', 'debian:upload'\]$",
        ):
            self.playground.create_workflow(
                task_name="qa",
                task_data=QAWorkflowData(
                    source_artifact=artifact.id,
                    binary_artifacts=LookupMultiple(
                        ("internal@collections/name:build-amd64",)
                    ),
                    vendor="debian",
                    codename="sid",
                ),
            )

    def test_get_input_artifacts_mandatory_data(self) -> None:
        upload_artifacts = self.playground.create_upload_artifacts()
        wr = self.playground.create_workflow(
            task_name="qa",
            task_data=QAWorkflowData(
                source_artifact=upload_artifacts.upload.id,
                binary_artifacts=LookupMultiple((self.binary_artifact.id,)),
                enable_blhc=False,
                vendor="debian",
                codename="sid",
            ),
        )
        workflow = self.get_workflow(wr)

        self.assertIsNone(workflow.dynamic_data)
        self.assertEqual(
            workflow.get_input_artifacts(),
            [
                InputArtifactSingle(
                    lookup=workflow.data.source_artifact,
                    label="source_artifact",
                    artifact_id=None,
                ),
                InputArtifactMultiple(
                    lookup=workflow.data.binary_artifacts,
                    label="binary_artifacts",
                    artifact_ids=None,
                ),
            ],
        )

        workflow.dynamic_data = workflow.build_dynamic_data(TaskDatabase(wr))
        self.assertIsNotNone(workflow.dynamic_data)

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

    def test_get_input_artifacts_with_optional_data(self) -> None:
        upload_artifacts = self.playground.create_upload_artifacts()
        reference_source_artifact = (
            self.playground.create_minimal_source_package_artifact()
        )

        wr = self.playground.create_workflow(
            task_name="qa",
            task_data=QAWorkflowData(
                source_artifact=upload_artifacts.upload.id,
                binary_artifacts=LookupMultiple(
                    ("internal@collections/name:build-amd64",)
                ),
                reference_source_artifact=reference_source_artifact.id,
                enable_blhc=False,
                vendor="debian",
                codename="sid",
            ),
            internal_collection_artifacts=[
                (self.binary_artifact, {"name": "build-amd64"}),
                (self.package_build_log, {"name": "buildlog-amd64"}),
            ],
        )
        workflow = self.get_workflow(wr)

        workflow.dynamic_data = QAWorkflowDynamicData(
            source_artifact_id=upload_artifacts.source.id,
            binary_artifacts_ids=[self.binary_artifact.id],
            reference_source_artifact_id=reference_source_artifact.id,
        )

        assert workflow.data.reference_source_artifact is not None
        self.assertEqual(
            workflow.get_input_artifacts(),
            [
                InputArtifactSingle(
                    lookup=workflow.data.source_artifact,
                    label="source_artifact",
                    artifact_id=upload_artifacts.source.id,
                ),
                InputArtifactMultiple(
                    lookup=workflow.data.binary_artifacts,
                    label="binary_artifacts",
                    artifact_ids=workflow.dynamic_data.binary_artifacts_ids,
                ),
                InputArtifactSingle(
                    lookup=workflow.data.reference_source_artifact,
                    label="reference_source_artifact",
                    artifact_id=reference_source_artifact.id,
                ),
            ],
        )

    def test_source_package_upload(self) -> None:
        """`source_artifact` can be an upload."""
        upload_artifacts = self.playground.create_upload_artifacts()

        wr = self.playground.create_workflow(
            task_name="qa",
            task_data=QAWorkflowData(
                source_artifact=upload_artifacts.upload.id,
                binary_artifacts=LookupMultiple(
                    ("internal@collections/name:build-amd64",)
                ),
                enable_blhc=False,
                vendor="debian",
                codename="sid",
            ),
            internal_collection_artifacts=[
                (self.binary_artifact, {"name": "build-amd64"})
            ],
        )
        workflow = self.get_workflow(wr)
        self.assertEqual(
            workflow_utils.source_package(workflow), upload_artifacts.source
        )

    @preserve_db_task_registry()
    def test_populate(self) -> None:
        """Test populate."""
        architectures = ["amd64"]
        source_artifact = self.playground.create_source_artifact(name="hello")

        workflow = self.orchestrate(
            extra_data={
                "source_artifact": source_artifact.id,
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "architectures": ["amd64"],
                "extra_repositories": [
                    {
                        "url": "http://example.com/",
                        "suite": "bookworm",
                        "components": ["main"],
                    }
                ],
            },
            architectures=architectures,
            sbuild_workflow_source_artifact=source_artifact,
        )

        autopkgtest = workflow.children.get(
            task_name="autopkgtest", task_type=TaskTypes.WORKFLOW
        )

        assert workflow.parent is not None
        self.assertEqual(autopkgtest.status, WorkRequest.Statuses.RUNNING)
        self.assertEqual(
            autopkgtest.task_data,
            {
                "arch_all_build_architecture": "amd64",
                "architectures": ["amd64"],
                "backend": BackendType.AUTO,
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "codename": "bookworm",
                "enable_regression_tracking": False,
                "prefix": "",
                "qa_suite": None,
                "reference_prefix": "",
                "reference_qa_results": None,
                "source_artifact": source_artifact.id,
                "update_qa_results": False,
                "vendor": "debian",
                "extra_repositories": [
                    {
                        "url": "http://example.com/",
                        "suite": "bookworm",
                        "components": ["main"],
                    }
                ],
            },
        )
        self.assertEqual(
            autopkgtest.workflow_data_json,
            {
                "allow_failure": False,
                "display_name": "autopkgtest",
                "step": "autopkgtest",
            },
        )
        self.assertQuerySetEqual(autopkgtest.dependencies.all(), [])

        # AutopkgtestWorkflow.populate() was called and created its tasks
        self.assertQuerySetEqual(
            autopkgtest.children.order_by("id").values_list(
                "task_type",
                "task_name",
                "status",
                "dependencies__task_type",
                "dependencies__task_name",
            ),
            [
                (
                    TaskTypes.WORKER,
                    "autopkgtest",
                    WorkRequest.Statuses.BLOCKED,
                    TaskTypes.WORKER,
                    "sbuild",
                )
            ],
        )

        self.assertFalse(
            workflow.children.filter(
                task_name="reverse_dependencies_autopkgtest",
                task_type=TaskTypes.WORKFLOW,
            ).exists()
        )

        lintian = workflow.children.get(
            task_name="lintian", task_type=TaskTypes.WORKFLOW
        )

        self.assertEqual(lintian.status, WorkRequest.Statuses.RUNNING)
        self.assertEqual(
            lintian.task_data,
            {
                "arch_all_build_architecture": "amd64",
                "architectures": ["amd64"],
                "backend": BackendType.AUTO,
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "codename": "bookworm",
                "enable_regression_tracking": False,
                "fail_on_severity": LintianFailOnSeverity.ERROR,
                "prefix": "",
                "qa_suite": None,
                "reference_prefix": "",
                "reference_qa_results": None,
                "source_artifact": source_artifact.id,
                "update_qa_results": False,
                "vendor": "debian",
            },
        )
        self.assertEqual(
            lintian.workflow_data_json,
            {
                "allow_failure": False,
                "display_name": "lintian",
                "step": "lintian",
            },
        )
        self.assertQuerySetEqual(lintian.dependencies.all(), [])
        # LintianWorkflow.populate() was called and created its tasks
        self.assertQuerySetEqual(
            lintian.children.order_by("id").values_list(
                "task_type",
                "task_name",
                "status",
                "dependencies__task_type",
                "dependencies__task_name",
            ),
            [
                (
                    TaskTypes.WORKER,
                    "lintian",
                    WorkRequest.Statuses.BLOCKED,
                    TaskTypes.WORKER,
                    "sbuild",
                )
            ],
        )

        piuparts = workflow.children.get(
            task_name="piuparts", task_type=TaskTypes.WORKFLOW
        )

        self.assertEqual(piuparts.status, WorkRequest.Statuses.RUNNING)
        self.assertEqual(
            piuparts.task_data,
            {
                "arch_all_build_architecture": "amd64",
                "architectures": ["amd64"],
                "backend": BackendType.AUTO,
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "codename": "bookworm",
                "enable_regression_tracking": False,
                "prefix": "",
                "qa_suite": None,
                "reference_prefix": "",
                "reference_qa_results": None,
                "source_artifact": source_artifact.id,
                "update_qa_results": False,
                "vendor": "debian",
                "extra_repositories": [
                    {
                        "url": "http://example.com/",
                        "suite": "bookworm",
                        "components": ["main"],
                    }
                ],
            },
        )

        self.assertEqual(
            piuparts.workflow_data_json,
            {
                "allow_failure": False,
                "display_name": "piuparts",
                "step": "piuparts",
            },
        )
        self.assertQuerySetEqual(piuparts.dependencies.all(), [])

        # PiupartsWorkflow.populate() was called and created its tasks
        self.assertQuerySetEqual(
            piuparts.children.order_by("id").values_list(
                "task_type",
                "task_name",
                "status",
                "dependencies__task_type",
                "dependencies__task_name",
            ),
            [
                (
                    TaskTypes.WORKER,
                    "piuparts",
                    WorkRequest.Statuses.BLOCKED,
                    TaskTypes.WORKER,
                    "sbuild",
                )
            ],
        )

    @preserve_db_task_registry()
    def test_populate_use_available_architectures(self) -> None:
        """
        Test populate uses available architectures.

        The user didn't specify "architectures", so QAWorkflow checks
        available architectures and "all" and uses them.
        """
        for arch in ["amd64", "i386"]:
            self.playground.create_debian_environment(architecture=arch)
        source_artifact = self.playground.create_source_artifact(name="hello")
        binary_artifact = (
            self.playground.create_minimal_binary_package_artifact()
        )

        workflow = self.orchestrate(
            extra_data={
                "source_artifact": source_artifact.id,
                "binary_artifacts": [binary_artifact.id],
            },
            sbuild_workflow_source_artifact=source_artifact,
        )

        autopkgtest = workflow.children.get(
            task_name="autopkgtest", task_type=TaskTypes.WORKFLOW
        )
        self.assertEqual(
            autopkgtest.task_data["architectures"], ["all", "amd64", "i386"]
        )

    @preserve_db_task_registry()
    def test_populate_disable_autopkgtest_lintian_piuparts(self) -> None:
        """Populate does not create autopkgtest, lintian nor piuparts."""
        source_artifact = self.playground.create_source_artifact(name="hello")
        binary_artifact = (
            self.playground.create_minimal_binary_package_artifact()
        )
        workflow = self.orchestrate(
            extra_data={
                "source_artifact": source_artifact.id,
                "binary_artifacts": [binary_artifact.id],
                "architectures": ["amd64"],
                "enable_autopkgtest": False,
                "enable_lintian": False,
                "enable_piuparts": False,
            },
            sbuild_workflow_source_artifact=source_artifact,
        )

        self.assertFalse(
            workflow.children.filter(
                task_name="autopkgtest", task_type=TaskTypes.WORKFLOW
            ).exists()
        )
        self.assertFalse(
            workflow.children.filter(
                task_name="reverse_dependencies_autopkgtest",
                task_type=TaskTypes.WORKFLOW,
            ).exists()
        )
        self.assertFalse(
            workflow.children.filter(
                task_name="lintian", task_type=TaskTypes.WORKFLOW
            ).exists()
        )
        self.assertFalse(
            workflow.children.filter(
                task_name="piuparts", task_type=TaskTypes.WORKFLOW
            ).exists()
        )

    def test_populate_uploads(self) -> None:
        """The workflow accepts debian:upload artifacts in all inputs."""
        self.playground.create_collection(
            name="sid", category=CollectionCategory.SUITE
        )
        artifacts = self.playground.create_upload_artifacts(
            binaries=[
                ("hello", "amd64"),
                ("hello", "i386"),
                ("hello-doc", "all"),
            ]
        )
        architectures = ["amd64", "i386"]

        wr = self.playground.create_workflow(
            task_name="qa",
            task_data=QAWorkflowData(
                source_artifact=f"{artifacts.upload.id}@artifacts",
                binary_artifacts=LookupMultiple((artifacts.upload.id,)),
                architectures=architectures,
                vendor="debian",
                codename="sid",
                qa_suite=f"sid@{CollectionCategory.SUITE}",
                enable_autopkgtest=True,
                enable_reverse_dependencies_autopkgtest=True,
                enable_debdiff=True,
                enable_lintian=True,
                enable_piuparts=True,
                enable_blhc=True,
            ),
        )
        self.get_workflow(wr).populate()
        children = list(wr.children.order_by("id").all())
        expected_src_artifact = f"{artifacts.upload.id}@artifacts"
        expected_bin_artifacts = [expected_src_artifact]
        expected: dict[str, dict[str, Any]] = {
            "autopkgtest": {
                "source_artifact": expected_src_artifact,
                "binary_artifacts": expected_bin_artifacts,
            },
            "blhc": {
                "source_artifact": expected_src_artifact,
                "binary_artifacts": expected_bin_artifacts,
            },
            "debdiff": {
                "source_artifact": expected_src_artifact,
                "binary_artifacts": expected_bin_artifacts,
            },
            "lintian": {
                "source_artifact": expected_src_artifact,
                "binary_artifacts": expected_bin_artifacts,
            },
            "piuparts": {
                "source_artifact": expected_src_artifact,
                "binary_artifacts": expected_bin_artifacts,
            },
            "reverse_dependencies_autopkgtest": {
                "source_artifact": expected_src_artifact,
                "binary_artifacts": expected_bin_artifacts,
            },
        }
        self.assertEqual(len(children), len(expected))
        for child in children:
            with self.subTest(task_name=child.task_name):
                expected_data = expected[child.task_name]
                for k, v in expected_data.items():
                    self.assertEqual(
                        child.task_data.get(k), v, f"task_data[{k}]"
                    )

    @preserve_db_task_registry()
    def test_populate_enable_reverse_dependencies_autopkgtest(self) -> None:
        """Populate can enable reverse_dependencies_autopkgtest."""
        architectures = ["amd64"]
        source_artifact = self.playground.create_source_artifact(name="hello")
        sid = self.playground.create_collection(
            name="sid", category=CollectionCategory.SUITE
        )
        dep_source_artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.SOURCE_PACKAGE,
            data=DebianSourcePackage(
                name="depends",
                version="1",
                type="dpkg",
                dsc_fields={
                    "Package": "depends",
                    "Version": "1",
                    "Testsuite": "autopkgtest",
                },
            ),
        )
        sid.manager.add_artifact(
            dep_source_artifact,
            user=self.playground.get_default_user(),
            variables={"component": "main", "section": "devel"},
        )
        dep_binary_artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.BINARY_PACKAGE,
            data=DebianBinaryPackage(
                srcpkg_name="depends",
                srcpkg_version="1",
                deb_fields={
                    "Package": "depends",
                    "Version": "1",
                    "Architecture": "all",
                    "Depends": "hello",
                },
                deb_control_files=[],
            ),
        )
        sid.manager.add_artifact(
            dep_binary_artifact,
            user=self.playground.get_default_user(),
            variables={
                "component": "main",
                "section": "devel",
                "priority": "optional",
            },
        )

        workflow = self.orchestrate(
            extra_data={
                "source_artifact": source_artifact.id,
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "architectures": ["amd64"],
                "qa_suite": f"sid@{CollectionCategory.SUITE}",
                "enable_reverse_dependencies_autopkgtest": True,
            },
            architectures=architectures,
            sbuild_workflow_source_artifact=source_artifact,
        )

        rdep_autopkgtest = workflow.children.get(
            task_name="reverse_dependencies_autopkgtest",
            task_type=TaskTypes.WORKFLOW,
        )

        assert workflow.parent is not None
        self.assertEqual(rdep_autopkgtest.status, WorkRequest.Statuses.RUNNING)
        self.assertEqual(
            rdep_autopkgtest.task_data,
            {
                "prefix": "",
                "reference_prefix": "",
                "source_artifact": source_artifact.id,
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "qa_suite": f"sid@{CollectionCategory.SUITE}",
                "reference_qa_results": None,
                "enable_regression_tracking": False,
                "update_qa_results": False,
                "vendor": "debian",
                "codename": "bookworm",
                "backend": BackendType.AUTO,
                "architectures": ["amd64"],
                "arch_all_build_architecture": "amd64",
                "extra_repositories": None,
            },
        )
        self.assertEqual(
            rdep_autopkgtest.workflow_data_json,
            {
                "allow_failure": False,
                "display_name": "autopkgtests of reverse-dependencies",
                "step": "reverse-dependencies-autopkgtest",
            },
        )
        self.assertQuerySetEqual(rdep_autopkgtest.dependencies.all(), [])

        # ReverseDependenciesAutopkgtestWorkflow.populate() was called and
        # created its sub-workflow.
        autopkgtest = rdep_autopkgtest.children.get()
        self.assertEqual(autopkgtest.status, WorkRequest.Statuses.RUNNING)
        self.assertEqual(
            autopkgtest.task_data["source_artifact"],
            "sid@debian:suite/source-version:depends_1",
        )
        self.assertQuerySetEqual(
            autopkgtest.children.order_by("id").values_list(
                "task_type",
                "task_name",
                "status",
                "dependencies__task_type",
                "dependencies__task_name",
            ),
            [
                (
                    TaskTypes.WORKER,
                    "autopkgtest",
                    WorkRequest.Statuses.BLOCKED,
                    TaskTypes.WORKER,
                    "sbuild",
                )
            ],
        )

    @preserve_db_task_registry()
    def test_populate_update_qa_results(self) -> None:
        """Populate can update QA results."""
        architectures = ["amd64"]
        source_artifact = self.playground.create_source_artifact(name="hello")
        sid = self.playground.create_collection(
            name="sid", category=CollectionCategory.SUITE
        )
        self.playground.create_collection(
            name="sid", category=CollectionCategory.QA_RESULTS
        )
        dep_source_artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.SOURCE_PACKAGE,
            data=DebianSourcePackage(
                name="depends",
                version="1",
                type="dpkg",
                dsc_fields={
                    "Package": "depends",
                    "Version": "1",
                    "Testsuite": "autopkgtest",
                },
            ),
        )
        sid.manager.add_artifact(
            dep_source_artifact,
            user=self.playground.get_default_user(),
            variables={"component": "main", "section": "devel"},
        )
        dep_binary_artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.BINARY_PACKAGE,
            data=DebianBinaryPackage(
                srcpkg_name="depends",
                srcpkg_version="1",
                deb_fields={
                    "Package": "depends",
                    "Version": "1",
                    "Architecture": "all",
                    "Depends": "hello",
                },
                deb_control_files=[],
            ),
        )
        sid.manager.add_artifact(
            dep_binary_artifact,
            user=self.playground.get_default_user(),
            variables={
                "component": "main",
                "section": "devel",
                "priority": "optional",
            },
        )

        workflow = self.orchestrate(
            extra_data={
                "prefix": "reference-qa-result|",
                "reference_prefix": "",
                "source_artifact": source_artifact.id,
                # This would normally be the corresponding binary artifacts
                # in qa_suite, but that doesn't really matter for testing
                # purposes.
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "architectures": ["amd64"],
                "qa_suite": f"sid@{CollectionCategory.SUITE}",
                "reference_qa_results": f"sid@{CollectionCategory.QA_RESULTS}",
                "enable_regression_tracking": False,
                "update_qa_results": True,
                "enable_reverse_dependencies_autopkgtest": True,
                "enable_blhc": True,
            },
            architectures=architectures,
            sbuild_workflow_source_artifact=source_artifact,
        )

        autopkgtest = workflow.children.get(
            task_name="autopkgtest", task_type=TaskTypes.WORKFLOW
        )

        assert workflow.parent is not None
        self.assertEqual(autopkgtest.status, WorkRequest.Statuses.RUNNING)
        self.assertEqual(
            autopkgtest.task_data,
            {
                "arch_all_build_architecture": "amd64",
                "architectures": ["amd64"],
                "backend": BackendType.AUTO,
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "codename": "bookworm",
                "enable_regression_tracking": False,
                "prefix": "reference-qa-result|",
                "qa_suite": f"sid@{CollectionCategory.SUITE}",
                "reference_prefix": "",
                "reference_qa_results": f"sid@{CollectionCategory.QA_RESULTS}",
                "source_artifact": source_artifact.id,
                "update_qa_results": True,
                "vendor": "debian",
                "extra_repositories": None,
            },
        )
        self.assertEqual(
            autopkgtest.workflow_data_json,
            {
                "allow_failure": False,
                "display_name": "autopkgtest",
                "step": "autopkgtest",
            },
        )
        self.assertQuerySetEqual(autopkgtest.dependencies.all(), [])

        # AutopkgtestWorkflow.populate() was called and created its tasks.
        self.assertQuerySetEqual(
            autopkgtest.children.order_by("id").values_list(
                "task_type",
                "task_name",
                "status",
                "dependencies__task_type",
                "dependencies__task_name",
            ),
            [
                (
                    TaskTypes.WORKER,
                    "autopkgtest",
                    WorkRequest.Statuses.BLOCKED,
                    TaskTypes.WORKER,
                    "sbuild",
                )
            ],
        )

        rdep_autopkgtest = workflow.children.get(
            task_name="reverse_dependencies_autopkgtest",
            task_type=TaskTypes.WORKFLOW,
        )

        assert workflow.parent is not None
        self.assertEqual(rdep_autopkgtest.status, WorkRequest.Statuses.RUNNING)
        self.assertEqual(
            rdep_autopkgtest.task_data,
            {
                "prefix": "reference-qa-result|",
                "reference_prefix": "",
                "source_artifact": source_artifact.id,
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "qa_suite": f"sid@{CollectionCategory.SUITE}",
                "reference_qa_results": f"sid@{CollectionCategory.QA_RESULTS}",
                "enable_regression_tracking": False,
                "update_qa_results": True,
                "vendor": "debian",
                "codename": "bookworm",
                "backend": BackendType.AUTO,
                "architectures": ["amd64"],
                "arch_all_build_architecture": "amd64",
                "extra_repositories": None,
            },
        )
        self.assertEqual(
            rdep_autopkgtest.workflow_data_json,
            {
                "allow_failure": False,
                "display_name": "autopkgtests of reverse-dependencies",
                "step": "reverse-dependencies-autopkgtest",
            },
        )
        self.assertQuerySetEqual(rdep_autopkgtest.dependencies.all(), [])

        # ReverseDependenciesAutopkgtestWorkflow.populate() was called and
        # created its sub-workflow.
        sub_autopkgtest = rdep_autopkgtest.children.get()
        self.assertEqual(sub_autopkgtest.status, WorkRequest.Statuses.RUNNING)
        self.assertEqual(
            sub_autopkgtest.task_data["source_artifact"],
            "sid@debian:suite/source-version:depends_1",
        )
        self.assertQuerySetEqual(
            sub_autopkgtest.children.order_by("id").values_list(
                "task_type",
                "task_name",
                "status",
                "dependencies__task_type",
                "dependencies__task_name",
            ),
            [
                (
                    TaskTypes.WORKER,
                    "autopkgtest",
                    WorkRequest.Statuses.BLOCKED,
                    TaskTypes.WORKER,
                    "sbuild",
                )
            ],
        )

        lintian = workflow.children.get(
            task_name="lintian", task_type=TaskTypes.WORKFLOW
        )

        self.assertEqual(lintian.status, WorkRequest.Statuses.RUNNING)
        self.assertEqual(
            lintian.task_data,
            {
                "arch_all_build_architecture": "amd64",
                "architectures": ["amd64"],
                "backend": BackendType.AUTO,
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "codename": "bookworm",
                "enable_regression_tracking": False,
                "fail_on_severity": LintianFailOnSeverity.ERROR,
                "prefix": "reference-qa-result|",
                "qa_suite": f"sid@{CollectionCategory.SUITE}",
                "reference_prefix": "",
                "reference_qa_results": f"sid@{CollectionCategory.QA_RESULTS}",
                "source_artifact": source_artifact.id,
                "update_qa_results": True,
                "vendor": "debian",
            },
        )
        self.assertEqual(
            lintian.workflow_data_json,
            {
                "allow_failure": False,
                "display_name": "lintian",
                "step": "lintian",
            },
        )
        self.assertQuerySetEqual(lintian.dependencies.all(), [])

        # LintianWorkflow.populate() was called and created its tasks.
        self.assertQuerySetEqual(
            lintian.children.order_by("id").values_list(
                "task_type",
                "task_name",
                "status",
                "dependencies__task_type",
                "dependencies__task_name",
            ),
            [
                (
                    TaskTypes.WORKER,
                    "lintian",
                    WorkRequest.Statuses.BLOCKED,
                    TaskTypes.WORKER,
                    "sbuild",
                )
            ],
        )

        piuparts = workflow.children.get(
            task_name="piuparts", task_type=TaskTypes.WORKFLOW
        )

        self.assertEqual(piuparts.status, WorkRequest.Statuses.RUNNING)
        self.assertEqual(
            piuparts.task_data,
            {
                "arch_all_build_architecture": "amd64",
                "architectures": ["amd64"],
                "backend": BackendType.AUTO,
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "codename": "bookworm",
                "enable_regression_tracking": False,
                "prefix": "reference-qa-result|",
                "qa_suite": f"sid@{CollectionCategory.SUITE}",
                "reference_prefix": "",
                "reference_qa_results": f"sid@{CollectionCategory.QA_RESULTS}",
                "source_artifact": source_artifact.id,
                "update_qa_results": True,
                "vendor": "debian",
                "extra_repositories": None,
            },
        )

        self.assertEqual(
            piuparts.workflow_data_json,
            {
                "allow_failure": False,
                "display_name": "piuparts",
                "step": "piuparts",
            },
        )
        self.assertQuerySetEqual(piuparts.dependencies.all(), [])

        blhc = workflow.children.get(
            task_name="blhc", task_type=TaskTypes.WORKFLOW
        )

        assert workflow.parent is not None
        self.assertEqual(blhc.status, WorkRequest.Statuses.BLOCKED)
        self.assertEqual(
            blhc.task_data,
            {
                "arch_all_build_architecture": "amd64",
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "codename": "bookworm",
                "enable_regression_tracking": False,
                "prefix": "reference-qa-result|",
                "qa_suite": f"sid@{CollectionCategory.SUITE}",
                "reference_prefix": "",
                "reference_qa_results": f"sid@{CollectionCategory.QA_RESULTS}",
                "source_artifact": source_artifact.id,
                "update_qa_results": True,
                "vendor": "debian",
            },
        )
        self.assertEqual(
            blhc.workflow_data_json,
            {
                "allow_failure": False,
                "display_name": "build log hardening check",
                "step": "blhc",
            },
        )
        self.assertQuerySetEqual(
            blhc.dependencies.values_list("task_type", "task_name"),
            [(TaskTypes.WORKER, "sbuild")],
        )
        # No children yet; see test_populate_blhc.
        self.assertFalse(blhc.children.exists())

    @preserve_db_task_registry()
    def test_populate_enable_regression_tracking(self) -> None:
        """Populate can run in a mode that tracks regressions."""
        architectures = ["amd64"]
        source_artifact = self.playground.create_source_artifact(name="hello")
        sid = self.playground.create_collection(
            name="sid", category=CollectionCategory.SUITE
        )
        self.playground.create_collection(
            name="sid", category=CollectionCategory.QA_RESULTS
        )
        dep_source_artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.SOURCE_PACKAGE,
            data=DebianSourcePackage(
                name="depends",
                version="1",
                type="dpkg",
                dsc_fields={
                    "Package": "depends",
                    "Version": "1",
                    "Testsuite": "autopkgtest",
                },
            ),
        )
        sid.manager.add_artifact(
            dep_source_artifact,
            user=self.playground.get_default_user(),
            variables={"component": "main", "section": "devel"},
        )
        dep_binary_artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.BINARY_PACKAGE,
            data=DebianBinaryPackage(
                srcpkg_name="depends",
                srcpkg_version="1",
                deb_fields={
                    "Package": "depends",
                    "Version": "1",
                    "Architecture": "all",
                    "Depends": "hello",
                },
                deb_control_files=[],
            ),
        )
        sid.manager.add_artifact(
            dep_binary_artifact,
            user=self.playground.get_default_user(),
            variables={
                "component": "main",
                "section": "devel",
                "priority": "optional",
            },
        )

        workflow = self.orchestrate(
            extra_data={
                "reference_prefix": "reference-qa-result|",
                "source_artifact": source_artifact.id,
                # This would normally be the corresponding binary artifacts
                # in qa_suite, but that doesn't really matter for testing
                # purposes.
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "architectures": ["amd64"],
                "qa_suite": f"sid@{CollectionCategory.SUITE}",
                "reference_qa_results": f"sid@{CollectionCategory.QA_RESULTS}",
                "enable_regression_tracking": True,
                "enable_reverse_dependencies_autopkgtest": True,
                "enable_blhc": True,
            },
            architectures=architectures,
            sbuild_workflow_source_artifact=source_artifact,
        )

        autopkgtest = workflow.children.get(
            task_name="autopkgtest", task_type=TaskTypes.WORKFLOW
        )
        self.assertEqual(autopkgtest.status, WorkRequest.Statuses.RUNNING)
        self.assertDictContainsAll(
            autopkgtest.task_data,
            {
                "enable_regression_tracking": True,
                "reference_prefix": "reference-qa-result|",
                "reference_qa_results": f"sid@{CollectionCategory.QA_RESULTS}",
                "update_qa_results": False,
            },
        )
        self.assertEqual(
            autopkgtest.workflow_data_json,
            {
                "allow_failure": True,
                "display_name": "autopkgtest",
                "step": "autopkgtest",
            },
        )
        self.assertQuerySetEqual(autopkgtest.dependencies.all(), [])
        self.assertTrue(autopkgtest.children.exists())

        rdep_autopkgtest = workflow.children.get(
            task_name="reverse_dependencies_autopkgtest",
            task_type=TaskTypes.WORKFLOW,
        )
        self.assertEqual(rdep_autopkgtest.status, WorkRequest.Statuses.RUNNING)
        self.assertDictContainsAll(
            rdep_autopkgtest.task_data,
            {
                "enable_regression_tracking": True,
                "reference_prefix": "reference-qa-result|",
                "reference_qa_results": f"sid@{CollectionCategory.QA_RESULTS}",
                "update_qa_results": False,
            },
        )
        self.assertEqual(
            rdep_autopkgtest.workflow_data_json,
            {
                "allow_failure": True,
                "display_name": "autopkgtests of reverse-dependencies",
                "step": "reverse-dependencies-autopkgtest",
            },
        )
        self.assertQuerySetEqual(rdep_autopkgtest.dependencies.all(), [])

        # ReverseDependenciesAutopkgtestWorkflow.populate() was called and
        # created its sub-workflow.
        sub_autopkgtest = rdep_autopkgtest.children.get()
        self.assertEqual(sub_autopkgtest.status, WorkRequest.Statuses.RUNNING)
        self.assertEqual(
            sub_autopkgtest.task_data["source_artifact"],
            "sid@debian:suite/source-version:depends_1",
        )
        self.assertTrue(sub_autopkgtest.children.exists())

        lintian = workflow.children.get(
            task_name="lintian", task_type=TaskTypes.WORKFLOW
        )
        self.assertEqual(lintian.status, WorkRequest.Statuses.RUNNING)
        self.assertDictContainsAll(
            lintian.task_data,
            {
                "enable_regression_tracking": True,
                "reference_prefix": "reference-qa-result|",
                "reference_qa_results": f"sid@{CollectionCategory.QA_RESULTS}",
                "update_qa_results": False,
            },
        )
        self.assertEqual(
            lintian.workflow_data_json,
            {
                "allow_failure": True,
                "display_name": "lintian",
                "step": "lintian",
            },
        )
        self.assertQuerySetEqual(lintian.dependencies.all(), [])
        self.assertTrue(lintian.children.exists())

        piuparts = workflow.children.get(
            task_name="piuparts", task_type=TaskTypes.WORKFLOW
        )
        self.assertEqual(piuparts.status, WorkRequest.Statuses.RUNNING)
        self.assertDictContainsAll(
            piuparts.task_data,
            {
                "enable_regression_tracking": True,
                "reference_prefix": "reference-qa-result|",
                "reference_qa_results": f"sid@{CollectionCategory.QA_RESULTS}",
                "update_qa_results": False,
            },
        )
        self.assertEqual(
            piuparts.workflow_data_json,
            {
                "allow_failure": True,
                "display_name": "piuparts",
                "step": "piuparts",
            },
        )
        self.assertQuerySetEqual(piuparts.dependencies.all(), [])
        self.assertTrue(piuparts.children.exists())

        blhc = workflow.children.get(
            task_name="blhc", task_type=TaskTypes.WORKFLOW
        )
        self.assertEqual(blhc.status, WorkRequest.Statuses.BLOCKED)
        self.assertDictContainsAll(
            blhc.task_data,
            {
                "enable_regression_tracking": True,
                "reference_prefix": "reference-qa-result|",
                "reference_qa_results": f"sid@{CollectionCategory.QA_RESULTS}",
                "update_qa_results": False,
            },
        )
        self.assertEqual(
            blhc.workflow_data_json,
            {
                "allow_failure": True,
                "display_name": "build log hardening check",
                "step": "blhc",
            },
        )
        self.assertQuerySetEqual(
            blhc.dependencies.values_list("task_type", "task_name"),
            [(TaskTypes.WORKER, "sbuild")],
        )
        # No children yet.
        self.assertFalse(blhc.children.exists())

        # Complete sbuild to unblock the blhc workflow.
        assert workflow.parent is not None
        self.simulate_sbuild_workflow_completion(
            workflow.parent.children.get(
                task_type=TaskTypes.WORKFLOW, task_name="sbuild"
            )
        )
        blhc.refresh_from_db()
        self.assertEqual(blhc.status, WorkRequest.Statuses.PENDING)
        self.schedule_and_run_workflow(workflow.parent)
        blhc.refresh_from_db()
        self.assertEqual(blhc.status, WorkRequest.Statuses.RUNNING)
        self.assertTrue(blhc.children.exists())

        regression_analysis = workflow.children.get(
            task_type=TaskTypes.INTERNAL,
            task_name="workflow",
            workflow_data_json__step="regression-analysis",
        )
        self.assertEqual(
            regression_analysis.status, WorkRequest.Statuses.BLOCKED
        )
        regression_analysis_callbacks = [
            wr.children.get(
                workflow_data_json__step="regression-analysis-amd64"
            )
            for wr in (
                autopkgtest,
                sub_autopkgtest,
                lintian,
                piuparts,
                blhc,
            )
        ]
        self.assertQuerySetEqual(
            regression_analysis.dependencies.all(),
            regression_analysis_callbacks,
            ordered=False,
        )

    @preserve_db_task_registry()
    def test_populate_architectures_allowed(self) -> None:
        """Populate uses architectures and architectures_allowed."""
        source_artifact = self.playground.create_source_artifact(name="hello")
        binary_artifact_amd = (
            self.playground.create_minimal_binary_package_artifact(
                architecture="amd64"
            )
        )
        binary_artifact_i386 = (
            self.playground.create_minimal_binary_package_artifact(
                architecture="i386"
            )
        )
        binary_artifact_arm64 = (
            self.playground.create_minimal_binary_package_artifact(
                architecture="arm64"
            )
        )

        workflow = self.orchestrate(
            extra_data={
                "source_artifact": source_artifact.id,
                "binary_artifacts": [
                    binary_artifact_amd.id,
                    binary_artifact_i386.id,
                    binary_artifact_arm64.id,
                ],
                "architectures": ["amd64", "i386"],
                "architectures_allowlist": ["i386"],
            },
            sbuild_workflow_source_artifact=source_artifact,
        )

        autopkgtest = workflow.children.get(
            task_name="autopkgtest", task_type=TaskTypes.WORKFLOW
        )

        self.assertEqual(
            autopkgtest.task_data,
            {
                "arch_all_build_architecture": "amd64",
                "architectures": ["i386"],
                "backend": BackendType.AUTO,
                "binary_artifacts": [f"{binary_artifact_i386.id}@artifacts"],
                "codename": "bookworm",
                "enable_regression_tracking": False,
                "prefix": "",
                "qa_suite": None,
                "reference_prefix": "",
                "reference_qa_results": None,
                "source_artifact": source_artifact.id,
                "update_qa_results": False,
                "vendor": "debian",
                "extra_repositories": None,
            },
        )

    @preserve_db_task_registry()
    def test_architectures_deny(self) -> None:
        """Populate uses architectures and architectures_denylist."""
        source_artifact = self.playground.create_source_artifact(name="hello")
        binary_artifact_amd = (
            self.playground.create_minimal_binary_package_artifact(
                architecture="amd64"
            )
        )
        binary_artifact_i386 = (
            self.playground.create_minimal_binary_package_artifact(
                architecture="i386"
            )
        )
        binary_artifact_arm64 = (
            self.playground.create_minimal_binary_package_artifact(
                architecture="arm64"
            )
        )

        workflow = self.orchestrate(
            extra_data={
                "source_artifact": source_artifact.id,
                "binary_artifacts": [
                    binary_artifact_amd.id,
                    binary_artifact_i386.id,
                    binary_artifact_arm64.id,
                ],
                "architectures": ["amd64", "i386"],
                "architectures_denylist": ["i386"],
            },
            architectures=["amd64", "i386"],
            sbuild_workflow_source_artifact=source_artifact,
        )

        autopkgtest = workflow.children.get(
            task_name="autopkgtest", task_type=TaskTypes.WORKFLOW
        )

        self.assertEqual(
            autopkgtest.task_data,
            {
                "arch_all_build_architecture": "amd64",
                "architectures": ["amd64"],
                "backend": BackendType.AUTO,
                "binary_artifacts": [f"{binary_artifact_amd.id}@artifacts"],
                "codename": "bookworm",
                "enable_regression_tracking": False,
                "prefix": "",
                "qa_suite": None,
                "reference_prefix": "",
                "reference_qa_results": None,
                "source_artifact": source_artifact.id,
                "update_qa_results": False,
                "vendor": "debian",
                "extra_repositories": None,
            },
        )

    @preserve_db_task_registry()
    def test_populate_subworkflow_configuration(self) -> None:
        """Populate configures sub-workflows according to specified data."""
        self.playground.create_debian_environment()
        source_artifact = self.playground.create_source_artifact(name="hello")
        binary_artifact = (
            self.playground.create_minimal_binary_package_artifact()
        )

        workflow = self.orchestrate(
            extra_data={
                "source_artifact": source_artifact.id,
                "binary_artifacts": [binary_artifact.id],
                "autopkgtest_backend": BackendType.INCUS_VM,
                "lintian_backend": BackendType.UNSHARE,
                "lintian_fail_on_severity": LintianFailOnSeverity.PEDANTIC,
                "piuparts_backend": BackendType.INCUS_LXC,
                "piuparts_environment": "debian/match:codename=trixie",
            },
            sbuild_workflow_source_artifact=source_artifact,
        )

        autopkgtest = workflow.children.get(
            task_name="autopkgtest", task_type=TaskTypes.WORKFLOW
        )
        self.assertEqual(autopkgtest.task_data["backend"], BackendType.INCUS_VM)

        lintian = workflow.children.get(
            task_name="lintian", task_type=TaskTypes.WORKFLOW
        )
        self.assertEqual(lintian.task_data["backend"], BackendType.UNSHARE)
        self.assertEqual(
            lintian.task_data["fail_on_severity"],
            LintianFailOnSeverity.PEDANTIC,
        )

        piuparts = workflow.children.get(
            task_name="piuparts", task_type=TaskTypes.WORKFLOW
        )
        self.assertEqual(piuparts.task_data["backend"], BackendType.INCUS_LXC)
        self.assertEqual(
            piuparts.task_data["environment"], "debian/match:codename=trixie"
        )

    @preserve_db_task_registry()
    def test_populate_debdiff(self) -> None:
        """Populate creates a debdiff workflow."""
        self.playground.create_collection(
            name="bookworm",
            category=CollectionCategory.SUITE,
        )
        trixie = self.playground.create_collection(
            name="trixie",
            category=CollectionCategory.SUITE,
        )
        origin_source_artifact = self.playground.create_source_artifact(
            name="hello", version="1.0.0-1"
        )
        trixie.manager.add_artifact(
            origin_source_artifact,
            user=self.playground.get_default_user(),
            variables={"component": "main", "section": "devel"},
        )
        origin_binary_artifact = (
            self.playground.create_minimal_binary_package_artifact(
                srcpkg_name="hello",
                srcpkg_version="1.0.0-1",
                version="1.0.0-1",
                architecture="amd64",
            )
        )
        trixie.manager.add_artifact(
            origin_binary_artifact,
            user=self.playground.get_default_user(),
            variables={
                "component": "main",
                "section": "devel",
                "priority": "optional",
            },
        )

        source_artifact = self.playground.create_source_artifact(name="hello")
        workflow = self.orchestrate(
            architectures=["amd64"],
            extra_data={
                "source_artifact": source_artifact.id,
                "binary_artifacts": LookupMultiple(
                    ("internal@collections/name:build-amd64",)
                ),
                "architectures": ["amd64"],
                "enable_autopkgtest": False,
                "enable_lintian": False,
                "enable_piuparts": False,
                "enable_debdiff": True,
                "vendor": "debian",
                "codename": "bookworm",
                "qa_suite": "trixie@debian:suite",
            },
            sbuild_workflow_source_artifact=source_artifact,
        )
        self.playground.advance_work_request(workflow, mark_running=True)

        assert workflow.parent is not None
        sbuild_workflow = workflow.parent.children.get(
            task_name="sbuild", task_type=TaskTypes.WORKFLOW
        )
        self.playground.advance_work_request(sbuild_workflow, mark_running=True)

        sbuild_worker_amd64 = sbuild_workflow.children.get(
            task_name="sbuild", task_type=TaskTypes.WORKER
        )

        debdiff_workflow = workflow.children.get(
            task_name="debdiff", task_type=TaskTypes.WORKFLOW
        )

        self.assertEqual(
            debdiff_workflow.task_data,
            {
                "arch_all_build_architecture": "amd64",
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "codename": "bookworm",
                "original": "trixie@debian:suite",
                "source_artifact": source_artifact.id,
                "vendor": "debian",
            },
        )

        # No children and blocked: it needs the binary artifacts to be able
        # to create the debdiff tasks
        self.assertEqual(debdiff_workflow.children.count(), 0)
        self.assertEqual(debdiff_workflow.status, WorkRequest.Statuses.BLOCKED)
        self.assertQuerySetEqual(
            debdiff_workflow.dependencies.all(), [sbuild_worker_amd64]
        )

        # Create build artifacts and complete sbuild to unblock the debdiff
        # workflow.
        binaries, _ = self.simulate_sbuild_workflow_completion(sbuild_workflow)

        debdiff_workflow.refresh_from_db()
        self.assertEqual(debdiff_workflow.status, WorkRequest.Statuses.PENDING)
        self.schedule_and_run_workflow(workflow.parent)

        debdiff_workflow.refresh_from_db()
        self.assertEqual(debdiff_workflow.status, WorkRequest.Statuses.RUNNING)
        [debdiff_source_work_request, debdiff_binary_work_request] = (
            debdiff_workflow.children.order_by("id")
        )

        self.assertEqual(debdiff_source_work_request.task_name, "debdiff")
        self.assertEqual(
            debdiff_source_work_request.workflow_data_json,
            {
                "display_name": "DebDiff for source package",
                "step": "debdiff-source",
            },
        )
        self.assertEqual(
            debdiff_source_work_request.task_data,
            {
                "build_architecture": "amd64",
                "environment": "debian/match:codename=bookworm",
                "extra_flags": [],
                "input": {
                    "source_artifacts": [
                        "trixie@debian:suite/name:hello_1.0.0-1",
                        source_artifact.id,
                    ]
                },
            },
        )

        self.assertEqual(
            debdiff_binary_work_request.task_data,
            {
                "build_architecture": "amd64",
                "environment": "debian/match:codename=bookworm",
                "extra_flags": [],
                "input": {
                    "binary_artifacts": [
                        ["trixie@debian:suite/name:hello_1.0.0-1_amd64"],
                        [binaries["amd64"][0].id],
                    ]
                },
            },
        )

    @preserve_db_task_registry()
    def test_populate_blhc(self) -> None:
        """Populate enables blhc."""
        architectures = ["amd64"]
        source_artifact = self.playground.create_source_artifact(name="hello")
        sid = self.playground.create_collection(
            name="sid", category=CollectionCategory.SUITE
        )
        assert isinstance(sid.manager, DebianSuiteManager)

        workflow = self.orchestrate(
            extra_data={
                "source_artifact": source_artifact.id,
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "architectures": ["amd64"],
                "qa_suite": f"sid@{CollectionCategory.SUITE}",
                "enable_blhc": True,
            },
            architectures=architectures,
            sbuild_workflow_source_artifact=source_artifact,
        )

        assert workflow.parent is not None
        sbuild_workflow = workflow.parent.children.get(
            task_name="sbuild", task_type=TaskTypes.WORKFLOW
        )
        self.playground.advance_work_request(sbuild_workflow, mark_running=True)

        blhc_workflow = workflow.children.get(
            task_name="blhc", task_type=TaskTypes.WORKFLOW
        )

        # No children and blocked: it needs the binary artifacts first.
        self.assertFalse(blhc_workflow.children.exists())
        self.assertEqual(blhc_workflow.status, WorkRequest.Statuses.BLOCKED)
        self.assertQuerySetEqual(
            blhc_workflow.dependencies.values_list("task_type", "task_name"),
            [(TaskTypes.WORKER, "sbuild")],
        )

        # Create build artifacts and complete sbuild to unblock the blhc
        # workflow.
        _, build_logs = self.simulate_sbuild_workflow_completion(
            sbuild_workflow
        )

        blhc_workflow.refresh_from_db()
        self.assertEqual(blhc_workflow.status, WorkRequest.Statuses.PENDING)
        self.schedule_and_run_workflow(workflow.parent)

        blhc_workflow.refresh_from_db()
        self.assertEqual(blhc_workflow.children.count(), 1)
        self.assertEqual(blhc_workflow.status, WorkRequest.Statuses.RUNNING)

        [blhc_work_request] = blhc_workflow.children.all()

        self.assertEqual(blhc_work_request.task_name, "blhc")
        self.assertEqual(
            blhc_work_request.workflow_data_json,
            {
                "display_name": "build log hardening check for amd64",
                "step": "blhc-amd64",
            },
        )
        self.assertEqual(
            blhc_work_request.task_data,
            {
                "build_architecture": "amd64",
                "environment": "debian/match:codename=bookworm",
                "extra_flags": [],
                "input": {"artifact": f"{build_logs['amd64'].id}@artifacts"},
            },
        )

    def test_summarize_status(self) -> None:
        for statuses, expected_status in (
            (
                {
                    RegressionAnalysisStatus.REGRESSION,
                    RegressionAnalysisStatus.ERROR,
                    RegressionAnalysisStatus.IMPROVEMENT,
                    RegressionAnalysisStatus.STABLE,
                    RegressionAnalysisStatus.NO_RESULT,
                },
                RegressionAnalysisStatus.REGRESSION,
            ),
            (
                {
                    RegressionAnalysisStatus.ERROR,
                    RegressionAnalysisStatus.IMPROVEMENT,
                    RegressionAnalysisStatus.STABLE,
                    RegressionAnalysisStatus.NO_RESULT,
                },
                RegressionAnalysisStatus.ERROR,
            ),
            (
                {
                    RegressionAnalysisStatus.IMPROVEMENT,
                    RegressionAnalysisStatus.STABLE,
                    RegressionAnalysisStatus.NO_RESULT,
                },
                RegressionAnalysisStatus.IMPROVEMENT,
            ),
            (
                {
                    RegressionAnalysisStatus.STABLE,
                    RegressionAnalysisStatus.NO_RESULT,
                },
                RegressionAnalysisStatus.STABLE,
            ),
            (
                {RegressionAnalysisStatus.NO_RESULT},
                RegressionAnalysisStatus.NO_RESULT,
            ),
            (set(), RegressionAnalysisStatus.NO_RESULT),
        ):
            with self.subTest(statuses=repr(statuses)):
                self.assertEqual(
                    QAWorkflow._summarize_status(statuses), expected_status
                )

    def _prepare_test_callback_regression_analysis(
        self,
    ) -> tuple[WorkRequest, Artifact, Artifact]:
        reference_source_artifact = self.playground.create_source_artifact(
            name="hello", version="1.0-1"
        )
        reference_source_url = (
            f"http://example.com/{reference_source_artifact.get_absolute_url()}"
        )
        new_source_artifact = self.playground.create_source_artifact(
            name="hello", version="1.0-2"
        )
        self.playground.create_collection(
            name="sid", category=CollectionCategory.SUITE
        )
        sid_qa_results = self.playground.create_collection(
            name="sid", category=CollectionCategory.QA_RESULTS
        )

        reference_autopkgtest_task = self.playground.create_worker_task(
            task_name="autopkgtest",
            result=WorkRequest.Results.SUCCESS,
            validate=False,
        )
        reference_autopkgtest_artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.AUTOPKGTEST,
            data=DebianAutopkgtest(
                results={
                    "upstream": DebianAutopkgtestResult(
                        status=DebianAutopkgtestResultStatus.PASS
                    )
                },
                cmdline="unused",
                source_package=DebianAutopkgtestSource(
                    name="hello", version="1.0-1", url=reference_source_url
                ),
                architecture="amd64",
                distribution="sid",
            ),
            workspace=reference_autopkgtest_task.workspace,
            work_request=reference_autopkgtest_task,
        )
        sid_qa_results.manager.add_artifact(
            reference_autopkgtest_artifact,
            user=self.playground.get_default_user(),
            variables={
                "package": "hello",
                "version": "1.0-1",
                "architecture": "amd64",
                "timestamp": int(timezone.now().timestamp()),
                "work_request_id": reference_autopkgtest_task.id,
            },
        )

        workflow = self.orchestrate(
            extra_data={
                "reference_prefix": "reference-qa-result|",
                "source_artifact": new_source_artifact.id,
                # This would normally be the corresponding binary artifacts
                # in qa_suite, but that doesn't really matter for testing
                # purposes.
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "reference_source_artifact": reference_source_artifact.id,
                "architectures": ["amd64"],
                "qa_suite": f"sid@{CollectionCategory.SUITE}",
                "reference_qa_results": f"sid@{CollectionCategory.QA_RESULTS}",
                "enable_regression_tracking": True,
                # Simplify this test by only enabling autopkgtests.
                "enable_autopkgtest": True,
                "enable_lintian": False,
                "enable_piuparts": False,
                "enable_blhc": False,
            },
            architectures=["amd64"],
            sbuild_workflow_source_artifact=new_source_artifact,
        )
        return workflow, reference_autopkgtest_artifact, new_source_artifact

    @preserve_db_task_registry()
    def test_callback_regression_analysis_success(self) -> None:
        """The collation callback succeeds if there were no regressions."""
        workflow, reference_autopkgtest_artifact, new_source_artifact = (
            self._prepare_test_callback_regression_analysis()
        )
        assert workflow.parent is not None

        # Add an extra sub-workflow with no regression-analysis callbacks,
        # to make sure that doesn't cause a problem.
        workflow.create_child_workflow(task_name="noop")

        self.playground.advance_work_request(
            workflow.parent.children.get(
                task_type=TaskTypes.WORKFLOW, task_name="sbuild"
            ).children.get(task_type=TaskTypes.WORKER, task_name="sbuild"),
            result=WorkRequest.Results.SUCCESS,
        )
        autopkgtest = workflow.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="autopkgtest"
        )
        autopkgtest_task = autopkgtest.children.get(
            task_type=TaskTypes.WORKER, task_name="autopkgtest"
        )
        new_source_url = (
            f"http://example.com/{new_source_artifact.get_absolute_url()}"
        )
        new_autopkgtest_artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.AUTOPKGTEST,
            data=DebianAutopkgtest(
                results={
                    "upstream": DebianAutopkgtestResult(
                        status=DebianAutopkgtestResultStatus.PASS
                    )
                },
                cmdline="unused",
                source_package=DebianAutopkgtestSource(
                    name="hello", version="1.0-2", url=new_source_url
                ),
                architecture="amd64",
                distribution="sid",
            ),
            workspace=autopkgtest_task.workspace,
            work_request=autopkgtest_task,
        )
        self.playground.advance_work_request(
            autopkgtest_task, result=WorkRequest.Results.SUCCESS
        )

        self.schedule_and_run_workflow_callback(autopkgtest)
        self.schedule_and_run_workflow_callback(workflow)

        regression_analysis = workflow.children.get(
            task_type=TaskTypes.INTERNAL,
            task_name="workflow",
            workflow_data_json__step="regression-analysis",
        )
        self.assertEqual(
            regression_analysis.status, WorkRequest.Statuses.COMPLETED
        )
        self.assertEqual(
            regression_analysis.result, WorkRequest.Results.SUCCESS
        )
        autopkgtest.refresh_from_db()
        self.assertEqual(autopkgtest.status, WorkRequest.Statuses.COMPLETED)
        self.assertEqual(autopkgtest.result, WorkRequest.Results.SUCCESS)
        workflow.refresh_from_db()
        assert workflow.output_data is not None
        self.assertEqual(
            workflow.output_data.regression_analysis,
            {
                "": RegressionAnalysis(
                    original_source_version="1.0-1",
                    new_source_version="1.0-2",
                    status=RegressionAnalysisStatus.STABLE,
                ),
                "autopkgtest:hello:amd64": RegressionAnalysis(
                    original_source_version="1.0-1",
                    original_artifact_id=reference_autopkgtest_artifact.id,
                    new_source_version="1.0-2",
                    new_artifact_id=new_autopkgtest_artifact.id,
                    status=RegressionAnalysisStatus.STABLE,
                    details={"upstream": "stable"},
                ),
            },
        )

    @preserve_db_task_registry()
    def test_callback_regression_analysis_failure(self) -> None:
        """The collation callback fails if there were regressions."""
        workflow, reference_autopkgtest_artifact, new_source_artifact = (
            self._prepare_test_callback_regression_analysis()
        )
        assert workflow.parent is not None

        self.playground.advance_work_request(
            workflow.parent.children.get(
                task_type=TaskTypes.WORKFLOW, task_name="sbuild"
            ).children.get(task_type=TaskTypes.WORKER, task_name="sbuild"),
            result=WorkRequest.Results.SUCCESS,
        )
        autopkgtest = workflow.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="autopkgtest"
        )
        autopkgtest_task = autopkgtest.children.get(
            task_type=TaskTypes.WORKER, task_name="autopkgtest"
        )
        new_source_url = (
            f"http://example.com/{new_source_artifact.get_absolute_url()}"
        )
        new_autopkgtest_artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.AUTOPKGTEST,
            data=DebianAutopkgtest(
                results={
                    "upstream": DebianAutopkgtestResult(
                        status=DebianAutopkgtestResultStatus.FAIL
                    )
                },
                cmdline="unused",
                source_package=DebianAutopkgtestSource(
                    name="hello", version="1.0-2", url=new_source_url
                ),
                architecture="amd64",
                distribution="sid",
            ),
            workspace=autopkgtest_task.workspace,
            work_request=autopkgtest_task,
        )
        self.playground.advance_work_request(
            autopkgtest_task, result=WorkRequest.Results.SUCCESS
        )

        self.schedule_and_run_workflow_callback(autopkgtest)
        self.schedule_and_run_workflow_callback(workflow)

        regression_analysis = workflow.children.get(
            task_type=TaskTypes.INTERNAL,
            task_name="workflow",
            workflow_data_json__step="regression-analysis",
        )
        self.assertEqual(
            regression_analysis.status, WorkRequest.Statuses.COMPLETED
        )
        self.assertEqual(
            regression_analysis.result, WorkRequest.Results.FAILURE
        )
        workflow.refresh_from_db()
        self.assertEqual(workflow.status, WorkRequest.Statuses.COMPLETED)
        self.assertEqual(workflow.result, WorkRequest.Results.FAILURE)
        assert workflow.output_data is not None
        self.assertEqual(
            workflow.output_data.regression_analysis,
            {
                "": RegressionAnalysis(
                    original_source_version="1.0-1",
                    new_source_version="1.0-2",
                    status=RegressionAnalysisStatus.REGRESSION,
                ),
                "autopkgtest:hello:amd64": RegressionAnalysis(
                    original_source_version="1.0-1",
                    original_artifact_id=reference_autopkgtest_artifact.id,
                    new_source_version="1.0-2",
                    new_artifact_id=new_autopkgtest_artifact.id,
                    status=RegressionAnalysisStatus.REGRESSION,
                    details={"upstream": "regression"},
                ),
            },
        )
