# 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 debian pipeline workflow."""
from typing import Any
from unittest.mock import call, patch

from debusine.artifacts.models import (
    ArtifactCategory,
    CollectionCategory,
    KeyPurpose,
)
from debusine.db.models import WorkRequest, WorkflowTemplate
from debusine.server.workflows import DebianPipelineWorkflow
from debusine.server.workflows.base import (
    WorkflowRunError,
    orchestrate_workflow,
)
from debusine.tasks.models import BackendType, LookupMultiple, TaskTypes
from debusine.test.django import TestCase


class DebianPipelineWorkflowTests(TestCase):
    """Unit tests for :py:class:`DebianPipelineWorkflow`."""

    def create_debian_pipeline_workflow(
        self, *, extra_task_data: dict[str, Any]
    ) -> DebianPipelineWorkflow:
        """Create a debian pipeline workflow."""
        task_data = {
            "source_artifact": 10,
            "vendor": "debian",
            "codename": "bookworm",
        }
        task_data.update(extra_task_data)
        wr = self.playground.create_workflow(
            task_name="debian_pipeline", task_data=task_data
        )
        return DebianPipelineWorkflow(wr)

    def orchestrate(self, extra_data: dict[str, Any]) -> WorkRequest:
        """Create and orchestrate a DebianPipelineWorkflow."""
        template = WorkflowTemplate(
            name="debianpipeline",
            workspace=self.playground.get_default_workspace(),
            task_name="debian_pipeline",
        )

        data = {
            "source_artifact": 10,
            "vendor": "debian",
            "codename": "bookworm",
            "enable_autopkgtest": False,
            "enable_lintian": False,
            "enable_piuparts": False,
        }

        data.update(extra_data)

        wr = WorkRequest.objects.create_workflow(
            template=template,
            data=data,
            created_by=self.playground.get_default_user(),
        )
        self.assertEqual(wr.status, WorkRequest.Statuses.PENDING)
        wr.mark_running()
        orchestrate_workflow(wr)

        return wr

    def test_populate_use_available_architectures(self):
        """
        Test populate use available architectures.

        The user didn't specify "architectures", DebianPipelineWorkflow
        checks available architectures and "all" and use them.
        """
        source_artifact = self.playground.create_source_artifact(name="hello")

        collection = self.playground.create_collection(
            "debian",
            CollectionCategory.ENVIRONMENTS,
            workspace=self.playground.get_default_workspace(),
        )
        for arch in ["amd64", "i386"]:
            artifact, _ = self.create_artifact(
                category=ArtifactCategory.SYSTEM_TARBALL,
                data={"codename": "bookworm", "architecture": arch},
            )
            collection.manager.add_artifact(
                artifact, user=self.playground.get_default_user()
            )

        workflow = self.orchestrate(
            extra_data={
                "source_artifact": f"{source_artifact.id}@artifacts",
                "vendor": "debian",
                "codename": "bookworm",
            }
        )

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

        self.assertEqual(
            sbuild.task_data["architectures"], ["all", "amd64", "i386"]
        )

    def test_populate_sbuild(self):
        """Test populate create sbuild."""
        source_artifact = self.playground.create_source_artifact(name="hello")

        workflow = self.orchestrate(
            extra_data={
                "architectures": ["amd64"],
                "source_artifact": f"{source_artifact.id}@artifacts",
            }
        )

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

        self.assertEqual(
            sbuild.task_data,
            {
                "architectures": ["amd64"],
                "backend": BackendType.AUTO,
                "environment_variant": None,
                "input": {"source_artifact": f"{source_artifact.id}@artifacts"},
                "target_distribution": "debian:bookworm",
                "signing_template_names": {},
            },
        )
        self.assertEqual(
            sbuild.workflow_data_json,
            {"display_name": "sbuild", "step": "sbuild"},
        )

        # SbuildWorkflow.populate() was called and created its tasks
        self.assertTrue(sbuild.children.exists())

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

    def assert_qa(
        self, workflow: WorkRequest, task_data: dict[str, Any]
    ) -> None:
        """Assert workflow has a sub-workflow qa with task_data."""
        qa = workflow.children.get(task_name="qa", task_type=TaskTypes.WORKFLOW)
        self.assertEqual(qa.task_data, task_data)

        self.assertEqual(
            qa.workflow_data_json, {"display_name": "QA", "step": "qa"}
        )

        self.assertQuerySetEqual(
            qa.dependencies.all(),
            list(
                WorkRequest.objects.filter(
                    task_type=TaskTypes.WORKER, task_name="sbuild"
                )
            ),
        )

        # QaWorkflow.populate() was called and created its tasks
        self.assertTrue(qa.children.exists())

    def test_populate_qa_lintian(self):
        """Test populate create qa: lintian enabled."""
        source_artifact = self.playground.create_source_artifact(name="hello")

        workflow = self.orchestrate(
            extra_data={
                "architectures": ["amd64"],
                "source_artifact": f"{source_artifact.id}@artifacts",
                "enable_lintian": True,
            }
        )
        self.assert_qa(
            workflow,
            {
                "arch_all_host_architecture": "amd64",
                "architectures": ["amd64"],
                "autopkgtest_backend": BackendType.AUTO,
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "codename": "bookworm",
                "enable_autopkgtest": False,
                "enable_lintian": True,
                "enable_piuparts": False,
                "lintian_backend": BackendType.AUTO,
                "lintian_fail_on_severity": "none",
                "piuparts_backend": BackendType.AUTO,
                "source_artifact": f"{source_artifact.id}@artifacts",
                "vendor": "debian",
            },
        )

    def test_populate_qa_autopkgtest(self):
        """Test populate create qa: autopkgtest enabled."""
        source_artifact = self.playground.create_source_artifact(name="hello")

        workflow = self.orchestrate(
            extra_data={
                "architectures": ["amd64"],
                "source_artifact": f"{source_artifact.id}@artifacts",
                "enable_autopkgtest": True,
            }
        )

        self.assert_qa(
            workflow,
            {
                "arch_all_host_architecture": "amd64",
                "architectures": ["amd64"],
                "autopkgtest_backend": BackendType.AUTO,
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "codename": "bookworm",
                "enable_autopkgtest": True,
                "enable_lintian": False,
                "enable_piuparts": False,
                "lintian_backend": BackendType.AUTO,
                "lintian_fail_on_severity": "none",
                "piuparts_backend": BackendType.AUTO,
                "source_artifact": f"{source_artifact.id}@artifacts",
                "vendor": "debian",
            },
        )

    def test_populate_qa_piuparts(self):
        """Test populate create qa: piuparts enabled."""
        source_artifact = self.playground.create_source_artifact(name="hello")

        workflow = self.orchestrate(
            extra_data={
                "architectures": ["amd64"],
                "source_artifact": f"{source_artifact.id}@artifacts",
                "enable_piuparts": True,
            }
        )

        self.assert_qa(
            workflow,
            {
                "arch_all_host_architecture": "amd64",
                "architectures": ["amd64"],
                "autopkgtest_backend": BackendType.AUTO,
                "binary_artifacts": ["internal@collections/name:build-amd64"],
                "codename": "bookworm",
                "enable_autopkgtest": False,
                "enable_lintian": False,
                "enable_piuparts": True,
                "lintian_backend": BackendType.AUTO,
                "lintian_fail_on_severity": "none",
                "piuparts_backend": BackendType.AUTO,
                "source_artifact": f"{source_artifact.id}@artifacts",
                "vendor": "debian",
            },
        )

    def test_populate_architectures_allow_deny(self):
        """Populate uses architectures, architectures_allow/deny."""
        source_artifact = self.playground.create_source_artifact(name="hello")

        workflow = self.orchestrate(
            extra_data={
                "source_artifact": f"{source_artifact.id}@artifacts",
                "architectures": ["amd64", "i386", "arm64"],
                "architectures_allowlist": ["amd64", "i386"],
                "architectures_denylist": ["amd64"],
            }
        )

        sbuild = workflow.children.get(
            task_name="sbuild", task_type=TaskTypes.WORKFLOW
        )
        self.assertEqual(
            sbuild.task_data,
            {
                "architectures": ["i386"],
                "backend": BackendType.AUTO,
                "environment_variant": None,
                "input": {"source_artifact": f"{source_artifact.id}@artifacts"},
                "target_distribution": "debian:bookworm",
                "signing_template_names": {},
            },
        )

    def test_populate_make_signed_source_missing_signed_source_purpose(self):
        """Populate raise WorkflowRunError: missing required field."""
        source_artifact = self.playground.create_source_artifact(name="hello")

        msg = (
            'orchestrator failed: "make_signed_source_purpose" must be '
            'set when signing the source'
        )
        with self.assertRaisesRegex(WorkflowRunError, msg):
            self.orchestrate(
                extra_data={
                    "source_artifact": f"{source_artifact.id}@artifacts",
                    "architectures": ["amd64"],
                    "enable_make_signed_source": True,
                    "signing_template_names": {"amd64": ["hello"]},
                }
            )

    def test_populate_make_signed_source_missing_make_signed_source_key(self):
        """Populate raise WorkflowRunError: missing required field."""
        source_artifact = self.playground.create_source_artifact(name="hello")

        msg = (
            'orchestrator failed: "make_signed_source_key" must be set '
            'when signing the source'
        )
        with self.assertRaisesRegex(WorkflowRunError, msg):
            self.orchestrate(
                extra_data={
                    "source_artifact": f"{source_artifact.id}@artifacts",
                    "architectures": ["amd64"],
                    "enable_make_signed_source": True,
                    "make_signed_source_purpose": KeyPurpose.OPENPGP,
                    "signing_template_names": {"amd64": ["hello"]},
                }
            )

    def test_populate_make_signed_source(self):
        """Populate create a make_signed_source workflow."""
        source_artifact = self.playground.create_source_artifact(name="hello")

        with patch.object(
            DebianPipelineWorkflow,
            "requires_artifact",
            wraps=DebianPipelineWorkflow.requires_artifact,
        ) as requires_artifact_wrapper:
            workflow = self.orchestrate(
                extra_data={
                    "architectures": ["amd64"],
                    "source_artifact": f"{source_artifact.id}@artifacts",
                    "enable_make_signed_source": True,
                    "make_signed_source_purpose": KeyPurpose.OPENPGP,
                    "signing_template_names": {
                        "amd64": ["hello"],
                    },
                    "make_signed_source_key": 80,
                }
            )

        make_signed_source = workflow.children.get(
            task_name="make_signed_source", task_type=TaskTypes.WORKFLOW
        )
        self.assertEqual(
            make_signed_source.task_data,
            {
                "architectures": ["amd64"],
                "binary_artifacts": [
                    "internal@collections/name:build-amd64",
                ],
                "codename": "bookworm",
                "key": 80,
                "purpose": "openpgp",
                "sbuild_backend": BackendType.AUTO,
                "signing_template_artifacts": [
                    "internal@collections/name:signing-template-amd64-hello"
                ],
                "vendor": "debian",
            },
        )

        self.assertEqual(
            make_signed_source.workflow_data_json,
            {
                "display_name": "make signed source",
                "step": "make_signed_source",
            },
        )

        # There are two calls to self.requires_artifact:
        # self.requires_artifact(wr, binary_artifacts)
        # self.requires_artifact(wr, LookupMultiple.parse_obj...)
        # But both of them are adding the same WorkRequest to depend on:
        # the sbuild work request (which generates the binary artifacts and the
        # signing artifacts
        self.assertQuerySetEqual(
            make_signed_source.dependencies.all(),
            list(
                WorkRequest.objects.filter(
                    task_type=TaskTypes.WORKER, task_name="sbuild"
                )
            ),
        )

        # Assert that the correct calls were made
        requires_artifact_wrapper.assert_has_calls(
            [
                call(
                    make_signed_source,
                    LookupMultiple.parse_obj(
                        ["internal@collections/name:build-amd64"]
                    ),
                ),
                call(
                    make_signed_source,
                    LookupMultiple.parse_obj(
                        [
                            "internal@collections/"
                            "name:signing-template-amd64-hello"
                        ]
                    ),
                ),
            ]
        )

        # MakeSignedSource.populate() was called and created its tasks
        self.assertTrue(make_signed_source.children.exists())

    def test_populate_package_upload(self):
        """Populate create an upload_package workflow."""
        source_artifact = self.playground.create_source_artifact(name="hello")

        workflow = self.orchestrate(
            extra_data={
                "source_artifact": f"{source_artifact.id}@artifacts",
                "architectures": ["amd64", "i386"],
                "enable_make_signed_source": True,
                "make_signed_source_purpose": KeyPurpose.OPENPGP,
                "signing_template_names": {"amd64": ["hello"]},
                "make_signed_source_key": 80,
                "enable_upload": True,
                "vendor": "debian",
                "codename": "trixie",
                "upload_merge_uploads": False,
                "upload_since_version": "1.1",
                "upload_target": "ftp://user@example.org/pub/UploadQueue1/",
                "upload_target_distribution": "debian:testing",
            }
        )

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

        self.assertEqual(
            package_upload.task_data,
            {
                "binary_artifacts": [
                    "internal@collections/name:build-amd64",
                    "internal@collections/name:build-i386",
                ],
                "codename": "trixie",
                "key": None,
                "merge_uploads": False,
                "require_signature": True,
                "since_version": "1.1",
                "source_artifact": f"{source_artifact.id}@artifacts",
                "target": "ftp://user@example.org/pub/UploadQueue1/",
                "target_distribution": "debian:testing",
                "vendor": "debian",
            },
        )

        self.assertEqual(
            package_upload.workflow_data_json,
            {"display_name": "package upload", "step": "package_upload"},
        )

        self.assertQuerySetEqual(
            package_upload.dependencies.all(),
            list(
                WorkRequest.objects.filter(
                    task_type=TaskTypes.WORKER, task_name="sbuild"
                )
            ),
        )

        # PackageUpload.populate() was called and created its tasks
        self.assertTrue(package_upload.children.exists())

    def test_populate_upload_package_no_include_source_binaries(self):
        """Populate create upload_workflow without source neither binaries."""
        source_artifact = self.playground.create_source_artifact(name="hello")

        workflow = self.orchestrate(
            extra_data={
                "architectures": ["amd64"],
                "source_artifact": f"{source_artifact.id}@artifacts",
                "enable_make_signed_source": True,
                "make_signed_source_purpose": KeyPurpose.OPENPGP,
                "signing_template_names": {"amd64": ["hello"]},
                "make_signed_source_key": 80,
                "enable_upload": True,
                "vendor": "debian",
                "codename": "trixie",
                "upload_merge_uploads": False,
                "upload_since_version": "1.1",
                "upload_target": "ftp://user@example.org/pub/UploadQueue1/",
                "upload_include_source": False,
                "upload_include_binaries": False,
            }
        )

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

        self.assertEqual(
            package_upload.task_data,
            {
                "binary_artifacts": [],
                "codename": "trixie",
                "key": None,
                "merge_uploads": False,
                "require_signature": True,
                "since_version": "1.1",
                "source_artifact": None,
                "target": "ftp://user@example.org/pub/UploadQueue1/",
                "target_distribution": None,
                "vendor": "debian",
            },
        )

        self.assertEqual(
            package_upload.workflow_data_json,
            {"display_name": "package upload", "step": "package_upload"},
        )

    def test_get_label(self):
        """Test get_label."""
        w = self.create_debian_pipeline_workflow(extra_task_data={})
        self.assertEqual(w.get_label(), "run Debian pipeline")
