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

import datetime as dt
from collections.abc import Sequence
from typing import Any, ClassVar, NewType
from unittest import mock

from django.test import override_settings
from django.utils import timezone

from debusine.artifacts.models import (
    ArtifactCategory,
    BareDataCategory,
    CollectionCategory,
    DebianBlhc,
    DebianSourcePackage,
    TaskTypes,
)
from debusine.client.models import LookupChildType, LookupResultType
from debusine.db.models import (
    Artifact,
    Collection,
    CollectionItem,
    WorkRequest,
    Workspace,
)
from debusine.db.models.lookups import LookupResult
from debusine.db.models.work_requests import SkipWorkRequest
from debusine.server.workflows import BlhcWorkflow, WorkflowValidationError
from debusine.server.workflows.models import (
    BaseWorkflowData,
    BlhcWorkflowData,
    BlhcWorkflowDynamicData,
    RegressionTrackingUpdateQAResults,
    SbuildWorkflowData,
    WorkRequestWorkflowData,
)
from debusine.server.workflows.tests.helpers import (
    SampleWorkflow,
    WorkflowTestCase,
)
from debusine.server.workflows.tests.test_regression_tracking import (
    RegressionTrackingWorkflowTestMixin,
)
from debusine.tasks.models import (
    ActionSkipIfLookupResultChanged,
    ActionUpdateCollectionWithArtifacts,
    BaseDynamicTaskData,
    BlhcFlags,
    InputArtifactMultiple,
    InputArtifactSingle,
    LookupMultiple,
    OutputData,
    RegressionAnalysisStatus,
    SbuildData,
    SbuildInput,
)
from debusine.test.django import preserve_db_task_registry

Architecture = NewType("Architecture", str)


class BlhcWorkflowTests(
    RegressionTrackingWorkflowTestMixin, WorkflowTestCase[BlhcWorkflow]
):
    """Unit tests for :py:class:`BlhcWorkflow`."""

    package_build_log_artifacts: ClassVar[dict[Architecture, Artifact]]
    source_artifact: ClassVar[Artifact]

    workspace: ClassVar[Workspace]

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up common data."""
        super().setUpTestData()

        cls.workspace = cls.playground.get_default_workspace()

        cls.source_artifact = cls.playground.create_source_artifact(
            name="hello", version="1.0-1"
        )

        cls.package_build_log_artifacts = {}
        for arch in ["amd64", "arm64", "all"]:
            artifact = cls.playground.create_build_log_artifact(
                source="hello", version="1.0-1", build_arch=arch
            )

            cls.package_build_log_artifacts[Architecture(arch)] = artifact

    def orchestrate(
        self,
        *,
        task_data: BlhcWorkflowData,
        source_artifact: Artifact,
        architectures: Sequence[str] | None = None,
        parent: WorkRequest | None = None,
        pipeline_task_name: str = "examplepipeline",
    ) -> WorkRequest:
        """Create and orchestrate a BlhcWorkflow."""
        if architectures is None:
            architectures = ["amd64", "arm64", "all"]

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

            TASK_NAME = pipeline_task_name

            def populate(self_) -> None:
                """Populate the pipeline."""
                source_data = source_artifact.create_data()
                assert isinstance(source_data, DebianSourcePackage)
                assert architectures is not None

                sbuild, _ = self_.work_request_ensure_child_workflow(
                    task_name="sbuild",
                    task_data=SbuildWorkflowData(
                        input=SbuildInput(source_artifact=source_artifact.id),
                        target_distribution="debian:trixie",
                        architectures=list(architectures),
                    ),
                    workflow_data=WorkRequestWorkflowData(
                        display_name="sbuild", step="sbuild"
                    ),
                )
                self.playground.advance_work_request(sbuild, mark_running=True)
                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=trixie",
                        ),
                    )
                    self_.provides_artifact(
                        child,
                        ArtifactCategory.PACKAGE_BUILD_LOG,
                        name=f"buildlog-{arch}",
                        data={
                            "source_package_name": source_data.name,
                            "source_package_version": source_data.version,
                            "architecture": arch,
                        },
                    )
                sbuild.unblock_workflow_children()

                blhc, _ = self_.work_request_ensure_child_workflow(
                    task_name="blhc",
                    task_data=task_data,
                    workflow_data=WorkRequestWorkflowData(
                        display_name="blhc",
                        step="blhc",
                    ),
                )
                self_.orchestrate_child(blhc)

        root = self.playground.create_workflow(
            task_name=pipeline_task_name, parent=parent
        )
        self.assertTrue(
            self.playground.orchestrate_workflow(root, use_data=False)
        )

        return root

    def setup_blhc_workflow(
        self,
        *,
        binary_artifacts: LookupMultiple | None = None,
        package_build_logs: LookupMultiple | None = None,
        extra_task_data: dict[str, Any] | None = None,
        include_source_artifact: bool = True,
    ) -> None:
        """
        Create and schedule a Blhc workflow.

        :param binary_artifacts: binary artifacts for the workflow.
        :param package_build_logs: build logs for the workflow. If both
          ``binary_artifacts`` and ``package_build_logs`` are None, use all
          the build log artifacts created by ``setUpTestData()``.
        :param extra_task_data: any extra task data to pass to the workflow.
        :param include_source_artifact: True to include self.source_artifact.
        """
        if binary_artifacts is None and package_build_logs is None:
            package_build_logs = LookupMultiple(
                tuple(
                    package_build_log.id
                    for package_build_log in (
                        self.package_build_log_artifacts.values()
                    )
                )
            )

        task_data = BlhcWorkflowData(
            source_artifact=(
                self.source_artifact.id if include_source_artifact else None
            ),
            binary_artifacts=binary_artifacts,
            package_build_logs=package_build_logs,
            vendor="debian",
            codename="bookworm",
        )

        if extra_task_data is not None:
            task_data = task_data.model_copy(update=extra_task_data)

        example_pipeline = self.orchestrate(
            task_data=task_data,
            source_artifact=self.source_artifact,
        )
        work_request = example_pipeline.children.get(
            task_name="blhc", task_type=TaskTypes.WORKFLOW
        )
        self.task_set_current(self.get_workflow(work_request))

    def add_qa_result(
        self,
        qa_results: Collection,
        package: str,
        version: str,
        architecture: str,
        *,
        timestamp: dt.datetime | None = None,
    ) -> CollectionItem:
        """Add a blhc result to a ``debian:qa-results`` collection."""
        work_request = self.playground.create_worker_task(
            task_name="blhc",
            result=WorkRequest.Results.SUCCESS,
            validate=False,
            use_data=False,
        )
        return qa_results.manager.add_artifact(
            self.playground.create_artifact(
                category=ArtifactCategory.BLHC,
                data=DebianBlhc(
                    source=package, version=version, architecture=architecture
                ),
            )[0],
            user=self.playground.get_default_user(),
            variables={
                "package": package,
                "version": version,
                "architecture": architecture,
                "timestamp": int((timestamp or timezone.now()).timestamp()),
                "work_request_id": work_request.id,
            },
        )

    @preserve_db_task_registry()
    def test_validate_input(self) -> None:
        """validate_input passes a valid case."""
        # validate_input() is called and does not raise
        self.setup_blhc_workflow()

    @preserve_db_task_registry()
    def test_validate_input_bad_qa_suite(self) -> None:
        """validate_input raises errors in looking up a suite."""
        self.setup_blhc_workflow(
            extra_task_data={"qa_suite": "nonexistent@debian:suite"}
        )

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

    @preserve_db_task_registry()
    def test_validate_input_bad_reference_qa_results(self) -> None:
        """validate_input raises errors in looking up reference QA results."""
        self.setup_blhc_workflow(
            extra_task_data={
                "reference_qa_results": "nonexistent@debian:qa-results"
            }
        )

        with self.assertRaisesRegex(
            WorkflowValidationError,
            "'nonexistent@debian:qa-results' does not exist or is hidden",
        ):
            self.task.validate_input()

    def test_has_current_reference_qa_result_no_match(self) -> None:
        """_has_current_reference_qa_result: no matching result."""
        self.playground.create_collection("sid", CollectionCategory.SUITE)
        sid_qa_results = self.playground.create_collection(
            "sid", CollectionCategory.QA_RESULTS
        )
        self.add_qa_result(sid_qa_results, "other", "1.0-1", "amd64")
        build_log = self.playground.create_build_log_artifact(
            source="hello", version="1.0-1"
        )

        wr = self.playground.create_workflow(
            task_name="blhc",
            task_data=BlhcWorkflowData(
                prefix="reference-qa-result|",
                package_build_logs=LookupMultiple((build_log.id,)),
                qa_suite=f"sid@{CollectionCategory.SUITE}",
                reference_qa_results=f"sid@{CollectionCategory.QA_RESULTS}",
                update_qa_results=RegressionTrackingUpdateQAResults.YES,
                vendor="debian",
                codename="sid",
            ),
        )

        self.assertFalse(
            self.get_workflow(wr)._has_current_reference_qa_result(
                LookupResult(
                    result_type=LookupResultType.ARTIFACT,
                    lookup=build_log.id,
                    artifact=build_log,
                ),
                "amd64",
            )
        )

    def test_has_current_reference_qa_result_different_version(self) -> None:
        """_has_current_reference_qa_result: result for different version."""
        self.playground.create_collection("sid", CollectionCategory.SUITE)
        sid_qa_results = self.playground.create_collection(
            "sid", CollectionCategory.QA_RESULTS
        )
        self.add_qa_result(sid_qa_results, "hello", "1.0-1", "amd64")
        build_log = self.playground.create_build_log_artifact(
            source="hello", version="1.0-2"
        )

        wr = self.playground.create_workflow(
            task_name="blhc",
            task_data=BlhcWorkflowData(
                prefix="reference-qa-result|",
                package_build_logs=LookupMultiple((build_log.id,)),
                qa_suite=f"sid@{CollectionCategory.SUITE}",
                reference_qa_results=f"sid@{CollectionCategory.QA_RESULTS}",
                update_qa_results=RegressionTrackingUpdateQAResults.YES,
                vendor="debian",
                codename="sid",
            ),
        )

        self.assertFalse(
            self.get_workflow(wr)._has_current_reference_qa_result(
                LookupResult(
                    result_type=LookupResultType.ARTIFACT,
                    lookup=build_log.id,
                    artifact=build_log,
                ),
                "amd64",
            )
        )

    def test_has_current_reference_qa_result_unexpected(self) -> None:
        """_has_current_reference_qa_result: unexpected result."""
        self.playground.create_collection("sid", CollectionCategory.SUITE)
        sid_qa_results = self.playground.create_collection(
            "sid", CollectionCategory.QA_RESULTS
        )
        self.add_qa_result(sid_qa_results, "hello", "1.0-1", "amd64")
        build_log = self.playground.create_build_log_artifact(
            source="hello", version="1.0-1"
        )

        wr = self.playground.create_workflow(
            task_name="blhc",
            task_data=BlhcWorkflowData(
                prefix="reference-qa-result|",
                package_build_logs=LookupMultiple((build_log.id,)),
                qa_suite=f"sid@{CollectionCategory.SUITE}",
                reference_qa_results=f"sid@{CollectionCategory.QA_RESULTS}",
                update_qa_results=RegressionTrackingUpdateQAResults.YES,
                vendor="debian",
                codename="sid",
            ),
        )

        with self.assertRaisesRegex(ValueError, r"^Unexpected result"):
            artifact, _ = self.playground.create_artifact()
            self.get_workflow(wr)._has_current_reference_qa_result(
                LookupResult(
                    result_type=LookupResultType.ARTIFACT,
                    lookup=artifact.id,
                    artifact=artifact,
                ),
                "amd64",
            )

    def test_has_current_reference_qa_result_current(self) -> None:
        """_has_current_reference_qa_result: current result."""
        self.playground.create_collection("sid", CollectionCategory.SUITE)
        sid_qa_results = self.playground.create_collection(
            "sid", CollectionCategory.QA_RESULTS
        )
        self.add_qa_result(sid_qa_results, "hello", "1.0-1", "amd64")
        build_log = self.playground.create_build_log_artifact(
            source="hello", version="1.0-1"
        )

        wr = self.playground.create_workflow(
            task_name="blhc",
            task_data=BlhcWorkflowData(
                prefix="reference-qa-result|",
                package_build_logs=LookupMultiple((build_log.id,)),
                qa_suite=f"sid@{CollectionCategory.SUITE}",
                reference_qa_results=f"sid@{CollectionCategory.QA_RESULTS}",
                update_qa_results=RegressionTrackingUpdateQAResults.YES,
                vendor="debian",
                codename="sid",
            ),
        )

        self.assertTrue(
            self.get_workflow(wr)._has_current_reference_qa_result(
                LookupResult(
                    result_type=LookupResultType.ARTIFACT,
                    lookup=build_log.id,
                    artifact=build_log,
                ),
                "amd64",
            )
        )

    def test_has_current_reference_qa_result_current_promise(self) -> None:
        """_has_current_reference_qa_result: current result matching promise."""
        self.playground.create_collection("sid", CollectionCategory.SUITE)
        sid_qa_results = self.playground.create_collection(
            "sid", CollectionCategory.QA_RESULTS
        )
        self.add_qa_result(sid_qa_results, "hello", "1.0-1", "amd64")
        collection = self.playground.create_collection(
            "test", CollectionCategory.TEST
        )
        build_log_promise = self.playground.create_bare_data_item(
            collection,
            "test",
            category=BareDataCategory.PROMISE,
            data={
                "promise_work_request_id": 2,
                "promise_workflow_id": 1,
                "promise_category": ArtifactCategory.PACKAGE_BUILD_LOG,
                "source_package_name": "hello",
                "source_package_version": "1.0-1",
            },
        )

        wr = self.playground.create_workflow(
            task_name="blhc",
            task_data=BlhcWorkflowData(
                prefix="reference-qa-result|",
                package_build_logs=LookupMultiple.model_validate(
                    [f"test@{CollectionCategory.TEST}/name:test"]
                ),
                qa_suite=f"sid@{CollectionCategory.SUITE}",
                reference_qa_results=f"sid@{CollectionCategory.QA_RESULTS}",
                update_qa_results=RegressionTrackingUpdateQAResults.YES,
                vendor="debian",
                codename="sid",
            ),
        )

        self.assertTrue(
            self.get_workflow(wr)._has_current_reference_qa_result(
                LookupResult(
                    result_type=LookupResultType.BARE,
                    lookup=f"test@{CollectionCategory.TEST}/test",
                    collection_item=build_log_promise,
                ),
                "amd64",
            )
        )

    @preserve_db_task_registry()
    def test_populate(self) -> None:
        self.setup_blhc_workflow(
            extra_task_data={
                "extra_flags": [BlhcFlags.BINDNOW, BlhcFlags.LINE_NUMBERS]
            }
        )

        # One work request per architecture
        self.assertQuerySetEqual(
            self.work_request.children.order_by("id").values_list(
                "task_type", "task_name", "task_data", "workflow_data_json"
            ),
            [
                (
                    TaskTypes.WORKER,
                    "blhc",
                    {
                        "environment": "debian/match:codename=bookworm",
                        "build_architecture": (
                            "amd64" if architecture == "all" else architecture
                        ),
                        "input": {"artifact": artifact.id},
                        "extra_flags": [
                            BlhcFlags.BINDNOW,
                            BlhcFlags.LINE_NUMBERS,
                        ],
                    },
                    {
                        "display_name": (
                            f"build log hardening check for {architecture}"
                        ),
                        "step": f"blhc-{architecture}",
                    },
                )
                for architecture, artifact in sorted(
                    self.package_build_log_artifacts.items()
                )
            ],
        )

    def test_populate_with_uploads(self) -> None:
        source_upload = self.playground.create_upload_artifacts(
            binary=False
        ).upload
        binary_upload = self.playground.create_upload_artifacts(
            source=False, binaries=[("hello", "amd64")]
        ).upload
        log = self.package_build_log_artifacts[Architecture("amd64")]
        self.playground.create_artifact_relation(
            artifact=log, target=binary_upload
        )
        wr = self.playground.create_workflow(
            task_name="blhc",
            task_data=BlhcWorkflowData(
                source_artifact=source_upload.id,
                binary_artifacts=LookupMultiple((binary_upload.id,)),
                vendor="debian",
                codename="sid",
            ),
        )
        self.get_workflow(wr).populate()
        children = list(wr.children.all())
        self.assertEqual(len(children), 1)
        child = children[0]
        self.assertEqual(
            child.task_data["input"], {"artifact": f"{log.id}@artifacts"}
        )

    def test_populate_has_current_reference_qa_result(self) -> None:
        """The workflow does nothing with a current reference QA result."""
        self.playground.create_collection("sid", CollectionCategory.SUITE)
        sid_qa_results = self.playground.create_collection(
            "sid", CollectionCategory.QA_RESULTS
        )
        self.add_qa_result(sid_qa_results, "hello", "1.0-1", "amd64")
        build_log = self.playground.create_build_log_artifact(
            source="hello", version="1.0-1"
        )

        wr = self.playground.create_workflow(
            task_name="blhc",
            task_data=BlhcWorkflowData(
                prefix="reference-qa-result|",
                package_build_logs=LookupMultiple((build_log.id,)),
                qa_suite=f"sid@{CollectionCategory.SUITE}",
                reference_qa_results=f"sid@{CollectionCategory.QA_RESULTS}",
                update_qa_results=RegressionTrackingUpdateQAResults.YES,
                vendor="debian",
                codename="sid",
            ),
        )

        self.get_workflow(wr).populate()

        self.assertQuerySetEqual(wr.children.all(), [])

    def test_populate_has_current_reference_qa_result_force(self) -> None:
        """The workflow can be forced despite a current reference QA result."""
        self.playground.create_collection("sid", CollectionCategory.SUITE)
        sid_qa_results = self.playground.create_collection(
            "sid", CollectionCategory.QA_RESULTS
        )
        self.add_qa_result(sid_qa_results, "hello", "1.0-1", "amd64")
        build_log = self.playground.create_build_log_artifact(
            source="hello", version="1.0-1"
        )

        wr = self.playground.create_workflow(
            task_name="blhc",
            task_data=BlhcWorkflowData(
                prefix="reference-qa-result|",
                package_build_logs=LookupMultiple((build_log.id,)),
                qa_suite=f"sid@{CollectionCategory.SUITE}",
                reference_qa_results=f"sid@{CollectionCategory.QA_RESULTS}",
                update_qa_results=RegressionTrackingUpdateQAResults.FORCE,
                vendor="debian",
                codename="sid",
            ),
        )

        self.get_workflow(wr).populate()

        self.assertTrue(
            wr.children.filter(
                task_type=TaskTypes.WORKER, task_name="blhc"
            ).exists()
        )

    def test_populate_previous_reference_qa_result_outdated(self) -> None:
        """The workflow produces a reference QA result if it is outdated."""
        sid_suite = self.playground.create_collection(
            "sid", CollectionCategory.SUITE
        )
        sid_qa_results = self.playground.create_collection(
            "sid", CollectionCategory.QA_RESULTS
        )
        old_qa_result = self.add_qa_result(
            sid_qa_results, "hello", "1.0-1", "amd64"
        )
        build_log = self.playground.create_build_log_artifact(
            source="hello", version="1.0-2"
        )
        source_item = sid_suite.manager.add_artifact(
            self.playground.create_source_artifact(
                name="hello", version="1.0-2"
            ),
            user=self.playground.get_default_user(),
            variables={"component": "main", "section": "devel"},
        )

        wr = self.playground.create_workflow(
            task_name="blhc",
            task_data=BlhcWorkflowData(
                prefix="reference-qa-result|",
                package_build_logs=LookupMultiple((build_log.id,)),
                qa_suite=f"sid@{CollectionCategory.SUITE}",
                reference_qa_results=f"sid@{CollectionCategory.QA_RESULTS}",
                update_qa_results=RegressionTrackingUpdateQAResults.YES,
                vendor="debian",
                codename="sid",
            ),
        )

        self.get_workflow(wr).populate()

        [child] = wr.children.all()
        self.assertEqual(child.status, WorkRequest.Statuses.BLOCKED)
        self.assertEqual(child.task_type, TaskTypes.WORKER)
        self.assertEqual(child.task_name, "blhc")
        self.assertEqual(
            child.task_data,
            {
                "environment": "debian/match:codename=sid",
                "build_architecture": "amd64",
                "input": {"artifact": build_log.id},
                "extra_flags": [],
            },
        )
        provide_action = ActionUpdateCollectionWithArtifacts(
            collection="internal@collections",
            name_template=("reference-qa-result|blhc-amd64"),
            artifact_filters={"category": ArtifactCategory.BLHC},
        )
        skip_action = ActionSkipIfLookupResultChanged(
            lookup=(
                f"sid@{CollectionCategory.QA_RESULTS}/latest:blhc_hello_amd64"
            ),
            collection_item_id=old_qa_result.id,
            promise_name="reference-qa-result|blhc-amd64",
        )
        qa_result_action = ActionUpdateCollectionWithArtifacts(
            collection=f"sid@{CollectionCategory.QA_RESULTS}",
            variables={
                "package": "hello",
                "version": "1.0-2",
                "architecture": "amd64",
                "timestamp": int(source_item.created_at.timestamp()),
                "work_request_id": child.id,
            },
            artifact_filters={"category": ArtifactCategory.BLHC},
        )
        self.assert_work_request_event_reactions(
            child,
            on_assignment=[skip_action],
            on_failure=[qa_result_action],
            on_success=[provide_action, qa_result_action],
        )

        # Completing the work request stores the QA result.
        result, _ = self.playground.create_artifact(
            category=ArtifactCategory.BLHC,
            data=DebianBlhc(
                source="hello", version="1.0-2", architecture="amd64"
            ),
            work_request=child,
        )
        self.playground.advance_work_request(
            child, result=WorkRequest.Results.SUCCESS, use_data=False
        )
        self.assertEqual(
            child.lookup_single(
                f"sid@{CollectionCategory.QA_RESULTS}/latest:blhc_hello_amd64",
                expect_type=LookupChildType.ARTIFACT,
            ).artifact,
            result,
        )

        # Population is idempotent, even if qa_suite changes.
        with (
            mock.patch.object(
                BlhcWorkflow,
                "_has_current_reference_qa_result",
                return_value=False,
            ),
            mock.patch.object(
                BlhcWorkflow,
                "qa_suite_changed",
                mock.PropertyMock(
                    return_value=timezone.now() + dt.timedelta(hours=1)
                ),
            ),
        ):
            self.get_workflow(wr).populate()
        self.assertEqual(wr.children.count(), 1)
        child.refresh_from_db()
        self.assert_work_request_event_reactions(
            child,
            on_assignment=[skip_action],
            on_failure=[qa_result_action],
            on_success=[provide_action, qa_result_action],
        )

    def test_populate_reference_qa_result_backs_off(self) -> None:
        """Reference tasks are skipped if another workflow got there first."""
        self.playground.create_collection("sid", CollectionCategory.SUITE)
        sid_qa_results = self.playground.create_collection(
            "sid", CollectionCategory.QA_RESULTS
        )
        self.playground.create_debian_environment(
            codename="sid", variant="blhc"
        )
        build_log = self.playground.create_build_log_artifact(
            source="hello", version="1.0-1"
        )

        wr = self.playground.create_workflow(
            task_name="blhc",
            task_data=BlhcWorkflowData(
                prefix="reference-qa-result|",
                package_build_logs=LookupMultiple((build_log.id,)),
                qa_suite=f"sid@{CollectionCategory.SUITE}",
                reference_qa_results=f"sid@{CollectionCategory.QA_RESULTS}",
                update_qa_results=RegressionTrackingUpdateQAResults.YES,
                vendor="debian",
                codename="sid",
            ),
        )

        self.get_workflow(wr).populate()

        racing_qa_result = self.add_qa_result(
            sid_qa_results, "hello", "1.0-1", "amd64"
        )
        [child] = wr.children.all()
        self.playground.advance_work_request(child, mark_pending=True)

        with self.assertRaises(SkipWorkRequest):
            child.assign_worker(self.playground.create_worker())

        self.assertEqual(child.status, WorkRequest.Statuses.COMPLETED)
        self.assertEqual(child.result, WorkRequest.Results.SKIPPED)
        self.assertEqual(
            child.output_data,
            OutputData(
                skip_reason=(
                    f"Result of lookup "
                    f"'sid@{CollectionCategory.QA_RESULTS}/"
                    f"latest:blhc_hello_amd64' changed"
                )
            ),
        )
        self.assertEqual(
            child.lookup_single(
                f"sid@{CollectionCategory.QA_RESULTS}/latest:blhc_hello_amd64",
                expect_type=LookupChildType.ARTIFACT,
            ).collection_item,
            racing_qa_result,
        )

    @override_settings(DISABLE_AUTOMATIC_SCHEDULING=True)
    @preserve_db_task_registry()
    def test_callback_regression_analysis(self) -> None:
        self.playground.create_collection("sid", CollectionCategory.SUITE)
        self.playground.create_collection("sid", CollectionCategory.QA_RESULTS)
        architectures = ["amd64", "arm64", "i386", "ppc64el", "s390x"]
        reference_source_artifact = self.playground.create_source_artifact(
            name="hello", version="0.9-1"
        )

        root = self.playground.create_workflow(mark_running=True)
        reference = self.orchestrate(
            task_data=BlhcWorkflowData(
                prefix="reference-qa-result|",
                qa_suite=f"sid@{CollectionCategory.SUITE}",
                reference_qa_results=f"sid@{CollectionCategory.QA_RESULTS}",
                update_qa_results=RegressionTrackingUpdateQAResults.YES,
                source_artifact=reference_source_artifact.id,
                package_build_logs=LookupMultiple.model_validate(
                    [
                        f"internal@collections/name:buildlog-{arch}"
                        for arch in architectures
                        if arch != "ppc64el"
                    ]
                ),
                vendor="debian",
                codename="sid",
            ),
            source_artifact=reference_source_artifact,
            architectures=architectures,
            parent=root,
            pipeline_task_name="referencepipeline",
        )
        new = self.orchestrate(
            task_data=BlhcWorkflowData(
                reference_prefix="reference-qa-result|",
                qa_suite=f"sid@{CollectionCategory.SUITE}",
                reference_qa_results=f"sid@{CollectionCategory.QA_RESULTS}",
                enable_regression_tracking=True,
                source_artifact=self.source_artifact.id,
                package_build_logs=LookupMultiple.model_validate(
                    [
                        f"internal@collections/name:buildlog-{arch}"
                        for arch in architectures
                    ]
                ),
                vendor="debian",
                codename="sid",
            ),
            source_artifact=self.source_artifact,
            architectures=architectures,
            parent=root,
        )

        # Unblock the blhc tasks.
        for sbuild in reference.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="sbuild"
        ).children.filter(task_type=TaskTypes.WORKER, task_name="sbuild"):
            self.playground.advance_work_request(
                sbuild, result=WorkRequest.Results.SUCCESS, use_data=False
            )
        for sbuild in new.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="sbuild"
        ).children.filter(task_type=TaskTypes.WORKER, task_name="sbuild"):
            self.playground.advance_work_request(
                sbuild, result=WorkRequest.Results.SUCCESS, use_data=False
            )

        reference_blhc_workflow = reference.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="blhc"
        )
        reference_tasks = {
            wr.task_data["build_architecture"]: wr
            for wr in reference_blhc_workflow.children.filter(
                task_type=TaskTypes.WORKER, task_name="blhc"
            )
        }
        reference_results = {
            architecture: self.playground.create_artifact(
                category=ArtifactCategory.BLHC,
                data=DebianBlhc(
                    source="hello", version="0.9-1", architecture=architecture
                ),
                work_request=reference_tasks[architecture],
            )[0]
            for architecture in architectures
            if architecture != "ppc64el"
        }
        new_blhc_workflow = new.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="blhc"
        )
        new_tasks = {
            wr.task_data["build_architecture"]: wr
            for wr in new_blhc_workflow.children.filter(
                task_type=TaskTypes.WORKER, task_name="blhc"
            )
        }
        new_results = {
            architecture: self.playground.create_artifact(
                category=ArtifactCategory.BLHC,
                data=DebianBlhc(
                    source="hello", version="1.0-1", architecture=architecture
                ),
                work_request=new_tasks[architecture],
            )[0]
            for architecture in architectures
        }

        self.assertIsNone(new_blhc_workflow.output_data)
        with self.schedule_and_run() as work_requests:
            self.assertEqual(work_requests, [])
        self.playground.advance_work_request(
            reference_tasks["amd64"],
            result=WorkRequest.Results.SUCCESS,
            use_data=False,
        )
        with self.schedule_and_run() as work_requests:
            self.assertEqual(work_requests, [])
        self.playground.advance_work_request(
            new_tasks["amd64"],
            result=WorkRequest.Results.SUCCESS,
            use_data=False,
        )

        self.schedule_and_run_workflow_callback(new_blhc_workflow)

        expected_analysis: list[tuple[str, RegressionAnalysisStatus, None]] = [
            ("amd64", RegressionAnalysisStatus.STABLE, None)
        ]
        new_blhc_workflow.refresh_from_db()
        self.assert_regression_analysis(
            new_blhc_workflow,
            "0.9-1",
            reference_results,
            "1.0-1",
            new_results,
            expected_analysis,
        )

        expected_callback_statuses = {
            "amd64": WorkRequest.Statuses.COMPLETED,
            "arm64": WorkRequest.Statuses.BLOCKED,
            "i386": WorkRequest.Statuses.BLOCKED,
            "ppc64el": WorkRequest.Statuses.BLOCKED,
            "s390x": WorkRequest.Statuses.BLOCKED,
        }
        self.assert_callback_statuses(
            new_blhc_workflow, expected_callback_statuses
        )

        for architecture, reference_result, new_result, status in (
            (
                "arm64",
                WorkRequest.Results.SUCCESS,
                WorkRequest.Results.FAILURE,
                RegressionAnalysisStatus.REGRESSION,
            ),
            (
                "i386",
                WorkRequest.Results.FAILURE,
                WorkRequest.Results.FAILURE,
                RegressionAnalysisStatus.STABLE,
            ),
            (
                "ppc64el",
                None,
                WorkRequest.Results.SUCCESS,
                RegressionAnalysisStatus.NO_RESULT,
            ),
            (
                "s390x",
                WorkRequest.Results.FAILURE,
                WorkRequest.Results.SUCCESS,
                RegressionAnalysisStatus.IMPROVEMENT,
            ),
        ):
            if reference_result is not None:
                self.playground.advance_work_request(
                    reference_tasks[architecture],
                    result=reference_result,
                    use_data=False,
                )
            self.playground.advance_work_request(
                new_tasks[architecture], result=new_result, use_data=False
            )
            expected_analysis.append((architecture, status, None))
            expected_callback_statuses[architecture] = (
                WorkRequest.Statuses.PENDING
            )

        self.assert_callback_statuses(
            new_blhc_workflow, expected_callback_statuses
        )

        # Running any one of the pending callbacks is enough, but in fact
        # one is scheduled for each architecture and the scheduler will
        # catch up with them over multiple runs.  Simulate this and check
        # that the result is correct at each stage.
        for architecture in ("arm64", "i386", "ppc64el", "s390x"):
            self.schedule_and_run_workflow_callback(new_blhc_workflow)

            new_blhc_workflow.refresh_from_db()
            assert new_blhc_workflow.output_data is not None
            self.assert_regression_analysis(
                new_blhc_workflow,
                "0.9-1",
                reference_results,
                "1.0-1",
                new_results,
                expected_analysis,
            )
            expected_callback_statuses[architecture] = (
                WorkRequest.Statuses.COMPLETED
            )
            self.assert_callback_statuses(
                new_blhc_workflow, expected_callback_statuses
            )

    @preserve_db_task_registry()
    def test_compute_scheduler_tags(self) -> None:
        package_build_log_1 = self.playground.create_build_log_artifact(
            source="hello"
        )
        package_build_log_2 = self.playground.create_build_log_artifact(
            source="hello-traditional"
        )
        self.setup_blhc_workflow(
            package_build_logs=LookupMultiple(
                (
                    f"{package_build_log_1.id}@artifacts",
                    f"{package_build_log_2.id}@artifacts",
                )
            )
        )
        self.assertComputesSchedulerTags()

    @preserve_db_task_registry()
    def test_compute_dynamic_data_with_binary_artifacts(self) -> None:
        binary_uploads = {
            architecture: self.playground.create_upload_artifacts(
                source=False, binaries=[("hello", architecture)]
            ).upload
            for architecture in ("amd64", "s390x")
        }
        package_build_logs = {
            architecture: self.playground.create_build_log_artifact(
                build_arch=architecture
            )
            for architecture in binary_uploads
        }
        for architecture in binary_uploads:
            self.playground.create_artifact_relation(
                artifact=package_build_logs[architecture],
                target=binary_uploads[architecture],
            )

        self.setup_blhc_workflow(
            binary_artifacts=LookupMultiple(
                tuple(
                    f"{artifact.id}@artifacts"
                    for artifact in binary_uploads.values()
                )
            )
        )

        self.assertEqual(
            self.compute_dynamic_data(self.task),
            BlhcWorkflowDynamicData(
                subject="hello",
                source_artifact_id=self.source_artifact.id,
                binary_artifacts_ids=[
                    artifact.id for artifact in binary_uploads.values()
                ],
                package_build_logs_ids=sorted(
                    [artifact.id for artifact in package_build_logs.values()]
                ),
            ),
        )

    @preserve_db_task_registry()
    def test_compute_dynamic_data_with_package_build_logs(self) -> None:
        package_build_log_1 = self.playground.create_build_log_artifact(
            source="hello"
        )
        package_build_log_2 = self.playground.create_build_log_artifact(
            source="hello-traditional"
        )

        self.setup_blhc_workflow(
            package_build_logs=LookupMultiple(
                (
                    f"{package_build_log_1.id}@artifacts",
                    f"{package_build_log_2.id}@artifacts",
                )
            )
        )

        self.assertEqual(
            self.compute_dynamic_data(self.task),
            BlhcWorkflowDynamicData(
                subject="hello hello-traditional",
                source_artifact_id=self.source_artifact.id,
                package_build_logs_ids=[
                    package_build_log_1.id,
                    package_build_log_2.id,
                ],
            ),
        )

    @preserve_db_task_registry()
    def test_get_input_artifacts_with_binary_artifacts(self) -> None:
        binary_uploads = {
            architecture: self.playground.create_upload_artifacts(
                source=False, binaries=[("hello", architecture)]
            ).upload
            for architecture in ("amd64", "s390x")
        }

        self.setup_blhc_workflow(
            binary_artifacts=LookupMultiple(
                tuple(
                    f"{artifact.id}@artifacts"
                    for artifact in binary_uploads.values()
                )
            )
        )

        assert self.task.data.source_artifact is not None
        assert self.task.data.binary_artifacts is not None
        self.assertEqual(
            self.task.get_input_artifacts(),
            [
                InputArtifactSingle(
                    lookup=self.task.data.source_artifact,
                    label="source_artifact",
                    artifact_id=self.source_artifact.id,
                ),
                InputArtifactMultiple.from_ids(
                    lookup=self.task.data.binary_artifacts,
                    label="binary_artifacts",
                    artifact_ids=[
                        artifact.id for artifact in binary_uploads.values()
                    ],
                ),
            ],
        )

    @preserve_db_task_registry()
    def test_get_input_artifacts_with_package_build_logs(self) -> None:
        package_build_log_1 = self.playground.create_build_log_artifact(
            source="hello"
        )
        package_build_log_2 = self.playground.create_build_log_artifact(
            source="hello-traditional"
        )

        self.setup_blhc_workflow(
            package_build_logs=LookupMultiple(
                (
                    f"{package_build_log_1.id}@artifacts",
                    f"{package_build_log_2.id}@artifacts",
                )
            )
        )

        assert self.task.data.source_artifact is not None
        assert self.task.data.package_build_logs is not None
        self.assertEqual(
            self.task.get_input_artifacts(),
            [
                InputArtifactSingle(
                    lookup=self.task.data.source_artifact,
                    label="source_artifact",
                    artifact_id=self.source_artifact.id,
                ),
                InputArtifactMultiple.from_ids(
                    lookup=self.task.data.package_build_logs,
                    label="package_build_logs",
                    artifact_ids=[
                        package_build_log_1.id,
                        package_build_log_2.id,
                    ],
                ),
            ],
        )

    @preserve_db_task_registry()
    def test_get_input_artifacts_without_source_artifact(self) -> None:
        package_build_log_1 = self.playground.create_build_log_artifact(
            source="hello"
        )
        package_build_log_2 = self.playground.create_build_log_artifact(
            source="hello-traditional"
        )

        self.setup_blhc_workflow(
            package_build_logs=LookupMultiple(
                (
                    f"{package_build_log_1.id}@artifacts",
                    f"{package_build_log_2.id}@artifacts",
                )
            ),
            include_source_artifact=False,
        )

        assert self.task.data.package_build_logs is not None
        self.assertEqual(
            self.task.get_input_artifacts(),
            [
                InputArtifactMultiple.from_ids(
                    lookup=self.task.data.package_build_logs,
                    label="package_build_logs",
                    artifact_ids=[
                        package_build_log_1.id,
                        package_build_log_2.id,
                    ],
                ),
            ],
        )
