# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""Unit tests for UpdateEnvironmentsWorkflow."""

from typing import Any

import debusine.worker.tags as wtags
from debusine.artifacts.models import (
    ArtifactCategory,
    CollectionCategory,
    TaskTypes,
)
from debusine.client.models import RuntimeParameter
from debusine.db.models import (
    TaskDatabase,
    WorkRequest,
    WorkflowTemplate,
    default_workspace,
)
from debusine.server.collections.tests.utils import CollectionTestMixin
from debusine.server.workflows import UpdateEnvironmentsWorkflow
from debusine.server.workflows.base import orchestrate_workflow
from debusine.server.workflows.tests.helpers import WorkflowTestBase
from debusine.tasks.models import (
    ActionUpdateCollectionWithArtifacts,
    BackendType,
    BaseDynamicTaskData,
)


class UpdateEnvironmentsWorkflowTests(
    CollectionTestMixin, WorkflowTestBase[UpdateEnvironmentsWorkflow]
):
    """Unit tests for UpdateEnvironmentsWorkflow."""

    def create_update_environments_template(
        self,
        name: str = "update_environments",
        static_parameters: dict[str, Any] | None = None,
    ) -> WorkflowTemplate:
        """Create an update_environments WorkflowTemplate."""
        return WorkflowTemplate.objects.create(
            name=name,
            workspace=default_workspace(),
            task_name="update_environments",
            static_parameters=static_parameters or {},
            runtime_parameters=RuntimeParameter.ANY,
        )

    def add_target(
        self,
        codename: str = "bookworm",
        codenames: list[str] | None = None,
        aliases: list[str] | None = None,
        architectures: list[str] | None = None,
        backends: list[str] | None = None,
        variants: list[str | None] | None = None,
        mmdebstrap_template: dict[str, Any] | None = None,
        simplesystemimagebuild_template: dict[str, Any] | None = None,
    ) -> None:
        if not hasattr(self, "targets"):
            self.targets = []
        # Compute default values for parameters
        if codenames is None:
            codenames = [codename]
        if architectures is None:
            architectures = ["amd64"]
        if aliases is None:
            aliases = [f"{codenames[0]}-alias"]
        if (
            mmdebstrap_template is None
            and simplesystemimagebuild_template is None
        ):
            # Ensure we build something, defaults to tarball with mmdebstrap
            mmdebstrap_template = {}
        if mmdebstrap_template is not None:
            mmdebstrap_template.setdefault(
                "bootstrap_repositories",
                [{"mirror": "https://deb.debian.org/debian"}],
            )
        if simplesystemimagebuild_template is not None:
            simplesystemimagebuild_template.setdefault(
                "bootstrap_repositories",
                [{"mirror": "https://deb.debian.org/debian"}],
            )
            simplesystemimagebuild_template.setdefault(
                "disk_image",
                {
                    "format": "qcow2",
                    "partitions": [{"size": 2, "filesystem": "ext4"}],
                },
            )

        # Generate the target entry
        target: dict[str, Any] = {
            "codenames": codenames,
            "codename_aliases": {codenames[0]: aliases},
            "architectures": architectures,
            "mmdebstrap_template": mmdebstrap_template,
            "simplesystemimagebuild_template": simplesystemimagebuild_template,
        }
        if backends is not None:
            target["backends"] = backends
        if variants is not None:
            target["variants"] = variants

        self.targets.append(target)

    def orchestrate_workflow(self) -> WorkRequest:
        template = self.create_update_environments_template(
            static_parameters={"vendor": "debian", "targets": self.targets}
        )
        wr = self.playground.create_workflow(template, task_data={})
        self.assertEqual(wr.status, WorkRequest.Statuses.PENDING)
        self.assertTrue(orchestrate_workflow(wr))

        return wr

    def test_create_orchestrator(self) -> None:
        """Instantiate an UpdateEnvironmentsWorkflow."""
        self.add_target(mmdebstrap_template={})
        wr = self.orchestrate_workflow()
        w = self.get_workflow(wr)

        self.assertEqual(w.data.vendor, "debian")
        self.assertEqual(len(w.data.targets), 1)
        self.assertEqual(w.data.targets[0].codenames, ["bookworm"])
        self.assertEqual(w.data.targets[0].architectures, ["amd64"])
        assert w.data.targets[0].mmdebstrap_template is not None
        self.assertIsNotNone(
            w.data.targets[0].mmdebstrap_template.get("bootstrap_repositories")
        )

    def test_create_work_requests(self) -> None:
        """Create specified work requests."""
        self.add_target(mmdebstrap_template={}, aliases=[])
        self.add_target(
            codename="trixie",
            aliases=["sid"],
            architectures=["amd64", "arm64"],
            variants=[None, "autopkgtest", "sbuild"],
            backends=[
                BackendType.UNSHARE,
                BackendType.INCUS_LXC,
            ],
            simplesystemimagebuild_template={
                "bootstrap_options": {"variant": "minbase"}
            },
        )
        wr = self.orchestrate_workflow()

        children = list(wr.children.order_by("id"))
        self.assertEqual(len(children), 3)

        expected_bookworm_task_data = {
            "bootstrap_repositories": [
                {"mirror": "https://deb.debian.org/debian", "suite": "bookworm"}
            ],
        }
        expected_bookworm_reactions = [
            ActionUpdateCollectionWithArtifacts(
                artifact_filters={"category": ArtifactCategory.SYSTEM_TARBALL},
                collection="debian@debian:environments",
                variables={"codename": "bookworm"},
            )
        ]

        self.assertEqual(children[0].status, WorkRequest.Statuses.PENDING)
        self.assertEqual(children[0].task_type, TaskTypes.WORKER)
        self.assertEqual(children[0].task_name, "mmdebstrap")
        self.assertEqual(
            children[0].task_data,
            {
                "bootstrap_options": {"architecture": "amd64"},
                **expected_bookworm_task_data,
            },
        )
        self.assertQuerySetEqual(children[0].dependencies.all(), [])
        self.assertEqual(
            children[0].workflow_data_json,
            {
                "display_name": (
                    "Build tarball for bookworm / amd64 / no-backend "
                    "/ no-variant"
                ),
                "step": "mmdebstrap/bookworm/amd64/no-backend/no-variant",
            },
        )
        self.assert_work_request_event_reactions(
            children[0], on_success=expected_bookworm_reactions
        )

        expected_trixie_task_data = {
            "bootstrap_repositories": [
                {
                    "mirror": "https://deb.debian.org/debian",
                    "suite": "trixie",
                }
            ],
            "disk_image": {
                "format": "qcow2",
                "partitions": [{"size": 2, "filesystem": "ext4"}],
            },
        }
        expected_trixie_reactions = [
            ActionUpdateCollectionWithArtifacts(
                artifact_filters={"category": ArtifactCategory.SYSTEM_IMAGE},
                collection="debian@debian:environments",
                variables={
                    "codename": codename,
                    "backend": backend,
                    **({} if variant is None else {"variant": variant}),
                },
            )
            for codename, variant, backend in (
                ("trixie", None, BackendType.UNSHARE),
                ("trixie", None, BackendType.INCUS_LXC),
                ("trixie", "autopkgtest", BackendType.UNSHARE),
                ("trixie", "autopkgtest", BackendType.INCUS_LXC),
                ("trixie", "sbuild", BackendType.UNSHARE),
                ("trixie", "sbuild", BackendType.INCUS_LXC),
                ("sid", None, BackendType.UNSHARE),
                ("sid", None, BackendType.INCUS_LXC),
                ("sid", "autopkgtest", BackendType.UNSHARE),
                ("sid", "autopkgtest", BackendType.INCUS_LXC),
                ("sid", "sbuild", BackendType.UNSHARE),
                ("sid", "sbuild", BackendType.INCUS_LXC),
            )
        ]

        self.assertEqual(children[1].status, WorkRequest.Statuses.PENDING)
        self.assertEqual(children[1].task_type, TaskTypes.WORKER)
        self.assertEqual(children[1].task_name, "simplesystemimagebuild")
        self.assertEqual(
            children[1].task_data,
            {
                "bootstrap_options": {
                    "architecture": "amd64",
                    "variant": "minbase",
                },
                **expected_trixie_task_data,
            },
        )
        self.assertQuerySetEqual(children[1].dependencies.all(), [])
        self.assertEqual(
            children[1].workflow_data_json,
            {
                "display_name": (
                    "Build image for trixie / amd64 / unshare|"
                    "incus-lxc / no-variant|autopkgtest|sbuild"
                ),
                "step": (
                    "simplesystemimagebuild/trixie/amd64/"
                    "unshare|incus-lxc/no-variant|autopkgtest|sbuild"
                ),
            },
        )
        self.assert_work_request_event_reactions(
            children[1], on_success=expected_trixie_reactions
        )

        self.assertEqual(children[2].status, WorkRequest.Statuses.PENDING)
        self.assertEqual(children[2].task_type, TaskTypes.WORKER)
        self.assertEqual(children[2].task_name, "simplesystemimagebuild")
        self.assertEqual(
            children[2].task_data,
            {
                "bootstrap_options": {
                    "architecture": "arm64",
                    "variant": "minbase",
                },
                **expected_trixie_task_data,
            },
        )
        self.assertQuerySetEqual(children[2].dependencies.all(), [])
        self.assertEqual(
            children[2].workflow_data_json,
            {
                "display_name": (
                    "Build image for trixie / arm64 / "
                    "unshare|incus-lxc / no-variant|autopkgtest|sbuild"
                ),
                "step": (
                    "simplesystemimagebuild/trixie/arm64/"
                    "unshare|incus-lxc/no-variant|autopkgtest|sbuild"
                ),
            },
        )
        self.assert_work_request_event_reactions(
            children[2], on_success=expected_trixie_reactions
        )

        # populate() is idempotent.
        orchestrator = self.get_workflow(wr)
        orchestrator.populate()
        self.assertEqual(wr.children.count(), 3)

    def test_event_reaction(self) -> None:
        """The event reaction created by the workflow can be handled."""
        self.add_target()
        wr = self.orchestrate_workflow()

        child = WorkRequest.objects.get(parent=wr)
        child.status = WorkRequest.Statuses.PENDING
        collection = self.playground.create_collection(
            name="debian", category=CollectionCategory.ENVIRONMENTS
        )
        tarball, _ = self.playground.create_artifact(
            category=ArtifactCategory.SYSTEM_TARBALL,
            data={"architecture": "amd64"},
            work_request=child,
        )

        self.playground.advance_work_request(
            child, result=WorkRequest.Results.SUCCESS
        )

        item = collection.manager.lookup(
            "match:format=tarball:codename=bookworm:architecture=amd64"
        )
        assert item is not None
        self.assertEqual(item.artifact, tarball)

    def test_compute_system_required_tags(self) -> None:
        self.add_target()
        wr = self.orchestrate_workflow()
        workflow = self.get_workflow(wr)

        self.assertCountEqual(
            workflow.compute_system_required_tags(),
            [wtags.WORKER_TYPE_NOT_ASSIGNABLE],
        )

    def test_compute_dynamic_data(self) -> None:
        self.add_target(architectures=["amd64", "i386"])
        self.add_target(architectures=["arm64"])
        wr = self.orchestrate_workflow()
        w = self.get_workflow(wr)

        self.assertEqual(
            w.compute_dynamic_data(TaskDatabase(wr)),
            BaseDynamicTaskData(
                subject="debian",
                parameter_summary="debian (3 environments)",
            ),
        )

    def test_combination_logic_single_target(self) -> None:
        self.add_target(
            codenames=["bullseye", "bookworm", "trixie", "sid"],
            architectures=["amd64", "arm64", "armhf"],
            mmdebstrap_template={},
            simplesystemimagebuild_template={},
        )
        wr = self.orchestrate_workflow()
        # 4 codenames, 3 architectures, 2 tasks (tarball+image)
        self.assertEqual(wr.children.count(), 4 * 3 * 2)

    def test_combination_logic_multiple_targets(self) -> None:
        # Add two variations just on the backend
        self.add_target(backends=[BackendType.UNSHARE])
        self.add_target(backends=[BackendType.INCUS_LXC])
        # Add three variations just on the variant
        self.add_target(variants=[None, "lintian"])
        self.add_target(variants=["sbuild"])
        self.add_target(variants=["variant1", "variant2"])
        wr = self.orchestrate_workflow()
        # We should have 5 work requests
        self.assertEqual(wr.children.count(), 5)

    def test_combination_logic_duplicate_between_targets(self) -> None:
        # Each target represents 4 combinations
        self.add_target(
            codenames=["bookworm", "trixie"], architectures=["amd64", "i386"]
        )
        self.add_target(
            codenames=["trixie", "sid"], architectures=["i386", "s390x"]
        )
        wr = self.orchestrate_workflow()
        # But 1 of them (trixie/i386) is common so 7 unique environments
        self.assertEqual(wr.children.count(), 7)
