# Copyright 2023 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 classes in local_artifacts.py."""

import base64
import json
from datetime import datetime, timedelta
from pathlib import Path
from unittest import mock

import debian.deb822 as deb822

import debusine.artifacts.models as data_models
from debusine.artifacts import (
    BinaryPackage,
    BinaryPackages,
    BlhcArtifact,
    LintianArtifact,
    LocalArtifact,
    PackageBuildLog,
    SigningInputArtifact,
    SigningKeyArtifact,
    SigningOutputArtifact,
    SourcePackage,
    Upload,
    WorkRequestDebugLogs,
)
from debusine.artifacts.local_artifact import (
    AutopkgtestArtifact,
    DebianSystemImageArtifact,
    DebianSystemTarballArtifact,
    deb822dict_to_dict,
)
from debusine.client.models import FileRequest
from debusine.test import TestCase


def unregister_local_artifact_subclass(
    category: data_models.ArtifactCategory,
) -> None:
    """
    Unregister an artifact category.

    Note that the only way to register a category is to define a new subclass.
    """
    del LocalArtifact._local_artifacts_category_to_class[category]


class LocalArtifactTests(TestCase):
    """Tests for LocalArtifact class."""

    def setUp(self):
        """Set up test."""  # noqa: D202

        # Register a custom artifact category, taking care that it doesn't
        # pollute other tests.
        class TestArtifact(LocalArtifact[data_models.EmptyArtifactData]):
            """Custom artifact for testing."""

            _category = data_models.ArtifactCategory.TEST

        self.addCleanup(
            unregister_local_artifact_subclass, TestArtifact._category
        )

        self.category = TestArtifact._category
        self.data = {}
        self.files = {}

        self.local_artifact = TestArtifact(
            category=self.category,
            files=self.files,
            data=self.data,
        )

    def test_add_local_file_in_root(self):
        """add_local_file() add the file in .files."""
        file = self.create_temporary_file()
        self.local_artifact.add_local_file(file)

        self.assertEqual(
            self.local_artifact.files,
            {file.name: file},
        )

    def test_add_local_file_in_override_name(self):
        """add_local_file() with override_name."""
        file = self.create_temporary_file()

        name = "new_name.txt"
        self.local_artifact.add_local_file(file, override_name=name)

        self.assertEqual(self.local_artifact.files, {name: file})

    def test_add_local_file_artifact_base_dir_does_not_exist(self):
        """add_local_file() raise ValueError: directory does not exist."""
        file = self.create_temporary_file()
        artifact_base_dir = Path("/does/not/exist")
        with self.assertRaisesRegex(
            ValueError,
            f'"{artifact_base_dir}" does not exist or is not a directory',
        ):
            self.local_artifact.add_local_file(
                file, artifact_base_dir=artifact_base_dir
            )

    def test_add_local_file_relative_to_artifact_base_dir_does_not_exist(self):
        """add_local_file() raise ValueError: filename does not exist."""
        artifact_base_dir = self.create_temporary_directory()
        filename = Path("README.txt")

        filename_absolute = artifact_base_dir / filename

        with self.assertRaisesRegex(
            ValueError, f'"{filename_absolute}" does not exist'
        ):
            self.local_artifact.add_local_file(
                filename, artifact_base_dir=artifact_base_dir
            )

    def test_add_local_file_absolute_does_not_exist(self):
        """add_local_file() raise ValueError: filename does not exist."""
        empty_dir = self.create_temporary_directory()

        filename = empty_dir / "a-file-does-not-exist.txt"

        with self.assertRaisesRegex(ValueError, f'"{filename}" does not exist'):
            self.local_artifact.add_local_file(filename)

    def test_add_local_file_is_not_a_file(self):
        """add_local_file() raise ValueError: expected file is a directory."""
        empty_dir = self.create_temporary_directory()

        with self.assertRaisesRegex(ValueError, f'"{empty_dir}" is not a file'):
            self.local_artifact.add_local_file(empty_dir)

    def test_add_local_file_relative_to_artifact_base_dir(self):
        """add_local_file() add relative file from an absolute directory."""
        artifact_base_dir = self.create_temporary_directory()
        filename = Path("README.txt")

        (file := artifact_base_dir / filename).write_bytes(b"")

        self.local_artifact.add_local_file(
            filename, artifact_base_dir=artifact_base_dir
        )

        self.assertEqual(self.local_artifact.files, {'README.txt': file})

    def test_add_local_file_artifact_raise_value_error_dir_is_relative(self):
        """add_local_file() raise ValueError if base_dir is relative."""
        directory = Path("does-not-exist")
        file = directory / "README.txt"
        with self.assertRaisesRegex(
            ValueError, f'"{directory}" must be absolute'
        ):
            self.local_artifact.add_local_file(
                file, artifact_base_dir=directory
            )

    def test_add_local_file_absolute_path_no_artifact_base_dir(self):
        """add_local_file() adds file. No artifact path or artifact_dir."""
        file = self.create_temporary_file()

        self.local_artifact.add_local_file(file)

        self.assertEqual(
            self.local_artifact.files,
            {file.name: file},
        )

    def test_add_local_file_duplicated_raise_value_error(self):
        """add_local_file() raise ValueError: file is already added."""
        file = self.create_temporary_file()
        self.local_artifact.add_local_file(file)

        with self.assertRaisesRegex(
            ValueError,
            rf"^File with the same path \({file.name}\) "
            rf'is already in the artifact \("{file}"\ and "{file}"\)',
        ):
            self.local_artifact.add_local_file(file)

    def test_serialize_for_create(self):
        """serialize_for_create_artifact return the expected dictionary."""
        workspace = "workspace"
        self.assertEqual(
            self.local_artifact.serialize_for_create_artifact(
                workspace=workspace
            ),
            {
                "category": self.category,
                "data": self.data,
                "expire_at": None,
                "files": self.files,
                "workspace": workspace,
                "work_request": None,
            },
        )

    def test_serialize_for_create_no_workspace(self):
        """serialize_for_create_artifact return dictionary without workspace."""
        self.assertNotIn(
            "workspace",
            self.local_artifact.serialize_for_create_artifact(workspace=None),
        )

    def test_validate_model_on_serialization(self):
        """serialize_for_create_artifact raise ValueError: invalid artifact."""
        file = self.create_temporary_file(suffix=".changes")
        artifact = Upload(
            files={file.name: file},
            category=Upload._category,
            data=data_models.DebianUpload(
                type="dpkg",
                changes_fields={
                    "Architecture": "amd64",
                    "Files": [
                        {"name": "hello_1.0_amd64.deb"},
                    ],
                },
            ),
        )

        # Remove the only file. The "artifact" object will not validate
        artifact.files.popitem()

        with self.assertRaisesRegex(ValueError, "^Model validation failed:"):
            artifact.serialize_for_create_artifact(workspace="System")

    def test_serialize_for_create_with_work_request(self):
        """serialize_for_create_artifact include the work_request."""
        workspace = "workspace"
        work_request = 5

        serialized = self.local_artifact.serialize_for_create_artifact(
            workspace=workspace, work_request=work_request
        )

        self.assertEqual(serialized["work_request"], work_request)

    def test_validate_category(self):
        """Creating a new artifact validates its category."""
        self.assertRaisesRegex(
            ValueError,
            r"Invalid category: 'nonexistent'",
            WorkRequestDebugLogs,
            category="nonexistent",
            files=self.files,
            data=self.data,
        )

    def test_validate_files_length(self):
        """_validate_files_length return the files."""
        files = {"package1.deb": Path(), "package2.deb": Path()}
        self.assertEqual(
            LocalArtifact._validate_files_length(files, len(files)), files
        )

    def test_validate_files_length_raise_error(self):
        """_validate_files_length raise ValueError."""
        files = {}
        expected = 1
        with self.assertRaisesRegex(
            ValueError, f"Expected number of files: {expected} Actual: 0"
        ):
            LocalArtifact._validate_files_length(files, expected)

    def test_serialize_for_create_with_expire_at(self):
        """serialize_for_create_artifact include the expire_at."""
        expire_at = datetime.now() + timedelta(days=1)

        serialized = self.local_artifact.serialize_for_create_artifact(
            workspace="workspac", expire_at=expire_at
        )

        self.assertEqual(serialized["expire_at"], expire_at.isoformat())


class PackageBuildLogTests(TestCase):
    """Tests for PackageBuildLog."""

    def setUp(self):
        """Set up for the tests."""
        self.file = FileRequest(
            size=10, checksums={"sha256": "hash"}, type="file"
        )

        self.directory = self.create_temporary_directory()

        self.build_filename = "log.build"
        self.build_file = self.directory / self.build_filename
        self.build_file.write_text("A line of log")

    def test_create(self):
        """create() method return the expected PackageBuildLog."""
        source = "hello"
        version = "2.10-2"

        artifact = PackageBuildLog.create(
            file=self.directory / "log.build",
            source=source,
            version=version,
        )

        expected_data = {
            "source": source,
            "version": version,
            "filename": "log.build",
        }
        expected_files = {self.build_filename: self.build_file}

        expected = PackageBuildLog(
            category=PackageBuildLog._category,
            files=expected_files,
            data=expected_data,
        )
        self.assertEqual(artifact, expected)
        self.assertIsInstance(artifact, PackageBuildLog)

    def test_validate_files_must_be_one_raise(self):
        """Raise ValueError: unexpected number of files."""
        with self.assertRaisesRegex(
            ValueError, "^Expected number of files: 1 Actual: 0$"
        ):
            PackageBuildLog.validate_files_length_is_one({})

    def test_validate_files_number_success(self):
        """files_number_must_be_one() return files."""
        files = {"log.build": self.file}

        self.assertEqual(
            PackageBuildLog.validate_files_length_is_one(files),
            files,
        )

    def test_validate_file_ends_with_build(self):
        """File ends with .build: files_must_end_in_build return files."""
        files = {"log.build": self.file}

        self.assertEqual(PackageBuildLog.file_must_end_in_build(files), files)

    def test_validate_file_ends_with_build_raise_value_error(self):
        """Raise ValueError: file does not end in .build."""
        file = self.create_temporary_file(suffix=".txt")

        expected_message = (
            fr"""^Valid file suffixes: \['\.build'\]. """
            fr"""Invalid filename: "{file.name}"$"""
        )

        with self.assertRaisesRegex(ValueError, expected_message):
            PackageBuildLog.file_must_end_in_build({file.name: file})

    def test_json_serializable(self):
        """Artifact returned by create is JSON serializable."""
        artifact = PackageBuildLog.create(
            file=self.build_file, source="hello", version="2.10-2"
        )

        # No exception is raised
        json.dumps(
            artifact.serialize_for_create_artifact(workspace="some-workspace")
        )


class UploadTests(TestCase):
    """Tests for Upload."""

    def setUp(self):
        """Set up test."""
        self.workspace = "workspace"

        self.artifact = Upload(
            category=Upload._category,
            data=data_models.DebianUpload(
                type="dpkg",
                changes_fields={
                    "Architecture": "amd64",
                    "Files": [
                        {"name": "hello_1.0_amd64.deb"},
                    ],
                },
            ),
        )

        self.file = FileRequest(
            size=10, checksums={"sha256": "hash"}, type="file"
        )

    def test_create(self):
        """create() return the correct object."""
        directory = self.create_temporary_directory()

        changes_filename = "python-network.changes"
        changes_file = directory / changes_filename

        deb = self.create_temporary_file(
            suffix="_1.0_amd64.deb", directory=directory
        )
        dsc = self.create_temporary_file(suffix=".dsc", directory=directory)
        changes = self.write_changes_file(changes_file, [dsc, deb])

        artifact = Upload.create(changes_file=changes_file)

        files = {
            changes_filename: changes_file,
            dsc.name: dsc,
            deb.name: deb,
        }
        data = {
            "changes_fields": changes,
            "type": "dpkg",
        }

        expected = Upload(
            category=Upload._category,
            files=files,
            data=data,
        )

        self.assertEqual(artifact, expected)

    def test_create_skip_excluded_files(self):
        """Upload.create() does not add the excluded file."""
        changes_file = self.create_temporary_file(suffix=".changes")

        extra_file = self.create_temporary_file()

        self.write_changes_file(changes_file, files=[extra_file])

        binary_upload = Upload.create(
            changes_file=changes_file, exclude_files={extra_file}
        )

        files = {changes_file.name: changes_file}

        self.assertEqual(binary_upload.files, files)

    def test_create_no_files_in_changes(self):
        """Upload.create() return a package, .changes has no files."""
        changes_file = self.create_temporary_file(suffix=".changes")

        self.write_changes_file(changes_file, files=[])

        binary_upload = Upload.create(changes_file=changes_file)

        files = {changes_file.name: changes_file}

        self.assertEqual(binary_upload.files, files)

    def test_json_serializable(self):
        """Artifact returned by create is JSON serializable."""
        directory = self.create_temporary_directory()
        output_file = self.create_temporary_file(
            suffix=".deb", directory=directory
        )
        changes = directory / "package.changes"
        self.write_changes_file(changes, files=[output_file])
        artifact = Upload.create(changes_file=changes)

        json.dumps(
            artifact.serialize_for_create_artifact(workspace="some workspace")
        )

    def test_files_no_contain_changes_raise_value_error(self):
        """Raise ValueError: missing expected .changes file."""
        file = self.create_temporary_file()

        files = {file.name: file}

        expected_message = fr"Expecting 1 \.changes file in \['{file.name}'\]"
        with self.assertRaisesRegex(ValueError, expected_message):
            Upload.files_contain_changes(files)

    def test_files_contain_2_changes_raise_value_error(self):
        """Raise ValueError: missing expected .changes file."""
        file1 = self.create_temporary_file(suffix=".changes")
        file2 = self.create_temporary_file(suffix=".changes")

        files = {
            file1.name: file1,
            file2.name: file2,
        }

        expected_message = r"Expecting 1 \.changes file in \["
        with self.assertRaisesRegex(ValueError, expected_message):
            Upload.files_contain_changes(files)

    def test_files_contain_changes_return_files(self):
        """Return files: .changes found."""
        changes_file = self.create_temporary_file(suffix=".changes")

        self.write_changes_file(changes_file, files=[changes_file])

        files = {changes_file.name: changes_file}

        self.assertEqual(Upload.files_contain_changes(files), files)

    def test_files_contains_files_in_changes_call_implementation(self):
        """Test files_contains_files_in_dsc call the utils method."""
        result = {"a": "b"}
        patcher = mock.patch(
            "debusine.artifacts.local_artifact.files_in_meta_file_match_files"
        )
        mocked = patcher.start()
        mocked.return_value = result
        self.addCleanup(patcher.stop)

        files = {}

        self.assertEqual(Upload.files_contains_files_in_changes(files), result)

        mocked.assert_called_with(
            ".changes",
            deb822.Changes,
            files,
        )


class Deb822DictToDictTests(TestCase):
    """Tests for deb822dict_to_dict function."""

    def assert_deb822dict_return_correct_value(self, value, expected):
        """Use deb822dict_to_dict(value) and check compare with expected."""
        actual = deb822dict_to_dict(value)

        self.assertEqual(actual, expected)
        json.dumps(actual)

    def test_dict(self):
        """Python dictionary is returned as it is."""
        to_convert = {"a": "b"}
        expected = {"a": "b"}
        self.assert_deb822dict_return_correct_value(to_convert, expected)

    def test_deb822_dict(self):
        """Deb822Dict is converted to a Python dict."""
        to_convert = deb822.Deb822Dict({"a": "b"})
        expected = {"a": "b"}
        self.assert_deb822dict_return_correct_value(to_convert, expected)

    def test_list(self):
        """A list with a Deb822Dict is returned as expected."""
        to_convert = ["key", deb822.Deb822Dict({"a": "b"})]
        expected = ["key", {"a": "b"}]
        self.assert_deb822dict_return_correct_value(to_convert, expected)

    def test_str(self):
        """A string is returned as it is."""
        to_convert = deb822dict_to_dict("all")
        expected = "all"
        self.assert_deb822dict_return_correct_value(to_convert, expected)


class BinaryPackageTests(TestCase):
    """Tests for BinaryPackage."""

    def test_create(self):
        """create() return the expected package: correct data and files."""
        srcpkg_name = "hello-traditional"
        srcpkg_version = "2.10-2"
        version = "1.1.1"
        architecture = "amd64"
        directory = self.create_temporary_directory()
        file = directory / f"hello_{version}_amd64.deb"
        self.write_deb_file(
            file,
            source_name=srcpkg_name,
            source_version=srcpkg_version,
            control_file_names=["md5sums"],
        )

        package = BinaryPackage.create(file=file)

        expected_data = data_models.DebianBinaryPackage(
            srcpkg_name=srcpkg_name,
            srcpkg_version=srcpkg_version,
            deb_fields={
                "Package": "hello",
                "Version": version,
                "Architecture": architecture,
                "Maintainer": "Example Maintainer <example@example.org>",
                "Description": "Example description",
                "Source": f"{srcpkg_name} ({srcpkg_version})",
            },
            deb_control_files=["control", "md5sums"],
        )
        expected_files = {file.name: file}

        self.assertEqual(package.category, BinaryPackage._category)
        self.assertEqual(package.data, expected_data)
        self.assertEqual(package.files, expected_files)

    def test_files_must_end_in_deb_or_udeb(self):
        """files_must_end_in_deb_or_udeb() returns the files."""
        for name in ("hello_2.10-2_amd64.deb", "hello_2.10-2_amd64.udeb"):
            self.assertEqual(
                BinaryPackage.files_must_end_in_deb_or_udeb({name: None}),
                {name: None},
            )

    def test_files_must_end_in_deb_raise_value_error(self):
        """files_must_end_in_deb_or_udeb() raises ValueError."""
        filename = "README.txt"
        files = {filename: None}
        with self.assertRaisesRegex(
            ValueError,
            rf"^Valid file suffixes: \['.deb', '.udeb'\]. "
            rf"Invalid filename: \"{filename}\"$",
        ):
            BinaryPackage.files_must_end_in_deb_or_udeb(files)

    def test_files_exactly_one(self):
        """files_exactly_one() returns the files."""
        files = {"hello.deb": None}

        self.assertEqual(BinaryPackage.files_exactly_one(files), files)

    def test_files_exactly_one_no_files(self):
        """files_exactly_one() raises ValueError: zero files."""
        with self.assertRaisesRegex(ValueError, "Must have exactly one file"):
            BinaryPackage.files_exactly_one({})

    def test_files_exactly_one_too_many_files(self):
        """files_exactly_one() raises ValueError: too many files."""
        with self.assertRaisesRegex(ValueError, "Must have exactly one file"):
            BinaryPackage.files_exactly_one(
                {"hello.deb": None, "hello2.deb": None}
            )

    def test_json_serializable(self):
        """Artifact returned by create is JSON serializable."""
        directory = self.create_temporary_directory()
        file = directory / "hello_2.10-2_amd64.deb"
        self.write_deb_file(file)
        artifact = BinaryPackage.create(file=file)

        # This is to verity that serialize_for_create_artifact() is
        # not returning some object that might raise an exception on
        # json.dumps. E.g. if it returned (as a value of the dictionary) a set()
        json.dumps(
            artifact.serialize_for_create_artifact(workspace="some workspace")
        )


class BinaryPackagesTests(TestCase):
    """Tests for BinaryPackages."""

    def test_create(self):
        """create() return the expected package: correct data and files."""
        srcpkg_name = "hello"
        srcpkg_version = "2.10-2"
        version = "1.1.1"
        architecture = "amd64"
        directory = self.create_temporary_directory()
        files = [
            directory / f"hello-traditional_{version}_amd64.deb",
            directory / f"hello-dbg_{version}_amd64.deb",
        ]
        for file in files:
            file.touch()

        package = BinaryPackages.create(
            srcpkg_name=srcpkg_name,
            srcpkg_version=srcpkg_version,
            version=version,
            architecture=architecture,
            files=files,
        )

        expected_data = data_models.DebianBinaryPackages(
            srcpkg_name=srcpkg_name,
            srcpkg_version=srcpkg_version,
            version=version,
            architecture=architecture,
            packages=["hello-traditional", "hello-dbg"],
        )
        expected_files = {
            files[0].name: files[0],
            files[1].name: files[1],
        }

        self.assertEqual(package.category, BinaryPackages._category)
        self.assertEqual(package.data, expected_data)
        self.assertEqual(package.files, expected_files)

    def test_files_must_end_in_deb_or_udeb(self):
        """files_must_end_in_deb_or_udeb() return the files."""
        files = {
            "hello_2.10-2_amd64.deb": None,
            "hello_2.10-2_amd64.udeb": None,
        }
        self.assertEqual(
            BinaryPackages.files_must_end_in_deb_or_udeb(files),
            files,
        )

    def test_files_must_end_in_deb_raise_value_error(self):
        """files_must_end_in_deb_or_udeb() raise ValueError."""
        filename = "README.txt"
        files = {filename: None}
        with self.assertRaisesRegex(
            ValueError,
            rf"^Valid file suffixes: \['.deb', '.udeb'\]. "
            rf"Invalid filename: \"{filename}\"$",
        ):
            BinaryPackages.files_must_end_in_deb_or_udeb(files)

    def test_files_more_than_zero(self):
        """files_more_than_zero() return the files."""
        files = {"hello.deb": None, "hello2.deb": None}

        self.assertEqual(BinaryPackages.files_more_than_zero(files), files)

    def test_files_more_than_zero_raise_value_error(self):
        """files_more_than_zero() raise ValueError: zero files."""
        with self.assertRaisesRegex(ValueError, "Must have at least one file"):
            BinaryPackages.files_more_than_zero({})

    def test_json_serializable(self):
        """Artifact returned by create is JSON serializable."""
        file = self.create_temporary_file(suffix=".deb")
        artifact = BinaryPackages.create(
            srcpkg_name="hello",
            srcpkg_version="2.10-2",
            version="2.10-2",
            architecture="amd64",
            files=[file],
        )

        # This is to verify that serialize_for_create_artifact() is
        # not returning some object that might raise an exception on
        # json.dumps. E.g. if it returned (as a value of the dictionary) a set()
        json.dumps(
            artifact.serialize_for_create_artifact(workspace="some workspace")
        )


class WorkRequestDebugLogsTests(TestCase):
    """Tests for WorkRequestDebugLogs class."""

    def test_create(self):
        """Test create(): return instance with expected files and category."""
        log_file_1 = self.create_temporary_file()
        log_file_2 = self.create_temporary_file()
        artifact = WorkRequestDebugLogs.create(files=[log_file_1, log_file_2])

        expected = WorkRequestDebugLogs(
            category=WorkRequestDebugLogs._category,
            data=data_models.EmptyArtifactData(),
        )
        expected.files = {
            log_file_1.name: log_file_1,
            log_file_2.name: log_file_2,
        }

        self.assertEqual(artifact, expected)

    def test_json_serializable(self):
        """Artifact returned by create is JSON serializable."""
        log_file_1 = self.create_temporary_file()
        log_file_2 = self.create_temporary_file()
        artifact = WorkRequestDebugLogs.create(files=[log_file_1, log_file_2])

        # This is to verify that serialize_for_create_artifact() is
        # not returning some object that might raise an exception on
        # json.dumps. E.g. if it returned (as a value of the dictionary) a set()
        json.dumps(
            artifact.serialize_for_create_artifact(workspace="some workspace")
        )


class SourcePackageTests(TestCase):
    """Tests for SourcePackage class."""

    def test_create(self):
        """Test create(): return expected instance."""
        file = self.create_temporary_file()
        dsc_file = self.create_temporary_file(suffix=".dsc")

        dsc_fields = self.write_dsc_example_file(dsc_file)

        name = dsc_fields["Source"]
        version = dsc_fields["Version"]

        expected_files = {file.name: file, dsc_file.name: dsc_file}
        expected_data = data_models.DebianSourcePackage(
            name=name,
            version=version,
            type="dpkg",
            dsc_fields=dsc_fields,
        )

        package = SourcePackage.create(
            name=name, version=version, files=[file, dsc_file]
        )

        self.assertEqual(package.category, SourcePackage._category)
        self.assertEqual(package.files, expected_files)
        self.assertEqual(package.data, expected_data)

    def test_files_no_contain_dsc_raise_value_error(self):
        """Raise ValueError: missing expected .dsc file."""
        file = self.create_temporary_file()

        files = {file.name: file}

        expected_message = fr"Expecting 1 \.dsc file in \['{file.name}'\]"
        with self.assertRaisesRegex(ValueError, expected_message):
            SourcePackage.files_contain_one_dsc(
                files,
            )

    def test_files_contains_dsc_return_files(self):
        """Return all files: .dsc is in the files."""
        file = self.create_temporary_file(suffix=".dsc")

        files = {file.name: file}

        self.assertEqual(
            SourcePackage.files_contain_one_dsc(
                files,
            ),
            files,
        )

    def test_files_contains_files_in_dsc_call_implementation(self):
        """Test files_contains_files_in_dsc call the utils method."""
        result = {"a": "b"}
        patcher = mock.patch(
            "debusine.artifacts.local_artifact.files_in_meta_file_match_files"
        )
        mocked = patcher.start()
        mocked.return_value = result
        self.addCleanup(patcher.stop)

        files = {}

        self.assertEqual(
            SourcePackage.files_contains_files_in_dsc(files), result
        )

        mocked.assert_called_with(".dsc", deb822.Dsc, files)

    def test_json_serializable(self):
        """Artifact returned by create is JSON serializable."""
        dir_ = self.create_temporary_directory()
        dsc_file = self.create_temporary_file(suffix=".dsc", directory=dir_)
        dsc_fields = self.write_dsc_example_file(dsc_file)
        name = dsc_fields["Source"]
        version = dsc_fields["Version"]
        files = [dsc_file]
        for file in dsc_fields["Files"]:
            (dir_ / file["name"]).write_bytes(b"")
            files.append(dir_ / file["name"])
        package = SourcePackage.create(name=name, version=version, files=files)

        # This is to verify that serialize_for_create_artifact() is
        # not returning some object that might raise an exception on
        # json.dumps. E.g. if it returned (as a value of the dictionary) a set()
        json.dumps(
            package.serialize_for_create_artifact(workspace="some workspace")
        )


class LintianTests(TestCase):
    """Tests for the LintianArtifact."""

    @classmethod
    def setUpClass(cls):
        """Set up common data for the tests."""
        cls.output_content = (
            b"W: python-ping3 source: missing-license-"
            b"paragraph-in-dep5-copyright gpl-3 [debian/copyright:33]\n"
            b"P: python-ping3 source: maintainer-manual-page [debian/ping3.1]\n"
        )
        cls.summary = {
            "tags_count_by_severity": {"error": 1, "warning": 1},
            "package_filename": {
                "binutils": "binutils_1.0.dsc",
                "cynthiune.app": "cynthiune.app_1.0_amd64.deb",
            },
            "tags_found": ["license-problem-convert-utf-code", "vcs-obsolete"],
            "overridden_tags_found": [],
            "lintian_version": "1.0.0",
            "distribution": "bookworm",
        }
        cls.analysis_content = json.dumps(
            {
                "tags": [
                    {
                        "package": "cynthiune.app",
                        "severity": "warning",
                        "tag": "vcs-obsolete",
                        "note": "",
                        "pointer": "",
                        "explanation": "",
                        "comment": "",
                    },
                    {
                        "package": "binutils",
                        "severity": "error",
                        "tag": "license-problem-convert-utf-code",
                        "note": "Cannot ls",
                        "pointer": "src/ls.c",
                        "explanation": "",
                        "comment": "",
                    },
                ]
            }
        ).encode("utf-8")

    def test_create(self):
        """Test create(): return expected class with the files."""
        output = self.create_temporary_file(
            suffix=".txt", contents=self.output_content
        )
        analysis = self.create_temporary_file(
            suffix=".json", contents=self.analysis_content
        )

        package = LintianArtifact.create(
            lintian_output=output, analysis=analysis, summary=self.summary
        )

        self.assertEqual(package._category, LintianArtifact._category)
        self.assertEqual(
            package.data, data_models.DebianLintian(summary=self.summary)
        )
        self.assertEqual(
            package.files,
            {
                "lintian.txt": output,
                "analysis.json": analysis,
            },
        )

    def test_validate_file_analysis_is_json_valid(self):
        """Test validate_file_analysis_is_json: return files."""
        analysis = self.create_temporary_file(contents=b"{}")
        files = {"analysis.json": analysis}

        self.assertEqual(
            LintianArtifact._validate_file_analysis_is_json(files),
            files,
        )

    def test_validate_file_analysis_json_raise_value_error(self):
        """Test validate_file_analysis_is_json: raise ValueError."""
        analysis = self.create_temporary_file(contents=b":")  # invalid

        msg = "^analysis.json is not valid JSON:"
        with self.assertRaisesRegex(ValueError, msg):
            LintianArtifact._validate_file_analysis_is_json(
                {"analysis.json": analysis}
            )

    def test_validate_required_files_valid(self):
        """Test validate_required_files: is valid. return files."""
        files = {"analysis.json": "", "lintian.txt": ""}

        self.assertEqual(LintianArtifact._validate_required_files(files), files)

    def test_validate_required_files_missing_file_raise_value_error(self):
        """Test validate_required_files: not valid, raise ValueError."""
        msg = r"^Files required: " r"\['analysis.json', 'lintian.txt'\]$"

        with self.assertRaisesRegex(ValueError, msg):
            LintianArtifact._validate_required_files({"summary.json": ""})

    def test_json_serializable(self):
        """Artifact returned by create is JSON serializable."""
        output = self.create_temporary_file(
            suffix=".txt", contents=self.output_content
        )
        analysis = self.create_temporary_file(
            suffix=".json", contents=self.analysis_content
        )
        artifact = LintianArtifact.create(
            lintian_output=output, analysis=analysis, summary=self.summary
        )

        # This is to verify that serialize_for_create_artifact() is
        # not returning some object that might raise an exception on
        # json.dumps. E.g. if it returned (as a value of the dictionary) a set()
        json.dumps(
            artifact.serialize_for_create_artifact(workspace="some workspace")
        )


class AutopkgtestArtifactTests(TestCase):
    """Tests for AutopkgtestArtifact class."""

    def setUp(self):
        """Set up test."""
        self.data_contents = {
            "results": {
                "test": {
                    "status": "PASS",
                    "details": "test details",
                },
            },
            "cmdline": "/bin/true",
            "source_package": {
                "name": "hello",
                "version": "1.0",
                "url": "https://example.org/hello.deb",
            },
            "architecture": "amd64",
            "distribution": "debian:sid",
        }

    def test_create(self):
        """Test _create()."""
        directory = self.create_temporary_directory()

        # Create files in directory and subdirectories: to be included in
        # the package
        (summary_file := directory / "summary").write_text("summary content")

        (subdir := directory / "subdir").mkdir()
        (file_in_subdir := subdir / "some-file.txt").write_text("test")

        (binaries_subdir := directory / "binaries").mkdir()

        # Files in "binaries/" are not part of the artifact
        (binaries_subdir / "log.txt").write_text("log file")
        (binaries_subdir / "pkg.deb").write_text("deb")

        package = AutopkgtestArtifact.create(
            directory, data_models.DebianAutopkgtest(**self.data_contents)
        )

        self.assertIsInstance(package, AutopkgtestArtifact)
        self.assertEqual(
            package.data, data_models.DebianAutopkgtest(**self.data_contents)
        )

        self.assertEqual(
            package.files,
            {
                "subdir/some-file.txt": file_in_subdir,
                "summary": summary_file,
            },
        )

    def test_json_serializable(self):
        """Artifact returned by create is JSON serializable."""
        directory = self.create_temporary_directory()
        (directory / "summary").write_text("summary content")

        artifact = AutopkgtestArtifact.create(
            directory, data_models.DebianAutopkgtest(**self.data_contents)
        )

        # This is to verify that serialize_for_create_artifact() is
        # not returning some object that might raise an exception on
        # json.dumps. E.g. if it returned (as a value of the dictionary) a set()
        json.dumps(
            artifact.serialize_for_create_artifact(workspace="some workspace")
        )


class DebianSystemTarballArtifactTests(TestCase):
    """Tests for DebianSystemTarballArtifactTests."""

    def test_create(self):
        """Test _create()."""
        system_tar_xz = self.create_temporary_file(
            suffix=".tar.xz", contents=b"some-file-contents"
        )

        artifact = DebianSystemTarballArtifact.create(
            system_tar_xz,
            data={
                "vendor": "debian",
                "codename": "bookworm",
                "mirror": "https://deb.debian.org",
                "variant": "minbase",
                "pkglist": [],
                "architecture": "amd64",
                "with_dev": True,
                "with_init": True,
            },
        )

        self.assertEqual(artifact.files, {system_tar_xz.name: system_tar_xz})
        self.assertEqual(artifact.data.filename, system_tar_xz.name)

    def test_validate_file_name_ends_in_tar_xz(self):
        """validate_file_name_ends_in_tar_xz is valid: return files."""
        system_tar_xz = self.create_temporary_file(
            suffix=".tar.xz", contents=b"some-file-contents"
        )

        files = {system_tar_xz.name: system_tar_xz}

        self.assertEqual(
            DebianSystemTarballArtifact._validate_file_name_ends_in_tar_xz(
                files
            ),
            files,
        )

    def test_validate_file_name_ends_in_tar_xz_two_files_raise(self):
        """validate_file_name_ends_in_tar_xz raise ValueError with two files."""
        file1 = self.create_temporary_file(
            suffix="temp", contents=b"some-file-contents"
        )

        file2 = self.create_temporary_file(
            suffix="temp", contents=b"some-file-contents"
        )

        files = {file1.name: file1, file2.name: file2}

        msg = "DebianSystemTarballArtifact does not contain exactly one file"
        with self.assertRaisesRegex(ValueError, msg):
            DebianSystemTarballArtifact._validate_file_name_ends_in_tar_xz(
                files
            )

    def test_validate_file_name_ends_in_tar_xz_not_valid_raise(self):
        """validate_file_name_ends_in_tar_xz raise ValueError."""
        file = self.create_temporary_file(
            suffix="temp", contents=b"some-file-contents"
        )

        files = {file.name: file}

        msg = f"Invalid file name: '{file.name}'. Expected .tar.xz"
        with self.assertRaisesRegex(ValueError, msg):
            DebianSystemTarballArtifact._validate_file_name_ends_in_tar_xz(
                files
            )


class BlhcArtifactTests(TestCase):
    """Tests for BlhcArtifact class."""

    @classmethod
    def setUpClass(cls):
        """Set up common data for the tests."""
        cls.output_content = (
            b"LDFLAGS missing (-fPIE -pie): /usr/bin/c++  -g -O2 -fstack-"
            b"protector-strong -Wformat -Werror=format-security -Wdate-time "
            b"-D_FORTIFY_SOURCE=2  -Wl,-z,relro printf-test.cc.o "
            b"-o ../bin/printf-test\n"
        )

    def test_create(self):
        """Test _create()."""
        output = self.create_temporary_file(
            suffix=".txt", contents=self.output_content
        )

        package = BlhcArtifact.create(blhc_output=output)

        self.assertIsInstance(package, BlhcArtifact)
        self.assertEqual(package.data, data_models.EmptyArtifactData())

        self.assertEqual(
            package.files,
            {
                "blhc.txt": output,
            },
        )

    def test_json_serializable(self):
        """Artifact returned by create is JSON serializable."""
        """upload_artifact() and relation_create() is called."""
        # Create file for the artifact
        blhc_output = self.create_temporary_file(
            suffix=".txt", contents=b"NONVERBOSE BUILD: Compiling gkrust\n"
        )

        artifact = BlhcArtifact.create(blhc_output=blhc_output)

        # No exception is raised
        json.dumps(
            artifact.serialize_for_create_artifact(workspace="some-workspace")
        )


class DebianSystemImageArtifactTests(TestCase):
    """Tests for DebianSystemImageArtifactTests."""

    def test_create_tar_xz(self):
        """Test _create() with .tar.xz."""
        system_tar_xz = self.create_temporary_file(
            suffix=".tar.xz", contents=b"some-file-contents"
        )

        artifact = DebianSystemImageArtifact.create(
            system_tar_xz,
            {
                "image_format": "raw",
                "boot_mechanism": "efi",
                "vendor": "debian",
                "codename": "sid",
                "mirror": "https://deb.debian.org",
                "filesystem": "ext4",
                "size": 12345,
                "pkglist": {},
                "architecture": "amd64",
                "with_dev": True,
                "with_init": True,
            },
        )

        self.assertEqual(artifact.files, {system_tar_xz.name: system_tar_xz})
        self.assertEqual(artifact.data.filename, system_tar_xz.name)

    def test_create_qcow2(self):
        """Test _create() with .qcow2."""
        system_qcow2 = self.create_temporary_file(
            suffix=".qcow2", contents=b"some-file-contents"
        )

        artifact = DebianSystemImageArtifact.create(
            system_qcow2,
            {
                "image_format": "qcow2",
                "boot_mechanism": "efi",
                "vendor": "debian",
                "codename": "sid",
                "mirror": "https://deb.debian.org",
                "filesystem": "ext4",
                "size": 12345,
                "pkglist": {},
                "architecture": "amd64",
                "with_dev": True,
                "with_init": True,
            },
        )

        self.assertEqual(artifact.files, {system_qcow2.name: system_qcow2})
        self.assertEqual(artifact.data.filename, system_qcow2.name)

    def test_validate_file_name_ending_tar_xz(self):
        """validate_files is .tar.xz: return files."""
        system_tar_xz = self.create_temporary_file(
            suffix=".tar.xz", contents=b"some-file-contents"
        )

        files = {system_tar_xz.name: system_tar_xz}

        self.assertEqual(
            DebianSystemImageArtifact._validate_files(files),
            files,
        )

    def test_validate_file_name_ending_qcow2(self):
        """validate_files is .qcow2: return files."""
        system_qcow2 = self.create_temporary_file(
            suffix=".qcow2", contents=b"some-file-contents"
        )

        files = {system_qcow2.name: system_qcow2}

        self.assertEqual(
            DebianSystemImageArtifact._validate_files(files),
            files,
        )

    def test_validate_two_files(self):
        """validate_files raise ValueError if not one file."""
        file1 = self.create_temporary_file(
            suffix="temp", contents=b"some-file-contents"
        )

        file2 = self.create_temporary_file(
            suffix="temp", contents=b"some-file-contents"
        )

        files = {file1.name: file1, file2.name: file2}

        msg = "DebianSystemImageArtifact does not contain exactly one file"
        with self.assertRaisesRegex(ValueError, msg):
            DebianSystemImageArtifact._validate_files(files)

    def test_validate_file_name_ending_not_valid_raise(self):
        """validate_files raise ValueError with other file ending."""
        file = self.create_temporary_file(
            suffix="temp", contents=b"some-file-contents"
        )

        files = {file.name: file}

        msg = f"Invalid file name: '{file.name}'. Expected .tar.xz or qcow2"
        with self.assertRaisesRegex(ValueError, msg):
            DebianSystemImageArtifact._validate_files(files)


class SigningKeyArtifactTests(TestCase):
    """Tests for SigningKeyArtifact."""

    def test_create(self) -> None:
        """`create` correctly encodes the given parameters."""
        artifact = SigningKeyArtifact.create(
            data_models.KeyPurpose.UEFI, "0" * 64, b"a public key"
        )

        self.assertEqual(
            artifact.data,
            {
                "purpose": data_models.KeyPurpose.UEFI,
                "fingerprint": "0" * 64,
                "public_key": base64.b64encode(b"a public key").decode(),
            },
        )
        self.assertEqual(artifact.public_key_bytes, b"a public key")


class SigningInputArtifactTests(TestCase):
    """Tests for SigningInputArtifact."""

    def test_create(self) -> None:
        """Create a valid SigningInputArtifact."""
        base_dir = self.create_temporary_directory()
        unsigned = base_dir / "package" / "dir" / "file"
        unsigned.parent.mkdir(parents=True)
        unsigned.write_text("contents")

        artifact = SigningInputArtifact.create([unsigned], base_dir)

        artifact.validate_model()
        self.assertEqual(artifact.data, {"trusted_certs": None})
        self.assertEqual(artifact.files, {"package/dir/file": unsigned})

    def test_create_trusted_certs(self):
        """Create a SigningInputArtifact with trusted_certs."""
        base_dir = self.create_temporary_directory()
        unsigned_1 = base_dir / "package" / "dir" / "file"
        unsigned_1.parent.mkdir(parents=True)
        unsigned_1.write_text("contents")
        unsigned_2 = base_dir / "package" / "another-dir" / "another-file"
        unsigned_2.parent.mkdir(parents=True)
        unsigned_2.write_text("other-contents")

        artifact = SigningInputArtifact.create(
            [unsigned_1, unsigned_2], base_dir, trusted_certs=["0" * 64]
        )

        artifact.validate_model()
        self.assertEqual(artifact.data, {"trusted_certs": ["0" * 64]})
        self.assertEqual(
            artifact.files,
            {
                "package/dir/file": unsigned_1,
                "package/another-dir/another-file": unsigned_2,
            },
        )

    def test_create_no_files(self):
        """A SigningInputArtifact must have at least one file."""
        base_dir = self.create_temporary_directory()

        artifact = SigningInputArtifact.create([], base_dir)

        with self.assertRaisesRegex(ValueError, "Expected at least one file"):
            artifact.validate_model()


class SigningOutputArtifactTests(TestCase):
    """Tests for SigningOutputArtifact."""

    def test_create(self) -> None:
        """Create a valid SigningOutputArtifact."""
        base_dir = self.create_temporary_directory()
        output_file = base_dir / "package" / "dir" / "file.sig"
        output_file.parent.mkdir(parents=True)
        output_file.write_text("signature")
        result = data_models.SigningResult(
            file="package/dir/file", output_file="package/dir/file.sig"
        )

        artifact = SigningOutputArtifact.create(
            data_models.KeyPurpose.UEFI,
            "0" * 64,
            [result],
            [output_file],
            base_dir,
        )

        artifact.validate_model()
        self.assertEqual(
            artifact.data,
            {
                "purpose": data_models.KeyPurpose.UEFI,
                "fingerprint": "0" * 64,
                "results": [result],
            },
        )
        self.assertEqual(artifact.files, {"package/dir/file.sig": output_file})
