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

import re
from contextlib import ExitStack
from typing import Any
from unittest import TestCase

import debusine.artifacts.models as data_models
from debusine.assets.models import KeyPurpose


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=repr(enum_cls)):
                for el in enum_cls:
                    with self.subTest(el=el.value):
                        self.assertEqual(str(el), el.value)


class DebusineHistoricalTaskRunTests(TestCase):
    """Test the DebusineHistoricalTaskRun model."""

    def test_task_type_validation(self) -> None:
        """task_type must be a valid task type."""
        kwargs: dict[str, Any] = {
            "task_name": "noop",
            "subject": "subject",
            "context": "sid/amd64",
            "timestamp": 0,
            "work_request_id": 1,
            "result": "success",
            "runtime_statistics": data_models.RuntimeStatistics(duration=1),
        }
        data_models.DebusineHistoricalTaskRun(
            task_type=data_models.TaskTypes.WORKER, **kwargs
        )
        with self.assertRaisesRegex(ValueError, r"Input should be.*type=enum"):
            data_models.DebusineHistoricalTaskRun(
                task_type="does-not-exist", **kwargs
            )


class DebusineTaskConfigurationTests(TestCase):
    """Test the DebusineTaskConfiguration model."""

    def test_name(self) -> None:
        """Test name generation."""
        data = data_models.DebusineTaskConfiguration(template="test")
        self.assertEqual(data.name(), "template:test")

        data = data_models.DebusineTaskConfiguration(
            task_type=data_models.TaskTypes.WORKER,
            task_name="noop",
            subject="subject",
            context="sid/amd64",
        )
        self.assertEqual(data.name(), "Worker:noop:subject:sid/amd64")

        data = data_models.DebusineTaskConfiguration(
            task_type=data_models.TaskTypes.WORKER,
            task_name="noop",
            subject="subject",
            context="source:debusine",
        )
        self.assertEqual(data.name(), "Worker:noop:subject:source%3Adebusine")

    def test_task_type_validation(self) -> None:
        """task_type must be a valid task type."""
        kwargs: dict[str, Any] = {
            "task_name": "noop",
            "subject": "subject",
            "context": "sid/amd64",
        }
        data_models.DebusineTaskConfiguration(
            task_type=data_models.TaskTypes.WORKER, **kwargs
        )
        with self.assertRaisesRegex(ValueError, r"Input should be.*type=enum"):
            data_models.DebusineTaskConfiguration(
                task_type="does-not-exist", **kwargs
            )
        with self.assertRaisesRegex(ValueError, "should be set"):
            data_models.DebusineTaskConfiguration(task_type=None, **kwargs)
        with self.assertRaisesRegex(ValueError, "should be set"):
            data_models.DebusineTaskConfiguration(**kwargs)

    def test_task_name_validation(self) -> None:
        """Task names must not contain colons."""
        kwargs: dict[str, Any] = {
            "task_type": data_models.TaskTypes.WORKER,
            "subject": "subject",
            "context": "sid/amd64",
        }
        data_models.DebusineTaskConfiguration(task_name="noop", **kwargs)
        with self.assertRaisesRegex(ValueError, "may not contain ':'"):
            data_models.DebusineTaskConfiguration(task_name="no:op", **kwargs)
        with self.assertRaisesRegex(ValueError, "should be set"):
            data_models.DebusineTaskConfiguration(task_name=None, **kwargs)
        with self.assertRaisesRegex(ValueError, "should be set"):
            data_models.DebusineTaskConfiguration(**kwargs)

    def test_subject_validation(self) -> None:
        """Subject must not contain colons."""
        kwargs: dict[str, Any] = {
            "task_type": data_models.TaskTypes.WORKER,
            "task_name": "noop",
            "context": "sid/amd64",
        }
        data_models.DebusineTaskConfiguration(subject="subject", **kwargs)
        data_models.DebusineTaskConfiguration(subject=None, **kwargs)
        data_models.DebusineTaskConfiguration(**kwargs)
        with self.assertRaisesRegex(ValueError, "may not contain ':'"):
            data_models.DebusineTaskConfiguration(subject="sub:ject", **kwargs)

    def test_context_validation(self) -> None:
        """Context must not contain colons."""
        kwargs: dict[str, Any] = {
            "task_type": data_models.TaskTypes.WORKER,
            "task_name": "noop",
            "subject": "subject",
        }
        data_models.DebusineTaskConfiguration(
            context="source:debusine", **kwargs
        )
        data_models.DebusineTaskConfiguration(context="sid/amd64", **kwargs)
        data_models.DebusineTaskConfiguration(context="a b c", **kwargs)
        data_models.DebusineTaskConfiguration(context=None, **kwargs)
        data_models.DebusineTaskConfiguration(**kwargs)

    def test_template_excludes_task_match(self) -> None:
        """Template excludes the use of task match fields."""
        for name, value in (
            ("task_type", data_models.TaskTypes.WORKER),
            ("task_name", "noop"),
            ("subject", "subject"),
            ("context", "sid/amd64"),
        ):
            with (
                self.subTest(name=name),
                self.assertRaisesRegex(
                    ValueError, "should be empty if template is set"
                ),
            ):
                kwargs: dict[str, Any] = {name: value}
                data_models.DebusineTaskConfiguration(template="test", **kwargs)

    def test_get_lookup_names(self) -> None:
        """Test get_lookup_names."""
        f = data_models.DebusineTaskConfiguration.get_lookup_names
        for args, expected in (
            (("t", "n", None, None), ["t:n::"]),
            (("t", "n", "s", None), ["t:n::", "t:n:s:"]),
            (("t", "n", None, "c"), ["t:n::", "t:n::c"]),
            (("t", "n", "s", "c"), ["t:n::", "t:n::c", "t:n:s:", "t:n:s:c"]),
            (
                ("t", "n", "s", ":"),
                ["t:n::", "t:n::%3A", "t:n:s:", "t:n:s:%3A"],
            ),
        ):
            with self.subTest(args=args):
                self.assertEqual(f(*args), expected)

    def test_path_validation_succeeds(self) -> None:
        for path in (
            None,
            "foo.yaml",
            "foo/bar.yaml",
            "foo..bar.yaml",
            ".foo/bar.yaml",
            "..foo.yaml",
            "foo...yaml",
        ):
            with self.subTest(path=path):
                item = data_models.DebusineTaskConfiguration(
                    template="test", path=path
                )
                self.assertEqual(item.path, path)

    def test_path_validation_fails(self) -> None:
        absolute = "must not be an absolute path"
        not_yaml = r"must end with \.yaml or \.yml"
        single_dot = r"must not contain '\.' as a path element"
        double_dot = r"must not contain '\.\.' as a path element"

        for path, msg in (
            ("", not_yaml),
            ("/", not_yaml),
            ("/foo", not_yaml),
            ("/foo.yaml", absolute),
            ("./", not_yaml),
            ("./foo.yaml", single_dot),
            ("foo/./bar.yaml", single_dot),
            ("foo/.", not_yaml),
            ("foo.yaml/", not_yaml),
            ("../foo.yaml", double_dot),
            ("foo/bar/../baz.yaml", double_dot),
            ("foo/bar/baz/..", not_yaml),
        ):
            with (
                self.subTest(path=path),
                self.assertRaisesRegex(
                    ValueError, rf"{re.escape(repr(path))} {msg}"
                ),
            ):
                data_models.DebusineTaskConfiguration(
                    template="test", path=path
                )

    def test_path_position_length(self) -> None:
        for path, pos, validates in (
            ("x.yaml", 0, True),
            ("x.yaml", 12345678901234567890, True),
            ("x" * 248 + ".yaml", 0, True),
            ("x" * 249 + ".yaml", 1, False),
            ("x" * 248 + ".yaml", 10, False),
            ("x" * 245 + ".yaml", 1000, True),
            ("x" * 245 + ".yaml", 10000, False),
        ):
            with self.subTest(path=path, pos=pos), ExitStack() as stack:
                if not validates:
                    stack.enter_context(
                        self.assertRaisesRegex(
                            ValueError,
                            r"path:position string .+ characters"
                            " and must be no longer than 255",
                        )
                    )
                data_models.DebusineTaskConfiguration(
                    template="test", path=path, position=pos
                )

    def test_taglist_tagexpr_validation(self) -> None:
        for value, valid in (
            ([], True),
            (["a"], True),
            ("", False),
            ("a", True),
            ("a or b", True),
            ("a or", False),
        ):
            with self.subTest(value=value), ExitStack() as stack:
                if not valid:
                    stack.enter_context(
                        self.assertRaisesRegex(
                            ValueError, "invalid tag expression:"
                        )
                    )
                data_models.DebusineTaskConfiguration(
                    template="test", tags_provided=value
                )
                data_models.DebusineTaskConfiguration(
                    template="test", tags_required=value
                )


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

    def test_get_label(self) -> None:
        """get_label returns None."""
        data = data_models.EmptyArtifactData()
        label = data.get_label()
        self.assertIsNone(label)


class DebianPackageBuildLogTests(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",
            architecture="amd64",
            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=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 DebianBinaryPackageTests(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 DebianBinaryPackagesTests(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 DebianSystemTarballTests(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",
            components=["main"],
            mirror="https://deb.debian.org",
            variant=None,
            pkglist={},
            architecture="amd64",
            with_dev=False,
            with_init=False,
        )
        self.assertEqual(data.get_label(), "bookworm.tar.gz")


class DebianSystemImageTests(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",
            components=["main"],
            mirror="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 DebianLintianTests(TestCase):
    """Test the DebianLintian model."""

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

        def make_lintian(**kwargs: str) -> data_models.DebianLintian:
            return data_models.DebianLintian(
                architecture="amd64",
                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().get_label(), "lintian (empty)")
        self.assertEqual(
            make_lintian(hello="hello_1.0-1.dsc").get_label(), "lintian: hello"
        )
        self.assertEqual(
            make_lintian(
                libhello="libhello_1.0-1.deb", hello="hello_1.0-1.deb"
            ).get_label(),
            "lintian: hello, libhello",
        )


class DebianAutopkgtestTests(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="https://deb.debian.org/pool/h/hello",
            ),
            architecture="amd64",
            distribution="bookworm",
        )
        self.assertEqual(data.get_label(), "hello")


class DebianPiupartsTests(TestCase):
    """Test the DebianPiuparts model."""

    def test_get_label(self) -> None:
        data = data_models.DebianPiuparts(
            piuparts_version="1.6.0",
            binary_packages={"hello": "1.0", "hello-doc": "1.0"},
            architecture="amd64",
            distribution="debian:bookworm",
        )
        self.assertEqual(data.get_label(), "piuparts: hello, hello-doc")


class DebusineSigningInputTests(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()
        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[call-arg]
        )

        self.assertEqual(data.extra_field, "extra_value")  # type: ignore[attr-defined]

    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[call-arg]
            )


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 DebusineSigningOutputTests(TestCase):
    """Test the DebusineSigningOutput model."""

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


class DebianBlhcTests(TestCase):
    """Test the DebianBlhc model."""

    def test_get_label_no_architecture(self) -> None:
        data = data_models.DebianBlhc()
        self.assertEqual(data.get_label(), "blhc analysis")

    def test_get_label(self) -> None:
        data = data_models.DebianBlhc(
            source="hello", version="1.0", architecture="amd64"
        )
        self.assertEqual(data.get_label(), "blhc analysis for hello_1.0_amd64")


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

    def test_get_label_override(self) -> None:
        data = data_models.DebDiff(
            original="original-package",
            new="new-package",
            label="A → B",
        )
        self.assertEqual(data.get_label(), "A → B")

    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"
        )


class DebianRepositoryIndexTests(TestCase):
    """Test the DebianRepositoryIndex model."""

    def test_get_label(self) -> None:
        data = data_models.DebianRepositoryIndex(path="main/source/Sources.xz")
        self.assertEqual(data.get_label(), "main/source/Sources.xz")


class GetSourcePackageNameTests(TestCase):
    """Tests for function get_source_package_name."""

    def test_DebianSourcePackage(self) -> None:
        artifact_data = data_models.DebianSourcePackage(
            name="hello", version="1.0", type="dpkg", dsc_fields={}
        )
        self.assertEqual(
            data_models.get_source_package_name(artifact_data), "hello"
        )

    def test_DebianUpload(self) -> None:
        artifact_data = data_models.DebianUpload(
            type="dpkg",
            changes_fields={
                "Source": "hello",
                "Architecture": "amd64",
                "Files": [{"name": "hello_1.0_amd64.deb"}],
            },
        )
        self.assertEqual(
            data_models.get_source_package_name(artifact_data), "hello"
        )

    def test_DebianBinaryPackage(self) -> None:
        artifact_data = data_models.DebianBinaryPackage(
            srcpkg_name="hello",
            srcpkg_version="1.0-1",
            deb_fields={
                "Package": "hello-bin",
                "Version": "1.0-1+b1",
                "Architecture": "amd64",
            },
            deb_control_files=[],
        )
        self.assertEqual(
            data_models.get_source_package_name(artifact_data), "hello"
        )

    def test_DebianBinaryPackages(self) -> None:
        artifact_data = data_models.DebianBinaryPackages(
            srcpkg_name="hello",
            srcpkg_version="1.0-1",
            version="1.0",
            architecture="amd64",
            packages=[],
        )
        self.assertEqual(
            data_models.get_source_package_name(artifact_data), "hello"
        )

    def test_invalid_type(self) -> None:
        artifact_data = data_models.EmptyArtifactData()

        with self.assertRaisesRegex(
            TypeError, "^Unexpected type: EmptyArtifactData$"
        ):
            data_models.get_source_package_name(
                artifact_data  # type: ignore[arg-type]
            )


class GetSourcePackageVersionTests(TestCase):
    """Tests for function get_source_package_version."""

    def test_DebianSourcePackage(self) -> None:
        artifact_data = data_models.DebianSourcePackage(
            name="hello", version="1.0", type="dpkg", dsc_fields={}
        )
        self.assertEqual(
            data_models.get_source_package_version(artifact_data), "1.0"
        )

    def test_DebianUpload(self) -> None:
        artifact_data = data_models.DebianUpload(
            type="dpkg",
            changes_fields={
                "Source": "hello",
                "Version": "1.0",
                "Architecture": "source",
                "Files": [{"name": "hello_1.0_source.dsc"}],
            },
        )
        self.assertEqual(
            data_models.get_source_package_version(artifact_data), "1.0"
        )

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

        self.assertEqual(
            data_models.get_source_package_version(artifact_data), "1.0"
        )

    def test_DebianBinaryPackages(self) -> None:
        artifact_data = data_models.DebianBinaryPackages(
            srcpkg_name="test",
            srcpkg_version="1.0",
            version="1.0-1",
            architecture="amd64",
            packages=[],
        )

        self.assertEqual(
            data_models.get_source_package_version(artifact_data), "1.0"
        )

    def test_invalid_type(self) -> None:
        artifact_data = data_models.EmptyArtifactData()

        with self.assertRaisesRegex(
            TypeError, "^Unexpected type: EmptyArtifactData$"
        ):
            data_models.get_source_package_version(
                artifact_data  # type: ignore[arg-type]
            )


class GetBinaryPackageNameTests(TestCase):
    """Tests for function get_binary_package_name."""

    def test_DebianBinaryPackage(self) -> None:
        artifact_data = data_models.DebianBinaryPackage(
            srcpkg_name="hello",
            srcpkg_version="1.0-1",
            deb_fields={
                "Package": "hello-bin",
                "Version": "1.0-1+b1",
                "Architecture": "amd64",
            },
            deb_control_files=[],
        )
        self.assertEqual(
            data_models.get_binary_package_name(artifact_data), "hello-bin"
        )

    def test_invalid_type(self) -> None:
        artifact_data = data_models.EmptyArtifactData()

        with self.assertRaisesRegex(
            TypeError, "^Unexpected type: EmptyArtifactData$"
        ):
            data_models.get_binary_package_name(
                artifact_data  # type: ignore[arg-type]
            )


class GetBinaryPackageVersionTests(TestCase):
    """Tests for function get_binary_package_version."""

    def test_DebianUpload(self) -> None:
        artifact_data = data_models.DebianUpload(
            type="dpkg",
            changes_fields={
                "Source": "hello",
                "Version": "1.0-1",
                "Architecture": "amd64",
                "Files": [{"name": "hello_1.0-1_amd64.deb"}],
            },
        )

        self.assertEqual(
            data_models.get_binary_package_version(artifact_data), "1.0-1"
        )

    def test_DebianBinaryPackage(self) -> None:
        artifact_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_models.get_binary_package_version(artifact_data), "1.0-1+b1"
        )

    def test_invalid_type(self) -> None:
        artifact_data = data_models.EmptyArtifactData()

        with self.assertRaisesRegex(
            TypeError, "^Unexpected type: EmptyArtifactData$"
        ):
            data_models.get_binary_package_version(
                artifact_data  # type: ignore[arg-type]
            )


class GetArchitecturesTests(TestCase):
    """Tests for function get_architectures."""

    def test_DebianBinaryPackages(self) -> None:
        artifact_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_models.get_architectures(artifact_data), {"amd64"}
        )

    def test_DebianPackageBuildLog(self) -> None:
        data = data_models.DebianPackageBuildLog(
            source="test",
            version="1.0-1",
            architecture="amd64",
            filename="test_1.0-1_amd64.buildlog",
        )
        self.assertEqual(data_models.get_architectures(data), {"amd64"})

    def test_DebianBinaryPackage(self) -> None:
        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_models.get_architectures(data), {"amd64"})

    def test_DebianUpload(self) -> None:
        data = data_models.DebianUpload(
            type="dpkg",
            changes_fields={
                "Source": "hello",
                "Architecture": "amd64",
                "Files": [{"name": "hello_1.0_amd64.deb"}],
            },
        )
        self.assertEqual(data_models.get_architectures(data), {"amd64"})

    def test_mixed_DebianUpload(self) -> None:
        data = data_models.DebianUpload(
            type="dpkg",
            changes_fields={
                "Source": "hello",
                "Architecture": "amd64 all source",
                "Files": [
                    {"name": "hello_1.0.dsc"},
                    {"name": "hello_1.0_amd64.deb"},
                    {"name": "hello-doc_1.0_all.deb"},
                ],
            },
        )
        self.assertEqual(data_models.get_architectures(data), {"amd64", "all"})

    def test_invalid_type(self) -> None:
        data = data_models.EmptyArtifactData()

        with self.assertRaisesRegex(
            TypeError, "^Unexpected type: EmptyArtifactData$"
        ):
            data_models.get_architectures(data)  # type: ignore[arg-type]
