# 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 pydantic

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_source_only_buildinfo_rejects_binary_buildinfo(
        self,
    ) -> None:
        """metadata_source_only_buildinfo rejects a binary .buildinfo."""
        data = {
            "Architecture": "source",
            "Files": [
                {"name": "foo.dsc"},
                {"name": "foo_amd64.buildinfo"},
            ],
        }
        with self.assertRaisesRegex(
            ValueError,
            r"Unexpected non-source \.buildinfo files "
            r"\['foo_amd64\.buildinfo'\] found in source-only upload\.",
        ):
            data_models.DebianUpload.metadata_source_only_buildinfo(data)

    def test_metadata_source_only_buildinfo_accepts_source_buildinfo(
        self,
    ) -> None:
        """metadata_source_only_buildinfo accepts a source .buildinfo."""
        data = {
            "Architecture": "source",
            "Files": [
                {"name": "foo.dsc"},
                {"name": "foo_source.buildinfo"},
            ],
        }
        self.assertEqual(
            data,
            data_models.DebianUpload.metadata_source_only_buildinfo(data),
        )

    def test_metadata_source_only_buildinfo_ignores_mixed_uploads(
        self,
    ) -> None:
        """metadata_source_only_buildinfo ignores non source-only uploads."""
        data = {
            "Architecture": "amd64 source",
            "Files": [
                {"name": "foo.dsc"},
                {"name": "foo_amd64.deb"},
                {"name": "foo_amd64.buildinfo"},
            ],
        }
        self.assertEqual(
            data,
            data_models.DebianUpload.metadata_source_only_buildinfo(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 DebianDebugSymbolsTests(TestCase):
    """Test the DebianDebugSymbols model."""

    def test_valid_input(self) -> None:
        data = data_models.DebianDebugSymbols(build_ids=["a" * 40, "b" * 40])
        self.assertEqual(data.build_ids, ["a" * 40, "b" * 40])

    def test_invalid_inputs(self) -> None:
        invalid_cases = [
            ("wrong length", ["abc123"], "abc123"),
            ("uppercase", ["A" * 40], "A" * 40),
            ("non hex", ["g" * 40], "g" * 40),
            ("duplicates", ["a" * 40, "a" * 40], "duplicate build_id"),
        ]
        for name, build_ids, expected_msg in invalid_cases:
            with (
                self.subTest(case=name),
                self.assertRaisesRegex(pydantic.ValidationError, expected_msg),
            ):
                data_models.DebianDebugSymbols(build_ids=build_ids)

    def test_get_label_single(self) -> None:
        data = data_models.DebianDebugSymbols(build_ids=["a" * 40])
        self.assertEqual(data.get_label(), "debug-symbols (1 build-id)")

    def test_get_label_double(self) -> None:
        data = data_models.DebianDebugSymbols(build_ids=["a" * 40, "b" * 40])
        self.assertEqual(data.get_label(), "debug-symbols (2 build-ids)")


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]
