# 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 classes in models.py."""
from typing import Any
from unittest import TestCase

import debusine.artifacts.models as data_models

try:
    import pydantic.v1 as pydantic
except ImportError:
    import pydantic as pydantic  # type: ignore


class EnumTests(TestCase):
    """Tests for model enums."""

    def test_enums_str(self) -> None:
        """Test enum stringification."""
        for enum_cls in (
            data_models.DebianAutopkgtestResultStatus,
            data_models.DebianLintianSeverity,
        ):
            with self.subTest(enum_cls=enum_cls):
                for el in enum_cls:
                    with self.subTest(el=el):
                        self.assertEqual(str(el), el.value)


class EmptyArtifactDataTest(TestCase):
    """Test the EmptyArtifactData model."""

    def test_get_label(self) -> None:
        """get_label returns None."""
        data = data_models.EmptyArtifactData()
        label = data.get_label()  # type: ignore[func-returns-value]
        self.assertIsNone(label)


class DebianPackageBuildLogTest(TestCase):
    """Test the DebianPackageBuildLog model."""

    def test_get_label(self) -> None:
        """get_label returns source."""
        data = data_models.DebianPackageBuildLog(
            source="test", version="1.0-1", filename="test_1.0-1_amd64.buildlog"
        )
        self.assertEqual(data.get_label(), "test_1.0-1_amd64.buildlog")


class DebianSourcePackageTests(TestCase):
    """Test the DebianSourcePackage model."""

    def test_construct(self) -> None:
        """Test model constructor."""
        data = data_models.DebianSourcePackage(
            name="test-name",
            version="test-version",
            type="dpkg",
            dsc_fields={"key": "val"},
        )

        self.assertEqual(data.name, "test-name")
        self.assertEqual(data.version, "test-version")
        self.assertEqual(data.type, "dpkg")
        self.assertEqual(data.dsc_fields, {"key": "val"})
        self.assertEqual(data.get_label(), "test-name_test-version")

    def test_dsc_fields_missing(self) -> None:
        """Test checking that dsc_fields is present."""
        error_msg = r"dsc_fields\s+field required \(type=value_error\.missing\)"

        with self.assertRaisesRegex(Exception, error_msg):
            data_models.DebianSourcePackage(  # type: ignore[call-arg]
                name="test-name", version="test-version", type="dpkg"
            )


class DebianUploadTests(TestCase):
    """Test the DebianUpload model."""

    def test_label(self) -> None:
        """Test get_label scenarios."""

        def make_upload(
            files: list[str], **kwargs: Any
        ) -> data_models.DebianUpload:
            kwargs["Files"] = [{"name": name} for name in files]
            return data_models.DebianUpload(
                type="dpkg",
                changes_fields={"Architecture": "amd64", **kwargs},
            )

        self.assertEqual(
            make_upload(
                ["test.deb"], Source="test", Version="1.0-1"
            ).get_label(),
            "test_1.0-1",
        )
        self.assertEqual(
            make_upload(["test.deb"], Source="test").get_label(),
            "test.deb",
        )
        self.assertEqual(
            make_upload(["test.deb", "test.changes"]).get_label(),
            "test.changes",
        )
        self.assertEqual(
            make_upload(["test2.deb", "test1.deb"]).get_label(),
            "test2.deb",
        )
        upload = make_upload(["test.deb"])
        upload.changes_fields["Files"] = []
        self.assertIsNone(upload.get_label())

    def test_metadata_contains_architecture(self) -> None:
        """changes_fields must contain Architecture."""
        data = {
            "Files": [{"name": "foo.dsc"}],
        }
        with self.assertRaisesRegex(
            ValueError, r"changes_fields must contain Architecture"
        ):
            data_models.DebianUpload(type="dpkg", changes_fields=data)

    def test_metadata_contains_files(self) -> None:
        """changes_fields must contain Architecture."""
        data = {
            "Architecture": "amd64",
        }
        with self.assertRaisesRegex(
            ValueError, r"changes_fields must contain Files"
        ):
            data_models.DebianUpload(type="dpkg", changes_fields=data)

    def test_metadata_contains_debs_if_binary_requires_debs_for_binaries(
        self,
    ) -> None:
        """metadata_contains_debs_if_binary requires at least one .deb."""
        data = {
            "Architecture": "amd64",
            "Files": [{"name": "foo.dsc"}],
        }
        with self.assertRaisesRegex(
            ValueError,
            r"No \.debs found in \['foo\.dsc'\] which is expected to contain "
            r"binaries for amd64",
        ):
            data_models.DebianUpload.metadata_contains_debs_if_binary(data)

    def test_metadata_contains_debs_if_binary_ignores_source_uploads(
        self,
    ) -> None:
        """metadata_contains_debs_if_binary ignores source uploads."""
        data = {
            "Architecture": "source",
            "Files": [{"name": "foo.dsc"}],
        }
        self.assertEqual(
            data_models.DebianUpload.metadata_contains_debs_if_binary(data),
            data,
        )

    def test_metadata_contains_debs_if_binary_finds_debs_in_source_uploads(
        self,
    ) -> None:
        """metadata_contains_debs_if_binary finds debs in source uploads."""
        data = {
            "Architecture": "source",
            "Files": [{"name": "foo.deb"}],
        }
        with self.assertRaisesRegex(
            ValueError,
            r"Unexpected binary packages \['foo\.deb'\] found in source-only "
            r"upload\.",
        ):
            data_models.DebianUpload.metadata_contains_debs_if_binary(data)

    def test_metadata_contains_debs_if_binary_accepts_debs(self) -> None:
        """metadata_contains_debs_if_binary will accept one .deb."""
        data = {
            "Architecture": "amd64",
            "Files": [{"name": "foo_amd64.deb"}],
        }
        self.assertEqual(
            data,
            data_models.DebianUpload.metadata_contains_debs_if_binary(data),
        )

    def test_metadata_contains_debs_if_binary_accepts_debs_in_mixed(
        self,
    ) -> None:
        """metadata_contains_debs_if_binary will accept deb in mixed upload."""
        data = {
            "Architecture": "amd64 source",
            "Files": [{"name": "foo_amd64.deb"}],
        }
        self.assertEqual(
            data,
            data_models.DebianUpload.metadata_contains_debs_if_binary(data),
        )

    def test_metadata_contains_dsc_if_source_requires_1_dsc_for_source(
        self,
    ) -> None:
        """metadata_contains_dsc_if_source requires 1 .dsc."""
        data = {
            "Architecture": "source",
            "Files": [{"name": "foo.deb"}],
        }
        with self.assertRaisesRegex(
            ValueError,
            r"Expected to find one and only one source package in source "
            r"upload\. Found \[\].",
        ):
            data_models.DebianUpload.metadata_contains_dsc_if_source(data)

    def test_metadata_contains_dsc_if_source_rejects_2_dsc_for_source(
        self,
    ) -> None:
        """metadata_contains_dsc_if_source rejects 2 .dscs."""
        data = {
            "Architecture": "source",
            "Files": [{"name": "foo.dsc"}, {"name": "bar.dsc"}],
        }
        with self.assertRaisesRegex(
            ValueError,
            r"Expected to find one and only one source package in source "
            r"upload\. Found \['foo\.dsc', 'bar\.dsc'\]\.",
        ):
            data_models.DebianUpload.metadata_contains_dsc_if_source(data)

    def test_metadata_contains_dsc_if_source_ignores_binary_uploads(
        self,
    ) -> None:
        """metadata_contains_dsc_if_source ignores binary uploads."""
        data = {
            "Architecture": "amd64",
            "Files": [{"name": "foo.deb"}],
        }
        self.assertEqual(
            data_models.DebianUpload.metadata_contains_dsc_if_source(data), data
        )

    def test_metadata_contains_dsc_if_source_finds_dsc_in_bin_uploads(
        self,
    ) -> None:
        """metadata_contains_dsc_if_source finds dsc in binary uploads."""
        data = {
            "Architecture": "amd64",
            "Files": [{"name": "foo.dsc"}],
        }
        with self.assertRaisesRegex(
            ValueError,
            r"Binary uploads cannot contain source packages. "
            r"Found: \['foo\.dsc'\].",
        ):
            data_models.DebianUpload.metadata_contains_dsc_if_source(data)

    def test_metadata_contains_dsc_if_source_accepts_1_dsc(self) -> None:
        """metadata_contains_dsc_if_source will accept one .dsc."""
        data = {
            "Architecture": "source",
            "Files": [{"name": "foo.dsc"}],
        }
        self.assertEqual(
            data, data_models.DebianUpload.metadata_contains_dsc_if_source(data)
        )

    def test_metadata_contains_dsc_if_source_accepts_dsc_in_mixed(self) -> None:
        """metadata_contains_dsc_if_source accepts source in mixed upload."""
        data = {
            "Architecture": "amd64 source",
            "Files": [{"name": "foo.dsc"}],
        }
        self.assertEqual(
            data, data_models.DebianUpload.metadata_contains_dsc_if_source(data)
        )


class DebianBinaryPackageTest(TestCase):
    """Test the DebianBinaryPackage model."""

    def test_get_label(self) -> None:
        """get_label returns srcpkg_name."""
        data = data_models.DebianBinaryPackage(
            srcpkg_name="test",
            srcpkg_version="1.0-1",
            deb_fields={
                "Package": "test-bin",
                "Version": "1.0-1+b1",
                "Architecture": "amd64",
            },
            deb_control_files=[],
        )
        self.assertEqual(data.get_label(), "test-bin_1.0-1+b1_amd64")


class DebianBinaryPackagesTest(TestCase):
    """Test the DebianBinaryPackages model."""

    def test_get_label(self) -> None:
        """get_label returns srcpkg_name."""
        data = data_models.DebianBinaryPackages(
            srcpkg_name="test",
            srcpkg_version="1.0-1",
            version="1.0",
            architecture="amd64",
            packages=[],
        )
        self.assertEqual(data.get_label(), "test_1.0-1")


class DebianSystemTarballTest(TestCase):
    """Test the DebianSystemTarball model."""

    def test_get_label(self) -> None:
        """get_label returns filename."""
        data = data_models.DebianSystemTarball(
            filename="bookworm.tar.gz",
            vendor="debian",
            codename="bookworm",
            mirror=pydantic.parse_obj_as(
                pydantic.AnyUrl, "https://deb.debian.org"
            ),
            variant=None,
            pkglist={},
            architecture="amd64",
            with_dev=False,
            with_init=False,
        )
        self.assertEqual(data.get_label(), "bookworm.tar.gz")


class DebianSystemImageTest(TestCase):
    """Test the DebianSystemImage model."""

    def test_get_label(self) -> None:
        """get_label returns filename."""
        data = data_models.DebianSystemImage(
            filename="bookworm.qcow2",
            vendor="debian",
            codename="bookworm",
            mirror=pydantic.parse_obj_as(
                pydantic.AnyUrl, "https://deb.debian.org"
            ),
            variant=None,
            pkglist={},
            architecture="amd64",
            with_dev=False,
            with_init=False,
            image_format="qcow2",
            filesystem="ext4",
            size=123456,
            boot_mechanism="efi",
        )
        self.assertEqual(data.get_label(), "bookworm.qcow2")


class DebianLintianTest(TestCase):
    """Test the DebianLintian model."""

    def test_get_label(self) -> None:
        """get_label returns shortest package name."""

        def make_lintian(**kwargs: str) -> data_models.DebianLintian:
            return data_models.DebianLintian(
                summary=data_models.DebianLintianSummary(
                    tags_count_by_severity={},
                    package_filename=kwargs,
                    tags_found=[],
                    overridden_tags_found=[],
                    lintian_version="1.0",
                    distribution="bookworm",
                )
            )

        self.assertEqual(
            make_lintian(hello="hello_1.0-1.dsc").get_label(), "hello"
        )
        self.assertEqual(
            make_lintian(
                libhello="libhello_1.0-1.deb", hello="hello_1.0-1.deb"
            ).get_label(),
            "hello",
        )


class DebianAutopkgtestTest(TestCase):
    """Test the DebianAutopkgtest model."""

    def test_get_label(self) -> None:
        """get_label returns source_package.name."""
        data = data_models.DebianAutopkgtest(
            results={},
            cmdline="",
            source_package=data_models.DebianAutopkgtestSource(
                name="hello",
                version="1.0-1",
                url=pydantic.parse_obj_as(
                    pydantic.AnyUrl, "https://deb.debian.org/pool/h/hello"
                ),
            ),
            architecture="amd64",
            distribution="bookworm",
        )
        self.assertEqual(data.get_label(), "hello")


class DebusineSigningKeyTest(TestCase):
    """Test the DebusineSigningKey model."""

    def test_get_label(self) -> None:
        """get_label returns fingerprint."""
        data = data_models.DebusineSigningKey(
            purpose=data_models.KeyPurpose.UEFI,
            fingerprint="123456ABCD",
            public_key="",
        )
        self.assertEqual(data.get_label(), "123456ABCD")


class DebusineSigningInputTest(TestCase):
    """Test the DebusineSigningInput model."""

    def test_get_label(self) -> None:
        """get_label returns None."""
        data = data_models.DebusineSigningInput(trusted_certs=[])
        label = data.get_label()  # type: ignore[func-returns-value]
        self.assertIsNone(label)


class DebusinePromiseTests(TestCase):
    """Test the DebusinePromise."""

    def test_construct(self) -> None:
        """Test the model constructor with valid data."""
        data = data_models.DebusinePromise(
            promise_work_request_id=1,
            promise_workflow_id=100,
            promise_category="debian:binary-package",
        )

        self.assertEqual(data.promise_work_request_id, 1)
        self.assertEqual(data.promise_workflow_id, 100)
        self.assertEqual(data.promise_category, "debian:binary-package")

    def test_extra_fields_allowed(self) -> None:
        """Test that extra fields are allowed in the model."""
        data = data_models.DebusinePromise(
            promise_work_request_id=1,
            promise_workflow_id=100,
            promise_category="debian:binary-package",
            extra_field="extra_value",  # type: ignore
        )

        self.assertEqual(data.extra_field, "extra_value")  # type: ignore

    def test_forbidden_promise_prefix_in_extra_field(self) -> None:
        r"""Test that extra fields with 'promise\\_' are not allowed."""
        expected_msg = (
            "Field name 'promise_extra' starting with 'promise_' is not allowed"
        )
        with self.assertRaisesRegex(ValueError, expected_msg):
            data_models.DebusinePromise(
                promise_work_request_id=1,
                promise_workflow_id=100,
                promise_category="debian:binary-package",
                promise_extra="not_allowed",  # type: ignore
            )


class SigningResultTests(TestCase):
    """Test the SigningResult model."""

    def test_output_file(self) -> None:
        """A SigningResult may have an output_file and no error_message."""
        data_models.SigningResult(file="file", output_file="file.sig")

    def test_error_message(self) -> None:
        """A SigningResult may have an error_message and no output_file."""
        data_models.SigningResult(file="file", error_message="Boom")

    def test_both_output_file_and_error_message(self) -> None:
        """A SigningResult may not have both output_file and error_message."""
        with self.assertRaisesRegex(
            ValueError,
            "Exactly one of output_file and error_message must be set",
        ):
            data_models.SigningResult(
                file="file", output_file="file.sig", error_message="Boom"
            )

    def test_neither_output_file_nor_error_message(self) -> None:
        """A SigningResult must have either output_file or error_message."""
        with self.assertRaisesRegex(
            ValueError,
            "Exactly one of output_file and error_message must be set",
        ):
            data_models.SigningResult(file="file")


class DebusineSigningOutputTest(TestCase):
    """Test the DebusineSigningOutput model."""

    def test_get_label(self) -> None:
        """get_label returns None."""
        data = data_models.DebusineSigningOutput(
            purpose=data_models.KeyPurpose.UEFI,
            fingerprint="123456ABCD",
            results=[],
        )
        label = data.get_label()  # type: ignore[func-returns-value]
        self.assertIsNone(label)


class DebDiffTest(TestCase):
    """Test the DebDiff model."""

    def test_get_label(self) -> None:
        """get_label returns the debdiff command."""
        data = data_models.DebDiff(
            original="original-package",
            new="new-package",
        )
        self.assertEqual(
            data.get_label(), "debdiff original-package new-package"
        )
