# 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 the blhc task support on the worker."""

import itertools
from typing import override
from unittest.mock import call, patch

from debian.debian_support import Version

import debusine.tasks.tags as ttags
import debusine.worker.tags as wtags
from debusine.artifacts import BlhcArtifact
from debusine.artifacts.models import (
    ArtifactCategory,
    DebianPackageBuildLog,
    WorkRequestResults,
)
from debusine.client.models import LookupSingleResponseArtifact
from debusine.tasks import Blhc, TaskConfigError
from debusine.tasks.models import BlhcDynamicData
from debusine.tasks.tests.helpers import ExternalTaskHelperMixin
from debusine.test import TestCase
from debusine.test.test_utils import (
    create_remote_artifact,
    create_system_tarball_data,
)


class BlhcTests(ExternalTaskHelperMixin[Blhc], TestCase):
    """Test the Blhc class."""

    SAMPLE_TASK_DATA = {
        "input": {"artifact": 421},
        "build_architecture": "amd64",
        "environment": "debian/match:codename=bookworm",
    }

    build_log = DebianPackageBuildLog(
        source="hello",
        version="1.0",
        architecture="amd64",
        filename="hello_1.0_amd64.buildlog",
    )

    @override
    def setUp(self) -> None:
        super().setUp()
        self.resolver.set_field_artifact(
            "environment",
            id=1,
            category=ArtifactCategory.SYSTEM_TARBALL,
            data=create_system_tarball_data(),
        )
        self.resolver.set_field_artifact(
            "input_artifact",
            id=421,
            category=ArtifactCategory.PACKAGE_BUILD_LOG,
            data=self.build_log,
        )

    def test_configure_fails_with_missing_required_data(
        self,
    ) -> None:
        with self.assertRaises(TaskConfigError):
            self.configure_task(override={"input": {}})

    def test_configure_with_extra_flag(self) -> None:
        """Configuration included "extra_flags". Saved, no exceptions."""
        extra_flags = ["--bindnow"]

        directory = self.create_temporary_directory()
        (directory / "file.build").write_text("")

        self.configure_task(override={"extra_flags": extra_flags})

        self.patch_prepare_executor_instance()

        self.assertEqual(self.task.data.extra_flags, extra_flags)

        self.assertTrue(self.task.configure_for_execution(directory))

    def test_configure_with_invalid_flag(self) -> None:
        """Configuration included "extra_flags". Saved, no exceptions."""
        extra_flags = ["--unknown-flag"]

        directory = self.create_temporary_directory()
        (directory / "file.build").write_text("")

        with self.assertRaises(TaskConfigError):
            self.configure_task(override={"extra_flags": extra_flags})

    def test_configure_additional_properties_in_input(self) -> None:
        """Assert no additional properties in task_data input."""
        error_msg = "Extra inputs are not permitted"
        with self.assertRaisesRegex(TaskConfigError, error_msg):
            self.configure_task(
                task_data={
                    "input": {
                        "artifact": 5,
                        "some_additional_property": 87,
                    }
                }
            )

    def test_get_provided_worker_tags(self) -> None:
        self.assertCountEqual(
            self.task.get_provided_worker_tags(),
            [wtags.TASK_PREFIX + "worker:blhc:version:1"],
        )

    def test_compute_scheduler_tags(self) -> None:
        self.assertComputesSchedulerTags(
            provide_system={ttags.SOURCE_PACKAGE_PREFIX + "hello"},
            require_system={
                wtags.BUILD_ARCH_PREFIX + "amd64",
                wtags.EXECUTOR_PREFIX + "unshare",
            },
        )

    def test_compute_dynamic_data(self) -> None:
        """Dynamic data receives relevant artifact IDs."""
        self.assertEqual(
            self.compute_dynamic_data(self.task),
            BlhcDynamicData(
                environment_id=1,
                input_artifact_id=421,
                input_artifact_data=self.build_log,
                subject="hello",
                parameter_summary="hello_1.0 (amd64)",
                runtime_context=None,
            ),
        )

    def test_get_subject(self) -> None:
        self.assertEqual(self.get_subject(), "hello")

    def test_get_configuration_context(self) -> None:
        self.assertIsNone(self.get_configuration_context())

    def test_acceptable_input_categories(self) -> None:
        self.assertEqual(
            Blhc.inputs["input_artifact"].categories,  # type: ignore[attr-defined]
            (ArtifactCategory.PACKAGE_BUILD_LOG,),
        )

    def test_get_input_artifacts_ids(self) -> None:
        """Test get_input_artifacts_ids."""
        self.assertEqual(self.task.get_input_artifacts_ids(), [])

        self.task.dynamic_data = BlhcDynamicData(
            input_artifact_id=1, environment_id=2
        )
        self.assertEqual(self.task.get_input_artifacts_ids(), [1, 2])

    def test_configure_for_execution_from_artifact_error_no_build_logs(
        self,
    ) -> None:
        """configure_for_execution() no .build files: return False."""
        self.patch_prepare_executor_instance()
        download_directory = self.create_temporary_directory()
        (file1 := download_directory / "file1.dsc").write_text("")
        (file2 := download_directory / "file2.changes").write_text("")

        self.assertFalse(self.task.configure_for_execution(download_directory))

        files = sorted([str(file1), str(file2)])
        log_file_contents = (
            self.task.log_directory / "configure_for_execution.log"
        ).read_text()
        self.assertEqual(
            log_file_contents,
            f"No *.build files to be analyzed. Files: {files}\n",
        )

    def test_blhc_supports_debian_option_stretch(self) -> None:
        with patch.object(
            self.task,
            "get_package_version",
            return_value=Version("0.07+20161116+gitbf41976-1"),
        ):
            self.assertFalse(self.task._blhc_supports_debian_option())

    def test_blhc_supports_debian_option_buster(self) -> None:
        with patch.object(
            self.task, "get_package_version", return_value=Version("0.09-1")
        ):
            self.assertTrue(self.task._blhc_supports_debian_option())

    def test_cmdline_without_debian_option(self) -> None:
        self.task._blhc_target = self.create_temporary_file()

        with patch.object(
            self.task,
            "_blhc_supports_debian_option",
            autospec=True,
            return_value=False,
        ) as mock_blhc_supports_debian_option:
            cmdline = self.task._cmdline()

        self.assertEqual(cmdline, ["blhc", str(self.task._blhc_target)])
        mock_blhc_supports_debian_option.assert_called_once_with()

    def test_cmdline_with_debian_option(self) -> None:
        self.task._blhc_target = self.create_temporary_file()

        with patch.object(
            self.task,
            "_blhc_supports_debian_option",
            autospec=True,
            return_value=True,
        ) as mock_blhc_supports_debian_option:
            cmdline = self.task._cmdline()

        self.assertEqual(
            cmdline, ["blhc", "--debian", str(self.task._blhc_target)]
        )
        mock_blhc_supports_debian_option.assert_called_once_with()

    def test_cmdline_with_extra_flags(self) -> None:
        """Cmdline add extra_flags."""
        extra_flags = ["--bindnow"]
        self.configure_task(override={"extra_flags": extra_flags})
        self.task._blhc_target = self.create_temporary_file()

        with patch.object(
            self.task,
            "_blhc_supports_debian_option",
            autospec=True,
            return_value=True,
        ):
            cmdline = self.task._cmdline()

        self.assertIn(extra_flags[0], cmdline)
        self.assertEqual(cmdline[-1], str(self.task._blhc_target))

    def test_task_result_empty_file_success(self) -> None:
        """task_result() for an empty file return SUCCESS."""
        directory = self.create_temporary_directory()
        (directory / Blhc.CAPTURE_OUTPUT_FILENAME).write_text("")

        self.configure_task()
        self.assertEqual(
            self.task.task_result(0, directory), WorkRequestResults.SUCCESS
        )

    def test_task_result_failure_exit_code(self) -> None:
        """task_result() for an exit code of 8 return FAILURE."""
        directory = self.create_temporary_directory()
        (directory / Blhc.CAPTURE_OUTPUT_FILENAME).write_text("")

        self.configure_task()
        self.assertEqual(
            self.task.task_result(8, directory), WorkRequestResults.FAILURE
        )

    def test_upload_artifacts(self) -> None:
        """upload_artifact() and relation_create() is called."""
        exec_dir = self.create_temporary_directory()

        # Create file that will be attached when uploading the artifacts
        blhc_output = exec_dir / self.task.CAPTURE_OUTPUT_FILENAME
        blhc_output.write_text("NONVERBOSE BUILD:    Compiling gkrust\n")

        # Used to verify the relations
        self.task._source_artifacts_ids = [1]
        self.task.dynamic_data = BlhcDynamicData(
            input_artifact_id=2,
            input_artifact_data=self.build_log,
            environment_id=1,
        )

        # Debusine.upload_artifact is mocked to verify the call only
        debusine_mock = self.mock_debusine()

        workspace_name = "testing"

        uploaded_artifacts = [
            create_remote_artifact(id=10, workspace=workspace_name),
        ]

        debusine_mock.upload_artifact.side_effect = uploaded_artifacts

        # self.task.workspace_name is set by the Worker
        # and is the workspace that downloads the artifact
        # containing the files needed for Blhc
        self.task.workspace_name = workspace_name

        # The worker set self.task.work_request_id of the task
        work_request_id = 147
        self.task.work_request_id = work_request_id

        self.task.upload_artifacts(
            exec_dir, execution_result=WorkRequestResults.SUCCESS
        )

        # Debusine Mock upload_artifact expected calls
        upload_artifact_calls = []
        blhc_artifact = BlhcArtifact.create(
            blhc_output=blhc_output, build_log=self.build_log
        )

        upload_artifact_calls.append(
            call(
                blhc_artifact,
                workspace=workspace_name,
                work_request=work_request_id,
            )
        )

        # Debusine mock relation_create expected calls
        relation_create_calls = []
        for uploaded_artifact, source_artifact_id in itertools.product(
            uploaded_artifacts, self.task._source_artifacts_ids
        ):
            relation_create_calls.append(
                call(uploaded_artifact.id, source_artifact_id, "relates-to")
            )

        # Assert that the artifacts were uploaded and relations created
        debusine_mock.upload_artifact.assert_has_calls(upload_artifact_calls)
        debusine_mock.relation_create.assert_has_calls(relation_create_calls)

    def test_execute(self) -> None:
        """Test full (mocked) execution."""
        build_log = DebianPackageBuildLog(
            source="hello",
            version="1.0",
            architecture="i386",
            filename="hello_1.0_i386.buildlog",
        )
        self.configure_task(override={"input": {'artifact': 1}})
        self.task.work_request_id = 2
        self.task.workspace_name = "testing"
        self.task.dynamic_data = BlhcDynamicData(
            input_artifact_id=1, input_artifact_data=build_log, environment_id=2
        )
        download_directory = self.create_temporary_directory()

        debusine_mock = self.mock_debusine()
        debusine_mock.lookup_single.return_value = LookupSingleResponseArtifact(
            artifact=1
        )
        debusine_mock.download_artifact.return_value = True
        debusine_mock.upload_artifact.return_value = create_remote_artifact(
            id=2, workspace=self.task.workspace_name
        )

        self.assertTrue(self.task.fetch_input(download_directory))

        debusine_mock.download_artifact.assert_called_once_with(
            1, download_directory, tarball=False, workspace="testing"
        )

        f1_contents = "Compiling gkrust v0.1.0 (/build/rust)"
        (file1 := download_directory / "file.build").write_text(f1_contents)

        self.patch_prepare_executor_instance()

        self.assertTrue(self.task.configure_for_execution(download_directory))

        self.assertEqual(self.task._blhc_target, file1)
        with patch.object(
            self.task,
            "_blhc_supports_debian_option",
            autospec=True,
            return_value=True,
        ):
            self.assertEqual(
                self.task._cmdline(),
                [
                    "blhc",
                    "--debian",
                    str(file1),
                ],
            )

        # Create file that will be attached when uploading the artifacts
        blhc_output = download_directory / self.task.CAPTURE_OUTPUT_FILENAME
        blhc_output.write_text(
            "NONVERBOSE BUILD:    Compiling gkrust v0.1.0 (/build/rust)\n"
        )

        self.task.upload_artifacts(
            download_directory, execution_result=WorkRequestResults.SUCCESS
        )

        debusine_mock.upload_artifact.assert_called_once_with(
            BlhcArtifact.create(
                blhc_output=download_directory / Blhc.CAPTURE_OUTPUT_FILENAME,
                build_log=build_log,
            ),
            workspace=self.task.workspace_name,
            work_request=self.task.work_request_id,
        )
        debusine_mock.relation_create.assert_called_once_with(
            2, 1, "relates-to"
        )
