# Copyright 2024 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 make_signed_source workflow."""
from collections.abc import Sequence
from itertools import chain
from typing import Any

from debusine.artifacts.models import ArtifactCategory
from debusine.client.models import LookupChildType
from debusine.db.models import Artifact, WorkRequest
from debusine.server.workflows import MakeSignedSourceWorkflow
from debusine.server.workflows.base import Workflow, orchestrate_workflow
from debusine.server.workflows.models import BaseWorkflowData
from debusine.tasks.models import BaseDynamicTaskData, LookupMultiple, TaskTypes
from debusine.tasks.tests.helper_mixin import TestTaskMixin
from debusine.test.django import TestCase
from debusine.test.utils import preserve_task_registry


class MakeSignedSourceWorkflowTests(TestCase):
    """Unit tests for :py:class:`MakeSignedSourceWorkflow`."""

    def create_make_signed_source_workflow(
        self,
        *,
        extra_task_data: dict[str, Any],
    ) -> MakeSignedSourceWorkflow:
        """Create a make_signed_source workflow."""
        task_data = {
            "binary_artifacts": [
                "internal@collections/name:build-amd64",
                "internal@collections/name:build-i386",
            ],
            "signing_template_artifacts": [200, 201],
            "architectures": ["amd64", "i386"],
            "vendor": "debian",
            "codename": "bookworm",
            "purpose": "uefi",
            "key": 80,
        }
        task_data.update(extra_task_data)
        wr = self.playground.create_workflow(
            task_name="make_signed_source", task_data=task_data
        )
        return MakeSignedSourceWorkflow(wr)

    def create_signing_template_artifact(self, architecture: str) -> Artifact:
        """
        Create a signing template artifact.

        :param architecture: CollectionItem.data["architecture"]
        """
        artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.BINARY_PACKAGE,
            data={
                "srcpkg_name": "hello",
                "srcpkg_version": "1.0.0",
                "deb_fields": {
                    "Package": "hello",
                    "Version": "1.0.0",
                    "Architecture": architecture,
                },
                "deb_control_files": [],
            },
        )
        return artifact

    def orchestrate(
        self,
        task_data: dict[str, Any],
        architectures: Sequence[str],
        create_sbuild_promises: bool,
        extra_sbuild_architectures: Sequence[str] | None = None,
    ) -> WorkRequest:
        """
        Create and orchestrate a MakeSignedSourceWorkflow.

        :param task_data: data for the workflow
        :param architectures: for each architecture, depending on "mode" param,
          creates an sbuild child promising a binary SIGNING_INPUT and
          BINARY_PACKAGE promising artifacts or only the artifacts (see
          create_promises / create_artifacts)
        :param create_sbuild_promises: if True, create an `sbuild` children work
          request that provides, for each architecture an artifact named
          `build-{arch}` with data={"architecture": arch}
        """

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

            def populate(self) -> None:
                """Populate the pipeline."""
                if create_sbuild_promises:
                    sbuild = self.work_request.create_child(
                        task_type=TaskTypes.WORKFLOW, task_name="sbuild"
                    )
                    for arch in chain(
                        architectures, extra_sbuild_architectures or []
                    ):
                        child = sbuild.create_child(
                            task_name="sbuild",
                            task_data={"host_architecture": arch},
                        )

                        self.provides_artifact(
                            child,
                            ArtifactCategory.BINARY_PACKAGE,
                            f"build-{arch}",
                            data={"architecture": arch},
                        )
                        self.provides_artifact(
                            child,
                            ArtifactCategory.SIGNING_INPUT,
                            f"signing-{arch}",
                            data={"architecture": arch},
                        )

                make_signed_source = self.work_request.create_child(
                    task_type=TaskTypes.WORKFLOW,
                    task_name="make_signed_source",
                    task_data=task_data,
                )
                MakeSignedSourceWorkflow(make_signed_source).populate()

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

        root.mark_running()
        orchestrate_workflow(root)

        return root

    @preserve_task_registry()
    def test_populate(self) -> None:
        """Test populate."""
        # Expected architectures as per intersection of architectures
        # of signing_template_artifacts and binary_artifacts
        architectures = ("amd64", "i386")

        root = self.orchestrate(
            task_data={
                "architectures": architectures,
                "binary_artifacts": [
                    f"internal@collections/name:build-{arch}"
                    for arch in (*architectures, "arm64")
                ],
                "signing_template_artifacts": [
                    f"internal@collections/name:signing-{arch}"
                    for arch in architectures
                ],
                "vendor": "debian",
                "codename": "bookworm",
                "purpose": "uefi",
                "key": 80,
            },
            architectures=architectures,
            create_sbuild_promises=True,
            extra_sbuild_architectures=("arm64",),
        )

        make_signed_source = WorkRequest.objects.get(
            task_type=TaskTypes.WORKFLOW,
            task_name="make_signed_source",
            parent=root,
        )

        extract_for_signings = make_signed_source.children.filter(
            task_name="extractforsigning"
        )
        self.assertEqual(extract_for_signings.count(), len(architectures))

        signs = make_signed_source.children.filter(task_name="sign")
        self.assertEqual(signs.count(), len(architectures))

        assemble_sign_sources = make_signed_source.children.filter(
            task_name="assemblesignedsource"
        )
        self.assertEqual(assemble_sign_sources.count(), len(architectures))

        sbuild_workflows = make_signed_source.children.filter(
            task_name="sbuildworkflow"
        )
        self.assertEqual(sbuild_workflows.count(), len(architectures))

        for arch in architectures:
            collection_id = root.internal_collection_id
            extract_for_signing = extract_for_signings.get(
                task_data__input__template_artifact=(
                    f"{collection_id}@collections/name:signing-{arch}"
                )
            )
            self.assertEqual(
                extract_for_signing.task_data,
                {
                    "environment": "debian/match:codename=bookworm",
                    "input": {
                        "binary_artifacts": [
                            f"{collection_id}@collections/name:build-{arch}"
                        ],
                        "template_artifact": (
                            f"{collection_id}@"
                            f"collections/name:signing-{arch}"
                        ),
                    },
                },
            )
            self.assertEqual(
                extract_for_signing.workflow_data_json,
                {
                    "display_name": f"Extract for signing {arch}",
                    "step": f"extract-for-signing-{arch}",
                },
            )
            self.assertEqual(
                extract_for_signing.event_reactions_json,
                {
                    "on_creation": [],
                    "on_failure": [],
                    "on_success": [
                        {
                            "action": "update-collection-with-artifacts",
                            "artifact_filters": {
                                "category": "debusine:signing-input"
                            },
                            "collection": "internal@collections",
                            "name_template": (
                                "extracted-for-signing-{architecture}-"
                                "{binary_package_name}"
                            ),
                            "variables": {
                                "$binary_package_name": "binary_package_name",
                                "architecture": arch,
                            },
                        }
                    ],
                    "on_unblock": [],
                },
            )

            unsigned = LookupMultiple.parse_obj(
                {
                    "collection": "internal@collections",
                    "child_type": LookupChildType.ARTIFACT,
                    "category": ArtifactCategory.SIGNING_INPUT,
                    "name__startswith": "extracted-for-signing-",
                    "data__architecture": arch,
                }
            ).dict(exclude_unset=True)["__root__"]
            sign = signs.get(task_data__unsigned=unsigned)
            self.assertEqual(
                {
                    key: value
                    for key, value in sign.task_data.items()
                    if key != "unsigned"
                },
                {"key": 80, "purpose": "uefi"},
            )
            self.assertEqual(
                sign.workflow_data_json,
                {"display_name": f"Sign {arch}", "step": f"sign-{arch}"},
            )

            self.assertEqual(
                sign.event_reactions_json,
                {
                    "on_creation": [],
                    "on_failure": [],
                    "on_success": [
                        {
                            "action": "update-collection-with-artifacts",
                            "artifact_filters": {
                                "category": "debusine:signing-output"
                            },
                            "collection": "internal@collections",
                            "name_template": f"signed-{arch}",
                            "variables": None,
                        }
                    ],
                    "on_unblock": [],
                },
            )

            assemble_sign_source = assemble_sign_sources.get(
                task_data__signed=[f"internal@collections/name:signed-{arch}"]
            )

            self.assertEqual(
                assemble_sign_source.task_data,
                {
                    "environment": "debian/match:codename=bookworm",
                    "signed": [f"internal@collections/name:signed-{arch}"],
                    "template": (
                        f"{collection_id}@collections/name:signing-{arch}"
                    ),
                },
            )
            self.assertEqual(
                assemble_sign_source.workflow_data_json,
                {
                    "display_name": f"Assemble signed source {arch}",
                    "step": f"assemble-signed-source-{arch}",
                },
            )
            self.assertEqual(
                assemble_sign_source.event_reactions_json,
                {
                    "on_creation": [],
                    "on_failure": [],
                    "on_success": [
                        {
                            "action": "update-collection-with-artifacts",
                            "artifact_filters": {
                                "category": "debian:source-package"
                            },
                            "collection": "internal@collections",
                            "name_template": f"signed-source-{arch}",
                            "variables": None,
                        }
                    ],
                    "on_unblock": [],
                },
            )

            sbuild_workflow = sbuild_workflows.get(
                task_data__input__source_artifact=(
                    f"internal@collections/name:signed-source-{arch}"
                )
            )

            self.assertEqual(
                sbuild_workflow.task_data,
                {
                    "prefix": "signed-source/",
                    "architectures": ["all", "amd64", "i386"],
                    "backend": "auto",
                    "input": {
                        "source_artifact": (
                            f"internal@collections/name:signed-source-{arch}"
                        )
                    },
                    "target_distribution": "debian:bookworm",
                },
            )
            self.assertEqual(
                sbuild_workflow.workflow_data_json,
                {"display_name": f"Sbuild {arch}", "step": f"sbuild-{arch}"},
            )
            self.assertEqual(sbuild_workflow.event_reactions_json, {})

    @preserve_task_registry()
    def test_populate_with_artifacts(self) -> None:
        """
        Test populate: only children for specified architectures are generated.

        task_data["architectures"] specifies only one architecture.
        """
        architectures = ("amd64",)

        binary_artifact = (
            self.playground.create_minimal_binary_packages_artifact(
                "hello", "1.0.0", "1.0.0", "amd64"
            )
        )
        signing_artifact = self.create_signing_template_artifact("amd64")

        self.playground.create_minimal_binary_packages_artifact(
            "hello2", "1.0.0", "1.0.0", "i386"
        )
        self.create_signing_template_artifact("i386")

        root = self.orchestrate(
            task_data={
                "binary_artifacts": [f"{binary_artifact.id}@artifacts"],
                "signing_template_artifacts": [
                    f"{signing_artifact.id}@artifacts"
                ],
                "vendor": "debian",
                "codename": "bookworm",
                "purpose": "uefi",
                "key": 80,
                "architectures": architectures,
            },
            architectures=architectures,
            create_sbuild_promises=False,
        )

        make_signed_source = WorkRequest.objects.get(
            task_type=TaskTypes.WORKFLOW,
            task_name="make_signed_source",
            parent=root,
        )

        extract_for_signings = make_signed_source.children.filter(
            task_name="extractforsigning"
        )
        self.assertEqual(extract_for_signings.count(), len(architectures))

    def test_get_label(self) -> None:
        """Test get_label()."""
        w = self.create_make_signed_source_workflow(extra_task_data={})
        self.assertEqual(w.get_label(), "run sign source")
