# 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.
"""Inheritable test scenarios."""

import contextlib
import textwrap
from collections.abc import Generator
from functools import cached_property
from typing import Any, TYPE_CHECKING, override

from django.db.models import Model

from debusine.artifacts.models import (
    ArtifactCategory,
    BareDataCategory,
    CollectionCategory,
    DebianAutopkgtest,
    DebianAutopkgtestResult,
    DebianAutopkgtestResultStatus,
    DebianAutopkgtestSource,
    DebianBlhc,
    DebianLintian,
    DebianLintianSummary,
    DebianPiuparts,
    DebianSourcePackage,
    DebianUpload,
    TaskTypes,
)
from debusine.client.models import RuntimeParameter
from debusine.db.context import context
from debusine.db.models import (
    Artifact,
    ArtifactRelation,
    Collection,
    CollectionItem,
    Group,
    Scope,
    Token,
    User,
    WorkRequest,
    Worker,
    WorkerPool,
    WorkflowTemplate,
    Workspace,
)
from debusine.db.models.auth import Identity
from debusine.db.models.collections import CollectionRelation
from debusine.db.permissioncontext import PermissionContext
from debusine.server.workflows.base import orchestrate_workflow
from debusine.server.workflows.models import (
    RegressionTrackingUpdateQAResults,
    SbuildWorkflowData,
)
from debusine.tasks import DebDiff
from debusine.tasks.models import (
    BackendType,
    OutputData,
    RegressionAnalysis,
    RegressionAnalysisStatus,
    SbuildInput,
    WorkerType,
)

if TYPE_CHECKING:
    from debusine.db.playground.playground import Playground


class Scenario:
    """Base for inheritable test scenarios."""

    # Implementation notes:
    #
    # Database is rolled back for each test method after build() is called:
    #
    # * objects created at build() time remain valid in the database (and are
    #   reset to their build() state)
    # * objects created in other methods will become invalid at the end of the
    #   test method
    #
    # This means no @cached_property and other internally cached values that
    # are not created in build().
    #
    # See #626 for details.

    playground: "Playground"

    def __init__(self, *, set_current: bool = False) -> None:
        """Store scenario arguments."""
        self.needs_set_current = set_current

    def build(self, playground: "Playground") -> None:
        """
        Build the scenario.

        This is run by Playground with permission tests disabled and in a local
        context that will be restored at the end of the build.
        """
        self.playground = playground

    def set_current(self) -> None:
        """Set the current user and workspace from the scenario."""


class DefaultScopeUser(Scenario):
    """
    Quick access to the default scope and user.

    Optionally sets them in context.
    """

    scope: Scope
    user: User

    @override
    def build(self, playground: "Playground") -> None:
        super().build(playground)
        self.scope = self.playground.get_default_scope()
        self.user = self.playground.get_default_user()

    @override
    def set_current(self) -> None:
        super().set_current()
        context.set_permission_context_scoped(self.scope, self.context_ui())

    def create_user_token(self) -> Token:
        """Create a user token for self.user."""
        return self.playground.create_user_token(user=self.user)

    @cached_property
    def scope_owners(self) -> Group:
        """Return the group of scope owners."""
        return self.playground.create_group_role(self.scope, Scope.Roles.OWNER)

    @contextlib.contextmanager
    def assign_role(self, resource: Model, *roles: str) -> Generator[None]:
        """Temporarily assign roles to the scenario user on the resource."""
        with self.playground.assign_role(resource, self.user, *roles):
            yield

    def context_ui(self) -> PermissionContext:
        """Return a PermissionContext for this scenario user, in the UI case."""
        return PermissionContext.for_ui_user(self.user)


class DefaultScopeUserAPI(DefaultScopeUser):
    """DefaultScopeUser, plus a user token."""

    user_token: Token

    @override
    def build(self, playground: "Playground") -> None:
        super().build(playground)
        self.user_token = self.create_user_token()


class DefaultContext(DefaultScopeUser):
    """
    Quick access to the default scope, user and workspace.

    Optionally sets them in context.
    """

    workspace: Workspace
    default_task_configuration_collection: Collection

    @override
    def build(self, playground: "Playground") -> None:
        super().build(playground)
        self.workspace = self.playground.get_default_workspace()
        self.default_task_configuration_collection, _ = (
            Collection.objects.get_or_create(
                workspace=self.workspace,
                name="default",
                category=CollectionCategory.TASK_CONFIGURATION,
            )
        )

    @override
    def set_current(self) -> None:
        super().set_current()
        self.workspace.set_current()

    @cached_property
    def workspace_owners(self) -> Group:
        """Return the group of workspace owners."""
        return self.playground.create_group_role(
            self.workspace, Workspace.Roles.OWNER
        )


class DefaultContextAPI(DefaultScopeUserAPI, DefaultContext):
    """DefaultContext, plus a user token."""


class DefaultSbuild(DefaultContext):
    """Scenario with a source and an environment ready for a sbuild task."""

    source: Artifact
    environment_sid: Artifact

    @override
    def build(self, playground: "Playground") -> None:
        super().build(playground)
        self.source = self.playground.create_minimal_source_package_artifact()
        environment = self.playground.create_debian_environment(codename="sid")
        assert environment.artifact is not None
        self.environment_sid = environment.artifact


class DefaultSbuildAPI(DefaultScopeUserAPI, DefaultSbuild):
    """DefaultSbuild, plus a user token."""


class PackageBuildLog(DefaultContext):
    """Workspace with a package build log."""

    @override
    def build(self, playground: "Playground") -> None:
        super().build(playground)

        auotpkgtest_logs_collection, _ = Collection.objects.get_or_create(
            workspace=self.workspace,
            name="autopkgtest-logs",
            category=CollectionCategory.QA_RESULTS,
        )

        # Create an AutopkgtestLog artifact
        self.autopkgtest_log = textwrap.dedent(
            """\
            0s autopkgtest [19:23:39]: starting date and time: 2025-09-17 19:23:39+0000
            0s autopkgtest [19:23:39]: version 5.47~bpo12+1
            0s autopkgtest [19:23:39]: host debusine-worker-amd64-hades-04; command line: /usr/bin/autopkgtest --apt-upgrade --output-dir=artifact-dir --summary=artifact-dir/summary --no-built-binaries --needs-internet=run /tmp/debusine-fetch-exec-upload-6fvpyw4c/phing_3.1.0-1_all.deb /tmp/debusine-fetch-exec-upload-6fvpyw4c/phing_3.1.0-1.dsc -- incus artifact/2482231 -- --profile debusine
            7s autopkgtest [19:23:46]: testbed dpkg architecture: amd64
            7s autopkgtest [19:23:46]: testbed apt version: 3.1.5
            7s autopkgtest [19:23:46]: @@@@@@@@@@@@@@@@@@@@ test bed setup
            7s autopkgtest [19:23:46]: testbed release detected to be: None
            7s autopkgtest [19:23:46]: updating testbed package index (apt update)
            7s Get:1 http://deb.debian.org/debian sid InRelease [216 kB]
            78s autopkgtest [22:28:32]: @@@@@@@@@@@@@@@@@@@@ apt-source debci
            81s Get:1 http://deb.debian.org/debian unstable/main debci 3.12 (dsc) [2949 B]
            81s Get:2 http://deb.debian.org/debian unstable/main debci 3.12 (tar) [164 kB]
            81s dpkg-source: warning: cannot verify inline signature for ./debci_3.12.dsc: no acceptable signature found
            ...
            32s autopkgtest [12:16:27]: test pybuild-autopkgtest: preparing testbed
            32s Ign:1 file:/tmp/autopkgtest.0wEqIN/binaries  InRelease
            32s Get:2 file:/tmp/autopkgtest.0wEqIN/binaries  Release [816 B]
            32s Get:2 file:/tmp/autopkgtest.0wEqIN/binaries  Release [816 B]
            33s Ign:3 file:/tmp/autopkgtest.0wEqIN/binaries  Release.gpg
            33s Reading package lists...
            33s Reading package lists...
            33s Building dependency tree...
            102s autopkgtest [22:28:56]: test pybuild-autopkgtest: [-----------------------
            102s + tmpdir=/tmp/autopkgtest.4eb2Ze/autopkgtest_tmp
            102s + cp -r spec /tmp/autopkgtest.4eb2Ze/autopkgtest_tmp
            102s + cd /tmp/autopkgtest.4eb2Ze/autopkgtest_tmp
            102s + export COVERAGE=no
            102s + rspec
            103s autopkgtest [22:34:24]: test pybuild-autopkgtest:  - - - - - - - - - - results - - - - - - - - - -
            103s pybuild-autopkgtest           PASS
            109s autopkgtest [22:29:03]: test test-suite: preparing testbed
            109s qemu-system-x86_64: terminating on signal 15 from pid 2264680 (/usr/bin/python3)
            129s autopkgtest [22:29:23]: testbed dpkg architecture: amd64
            129s autopkgtest [22:29:23]: testbed apt version: 3.1.6
            130s autopkgtest [22:29:24]: @@@@@@@@@@@@@@@@@@@@ test bed setup
            130s autopkgtest [22:29:24]: updating testbed package index (apt update)
            130s Get:1 http://deb.debian.org/debian unstable InRelease [207 kB]
            130s Get:2 http://deb.debian.org/debian unstable/main Sources.diff/Index [63.6 kB]
            131s Get:3 http://deb.debian.org/debian unstable/contrib Sources.diff/Index [63.3 kB]
            131s Get:4 http://deb.debian.org/debian unstable/main amd64 Packages.diff/Index [63.6 kB]
            131s Get:5 http://deb.debian.org/debian unstable/main Sources T-2025-09-28-2013.43-F-2025-09-28-0810.40.pdiff [69.0 kB]
            131s Get:6 http://deb.debian.org/debian unstable/contrib Sources T-2025-09-28-1412.22-F-2025-09-28-1412.22.pdiff [31 B]
            132s autopkgtest [22:29:56]: test test-suite: [-----------------------
            132s + tmpdir=/tmp/autopkgtest.4eb2Ze/autopkgtest_tmp
            132s + cp -r spec /tmp/autopkgtest.4eb2Ze/autopkgtest_tmp
            132s + cd /tmp/autopkgtest.4eb2Ze/autopkgtest_tmp
            132s + export COVERAGE=no
            140s autopkgtest [22:29:57]: test test-suite:  - - - - - - - - - - results - - - - - - - - - -
            140s test-suite       FAIL
            200s autopkgtest [22:29:58]: test test-suite-skip: preparing testbed
            201s autopkgtest [22:30:56]: test test-suite-skip: [-----------------------
            some lines
            201s test-suite-skip       SKIP
            1079s autopkgtest [22:45:13]: @@@@@@@@@@@@@@@@@@@@ summary
            1079s pybuild-autopkgtest  PASS
            1079s test-suite           FAIL
            1080s test-suite-skip      SKIP
            1081s qemu-system-x86_64: terminating on signal 15 from pid 2264680 (/usr/bin/python3)
        """  # noqa: E501
        ).encode("utf-8")

        self.autopkgtest_log_artifact, _ = playground.create_artifact(
            category=ArtifactCategory.AUTOPKGTEST,
            data=DebianAutopkgtest(
                architecture="amd64",
                cmdline=textwrap.dedent(
                    """\
                    autopkgtest --apt-upgrade --output-dir=artifact-dir
                     --summary=artifact-d/summary --no-built-binaries
                     --needs-internet=run /tmp/debusine-d/phing_3.1.0-1_all.deb
                    /tmp/dir/phing_3.1.0-1.dsc -- incus artifact/2482231 --
                    --profile debusine
                    """
                ),
                distribution="debian:sid",
                results={
                    "help": DebianAutopkgtestResult(
                        details="(superficial)",
                        status=DebianAutopkgtestResultStatus.PASS,
                    ),
                    "php-lint": DebianAutopkgtestResult(
                        details="(superficial)",
                        status=DebianAutopkgtestResultStatus.PASS,
                    ),
                    "phpunit": DebianAutopkgtestResult(
                        details="(superficial)",
                        status=DebianAutopkgtestResultStatus.PASS,
                    ),
                    "version": DebianAutopkgtestResult(
                        details="(superficial)",
                        status=DebianAutopkgtestResultStatus.PASS,
                    ),
                },
                source_package=DebianAutopkgtestSource(
                    name="phing",
                    url=(
                        "https://debusine.debian.net/debian/developers/"
                        "artifact/2489330/download/phing_3.1.0-1.dsc"
                    ),
                    version="3.1.0-1",
                ),
            ),
            paths={"log": self.autopkgtest_log},
            create_files=True,
        )
        autopkgtest_log_file = (
            self.autopkgtest_log_artifact.fileinartifact_set.get(path="log")
        )
        autopkgtest_log_file.content_type = "text/plain; charset=utf-8"
        autopkgtest_log_file.save()

        # Add artifact to the collection
        environment = playground.create_debian_environment().artifact
        assert environment is not None
        work_request = playground.create_sbuild_work_request(
            environment=environment,
            source=playground.create_source_artifact(),
            build_architecture="amd64",
        )
        playground.advance_work_request(work_request, mark_pending=True)

        auotpkgtest_logs_collection.manager.add_artifact(
            self.autopkgtest_log_artifact,
            user=self.user,
            variables={
                "package": "hello",
                "version": "1.0.0-1",
                "architecture": "amd64",
                "timestamp": "10000000",
                "work_request_id": work_request.id,
            },
        )


class DebianWorkspace(DefaultContext):
    """Set up a Debian-like workspace."""

    env_sid_autopkgtest_amd64: CollectionItem
    env_sid_lintian_amd64: CollectionItem

    collection_archive: Collection
    collection_qa_results_sid: Collection
    collection_suite_sid: Collection

    template_upload_to_sid: WorkflowTemplate

    @override
    def build(self, playground: "Playground") -> None:
        super().build(playground)
        self.env_sid_amd64_unshare = playground.create_debian_environment(
            codename="sid",
            architecture="amd64",
            workspace=self.workspace,
        )
        self.env_sid_i386_unshare = playground.create_debian_environment(
            codename="sid",
            architecture="i386",
            workspace=self.workspace,
        )
        self.env_sid_sbuild_amd64 = playground.create_debian_environment(
            codename="sid",
            architecture="amd64",
            variant="sbuild",
            workspace=self.workspace,
        )
        self.env_sid_sbuild_i386 = playground.create_debian_environment(
            codename="sid",
            architecture="i386",
            variant="sbuild",
            workspace=self.workspace,
        )
        self.env_sid_autopkgtest_amd64 = playground.create_debian_environment(
            codename="sid",
            architecture="amd64",
            backend=BackendType.INCUS_LXC,
            variant="autopkgtest",
            workspace=self.workspace,
        )
        self.env_sid_autopkgtest_i386 = playground.create_debian_environment(
            codename="sid",
            architecture="i386",
            backend=BackendType.INCUS_LXC,
            variant="autopkgtest",
            workspace=self.workspace,
        )
        self.env_sid_lintian_amd64 = playground.create_debian_environment(
            codename="sid",
            architecture="amd64",
            variant="lintian",
            workspace=self.workspace,
        )
        self.env_sid_lintian_amd64 = playground.create_debian_environment(
            codename="sid",
            architecture="i386",
            variant="lintian",
            workspace=self.workspace,
        )
        self.collection_archive = self.playground.create_singleton_collection(
            category=CollectionCategory.ARCHIVE,
            workspace=self.workspace,
        )
        self.collection_qa_results_sid = self.playground.create_collection(
            workspace=self.workspace,
            name="sid",
            category=CollectionCategory.QA_RESULTS,
        )
        self.collection_suite_sid = self.playground.create_collection(
            workspace=self.workspace,
            name="sid",
            category=CollectionCategory.SUITE,
        )
        self.template_upload_to_sid = playground.create_workflow_template(
            "upload-to-unstable",
            "debian_pipeline",
            workspace=self.workspace,
            static_parameters={
                "autopkgtest_backend": "incus-lxc",
                "codename": "sid",
                "enable_debdiff": True,
                "enable_reverse_dependencies_autopkgtest": True,
                "enable_upload": True,
                "extra_repositories": [
                    {
                        "components": [
                            "contrib",
                            "non-free",
                            "non-free-firmware",
                        ],
                        "suite": "sid",
                        "url": "http://deb.debian.org/debian",
                    },
                    {
                        "components": [
                            "main",
                            "contrib",
                            "non-free",
                            "non-free-firmware",
                        ],
                        "suite": "buildd-sid",
                        "url": "http://incoming.debian.org/debian-buildd",
                    },
                ],
                "lintian_backend": "unshare",
                "piuparts_backend": "unshare",
                "qa_suite": "sid@debian:suite",
                "regression_tracking_qa_results": "sid@debian:qa-results",
                "sbuild_backend": "unshare",
                "upload_include_binaries": False,
                "upload_merge_uploads": False,
                "vendor": "debian",
            },
            runtime_parameters={
                "arch_all_build_architecture": RuntimeParameter.ANY,
                "architectures": RuntimeParameter.ANY,
                "autopkgtest_backend": ["incus-lxc", "incus-vm"],
                "enable_autopkgtest": RuntimeParameter.ANY,
                "enable_blhc": RuntimeParameter.ANY,
                "enable_confirmation": RuntimeParameter.ANY,
                "enable_debdiff": RuntimeParameter.ANY,
                "enable_lintian": RuntimeParameter.ANY,
                "enable_piuparts": RuntimeParameter.ANY,
                "enable_regression_tracking": RuntimeParameter.ANY,
                "enable_reverse_dependencies_autopkgtest": RuntimeParameter.ANY,
                "enable_upload": RuntimeParameter.ANY,
                "qa_failure_policy": RuntimeParameter.ANY,
                "source_artifact": RuntimeParameter.ANY,
                "upload_delayed_days": RuntimeParameter.ANY,
                "upload_include_binaries": RuntimeParameter.ANY,
                "upload_include_source": RuntimeParameter.ANY,
                "upload_merge_uploads": RuntimeParameter.ANY,
                "upload_require_signature": RuntimeParameter.ANY,
                "upload_since_version": RuntimeParameter.ANY,
            },
        )


class DebianWorkspaceAPI(DefaultContextAPI, DebianWorkspace):
    """DefaultContext, plus a user token."""


class UIRegressionTesting(DebianWorkspace):
    """UI scenario with a regression testing QA workflow."""

    #: Reference version of the sample package
    reference_source: Artifact
    reference_binaries: dict[str, Artifact]

    #: New version of the sample package
    new_source: Artifact

    #: Reverse dependency of the sample package
    dep_source: Artifact
    dep_binaries: dict[str, Artifact]

    #: Root workflow
    workflow_debian_pipeline: WorkRequest

    #: QA subworkflows
    workflow_qa_reference: WorkRequest
    workflow_qa: WorkRequest

    autopkgtest_libfoo_ref_i386: Artifact
    autopkgtest_libfoo_new_amd64: Artifact
    autopkgtest_libfoo_new_i386: Artifact
    autopkgtest_foo_ref_amd64: Artifact
    autopkgtest_foo_ref_i386: Artifact
    autopkgtest_foo_new_amd64: Artifact
    autopkgtest_foo_new_i386: Artifact

    ra_autopkgtest_amd64: RegressionAnalysis
    ra_autopkgtest_i386: RegressionAnalysis
    ra_autopkgtest_foo_amd64: RegressionAnalysis
    ra_autopkgtest_foo_i386: RegressionAnalysis
    ra_lintian_amd64: RegressionAnalysis
    ra_lintian_i386: RegressionAnalysis
    ra_piuparts_amd64: RegressionAnalysis
    ra_piuparts_i386: RegressionAnalysis
    ra_blhc_amd64: RegressionAnalysis
    ra_blhc_i386: RegressionAnalysis

    def __init__(
        self, *, set_current: bool = False, simulate: bool = True
    ) -> None:
        """Store scenario arguments."""
        super().__init__(set_current=set_current)
        self.simulate = simulate

    def _create_sample_package(
        self,
        source_name: str,
        version: str,
        *,
        binary_name: str | None = None,
        dsc_fields: dict[str, Any] | None = None,
        deb_fields: dict[str, Any] | None = None,
    ) -> tuple[Artifact, dict[str, Artifact]]:
        """Create a source package and its binaries, and add them to sid."""
        binary_name = binary_name or source_name
        source = self.playground.create_minimal_source_package_artifact(
            name=source_name,
            version=version,
            dsc_fields={"Binary": binary_name, **(dsc_fields or {})},
        )
        self.collection_suite_sid.manager.add_artifact(
            source,
            user=self.user,
            variables={"component": "main", "section": "devel"},
        )
        binaries = {
            arch: self.playground.create_minimal_binary_package_artifact(
                srcpkg_name=source_name,
                srcpkg_version=version,
                package=binary_name,
                version=version,
                architecture=arch,
                deb_fields=deb_fields,
            )
            for arch in ("amd64", "i386")
        }
        for binary in binaries.values():
            self.collection_suite_sid.manager.add_artifact(
                binary,
                user=self.user,
                variables={
                    "component": "main",
                    "section": "misc",
                    "priority": "optional",
                },
            )
        for arch in binaries:
            build_log = self.playground.create_build_log_artifact(
                source=source_name, version=version, build_arch=arch
            )
            self.playground.create_artifact_relation(
                artifact=build_log, target=binaries[arch]
            )
        return source, binaries

    def _add_regression_analysis(self, **analysis: RegressionAnalysis) -> None:
        od = self.workflow_qa.output_data
        assert od is not None
        assert od.regression_analysis is not None
        for name, a in analysis.items():
            od.regression_analysis[name] = a
        self.workflow_qa.output_data = od
        self.workflow_qa.save()

    def set_work_request_result(
        self, work_request: WorkRequest, result: WorkRequest.Results
    ) -> None:
        """Simulate that the work request completed with the given result."""
        work_request.configured_task_data = work_request.task_data
        work_request.save()
        self.playground.advance_work_request(work_request, result=result)

    def simulate_work_request_result(
        self,
        artifact: Artifact,
        work_request: WorkRequest,
        result: WorkRequest.Results = WorkRequest.Results.SUCCESS,
    ) -> None:
        """
        Generate an artifact as a result for a work request.

        The artifact is then added to collections according to the work request
        UPDATE_COLLECTION_WITH_ARTIFACTS success hooks.
        """
        artifact.created_by_work_request = work_request
        artifact.save()
        self.set_work_request_result(work_request, result)

    def add_result_autopkgtest(
        self,
        package: Artifact,
        architecture: str,
        *,
        parent: WorkRequest,
        result: WorkRequest.Results = WorkRequest.Results.SUCCESS,
        status: DebianAutopkgtestResultStatus = (
            DebianAutopkgtestResultStatus.PASS
        ),
    ) -> Artifact:
        """Add an Autopkgtest QA artifact to collection_qa_results_sid."""
        work_request = parent.children.get(
            task_type=TaskTypes.WORKER,
            task_name="autopkgtest",
            task_data__build_architecture=architecture,
        )
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.AUTOPKGTEST,
            data=DebianAutopkgtest(
                results={"test": DebianAutopkgtestResult(status=status)},
                cmdline="autopkgtest --this-is-a-test",
                source_package=DebianAutopkgtestSource(
                    name=package.data["name"],
                    version=package.data["version"],
                    url="https://example.org/test",
                ),
                architecture=architecture,
                distribution="debian",
            ),
        )
        self.simulate_work_request_result(artifact, work_request, result)
        return artifact

    def add_result_lintian(
        self,
        package: Artifact,  # noqa: ARG002, U100
        architecture: str,
        *,
        parent: WorkRequest,
        result: WorkRequest.Results = WorkRequest.Results.SUCCESS,
    ) -> Artifact:
        """Add a Lintian QA result artifact to collection_qa_results_sid."""
        work_request = parent.children.get(
            task_type=TaskTypes.WORKER,
            task_name="lintian",
            task_data__build_architecture=architecture,
        )
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.LINTIAN,
            data=DebianLintian(
                architecture=architecture,
                summary=DebianLintianSummary(
                    tags_count_by_severity={},
                    package_filename={},
                    tags_found=[],
                    overridden_tags_found=[],
                    lintian_version="3.14",
                    distribution="sid",
                ),
            ),
        )
        self.simulate_work_request_result(artifact, work_request, result)
        return artifact

    def add_result_piuparts(
        self,
        package: Artifact,  # noqa: ARG002, U100
        architecture: str,
        *,
        parent: WorkRequest,
        result: WorkRequest.Results = WorkRequest.Results.SUCCESS,
    ) -> Artifact:
        """Add a Piuparts QA result artifact to collection_qa_results_sid."""
        work_request = parent.children.get(
            task_type=TaskTypes.WORKER,
            task_name="piuparts",
            task_data__build_architecture=architecture,
        )
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.PIUPARTS,
            data=DebianPiuparts(
                piuparts_version="3.14",
                binary_packages={},
                architecture=architecture,
                distribution="debian",
            ),
        )
        self.simulate_work_request_result(artifact, work_request, result)
        return artifact

    def add_result_blhc(
        self,
        package: Artifact,
        architecture: str,
        parent: WorkRequest,
        result: WorkRequest.Results = WorkRequest.Results.SUCCESS,
    ) -> Artifact:
        """Add a blhc QA result artifact to collection_qa_results_sid."""
        work_request = parent.children.get(
            task_type=TaskTypes.WORKER,
            task_name="blhc",
            task_data__build_architecture=architecture,
        )
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.BLHC,
            data=DebianBlhc(
                source=package.data["name"],
                version=package.data["version"],
                architecture=architecture,
            ),
        )
        self.simulate_work_request_result(artifact, work_request, result)
        return artifact

    def simulate_sbuild(self) -> None:
        """Simulate results of sbuild workflow."""
        workflow_sbuild = self.workflow_debian_pipeline.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="sbuild"
        )
        for sbuild in workflow_sbuild.children.filter(
            task_type=TaskTypes.WORKER, task_name="sbuild"
        ):
            assert sbuild.workflow_data.step is not None
            arch = sbuild.workflow_data.step.removeprefix("build-")
            upload = self.playground.create_upload_artifacts(
                src_name=self.new_source.data["name"],
                version=self.new_source.data["version"],
                binary=True,
                source=False,
                binaries=[(self.new_source.data["name"], arch)],
                workspace=sbuild.workspace,
                work_request=sbuild,
            )
            build_log = self.playground.create_build_log_artifact(
                source=self.new_source.data["name"],
                version=self.new_source.data["version"],
                build_arch=arch,
                workspace=sbuild.workspace,
                work_request=sbuild,
            )
            self.playground.create_artifact_relation(
                artifact=build_log, target=upload.upload
            )
            self.set_work_request_result(sbuild, WorkRequest.Results.SUCCESS)

    def simulate_autopkgtest(self) -> None:
        """Simulate results of autopkgtest workflow."""
        # Results from a previous run
        workflow_autopkgtest_reference = (
            self.workflow_qa_reference.children.get(
                task_type=TaskTypes.WORKFLOW, task_name="autopkgtest"
            )
        )
        # No amd64 result
        self.autopkgtest_libfoo_ref_i386 = self.add_result_autopkgtest(
            self.reference_source, "i386", parent=workflow_autopkgtest_reference
        )

        # New run
        workflow_autopkgtest = self.workflow_qa.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="autopkgtest"
        )
        self.playground.advance_work_request(
            workflow_autopkgtest,
            mark_running=True,
            result=WorkRequest.Results.FAILURE,
        )
        self.autopkgtest_libfoo_new_amd64 = self.add_result_autopkgtest(
            self.new_source, "amd64", parent=workflow_autopkgtest
        )
        self.autopkgtest_libfoo_new_i386 = self.add_result_autopkgtest(
            self.new_source, "i386", parent=workflow_autopkgtest
        )
        self.ra_autopkgtest_amd64 = RegressionAnalysis(
            original_source_version=None,
            original_url=None,
            new_source_version=self.new_source.data["version"],
            new_url=f"/{self.workspace}/artifact/10000/",
            status=RegressionAnalysisStatus.NO_RESULT,
            details={"build": "regression"},
            original_artifact_id=None,
            new_artifact_id=self.autopkgtest_libfoo_new_amd64.id,
        )
        self.ra_autopkgtest_i386 = RegressionAnalysis(
            original_source_version=self.reference_source.data["version"],
            original_url=f"/{self.workspace}/artifact/10001/",
            new_source_version=self.new_source.data["version"],
            new_url="f/{self.workspace}/artifact/10002/",
            status=RegressionAnalysisStatus.STABLE,
            details={"build": "stable"},
            original_artifact_id=self.autopkgtest_libfoo_ref_i386.id,
            new_artifact_id=self.autopkgtest_libfoo_new_i386.id,
        )
        workflow_autopkgtest.output_data = OutputData(
            regression_analysis={
                "amd64": self.ra_autopkgtest_amd64,
                "i386": self.ra_autopkgtest_i386,
            }
        )
        workflow_autopkgtest.save()

        self._add_regression_analysis(
            **{
                f"autopkgtest:{self.new_source.data['name']}:amd64": (
                    self.ra_autopkgtest_amd64
                ),
                f"autopkgtest:{self.new_source.data['name']}:i386": (
                    self.ra_autopkgtest_i386
                ),
            }
        )

    def simulate_rev_dep_autopkgtest(self) -> None:
        """Simulate autopkgtest of reverse dependency."""
        # Results from a previous run
        workflow_autopkgtest_foo_reference = (
            self.workflow_qa_reference.children.get(
                task_type=TaskTypes.WORKFLOW,
                task_name="reverse_dependencies_autopkgtest",
            ).children.get()
        )
        self.autopkgtest_foo_ref_amd64 = self.add_result_autopkgtest(
            self.dep_source, "amd64", parent=workflow_autopkgtest_foo_reference
        )
        self.autopkgtest_foo_ref_i386 = self.add_result_autopkgtest(
            self.dep_source,
            "i386",
            result=WorkRequest.Results.FAILURE,
            parent=workflow_autopkgtest_foo_reference,
            status=DebianAutopkgtestResultStatus.FAIL,
        )

        # New run
        workflow_autopkgtest_foo = self.workflow_qa.children.get(
            task_type=TaskTypes.WORKFLOW,
            task_name="reverse_dependencies_autopkgtest",
        ).children.get()
        self.playground.advance_work_request(
            workflow_autopkgtest_foo,
            mark_running=True,
            result=WorkRequest.Results.SUCCESS,
        )
        self.autopkgtest_foo_new_amd64 = self.add_result_autopkgtest(
            self.dep_source, "amd64", parent=workflow_autopkgtest_foo
        )
        self.autopkgtest_foo_new_i386 = self.add_result_autopkgtest(
            self.dep_source, "i386", parent=workflow_autopkgtest_foo
        )
        self.ra_autopkgtest_foo_amd64 = RegressionAnalysis(
            original_source_version=self.dep_source.data["version"],
            original_url=f"/{self.workspace}/artifact/10010/",
            new_source_version=self.dep_source.data["version"],
            new_url=f"/{self.workspace}/artifact/10011/",
            status=RegressionAnalysisStatus.STABLE,
            details={"build": "stable"},
            original_artifact_id=self.autopkgtest_foo_ref_amd64.id,
            new_artifact_id=self.autopkgtest_foo_new_amd64.id,
        )
        self.ra_autopkgtest_foo_i386 = RegressionAnalysis(
            original_source_version=self.dep_source.data["version"],
            original_url=f"/{self.workspace}/artifact/10012/",
            new_source_version=self.dep_source.data["version"],
            new_url="f/{self.workspace}/artifact/10013/",
            status=RegressionAnalysisStatus.IMPROVEMENT,
            details={"build": "improvement"},
            original_artifact_id=self.autopkgtest_foo_ref_i386.id,
            new_artifact_id=self.autopkgtest_foo_new_i386.id,
        )
        workflow_autopkgtest_foo.output_data = OutputData(
            regression_analysis={
                "amd64": self.ra_autopkgtest_foo_amd64,
                "i386": self.ra_autopkgtest_foo_i386,
            }
        )
        workflow_autopkgtest_foo.save()

        self._add_regression_analysis(
            **{
                f"autopkgtest:{self.dep_source.data['name']}:amd64": (
                    self.ra_autopkgtest_foo_amd64
                ),
                f"autopkgtest:{self.dep_source.data['name']}:i386": (
                    self.ra_autopkgtest_foo_i386
                ),
            }
        )

    def simulate_lintian(self) -> None:
        """Simulate lintian."""
        # Results from a previous run
        workflow_lintian_reference = self.workflow_qa_reference.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="lintian"
        )
        ref_amd64 = self.add_result_lintian(
            self.reference_source, "amd64", parent=workflow_lintian_reference
        )
        ref_i386 = self.add_result_lintian(
            self.reference_source,
            "i386",
            result=WorkRequest.Results.FAILURE,
            parent=workflow_lintian_reference,
        )

        # New run
        workflow_lintian = self.workflow_qa.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="lintian"
        )
        self.playground.advance_work_request(
            workflow_lintian,
            mark_running=True,
            result=WorkRequest.Results.FAILURE,
        )
        new_amd64 = self.add_result_lintian(
            self.new_source, "amd64", parent=workflow_lintian
        )
        new_i386 = self.add_result_lintian(
            self.new_source, "i386", parent=workflow_lintian
        )
        self.ra_lintian_amd64 = RegressionAnalysis(
            original_source_version=self.new_source.data["version"],
            original_url=f"/{self.workspace}/artifact/10022/",
            new_source_version=self.new_source.data["version"],
            new_url=f"/{self.workspace}/artifact/10023/",
            status=RegressionAnalysisStatus.STABLE,
            details={"added_tags": [], "removed_tags": []},
            original_artifact_id=ref_amd64.id,
            new_artifact_id=new_amd64.id,
        )
        self.ra_lintian_i386 = RegressionAnalysis(
            original_source_version=self.new_source.data["version"],
            original_url=f"/{self.workspace}/artifact/10024/",
            new_source_version=self.new_source.data["version"],
            new_url=f"/{self.workspace}/artifact/10025/",
            status=RegressionAnalysisStatus.IMPROVEMENT,
            details={"added_tags": [], "removed_tags": ["no-nmu-in-changelog"]},
            original_artifact_id=ref_i386.id,
            new_artifact_id=new_i386.id,
        )
        workflow_lintian.output_data = OutputData(
            regression_analysis={
                "amd64": self.ra_lintian_amd64,
                "i386": self.ra_lintian_i386,
            }
        )
        workflow_lintian.save()

        self._add_regression_analysis(
            **{
                f"lintian:{self.new_source.data['name']}:amd64": (
                    self.ra_lintian_amd64
                ),
                f"lintian:{self.new_source.data['name']}:i386": (
                    self.ra_lintian_i386
                ),
            }
        )

    def simulate_piuparts(self) -> None:
        """Simulate piuparts."""
        # Results from a previous run
        workflow_piuparts_reference = self.workflow_qa_reference.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="piuparts"
        )
        ref_amd64 = self.add_result_piuparts(
            self.reference_source, "amd64", parent=workflow_piuparts_reference
        )
        ref_i386 = self.add_result_piuparts(
            self.reference_source,
            "i386",
            result=WorkRequest.Results.FAILURE,
            parent=workflow_piuparts_reference,
        )

        # New run
        workflow_piuparts = self.workflow_qa.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="piuparts"
        )
        self.playground.advance_work_request(
            workflow_piuparts,
            mark_running=True,
            result=WorkRequest.Results.SUCCESS,
        )
        new_amd64 = self.add_result_piuparts(
            self.new_source, "amd64", parent=workflow_piuparts
        )
        new_i386 = self.add_result_piuparts(
            self.new_source, "i386", parent=workflow_piuparts
        )
        self.ra_piuparts_amd64 = RegressionAnalysis(
            original_source_version=self.new_source.data["version"],
            original_url=f"/{self.workspace}/artifact/10030/",
            new_source_version=self.new_source.data["version"],
            new_url=f"/{self.workspace}/artifact/10031/",
            status=RegressionAnalysisStatus.STABLE,
            original_artifact_id=ref_amd64.id,
            new_artifact_id=new_amd64.id,
        )
        self.ra_piuparts_i386 = RegressionAnalysis(
            original_source_version=self.new_source.data["version"],
            original_url=f"/{self.workspace}/artifact/10032/",
            new_source_version=self.new_source.data["version"],
            new_url=f"/{self.workspace}/artifact/10033/",
            status=RegressionAnalysisStatus.IMPROVEMENT,
            original_artifact_id=ref_i386.id,
            new_artifact_id=new_i386.id,
        )
        workflow_piuparts.output_data = OutputData(
            regression_analysis={
                "amd64": self.ra_piuparts_amd64,
                "i386": self.ra_piuparts_i386,
            }
        )
        workflow_piuparts.save()

        self._add_regression_analysis(
            **{
                f"piuparts:{self.new_source.data['name']}:amd64": (
                    self.ra_piuparts_amd64
                ),
                f"piuparts:{self.new_source.data['name']}:i386": (
                    self.ra_piuparts_i386
                ),
            }
        )

    def simulate_blhc(self) -> None:
        """Simulate blhc."""
        # Results from a previous run
        workflow_blhc_reference = self.workflow_qa_reference.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="blhc"
        )
        ref_amd64 = self.add_result_blhc(
            self.reference_source, "amd64", parent=workflow_blhc_reference
        )
        ref_i386 = self.add_result_blhc(
            self.reference_source, "i386", parent=workflow_blhc_reference
        )

        # New run
        workflow_blhc = self.workflow_qa.children.get(
            task_type=TaskTypes.WORKFLOW, task_name="blhc"
        )
        self.playground.advance_work_request(
            workflow_blhc,
            mark_running=True,
            result=WorkRequest.Results.FAILURE,
        )
        new_amd64 = self.add_result_blhc(
            self.reference_source, "amd64", parent=workflow_blhc
        )
        new_i386 = self.add_result_blhc(
            self.reference_source,
            "i386",
            parent=workflow_blhc,
            result=WorkRequest.Results.FAILURE,
        )
        self.ra_blhc_amd64 = RegressionAnalysis(
            original_source_version=self.new_source.data["version"],
            original_url=f"/{self.workspace}/artifact/10040/",
            new_source_version=self.new_source.data["version"],
            new_url=f"/{self.workspace}/artifact/10041/",
            status=RegressionAnalysisStatus.STABLE,
            original_artifact_id=ref_amd64.id,
            new_artifact_id=new_amd64.id,
        )
        self.ra_blhc_i386 = RegressionAnalysis(
            original_source_version=self.new_source.data["version"],
            original_url=f"/{self.workspace}/artifact/10042/",
            new_source_version=self.new_source.data["version"],
            new_url=f"/{self.workspace}/artifact/10043/",
            status=RegressionAnalysisStatus.REGRESSION,
            original_artifact_id=ref_i386.id,
            new_artifact_id=new_i386.id,
        )
        workflow_blhc.output_data = OutputData(
            regression_analysis={
                "amd64": self.ra_blhc_amd64,
                "i386": self.ra_blhc_i386,
            }
        )
        workflow_blhc.save()

        self._add_regression_analysis(
            **{
                f"blhc:{self.new_source.data['name']}:amd64": (
                    self.ra_blhc_amd64
                ),
                f"blhc:{self.new_source.data['name']}:i386": (
                    self.ra_blhc_i386
                ),
            }
        )

    @override
    def build(self, playground: "Playground") -> None:
        super().build(playground)

        # Create reference version of package under test.
        self.reference_source, self.reference_binaries = (
            self._create_sample_package(
                "libfoo", "1.0-1", binary_name="libfoo1"
            )
        )

        # Create a package that depends on the package under test.
        self.dep_source, self.dep_binaries = self._create_sample_package(
            "foo",
            "2.0-1",
            dsc_fields={"Testsuite": "autopkgtest"},
            deb_fields={"Depends": "libfoo1"},
        )

        # Create new version of package under test.
        self.new_source = playground.create_source_artifact(
            name="libfoo", binaries=["libfoo1"], version="1.0-2"
        )

        # Root workflow.
        self.workflow_debian_pipeline = playground.create_workflow(
            self.template_upload_to_sid,
            task_data={
                "source_artifact": self.new_source.pk,
                "architectures": ["all", "amd64", "i386"],
                "enable_regression_tracking": True,
            },
            workspace=self.workspace,
        )
        assert (
            self.workflow_debian_pipeline.status == WorkRequest.Statuses.PENDING
        )
        assert orchestrate_workflow(self.workflow_debian_pipeline)

        # QA workflows.
        self.workflow_qa_reference = self.workflow_debian_pipeline.children.get(
            task_type=TaskTypes.WORKFLOW,
            task_name="qa",
            task_data__update_qa_results=RegressionTrackingUpdateQAResults.YES,
        )
        self.workflow_qa = self.workflow_debian_pipeline.children.get(
            task_type=TaskTypes.WORKFLOW,
            task_name="qa",
            task_data__update_qa_results=RegressionTrackingUpdateQAResults.NO,
        )
        self.workflow_qa.output_data = OutputData(
            regression_analysis={
                "": RegressionAnalysis(
                    original_source_version=self.reference_source.data[
                        "version"
                    ],
                    new_source_version=self.new_source.data["version"],
                    status=RegressionAnalysisStatus.REGRESSION,
                    original_artifact_id=self.reference_source.id,
                    new_artifact_id=self.new_source.id,
                ),
            }
        )
        self.workflow_qa.save()

        # Simulate workflow results
        if self.simulate:
            self.simulate_sbuild()
            self.simulate_autopkgtest()
            self.simulate_rev_dep_autopkgtest()
            self.simulate_lintian()
            self.simulate_piuparts()

            # Sbuild has completed so blhc can be populated
            context.clear_lookup_cache()
            orchestrate_workflow(self.workflow_debian_pipeline)
            self.simulate_blhc()


class UIPlayground(PackageBuildLog, UIRegressionTesting):
    """
    Base scenario for UI tests.

    This gives a default password to the test user.
    """

    suite: Collection
    env_amd64: Artifact
    env_s390x: Artifact
    source_hello: Artifact
    source_dpkg: Artifact
    source_udev: Artifact
    template_sbuild: WorkflowTemplate
    template_noop: WorkflowTemplate
    worker_pool: WorkerPool
    worker_static: Worker
    worker_celery: Worker
    worker_in_pool: Worker
    worker_in_pool_busy: Worker

    @override
    def build(self, playground: "Playground") -> None:
        super().build(playground)
        self.set_current()

        # Set a password for the test user
        self.user.set_password("playground")
        self.user.save()

        asset = self.playground.create_signing_key_asset()
        self.playground.create_asset_usage(asset)

        # Pretend the user comes from Salsa
        Identity.objects.create(
            user=self.user,
            issuer="salsa",
            subject="playground@debian.example.org",
            claims={"test": True},
        )

        other_user = User.objects.create_user(
            username="playground-other", first_name="Other", last_name="User"
        )

        # Make them owners of the playground workspace...
        role_group = self.playground.create_group_role(
            self.workspace,
            Workspace.Roles.OWNER,
            users=[self.user],
            name="Admins",
        )
        # ...and admin of the owners group
        role_group.set_user_role(self.user, Group.Roles.ADMIN)

        # Create a second group with both users in it
        other_group = Group.objects.create(name="Playground", scope=self.scope)
        other_group.add_user(self.user)
        other_group.add_user(other_user)

        # Create an unscoped Admins group
        ws_group = Group.objects.create(name="Admins", scope=self.scope)
        ws_group.add_user(self.user)
        ws_group.set_user_role(self.user, Group.Roles.ADMIN)

        # Create a worker pool and some workers
        self.worker_pool = self.playground.create_worker_pool("playground")

        self.worker_static = self.playground.create_worker(
            fqdn="playground.lan"
        )
        self.worker_celery = self.playground.create_worker(
            worker_type=WorkerType.CELERY, fqdn="playground.lan"
        )
        self.worker_in_pool = self.playground.create_worker(
            fqdn="playground.lan", worker_pool=self.worker_pool
        )
        self.worker_in_pool_busy = self.playground.create_worker(
            fqdn="playground.lan", worker_pool=self.worker_pool
        )

        # Create a Debian scope, to test multi-scope UI elements
        self.playground.get_or_create_scope(
            "debian", label="Debian", icon="web/icons/debian-openlogo-nd.svg"
        )

        # Create a sbuild workflow template
        self.template_sbuild = self.playground.create_workflow_template(
            name="Build package", task_name="sbuild", static_parameters={}
        )

        # Create a debian:suite collection
        self.suite = self.playground.create_collection(
            workspace=self.workspace,
            name="play_bookworm",
            category=CollectionCategory.SUITE,
            data={
                "may_reuse_versions": False,
                "release_fields": {
                    "Suite": "stable",
                    "Codename": "bookworm",
                },
                "components": [
                    "main",
                    "contrib",
                    "non-free-firmware",
                    "non-free",
                ],
                "architectures": [
                    "all",
                    "amd64",
                    "arm64",
                    "armel",
                    "armhf",
                    "i386",
                    "mips64el",
                    "mipsel",
                    "ppc64el",
                    "s390x",
                ],
            },
        )

        # create another debian:suite collection
        trixie = self.playground.create_collection(
            workspace=self.workspace,
            name="play_trixie",
            category=CollectionCategory.SUITE,
            data={
                "may_reuse_versions": False,
                "release_fields": {
                    "Suite": "stable",
                    "Codename": "trixie",
                },
                "components": [
                    "main",
                    "contrib",
                    "non-free-firmware",
                    "non-free",
                ],
                "architectures": [
                    "all",
                    "amd64",
                    "arm64",
                    "armel",
                    "armhf",
                    "i386",
                    "mips64el",
                    "mipsel",
                    "ppc64el",
                    "s390x",
                ],
            },
        )

        # Add collection relations
        CollectionRelation.objects.create(
            source=trixie,
            target=self.suite,
            created_by_user=self.user,
            type=CollectionRelation.Types.SUITE_BASED_ON,
        )

        # Add collection relation requires to see the re-ordering
        # create another debian:suite collection
        personal_suite = self.playground.create_collection(
            workspace=self.workspace,
            name="r-personal",
            category=CollectionCategory.SUITE,
        )

        trixie_backports_suite = self.playground.create_collection(
            workspace=self.workspace,
            name="trixie-backports",
            category=CollectionCategory.SUITE,
        )
        trixie_security_suite = self.playground.create_collection(
            workspace=self.workspace,
            name="trixie-security",
            category=CollectionCategory.SUITE,
        )

        CollectionRelation.objects.create(
            source=personal_suite,
            target=trixie,
            created_by_user=self.user,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            position=1,
        )
        CollectionRelation.objects.create(
            source=personal_suite,
            target=trixie_backports_suite,
            created_by_user=self.user,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            position=2,
        )
        CollectionRelation.objects.create(
            source=personal_suite,
            target=trixie_security_suite,
            created_by_user=self.user,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            position=3,
        )

        # Create debian environments to simulate builds
        item = self.playground.create_debian_environment(architecture="amd64")
        assert item.artifact is not None
        self.env_amd64 = item.artifact
        item = self.playground.create_debian_environment(architecture="s390x")
        assert item.artifact is not None
        self.env_s390x = item.artifact

        # Create the source packages
        self.source_hello = self.playground.create_source_artifact(
            name="hello", version="1.0-1", create_files=True
        )
        self.source_dpkg = self.playground.create_source_artifact(
            name="dpkg",
            binaries=["dpkg", "dpkg-doc", "libdpkg-perl"],
            version="1.21.22",
            create_files=True,
        )
        self.source_udev = self.playground.create_source_artifact(
            name="udev", version="252.26-1~deb12u2", create_files=True
        )

        # Populate the debian:suite collection with artifacts
        wr = self.playground.simulate_package_build(
            self.source_hello, architecture="amd64", worker=self.worker_static
        )
        self.add_results_to_suite(wr)
        wr = self.playground.simulate_package_build(
            self.source_dpkg,
            binaries=["dpkg"],
            architecture="amd64",
            worker=self.worker_static,
        )
        self.add_results_to_suite(wr)
        wr = self.playground.simulate_package_build(
            self.source_dpkg,
            binaries=["dpkg"],
            architecture="armhf",
            worker=self.worker_static,
        )
        self.add_results_to_suite(wr)
        wr = self.playground.simulate_package_build(
            self.source_dpkg,
            binaries=["dpkg-doc", "libdpkg-perl"],
            architecture="all",
            build_architecture="amd64",
            worker=self.worker_static,
        )
        self.add_results_to_suite(wr)

        # Create a variety of work requests
        wr = self.playground.create_worker_task(
            task_name="noop", mark_pending=True
        )
        wr.task_data = {"result": WorkRequest.Results.FAILURE}
        wr.configured_task_data = {"result": WorkRequest.Results.SUCCESS}
        self.playground.advance_work_request(
            wr, result=WorkRequest.Results.SUCCESS
        )
        self.playground.create_worker_task(
            task_name="noop", result=WorkRequest.Results.FAILURE
        )
        self.playground.create_worker_task(
            task_name="noop", result=WorkRequest.Results.ERROR
        )
        self.playground.create_worker_task(task_name="noop", mark_aborted=True)

        wr = self.playground.create_worker_task(
            task_name="noop", worker=self.worker_in_pool_busy, mark_running=True
        )

        self.simulate_sbuild_workflow(self.template_sbuild, self.source_udev)

        # Create a binary DebDiff artifact and its required relation
        debdiff_report = textwrap.dedent(
            """
            some debdiff text header"

            Files in second .deb but not in first
            -------------------------------------
            -rw-r--r--  root/root   /etc/simplemonitor/monitor.ini
            -rw-r--r--  root/root   DEBIAN/conffiles

            Files in first .deb but not in second
            ------------------------------------
            -rw-r--r--  root/root   /usr/lib/python3/dist-packages/pyaarlo
            -rw-r--r--  root/root   /usr/share/doc/python3-pyaarlo/READ.gz

            Control files: lines which differ (wdiff format)
            ------------------------------------------------
            Description: [-one description-] {+another description+}
            Homepage: [-https://github.com/twrecked/pyaarlo-]
            {+https://simplemonitor.readthedocs.io+}"""
        ).encode("utf-8")

        artifact, _ = playground.create_artifact(
            paths={DebDiff.CAPTURE_OUTPUT_FILENAME: debdiff_report},
            category=ArtifactCategory.DEBDIFF,
            create_files=True,
            data={"original": "hello-1.deb", "new": "hello-2.deb"},
        )

        target, _ = playground.create_artifact(
            category=ArtifactCategory.UPLOAD,
            data=DebianUpload(
                type="dpkg",
                changes_fields={
                    "Architecture": "amd64",
                    "Files": [{"name": "hello_1.0_amd64.deb"}],
                },
            ),
        )

        ArtifactRelation.objects.create(
            artifact=artifact,
            target=target,
            type=ArtifactRelation.Relations.RELATES_TO,
        )

        # Create a source DebDiff artifact and its required relation
        debdiff_report = textwrap.dedent(
            """\
            diff -u -N original/added.txt new/added.txt
            --- original/added.txt	1970-01-01 01:00:00.000000000 +0100
            +++ new/added.txt	2025-03-26 12:42:27.672906377 +0000
            @@ -0,0 +1 @@
            +new file
            diff -u -N original/changed.txt new/changed.txt
            --- original/changed.txt	2025-03-26 12:41:44.191924503 +0000
            +++ new/changed.txt	2025-03-26 12:42:21.945798421 +0000
            @@ -1,3 +1,2 @@
             1
             2
            -3
            diff -u -N original/removed.txt new/removed.txt
            --- original/removed.txt	2025-03-26 12:42:37.607381416 +0000
            +++ new/removed.txt	1970-01-01 01:00:00.000000000 +0100
            @@ -1 +0,0 @@
            -removed file
        """
        ).encode("utf-8")

        artifact, _ = self.playground.create_artifact(
            paths={DebDiff.CAPTURE_OUTPUT_FILENAME: debdiff_report},
            category=ArtifactCategory.DEBDIFF,
            create_files=True,
            data={"original": "hello-1.dsc", "new": "hello-2.dsc"},
        )

        target, _ = playground.create_artifact(
            category=ArtifactCategory.SOURCE_PACKAGE,
            data=DebianSourcePackage(
                name="hello",
                version="1.0",
                type="dpkg",
                dsc_fields={},
            ),
        )
        ArtifactRelation.objects.create(
            artifact=artifact,
            target=target,
            type=ArtifactRelation.Relations.RELATES_TO,
        )

        # Create a variety of workflows

        self.template_noop = playground.create_workflow_template(
            name="Dummy",
            task_name="noop",
        )

        # 3 completed workflow
        for _ in range(3):
            self.playground.create_workflow(
                task_name=self.template_noop, result=WorkRequest.Results.SUCCESS
            )

        # 2 running workflows
        for _ in range(2):
            self.playground.create_workflow(
                task_name=self.template_noop, mark_running=True
            )

        # 1 needs input workflow
        workflow_needs_input = self.playground.create_workflow(
            task_name=self.template_noop, mark_running=True
        )
        workflow_needs_input.workflow_runtime_status = (
            WorkRequest.RuntimeStatuses.NEEDS_INPUT
        )
        workflow_needs_input.save()

        # Create and populate the default task-configuration collection
        task_config_collection = self.default_task_configuration_collection
        task_config_collection.manager.add_bare_data(
            BareDataCategory.TASK_CONFIGURATION,
            user=self.user,
            data={
                "template": "reduce-parallelism",
                "override_values": {"build_options": "parallel=2"},
                "comment": "reduce parallelism to avoid crashing workers",
            },
        )
        task_config_collection.manager.add_bare_data(
            BareDataCategory.TASK_CONFIGURATION,
            user=self.user,
            data={
                "task_type": "Worker",
                "task_name": "sbuild",
                "subject": "openjdk-17",
                "use_templates": ["reduce-parallelism"],
                "comment": "reduce parallelism for openjdk-17",
            },
        )
        task_config_collection.manager.add_bare_data(
            BareDataCategory.TASK_CONFIGURATION,
            user=self.user,
            data={
                "task_type": "Workflow",
                "task_name": "debian_pipeline",
                "subject": "u-boot",
                "default_values": {
                    "architectures_allowlist": ["arm64", "armhf"],
                },
                "comment": "only build on ARM by default",
            },
        )

    def add_results_to_suite(
        self,
        work_request: WorkRequest,
    ) -> None:
        """
        Add the work request artifacts to the debian:suite collection.

        :param suite: the target suite, or self.suite by default
        """
        suite = self.suite

        source = Artifact.objects.get(
            pk=work_request.task_data["input"]["source_artifact"]
        )

        if not suite.child_items.active().filter(artifact=source).exists():
            suite.manager.add_artifact(
                source,
                user=self.user,
                variables={"component": "main", "section": "devel"},
            )
        for binary in Artifact.objects.filter(
            created_by_work_request=work_request,
            category=ArtifactCategory.BINARY_PACKAGE,
        ):
            suite.manager.add_artifact(
                binary,
                user=self.user,
                variables={
                    "component": "main",
                    "section": "devel",
                    "priority": "optional",
                },
            )

    @context.disable_permission_checks()
    def simulate_sbuild_workflow(
        self,
        template: WorkflowTemplate,
        source: Artifact,
    ) -> WorkRequest:
        """Simulate a sbuild workflow."""
        workflow = self.playground.create_workflow(
            template,
            task_data=SbuildWorkflowData(
                input=SbuildInput(source_artifact=source.pk),
                backend=BackendType.UNSHARE,
                target_distribution="debian:bookworm",
                architectures=["all", "amd64", "s390x"],
            ),
            mark_running=True,
        )

        # A successful build
        self.playground.simulate_package_build(
            source,
            workflow=workflow,
            architecture="amd64",
            worker=self.worker_static,
        )

        # A failed build
        wr_s390x = self.playground.create_sbuild_work_request(
            source=source,
            architecture="s390x",
            environment=self.env_s390x,
            workflow=workflow,
        )
        self.playground.advance_work_request(
            wr_s390x, result=WorkRequest.Results.FAILURE
        )

        # Retrying succeeded
        new_wr_s390x = wr_s390x.retry()
        self.playground.advance_work_request(
            new_wr_s390x, result=WorkRequest.Results.SUCCESS
        )

        return workflow
