# 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 mergeuploads task support on the worker."""
from pathlib import Path
from unittest import mock
from unittest.mock import call

from debusine.artifacts import Upload
from debusine.artifacts.models import (
    ArtifactCategory,
    CollectionCategory,
    DebianUpload,
    EmptyArtifactData,
)
from debusine.client.models import (
    LookupResultType,
    LookupSingleResponse,
    RemoteArtifact,
)
from debusine.tasks import MergeUploads, TaskConfigError
from debusine.tasks.models import LookupMultiple, MergeUploadsDynamicData
from debusine.tasks.server import ArtifactInfo
from debusine.tasks.tests.helper_mixin import (
    ExternalTaskHelperMixin,
    FakeTaskDatabase,
)
from debusine.test import TestCase
from debusine.test.utils import (
    create_artifact_response,
    create_system_tarball_data,
)


class MergeUploadsTests(ExternalTaskHelperMixin[MergeUploads], TestCase):
    """Test the MergeUploads class."""

    SAMPLE_TASK_DATA = {
        "environment": "debian/match:codename=bookworm",
        "input": {"uploads": [421, 666]},
    }

    def setUp(self) -> None:  # noqa: D102
        self.configure_task()

    def tearDown(self) -> None:
        """Delete directory to avoid ResourceWarning with python -m unittest."""
        if self.task._debug_log_files_directory is not None:
            self.task._debug_log_files_directory.cleanup()

    def test_compute_dynamic_data(self) -> None:
        """Dynamic data receives relevant artifact IDs."""
        binary_artifacts_lookup = LookupMultiple.parse_obj([421, 666])
        task_db = FakeTaskDatabase(
            single_lookups={
                # environment
                (
                    "debian/match:codename=bookworm:format=tarball:"
                    "backend=unshare",
                    CollectionCategory.ENVIRONMENTS,
                ): ArtifactInfo(
                    id=1,
                    category=ArtifactCategory.SYSTEM_TARBALL,
                    data=create_system_tarball_data(),
                ),
            },
            multiple_lookups={
                # input.uploads
                (binary_artifacts_lookup, None): [
                    ArtifactInfo(
                        id=421,
                        category=ArtifactCategory.UPLOAD,
                        data=DebianUpload(
                            type="dpkg",
                            changes_fields={
                                "Architecture": "amd64",
                                "Files": [{"name": "hello_1.0_amd64.deb"}],
                                "Source": "hello",
                            },
                        ),
                    ),
                    ArtifactInfo(
                        id=666,
                        category=ArtifactCategory.UPLOAD,
                        data=DebianUpload(
                            type="dpkg",
                            changes_fields={
                                "Architecture": "s390x",
                                "Files": [{"name": "hello_1.0_s390x.deb"}],
                                "Source": "hello",
                            },
                        ),
                    ),
                ]
            },
        )

        self.assertEqual(
            self.task.compute_dynamic_data(task_db),
            MergeUploadsDynamicData(
                environment_id=1,
                input_uploads_ids=[421, 666],
                subject="hello",
            ),
        )

    def test_compute_dynamic_data_subject_is_none(self) -> None:
        """Uploading packages with different source package names."""
        binary_artifacts_lookup = LookupMultiple.parse_obj([421, 666])
        task_db = FakeTaskDatabase(
            single_lookups={
                # environment
                (
                    "debian/match:codename=bookworm:format=tarball:"
                    "backend=unshare",
                    CollectionCategory.ENVIRONMENTS,
                ): ArtifactInfo(
                    id=1,
                    category=ArtifactCategory.SYSTEM_TARBALL,
                    data=create_system_tarball_data(),
                ),
            },
            multiple_lookups={
                # input.uploads
                (binary_artifacts_lookup, None): [
                    ArtifactInfo(
                        id=421,
                        category=ArtifactCategory.UPLOAD,
                        data=DebianUpload(
                            type="dpkg",
                            changes_fields={
                                "Architecture": "amd64",
                                "Files": [{"name": "hello_1.0_amd64.deb"}],
                                "Source": "hello",
                            },
                        ),
                    ),
                    ArtifactInfo(
                        id=666,
                        category=ArtifactCategory.UPLOAD,
                        data=DebianUpload(
                            type="dpkg",
                            changes_fields={
                                "Architecture": "s390x",
                                "Files": [{"name": "hello_1.0_s390x.deb"}],
                                "Source": "python",
                            },
                        ),
                    ),
                ]
            },
        )

        self.assertEqual(
            self.task.compute_dynamic_data(task_db),
            MergeUploadsDynamicData(
                environment_id=1,
                input_uploads_ids=[421, 666],
                subject=None,
            ),
        )

    def test_compute_dynamic_raise_task_error_wrong_category(self) -> None:
        binary_artifacts_lookup = LookupMultiple.parse_obj([421, 666])
        task_db = FakeTaskDatabase(
            single_lookups={
                # environment
                (
                    "debian/match:codename=bookworm:format=tarball:"
                    "backend=unshare",
                    CollectionCategory.ENVIRONMENTS,
                ): ArtifactInfo(
                    id=1,
                    category=ArtifactCategory.SYSTEM_TARBALL,
                    data=create_system_tarball_data(),
                ),
            },
            multiple_lookups={
                # input.uploads
                (binary_artifacts_lookup, None): [
                    ArtifactInfo(
                        id=421,
                        category=ArtifactCategory.SYSTEM_TARBALL,
                        data=EmptyArtifactData(),
                    ),
                    ArtifactInfo(
                        id=666,
                        category=ArtifactCategory.UPLOAD,
                        data=DebianUpload(
                            type="dpkg",
                            changes_fields={
                                "Architecture": "s390x",
                                "Files": [{"name": "hello_1.0_s390x.deb"}],
                                "Source": "python",
                            },
                        ),
                    ),
                ]
            },
        )

        with self.assertRaisesRegex(
            TaskConfigError,
            r"^input.uploads: unexpected artifact category: "
            r"'debian:system-tarball'. Valid categories: \['debian:upload'\]$",
        ):
            self.task.compute_dynamic_data(task_db)

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

    def test_fetch_input(self) -> None:
        """Test fetch_input: call fetch_artifact(artifact_id, directory)."""
        directory = self.create_temporary_directory()
        artifact1 = self.fake_debian_upload_artifact()
        artifact2 = self.fake_debian_upload_artifact()
        self.configure_task()
        self.task.work_request_id = 5
        self.task.dynamic_data = MergeUploadsDynamicData(
            environment_id=1, input_uploads_ids=[artifact1.id, artifact2.id]
        )
        debusine_mock = self.mock_debusine()
        debusine_mock.artifact_get.return_value = artifact1

        with mock.patch.object(
            self.task, "fetch_artifact", autospec=True, return_value=True
        ) as fetch_artifact_mocked:
            result = self.task.fetch_input(directory)

        self.assertTrue(result)
        fetch_artifact_mocked.assert_has_calls(
            [
                mock.call(artifact1.id, directory),
                mock.call(artifact2.id, directory),
            ]
        )

    def test_fetch_input_wrong_category(self) -> None:
        """Test fetch_input when input isn't a package upload."""
        directory = self.create_temporary_directory()
        artifact1 = create_artifact_response(id=12)
        artifact2 = self.fake_debian_upload_artifact()
        self.configure_task()
        self.task.work_request_id = 5
        self.task.dynamic_data = MergeUploadsDynamicData(
            environment_id=1, input_uploads_ids=[artifact1.id, artifact2.id]
        )
        debusine_mock = self.mock_debusine()
        debusine_mock.artifact_get.return_value = artifact1

        with mock.patch.object(
            self.task, "fetch_artifact", autospec=True
        ) as fetch_artifact_mocked:
            result = self.task.fetch_input(directory)

        self.assertFalse(result)

        assert self.task._debug_log_files_directory
        log_file_contents = (
            Path(self.task._debug_log_files_directory.name) / "fetch_input.log"
        ).read_text()
        self.assertEqual(
            log_file_contents,
            (
                "input.uploads points to a Testing, not the "
                "expected debian:upload.\n"
            ),
        )

        fetch_artifact_mocked.assert_not_called()

    def test_execute(self) -> None:
        """Test full (mocked) execution."""
        self.configure_task()
        self.task.work_request_id = 2
        self.task.workspace_name = "testing"
        self.task.dynamic_data = MergeUploadsDynamicData(
            environment_id=1, input_uploads_ids=[1, 2]
        )
        download_directory = self.create_temporary_directory()

        debusine_mock = self.mock_debusine()
        debusine_mock.lookup_single.return_value = LookupSingleResponse(
            result_type=LookupResultType.ARTIFACT, artifact=1
        )
        debusine_mock.download_artifact.return_value = True
        debusine_mock.upload_artifact.return_value = RemoteArtifact(
            id=2, workspace=self.task.workspace_name
        )

        f_in_contents = "Format: 1.8"
        (
            f_in1 := download_directory / "meritous_1.5-1.1_amd64.changes"
        ).write_text(f_in_contents)
        (
            f_in2 := download_directory / "meritous_1.5-1.1_all.changes"
        ).write_text(f_in_contents)
        (
            f_deb := download_directory / "meritous-data_1.5-1.1_all.deb"
        ).write_text('')

        self.patch_prepare_executor_instance()

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

        self.assertEqual(self.task._changes_files, [f_in2, f_in1])
        self.assertEqual(
            self.task._cmdline(),
            [
                "mergechanges",
                str(f_in2),
                str(f_in1),
            ],
        )

        execute_directory = self.create_temporary_directory()
        (f_out := execute_directory / "multi.changes")
        self.write_changes_file(f_out, [f_deb])
        self.task.upload_artifacts(execute_directory, execution_success=True)

        debusine_mock.upload_artifact.assert_called_once_with(
            Upload.create(
                changes_file=download_directory
                / "meritous_1.5-1.1_multi.changes"
            ),
            workspace=self.task.workspace_name,
            work_request=self.task.work_request_id,
        )
        debusine_mock.relation_create.assert_has_calls(
            [
                mock.call(2, 1, "extends"),
                mock.call(2, 2, "extends"),
            ]
        )

    def test_upload_artifacts(self) -> None:
        """upload_artifact() and relation_create() is called."""
        self.task.dynamic_data = MergeUploadsDynamicData(
            environment_id=1, input_uploads_ids=[1, 2]
        )
        download_directory = self.create_temporary_directory()

        # Create file that will be attached when uploading the artifacts
        f_in_contents = "Format: 1.8"
        (
            f_in1 := download_directory / "meritous_1.5-1.1_amd64.changes"
        ).write_text(f_in_contents)
        (
            f_in2 := download_directory / "meritous_1.5-1.1_all.changes"
        ).write_text(f_in_contents)
        (
            f_deb := download_directory / "meritous-data_1.5-1.1_all.deb"
        ).write_text('')

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

        workspace_name = "testing"

        uploaded_artifacts = [
            RemoteArtifact(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 MergeUploads
        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._changes_files = [f_in2, f_in1]
        execute_directory = self.create_temporary_directory()
        (f_out := execute_directory / "multi.changes")
        self.write_changes_file(f_out, [f_deb])
        with mock.patch.object(self.task, "executor_instance", autospec=True):
            self.task.upload_artifacts(
                execute_directory, execution_success=True
            )

        # Assert that the artifacts were uploaded and relations created
        debusine_mock.upload_artifact.assert_called_once_with(
            Upload.create(
                changes_file=download_directory
                / "meritous_1.5-1.1_multi.changes"
            ),
            workspace=workspace_name,
            work_request=work_request_id,
        )

        # Debusine mock relation_create expected calls
        debusine_mock.relation_create.assert_has_calls(
            [
                call(uploaded_artifacts[0].id, 1, "extends"),
                call(uploaded_artifacts[0].id, 2, "extends"),
            ]
        )

    def test_label(self) -> None:
        """Test get_label."""
        self.assertEqual(self.task.get_label(), "merge package uploads")
