# 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 SbuildWorkflow class."""
from typing import Any, cast

from django.test import TestCase

from debusine.artifacts.models import ArtifactCategory
from debusine.db.models import WorkRequest, WorkflowTemplate, default_workspace
from debusine.server.collections import DebianEnvironmentsManager
from debusine.server.collections.tests.utils import CollectionTestMixin
from debusine.server.scheduler import schedule
from debusine.server.workflows import SbuildWorkflow, WorkflowValidationError
from debusine.tasks.models import BackendType, TaskTypes


class SbuildWorkflowTests(CollectionTestMixin, TestCase):
    """Unit tests for SbuildWorkflow class."""

    def create_sbuild_template(
        self, name: str = "sbuild", task_data: dict[str, Any] | None = None
    ):
        """Create a sbuild WorkflowTemplate."""
        return WorkflowTemplate.objects.create(
            name=name,
            workspace=default_workspace(),
            task_name="sbuild",
            task_data=task_data or {},
        )

    def test_create_orchestrator(self):
        """Test instantiating a SbuildWorkflow."""
        wr = WorkRequest(
            task_data={
                "input": {
                    "source_artifact": 42,
                },
                "target_distribution": "debian:bookworm",
                "architectures": ["all", "amd64"],
            },
            workspace=default_workspace(),
        )
        w = SbuildWorkflow(wr)
        self.assertEqual(w.data.input.source_artifact, 42)
        self.assertEqual(w.data.target_distribution, "debian:bookworm")
        self.assertEqual(w.data.backend, BackendType.UNSHARE)
        self.assertEqual(w.data.architectures, ["all", "amd64"])

    def test_validate_input(self):
        """Test that validate_input passes a valid case."""
        user = self.get_test_user()

        environments = self.create_debian_environments_collection()
        environments_manager = cast(
            DebianEnvironmentsManager, environments.manager
        )

        data = {
            "codename": "bookworm",
            "architecture": "amd64",
            "variant": "apt",
        }
        tarball, _ = self.create_artifact(
            category=ArtifactCategory.SYSTEM_TARBALL,
            data={**data, "with_dev": True},
        )

        tarball_item = environments_manager.add_artifact(
            tarball, user=user, variables={"backend": "unshare"}
        )

        source = self.create_source_package(
            "hello", "1.0", dsc_fields={"Architecture": "all"}
        )

        template = self.create_sbuild_template()
        wr = WorkRequest.objects.create_workflow(
            template=template,
            data={
                "input": {
                    "source_artifact": source.pk,
                },
                "target_distribution": "debian:bookworm",
                "architectures": ["all", "amd64"],
            },
            created_by=user,
        )
        self.assertEqual(wr.status, WorkRequest.Statuses.PENDING)

        o = SbuildWorkflow(wr)
        self.assertEqual(o.source_package, source)
        self.assertEqual(o.architectures, {"all"})
        self.assertEqual(o.environment_type, "tarball")
        self.assertEqual(o._get_environment("amd64"), tarball_item.artifact)
        o.validate_input()

        # environment_variant
        data = {
            "codename": "bookworm",
            "architecture": "amd64",
            "variant": "buildd",
        }
        tarball, _ = self.create_artifact(
            category=ArtifactCategory.SYSTEM_TARBALL,
            data={**data, "with_dev": True},
        )
        # add with explicit :variant
        tarball_item = environments_manager.add_artifact(
            tarball,
            user=user,
            variables={"variant": "buildd", "backend": "unshare"},
        )

        wr = WorkRequest.objects.create_workflow(
            template=template,
            data={
                "input": {
                    "source_artifact": source.pk,
                },
                "target_distribution": "debian:bookworm",
                "architectures": ["all", "amd64"],
                "environment_variant": "buildd",
            },
            created_by=user,
        )
        o = SbuildWorkflow(wr)
        self.assertEqual(o._get_environment("amd64"), tarball_item.artifact)
        o.validate_input()

    def test_computation_of_architecture_intersection(self) -> None:
        """Test computation of architectures to build."""
        user = self.get_test_user()

        template = self.create_sbuild_template()

        dsc_archs = {
            "any": {"amd64", "arm64"},
            "all": {
                "all",
            },
            "any all": {"all", "amd64", "arm64"},
            "arm64": {
                "arm64",
            },
            "linux-any": {"amd64", "arm64"},
            "gnu-any-any": {"amd64", "arm64"},
        }
        for dsc_arch, expected_archs in dsc_archs.items():
            source = self.create_source_package(
                "hello", "1.0", dsc_fields={"Architecture": dsc_arch}
            )

            for wr_archs in (["amd64"], ["all"], ["arm64", "all"]):
                with self.subTest("", dsc_arch=dsc_arch, wr_archs=wr_archs):
                    o = SbuildWorkflow(
                        WorkRequest.objects.create_workflow(
                            template=template,
                            data={
                                "input": {
                                    "source_artifact": source.pk,
                                },
                                "target_distribution": "debian:bookworm",
                                "architectures": wr_archs,
                            },
                            created_by=user,
                        ),
                    )
                    archs = expected_archs.intersection(wr_archs)
                    if archs:
                        self.assertSetEqual(o.architectures, archs)
                    else:
                        with self.assertRaisesRegex(
                            WorkflowValidationError,
                            "None of the workflow architectures are supported"
                            " by this package",
                        ):
                            o.architectures

    def test_create_work_request_schroot(self) -> None:
        """Test creating a Sbuild workflow workrequest with schroot."""
        # Create a source package
        source = self.create_source_package(
            "hello", "1.0", dsc_fields={"Architecture": "all amd64 s390x"}
        )

        template = self.create_sbuild_template()
        wr = WorkRequest.objects.create_workflow(
            template=template,
            data={
                "input": {
                    "source_artifact": source.pk,
                },
                "backend": "schroot",
                "target_distribution": "debian:bookworm",
                "architectures": ["all", "amd64", "s390x"],
            },
            created_by=self.get_test_user(),
        )
        self.assertEqual(wr.status, WorkRequest.Statuses.PENDING)
        self.assertEqual(schedule(), [wr])

        expected_children = [
            (
                "all",
                {"build_components": ["all"], "host_architecture": "amd64"},
            ),
            (
                "amd64",
                {"build_components": ["any"], "host_architecture": "amd64"},
            ),
            (
                "s390x",
                {"build_components": ["any"], "host_architecture": "s390x"},
            ),
        ]

        children = list(WorkRequest.objects.filter(parent=wr).order_by("id"))
        self.assertEqual(len(children), len(expected_children))

        for child, expected_child in zip(children, expected_children):
            expected_arch, expected_data = expected_child
            self.assertEqual(child.status, WorkRequest.Statuses.PENDING)
            self.assertEqual(child.task_type, TaskTypes.WORKER)
            self.assertEqual(child.task_name, "sbuild")
            self.assertEqual(
                child.task_data,
                {
                    'backend': 'schroot',
                    'distribution': "bookworm",
                    'environment': None,
                    'input': {
                        'extra_binary_artifacts': [],
                        'source_artifact': source.pk,
                    },
                    **expected_data,
                },
            )
            self.assertQuerysetEqual(child.dependencies.all(), [])
            self.assertEqual(
                child.workflow_data_json,
                {
                    "display_name": f"Build {expected_arch}",
                    "step": f"build-{expected_arch}",
                },
            )

        # populate() idempotence
        orchestrator = SbuildWorkflow(wr)
        orchestrator.populate()
        children = list(WorkRequest.objects.filter(parent=wr))
        self.assertEqual(len(children), len(expected_children))

    def test_create_work_request_unshare(self) -> None:
        """Test creating a Sbuild workflow workrequest with unshare."""
        environments = self.create_debian_environments_collection()
        tarballs = {
            arch: self.create_artifact(
                category=ArtifactCategory.SYSTEM_TARBALL,
                data={
                    "codename": "bookworm",
                    "architecture": arch,
                    "variant": "apt",
                    "with_dev": True,
                },
            )[0]
            for arch in ("amd64", "s390x")
        }
        for arch in ("amd64", "s390x"):
            environments.manager.add_artifact(
                tarballs[arch],
                user=self.get_test_user(),
                variables={"backend": "unshare"},
            )

        # Create a source package
        source = self.create_source_package(
            "hello", "1.0", dsc_fields={"Architecture": "all amd64 s390x"}
        )

        template = self.create_sbuild_template()
        wr = WorkRequest.objects.create_workflow(
            template=template,
            data={
                "input": {
                    "source_artifact": source.pk,
                },
                "backend": "unshare",
                "target_distribution": "debian:bookworm",
                "architectures": ["all", "amd64", "s390x"],
            },
            created_by=self.get_test_user(),
        )
        self.assertEqual(wr.status, WorkRequest.Statuses.PENDING)
        self.assertEqual(schedule(), [wr])

        environment = (
            "debian/match:format=tarball:codename=bookworm"
            ":architecture={}:backend=unshare"
        )

        expected_children = [
            (
                "all",
                {
                    "build_components": ["all"],
                    "environment": environment.format("amd64"),
                    "host_architecture": "amd64",
                },
            ),
            (
                "amd64",
                {
                    "build_components": ["any"],
                    "environment": environment.format("amd64"),
                    "host_architecture": "amd64",
                },
            ),
            (
                "s390x",
                {
                    "build_components": ["any"],
                    "environment": environment.format("s390x"),
                    "host_architecture": "s390x",
                },
            ),
        ]

        children = list(WorkRequest.objects.filter(parent=wr).order_by("id"))
        self.assertEqual(len(children), len(expected_children))

        for child, expected_child in zip(children, expected_children):
            expected_arch, expected_data = expected_child
            self.assertEqual(child.status, WorkRequest.Statuses.PENDING)
            self.assertEqual(child.task_type, TaskTypes.WORKER)
            self.assertEqual(child.task_name, "sbuild")
            self.assertEqual(
                child.task_data,
                {
                    'backend': 'unshare',
                    'distribution': None,
                    'input': {
                        'extra_binary_artifacts': [],
                        'source_artifact': source.pk,
                    },
                    **expected_data,
                },
            )
            self.assertQuerysetEqual(child.dependencies.all(), [])
            self.assertEqual(
                child.workflow_data_json,
                {
                    "display_name": f"Build {expected_arch}",
                    "step": f"build-{expected_arch}",
                },
            )

        # populate() idempotence
        orchestrator = SbuildWorkflow(wr)
        orchestrator.populate()
        children = list(WorkRequest.objects.filter(parent=wr))
        self.assertEqual(len(children), len(expected_children))

    def test_source_package_error_handling(self):
        """Test source_package error handling."""
        template = self.create_sbuild_template()

        def create_orchestrator(
            source,
            target_distribution="debian:bookworm",
            architectures=["amd64"],
            **kwargs,
        ):
            user = self.get_test_user()
            workflow = WorkRequest.objects.create_workflow(
                template=template,
                data={
                    "input": {
                        "source_artifact": source.pk,
                    },
                    "target_distribution": target_distribution,
                    "architectures": architectures,
                    **kwargs,
                },
                created_by=user,
            )
            orchestrator = SbuildWorkflow(workflow)
            return orchestrator

        invalid_artifact, _ = self.create_artifact(category="test")
        orchestrator = create_orchestrator(invalid_artifact)
        with self.assertRaisesRegex(
            ValueError, "source artifact is type 'test'"
        ):
            orchestrator.source_package

        source = self.create_source_package(
            "hello", "1.0", dsc_fields={"Architecture": ""}
        )
        orchestrator = create_orchestrator(source)
        with self.assertRaisesRegex(
            WorkflowValidationError, "package architecture list is empty"
        ):
            orchestrator.architectures

        source = self.create_source_package(
            "hello", "1.0", dsc_fields={"Architecture": "any-armhf"}
        )
        orchestrator = create_orchestrator(
            source, architectures=["kfreebsd-i386"]
        )
        with self.assertRaisesRegex(
            WorkflowValidationError,
            "None of the workflow architectures are supported by this package",
        ):
            orchestrator.architectures

        # wildcards support
        source = self.create_source_package(
            "hello", "1.0", dsc_fields={"Architecture": "any-amd64"}
        )
        orchestrator = create_orchestrator(
            source, architectures=["i386", "hurd-amd64"]
        )
        self.assertEqual(orchestrator.architectures, {"hurd-amd64"})

        # wildcards are for packages, not workflows
        source = self.create_source_package(
            "hello", "1.0", dsc_fields={"Architecture": "amd64"}
        )
        orchestrator = create_orchestrator(source, architectures=["any"])
        with self.assertRaisesRegex(
            WorkflowValidationError,
            "None of the workflow architectures are supported by this package",
        ):
            orchestrator.architectures

        orchestrator = create_orchestrator(source, backend="qemu")
        self.assertEqual(orchestrator.environment_type, "image")

        self.create_debian_environments_collection()
        with self.assertRaisesRegex(
            WorkflowValidationError, "environment not found for"
        ):
            orchestrator._get_environment("any")

        orchestrator = create_orchestrator(source, target_distribution="Ubuntu")
        with self.assertRaisesRegex(
            WorkflowValidationError,
            "target_distribution must be in vendor:codename format",
        ):
            orchestrator.validate_input()
