#!/usr/bin/env python3

# 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.

"""Integration tests for Debusine's debian_pipeline workflow."""

import json
import socket
import subprocess
import unittest
from textwrap import dedent
from typing import override

import yaml

from debusine.artifacts.models import (
    ArtifactCategory,
    CollectionCategory,
    TaskTypes,
)
from debusine.tasks.models import BackendType
from utils.client import Client
from utils.common import Configuration
from utils.integration_test_helpers_mixin import IntegrationTestHelpersMixin
from utils.server import DebusineServer


class IntegrationWorkflowDebianPipelineTests(
    IntegrationTestHelpersMixin, unittest.TestCase
):
    """
    Integration test for the ``debian_pipeline`` workflow.

    These tests assume:
    - debusine-server is running
    - debusine-worker is running (connected to the server)
    - debusine-client is correctly configured
    """

    @override
    def setUp(self) -> None:
        super().setUp()
        # If debusine-server or nginx was launched just before this, the
        # server might not be available yet.
        self.assertTrue(
            DebusineServer.wait_for_server_ready(),
            f"debusine-server should be available (in "
            f"{Configuration.get_base_url()}) before the integration tests "
            f"are run",
        )

        self.architecture = subprocess.check_output(
            ["dpkg", "--print-architecture"], text=True
        ).strip()

    def add_artifact_to_suite(
        self, suite_name: str, artifact_id: int, **variables: str
    ) -> None:
        """Add a source or binary package artifact to a suite."""
        # TODO: We don't have a sensible command to do this yet.
        result = DebusineServer.execute_command(
            "shell",
            "-v",
            "0",
            "-c",
            dedent(
                f"""\
                from debusine.artifacts.models import *
                from debusine.db.models import *

                suite = Collection.objects.get(
                    category=CollectionCategory.SUITE, name={suite_name!r}
                )
                artifact = Artifact.objects.get(id={artifact_id})
                suite.manager.add_artifact(
                    artifact, user=system_user(), variables={variables!r}
                )
                """
            ),
        )
        self.assertEqual(result.returncode, 0)

    def find_workflow_children(
        self, workflow_id: int, **filters: str
    ) -> list[int]:
        """Find children of a given workflow, filtered by various criteria."""
        # TODO: We don't have a sensible command to do this yet.
        filter_expr = ", ".join(
            [f"{name}={value!r}" for name, value in filters.items()]
        )
        result = DebusineServer.execute_command(
            "shell",
            "-v",
            "0",
            "-c",
            dedent(
                f"""\
                import json

                from debusine.db.models import *

                workflow = WorkRequest.objects.get(id={workflow_id})
                children = workflow.children.filter({filter_expr})
                print(json.dumps([wr.id for wr in children]))
                """
            ),
        )
        self.assertEqual(result.returncode, 0)
        parsed_result = json.loads(result.stdout)
        assert isinstance(parsed_result, list)
        return parsed_result

    def verify_sbuild_artifacts(self, workflow_id: int) -> None:
        """Verify ``sbuild`` artifacts produced by a workflow."""
        [sbuild_workflow_id] = self.find_workflow_children(
            workflow_id, task_type=TaskTypes.WORKFLOW.value, task_name="sbuild"
        )
        [sbuild_task_id] = self.find_workflow_children(
            sbuild_workflow_id,
            task_type=TaskTypes.WORKER.value,
            task_name="sbuild",
        )
        sbuild_task = Client.execute_command(
            "work-request", "show", sbuild_task_id
        )

        debian_binary_package_artifacts = 0
        debian_upload_artifacts = 0
        debian_binary_packages_seen = set()
        for artifact in sbuild_task["artifacts"]:
            if artifact["category"] == ArtifactCategory.BINARY_PACKAGE:
                debian_binary_package_artifacts += 1

                self.assertEqual(artifact["data"]["srcpkg_name"], "hello")
                self.assertEqual(
                    artifact["data"]["deb_fields"]["Architecture"],
                    self.architecture,
                )

                package = artifact["data"]["deb_fields"]["Package"]
                debian_binary_packages_seen.add(package)
                version = artifact["data"]["deb_fields"]["Version"]
                self.assertEqual(
                    set(artifact["files"].keys()),
                    {f"{package}_{version}_{self.architecture}.deb"},
                )
            elif artifact["category"] == ArtifactCategory.UPLOAD:
                debian_upload_artifacts += 1

                self.assertEqual(artifact["data"]["type"], "dpkg")
                self.assertEqual(
                    artifact["data"]["changes_fields"]["Source"], "hello"
                )

                version = artifact["data"]["changes_fields"]["Version"]
                self.assertCountEqual(
                    artifact["files"].keys(),
                    [
                        f"hello_{version}_{self.architecture}.deb",
                        f"hello-dbgsym_{version}_{self.architecture}.deb",
                        f"hello_{version}_{self.architecture}.buildinfo",
                        f"hello_{version}_{self.architecture}.changes",
                    ],
                )

        self.assertEqual(debian_binary_package_artifacts, 2)
        self.assertEqual(debian_binary_packages_seen, {"hello", "hello-dbgsym"})
        self.assertEqual(debian_upload_artifacts, 1)

    def verify_autopkgtest_artifacts(self, qa_workflow_id: int) -> None:
        """Verify ``autopkgtest`` artifacts produced by a workflow."""
        [autopkgtest_workflow_id] = self.find_workflow_children(
            qa_workflow_id,
            task_type=TaskTypes.WORKFLOW.value,
            task_name="autopkgtest",
        )
        [autopkgtest_task_id] = self.find_workflow_children(
            autopkgtest_workflow_id,
            task_type=TaskTypes.WORKER.value,
            task_name="autopkgtest",
        )
        autopkgtest_task = Client.execute_command(
            "work-request", "show", autopkgtest_task_id
        )

        debian_autopkgtest_artifacts = 0
        for artifact in autopkgtest_task["artifacts"]:
            if artifact["category"] == ArtifactCategory.AUTOPKGTEST:
                debian_autopkgtest_artifacts += 1

                # Expected testinfo.json in the artifact
                self.assertIn("testinfo.json", artifact["files"].keys())

                # The log has a reasonable Content-Type
                self.assertEqual(
                    artifact["files"]["log"]["content_type"],
                    "text/plain; charset=us-ascii",
                )

                # Check some of the data contents
                self.assertIn("results", artifact["data"])
                self.assertIn("source_package", artifact["data"])

        self.assertEqual(debian_autopkgtest_artifacts, 1)

    def verify_lintian_artifacts(self, qa_workflow_id: int) -> None:
        """Verify ``lintian`` artifacts produced by a workflow."""
        [lintian_workflow_id] = self.find_workflow_children(
            qa_workflow_id,
            task_type=TaskTypes.WORKFLOW.value,
            task_name="lintian",
        )
        [lintian_task_id] = self.find_workflow_children(
            lintian_workflow_id,
            task_type=TaskTypes.WORKER.value,
            task_name="lintian",
        )
        lintian_task = Client.execute_command(
            "work-request", "show", lintian_task_id
        )

        debian_lintian_artifacts: list[str] = []

        for artifact in lintian_task["artifacts"]:
            if artifact["category"] == ArtifactCategory.LINTIAN:
                debian_lintian_artifacts.append(
                    artifact["data"]["architecture"]
                )

                # Expected analysis.json and lintian.txt
                self.assertCountEqual(
                    artifact["files"].keys(), ["analysis.json", "lintian.txt"]
                )

        self.assertCountEqual(
            debian_lintian_artifacts, ["source", self.architecture]
        )

    def verify_piuparts_artifacts(self, qa_workflow_id: int) -> None:
        """Verify ``piuparts`` artifacts produced by a workflow."""
        [piuparts_workflow_id] = self.find_workflow_children(
            qa_workflow_id,
            task_type=TaskTypes.WORKFLOW.value,
            task_name="piuparts",
        )
        [piuparts_task_id] = self.find_workflow_children(
            piuparts_workflow_id,
            task_type=TaskTypes.WORKER.value,
            task_name="piuparts",
        )
        piuparts_task = Client.execute_command(
            "work-request", "show", piuparts_task_id
        )

        debian_piuparts_artifacts: list[str] = []

        for artifact in piuparts_task["artifacts"]:
            if artifact["category"] == ArtifactCategory.PIUPARTS:
                debian_piuparts_artifacts.append(
                    artifact["data"]["architecture"]
                )

                # Expected piuparts.txt
                self.assertCountEqual(
                    artifact["files"].keys(), ["piuparts.txt"]
                )

        self.assertCountEqual(debian_piuparts_artifacts, [self.architecture])

    def verify_debdiff_artifacts(self, qa_workflow_id: int) -> None:
        """Verify ``debdiff`` artifacts produced by a workflow."""
        [debdiff_workflow_id] = self.find_workflow_children(
            qa_workflow_id,
            task_type=TaskTypes.WORKFLOW.value,
            task_name="debdiff",
        )
        [debdiff_source_task_id] = self.find_workflow_children(
            debdiff_workflow_id,
            task_type=TaskTypes.WORKER.value,
            task_name="debdiff",
            workflow_data_json__step="debdiff-source",
        )
        debdiff_source_task = Client.execute_command(
            "work-request", "show", debdiff_source_task_id
        )
        [debdiff_binary_task_id] = self.find_workflow_children(
            debdiff_workflow_id,
            task_type=TaskTypes.WORKER.value,
            task_name="debdiff",
            workflow_data_json__step=f"debdiff-binaries-{self.architecture}",
        )
        debdiff_binary_task = Client.execute_command(
            "work-request", "show", debdiff_binary_task_id
        )

        for task in (debdiff_source_task, debdiff_binary_task):
            debian_debdiff_artifacts = 0

            for artifact in task["artifacts"]:
                if artifact["category"] == ArtifactCategory.DEBDIFF:
                    debian_debdiff_artifacts += 1

                    # Expected debdiff.txt
                    self.assertCountEqual(
                        artifact["files"].keys(), ["debdiff.txt"]
                    )

            self.assertEqual(debian_debdiff_artifacts, 1)

    def verify_blhc_artifacts(self, qa_workflow_id: int) -> None:
        """Verify ``blhc`` artifacts produced by a workflow."""
        [blhc_workflow_id] = self.find_workflow_children(
            qa_workflow_id, task_type=TaskTypes.WORKFLOW.value, task_name="blhc"
        )
        [blhc_task_id] = self.find_workflow_children(
            blhc_workflow_id, task_type=TaskTypes.WORKER.value, task_name="blhc"
        )
        blhc_task = Client.execute_command("work-request", "show", blhc_task_id)

        debian_blhc_artifacts = 0

        for artifact in blhc_task["artifacts"]:
            if artifact["category"] == ArtifactCategory.BLHC:
                debian_blhc_artifacts += 1

                # Expected blhc.txt
                self.assertCountEqual(artifact["files"].keys(), ["blhc.txt"])

        # 1 artifact for each job
        self.assertEqual(debian_blhc_artifacts, 1)

    def test_debian_pipeline(self) -> None:
        """Create a ``debian_pipeline`` workflow."""
        # Create a suite to use as a reference (e.g. for debdiff), and add
        # source and binary packages for "hello" to it.  We'll also use this
        # source artifact to start our pipeline.
        suite_name = "trixie"
        self.create_suite(suite_name, architectures=[self.architecture])
        with self.apt_indexes(suite_name) as apt_path:
            source_artifact_id = self.create_artifact_source(apt_path, "hello")
            self.add_artifact_to_suite(
                suite_name,
                source_artifact_id,
                component="main",
                section="devel",
            )
            binary_artifact_id = self.create_artifact_binary(apt_path, "hello")
            self.add_artifact_to_suite(
                suite_name,
                binary_artifact_id,
                component="main",
                section="devel",
                priority="optional",
            )

        # Create a workflow template.
        template_name = "test-debian-pipeline"
        template_data = {
            "static_parameters": {
                "vendor": "debian",
                "codename": "trixie",
                "architectures": [self.architecture],
                "arch_all_build_architecture": self.architecture,
                "sbuild_backend": BackendType.UNSHARE,
                "qa_suite": f"{suite_name}@{CollectionCategory.SUITE}",
                "autopkgtest_backend": BackendType.UNSHARE,
                "lintian_backend": BackendType.UNSHARE,
                "piuparts_backend": BackendType.UNSHARE,
                "enable_debdiff": True,
            },
            "runtime_parameters": {
                "source_artifact": "any",
            },
        }
        workflow_template_result = Client.execute_command(
            "workflow-template",
            "create",
            "debian_pipeline",
            template_name,
            stdin=yaml.safe_dump(template_data),
        )
        assert workflow_template_result.returncode == 0

        # Start a workflow with the source artifact downloaded earlier.
        workflow_id = Client.execute_command(
            "workflow",
            "start",
            "--yaml",
            template_name,
            stdin=yaml.safe_dump({"source_artifact": source_artifact_id}),
        )["id"]
        assert Client.wait_for_work_request_completed(workflow_id, "success")

        # Verify artifacts produced by various parts of the workflow.
        self.verify_sbuild_artifacts(workflow_id)
        [qa_workflow_id] = self.find_workflow_children(
            workflow_id, task_type=TaskTypes.WORKFLOW.value, task_name="qa"
        )
        self.verify_autopkgtest_artifacts(qa_workflow_id)
        self.verify_lintian_artifacts(qa_workflow_id)
        self.verify_piuparts_artifacts(qa_workflow_id)
        self.verify_debdiff_artifacts(qa_workflow_id)
        self.verify_blhc_artifacts(qa_workflow_id)

    def test_publish(self) -> None:
        """Publish source and binary packages to a suite."""
        base_template_name = "base-publish"
        base_template_data = {
            "static_parameters": {
                "vendor": "debian",
                "codename": "trixie",
                "architectures": [self.architecture],
                "sbuild_backend": "unshare",
                "enable_autopkgtest": False,
                "enable_lintian": False,
                "enable_piuparts": False,
                "enable_blhc": False,
            },
            "runtime_parameters": {
                "source_artifact": "any",
            },
        }
        workflow_template_result = Client.execute_command(
            "workflow-template",
            "create",
            "debian_pipeline",
            base_template_name,
            stdin=yaml.safe_dump(base_template_data),
        )
        assert workflow_template_result.returncode == 0

        suite_name = "trixie-publish"
        self.create_suite(
            suite_name,
            architectures=[self.architecture],
            base_workflow_template=base_template_name,
        )
        self.add_suite_to_archive(suite_name)
        with self.apt_indexes("trixie") as apt_path:
            source_artifact_id = self.create_artifact_source(
                apt_path, "base-files"
            )

        workflow_id = Client.execute_command(
            "workflow",
            "start",
            "--yaml",
            f"publish-to-{suite_name}",
            stdin=yaml.safe_dump({"source_artifact": source_artifact_id}),
        )["id"]
        assert Client.wait_for_work_request_completed(workflow_id, "success")

        work_requests = Client.execute_command(
            "work-request", "list", "--yaml", "--limit", "1000"
        )
        update_suites_id = next(
            wr
            for wr in work_requests.parsed_stdout
            if wr["task_type"] == "Workflow"
            and wr["task_name"] == "update_suites"
        )["id"]
        assert Client.wait_for_work_request_completed(
            update_suites_id, "success"
        )

        archive_url = f"https://deb.{socket.getfqdn()}/debusine/System"
        with (
            self.apt_indexes(
                suite_name,
                url=archive_url,
                signed_by=self.get_archive_signing_key(),
            ) as apt_path,
            self._temporary_directory() as temp_path,
        ):
            subprocess.run(
                ["apt-get", "source", "--download-only", "base-files"],
                cwd=temp_path,
                env=self.make_apt_environment(apt_path),
                check=True,
            )
            subprocess.run(
                ["apt-get", "download", "base-files"],
                cwd=temp_path,
                env=self.make_apt_environment(apt_path),
                check=True,
            )
