# 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 django.test import override_settings
from django.utils import timezone

import debusine.worker.tags as wtags
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,
    TaskDatabase,
    WorkRequest,
    Workspace,
)
from debusine.db.models.work_requests import SkipWorkRequest
from debusine.server.collections.lookup import LookupResult
from debusine.server.scheduler import schedule
from debusine.server.workflows import BlhcWorkflow, WorkflowValidationError
from debusine.server.workflows.base import orchestrate_workflow
from debusine.server.workflows.models import (
    BaseWorkflowData,
    BlhcWorkflowData,
    BlhcWorkflowDynamicData,
    SbuildWorkflowData,
    WorkRequestWorkflowData,
)
from debusine.server.workflows.tests.helpers import (
    SampleWorkflow,
    WorkflowTestBase,
)
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, WorkflowTestBase[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(orchestrate_workflow(root))

        return root

    def create_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,
    ) -> BlhcWorkflow:
        """
        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
        )
        return 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
        )
        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.create_blhc_workflow()

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

        with self.assertRaisesRegex(
            WorkflowValidationError,
            "'nonexistent@debian:suite' does not exist or is hidden",
        ):
            w.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."""
        w = self.create_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",
        ):
            w.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=True,
                vendor="debian",
                codename="sid",
            ),
        )

        self.assertFalse(
            self.get_workflow(wr)._has_current_reference_qa_result(
                LookupResult(
                    result_type=LookupResultType.ARTIFACT, 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=True,
                vendor="debian",
                codename="sid",
            ),
        )

        self.assertFalse(
            self.get_workflow(wr)._has_current_reference_qa_result(
                LookupResult(
                    result_type=LookupResultType.ARTIFACT, 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=True,
                vendor="debian",
                codename="sid",
            ),
        )

        with self.assertRaisesRegex(ValueError, r"^Unexpected result"):
            self.get_workflow(wr)._has_current_reference_qa_result(
                LookupResult(
                    result_type=LookupResultType.ARTIFACT,
                    artifact=self.playground.create_artifact()[0],
                ),
                "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=True,
                vendor="debian",
                codename="sid",
            ),
        )

        self.assertTrue(
            self.get_workflow(wr)._has_current_reference_qa_result(
                LookupResult(
                    result_type=LookupResultType.ARTIFACT, 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=True,
                vendor="debian",
                codename="sid",
            ),
        )

        self.assertTrue(
            self.get_workflow(wr)._has_current_reference_qa_result(
                LookupResult(
                    result_type=LookupResultType.BARE,
                    collection_item=build_log_promise,
                ),
                "amd64",
            )
        )

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

        # One work request per architecture
        self.assertQuerySetEqual(
            blhc_workflow.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": f"{artifact.id}@artifacts"},
                        "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=True,
                vendor="debian",
                codename="sid",
            ),
        )

        self.get_workflow(wr).populate()

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

    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=True,
                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": f"{build_log.id}@artifacts"},
                "extra_flags": [],
            },
        )
        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=[
                ActionSkipIfLookupResultChanged(
                    lookup=(
                        f"sid@{CollectionCategory.QA_RESULTS}/"
                        f"latest:blhc_hello_amd64"
                    ),
                    collection_item_id=old_qa_result.id,
                    promise_name="reference-qa-result|blhc-amd64",
                )
            ],
            on_failure=[qa_result_action],
            on_success=[
                ActionUpdateCollectionWithArtifacts(
                    collection="internal@collections",
                    name_template=("reference-qa-result|blhc-amd64"),
                    artifact_filters={"category": ArtifactCategory.BLHC},
                ),
                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
        )
        self.assertEqual(
            child.lookup_single(
                f"sid@{CollectionCategory.QA_RESULTS}/latest:blhc_hello_amd64",
                expect_type=LookupChildType.ARTIFACT,
            ).artifact,
            result,
        )

    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=True,
                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=True,
                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
            )
        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
            )

        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
                ),
                workspace=reference.workspace,
                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
                ),
                workspace=new.workspace,
                work_request=new_tasks[architecture],
            )[0]
            for architecture in architectures
        }

        self.assertIsNone(new_blhc_workflow.output_data)
        self.assertEqual(schedule(), [])
        self.playground.advance_work_request(
            reference_tasks["amd64"], result=WorkRequest.Results.SUCCESS
        )
        self.assertEqual(schedule(), [])
        self.playground.advance_work_request(
            new_tasks["amd64"], result=WorkRequest.Results.SUCCESS
        )

        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
                )
            self.playground.advance_work_request(
                new_tasks[architecture], result=new_result
            )
            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_system_required_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"
        )

        workflow = self.create_blhc_workflow(
            package_build_logs=LookupMultiple(
                (
                    f"{package_build_log_1.id}@artifacts",
                    f"{package_build_log_2.id}@artifacts",
                )
            )
        )
        self.assertCountEqual(
            workflow.compute_system_required_tags(),
            [wtags.WORKER_TYPE_NOT_ASSIGNABLE],
        )

    @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],
            )

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

        self.assertEqual(
            workflow.compute_dynamic_data(TaskDatabase(workflow.work_request)),
            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"
        )

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

        self.assertEqual(
            workflow.compute_dynamic_data(TaskDatabase(workflow.work_request)),
            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")
        }

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

        assert workflow.data.source_artifact is not None
        assert workflow.data.binary_artifacts is not None
        self.assertEqual(
            workflow.get_input_artifacts(),
            [
                InputArtifactSingle(
                    lookup=workflow.data.source_artifact,
                    label="source_artifact",
                    artifact_id=self.source_artifact.id,
                ),
                InputArtifactMultiple(
                    lookup=workflow.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"
        )

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

        assert workflow.data.source_artifact is not None
        assert workflow.data.package_build_logs is not None
        self.assertEqual(
            workflow.get_input_artifacts(),
            [
                InputArtifactSingle(
                    lookup=workflow.data.source_artifact,
                    label="source_artifact",
                    artifact_id=self.source_artifact.id,
                ),
                InputArtifactMultiple(
                    lookup=workflow.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"
        )

        workflow = self.create_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 workflow.data.package_build_logs is not None
        self.assertEqual(
            workflow.get_input_artifacts(),
            [
                InputArtifactMultiple(
                    lookup=workflow.data.package_build_logs,
                    label="package_build_logs",
                    artifact_ids=[
                        package_build_log_1.id,
                        package_build_log_2.id,
                    ],
                ),
            ],
        )
