# 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 task input fields."""

import inspect
from collections.abc import Callable
from collections.abc import Collection as AbcCollection
from types import SimpleNamespace
from typing import Any, Self, cast, override
from unittest import mock

from debusine.artifacts.models import (
    ArtifactCategory,
    DebianBinaryPackage,
    DebianSourcePackage,
    DebianUpload,
    DebusineSigningInput,
    DebusineSigningOutput,
)
from debusine.assets import KeyPurpose
from debusine.client.models import RelationType
from debusine.tasks import BaseExternalTask, TaskConfigError
from debusine.tasks.inputs.inputs import (
    BuildArchitectureInput,
    DataFieldInput,
    DataListFieldInput,
    DebusineFQDNInput,
    EnvironmentInput,
    ExtraRepositoriesInput,
    MultiInput,
    MultiInputList,
    OptionalEnvironmentInput,
    OptionalSingleInput,
    SingleInput,
    SingleInputList,
    SuiteArchiveInput,
    TaskFieldContainer,
    TaskInput,
    UnsignedMultiInput,
    UploadArtifactsInput,
)
from debusine.tasks.inputs.mock_resolver import ConcreteFieldResolver
from debusine.tasks.inputs.resolver import FieldResolver
from debusine.tasks.inputs.serializers import ScalarSerializer
from debusine.tasks.models import (
    BaseDynamicTaskData,
    BaseTaskInputArtifact,
    InputArtifactMultiple,
    InputArtifactSingle,
    LookupMultiple,
)
from debusine.test import TestCase
from debusine.utils import extract_generic_type_arguments


class InputTests[TI: TaskInput[Any]](TestCase):
    """Test case that can work as a container for task input fields."""

    #: Field class to be tested, filled by __init_subclass__
    field_class: type[TI]
    #: Mock task type to use for self.task
    mock_task_class = BaseExternalTask[Any, Any]

    @override
    def __init_subclass__(cls, **kwargs: Any) -> None:
        """Fill lookup_type from typing information."""
        super().__init_subclass__(**kwargs)
        [cls.field_class] = extract_generic_type_arguments(cls, InputTests)

    @override
    def setUp(self) -> None:
        super().setUp()
        # self.field is typed here, but set later by test methods
        self.field: TI
        # Task database for resolving fields
        self.field_resolver = ConcreteFieldResolver()
        self.lookup_single_artifact = self.enterContext(
            mock.patch.object(self.field_resolver, "lookup_single_artifact")
        )
        self.lookup_multiple_artifacts = self.enterContext(
            mock.patch.object(self.field_resolver, "lookup_multiple_artifacts")
        )
        self.lookup_environment = self.enterContext(
            mock.patch.object(self.field_resolver, "lookup_environment")
        )
        self.get_server_setting = self.enterContext(
            mock.patch.object(self.field_resolver, "get_server_setting")
        )
        self.get_fallback_architecture = self.enterContext(
            mock.patch.object(self.field_resolver, "get_fallback_architecture")
        )
        self.export_suite_signing_keys = self.enterContext(
            mock.patch.object(self.field_resolver, "export_suite_signing_keys")
        )
        self.find_related_artifacts = self.enterContext(
            mock.patch.object(self.field_resolver, "find_related_artifacts")
        )
        self.get_field = self.enterContext(
            mock.patch.object(self.field_resolver, "get_field")
        )
        # Mock task to use for tests
        self.task = mock.MagicMock(spec=self.mock_task_class)
        self.task.name = "TASKNAME"
        self.task.data = SimpleNamespace()
        # Mock check_category
        self.check_category = self.enterContext(
            mock.patch(
                "debusine.tasks.inputs.inputs.BaseArtifactInput.check_category"
            )
        )

    def set_task_data_field_value(self, value: Any) -> None:
        """Mock self.field.get_field to return the given value."""
        self.get_field.return_value = value

    def resolve(self) -> Any:
        """Call self.field.resolve and return its result."""
        return self.field.resolve(self.field_resolver)

    def make_multi(
        self,
        *artifacts: BaseTaskInputArtifact,
        label: str | None = None,
        lookup: LookupMultiple | None = None,
    ) -> InputArtifactMultiple:
        """Create an InputArtifactMultiple instance."""
        assert hasattr(self.field, "field")
        return InputArtifactMultiple(
            label=label or self.field.field,
            lookup=lookup or self.get_field.return_value,
            artifacts=list(artifacts),
        )

    @staticmethod
    def artifact_info(*, artifact_id: int) -> InputArtifactSingle:
        """Return an InputArtifactSingle with artifact_id and example data."""
        return InputArtifactSingle(
            artifact_id=artifact_id,
            label="Sbuild hello (amd64)",
            category=ArtifactCategory.BINARY_PACKAGE,
            data=DebianBinaryPackage(
                srcpkg_name="hello",
                srcpkg_version="1.0.0",
                deb_fields={},
                deb_control_files=[],
            ),
            lookup="internal@collections/name:build-amd64",
        )

    def test_has_serializer(self) -> None:
        if not hasattr(self, "field_class") or inspect.isabstract(
            self.field_class
        ):
            return
        if not hasattr(self.field_class, "serializer"):
            self.fail(
                f"{self.field_class} has no serializer defined"
            )  # pragma: no cover


class ConcreteTaskInput(TaskInput[str]):
    """Concrete task input."""

    serializer = ScalarSerializer(str)

    def __init__(self, value: str) -> None:
        """Resolve input field to a string constant."""
        super().__init__()
        self.value = value

    @override
    def get_resolver_method(
        self, resolver: FieldResolver
    ) -> Callable[[Self], str]:
        # Reuse an existing method by casting it to the right type
        return cast(Callable[[Self], str], resolver.resolve_suitearchiveinput)


class TaskInputTests(InputTests[ConcreteTaskInput]):
    """Tests for :py:class:`TaskInput`."""

    def test_registered_in_inputs(self) -> None:
        class A(TaskFieldContainer):
            field = ConcreteTaskInput("test")

        self.assertIn("field", A.inputs)

    def test_input_name(self) -> None:
        class A(TaskFieldContainer):
            field = ConcreteTaskInput("test")

        self.assertEqual(A.inputs["field"].name, "field")

    def test_inputs_subclassing(self) -> None:
        class A(TaskFieldContainer):
            field1 = ConcreteTaskInput("one")

        class B(A):
            field2 = ConcreteTaskInput("two")

        self.assertIn("field1", A.inputs)
        self.assertNotIn("field2", A.inputs)
        self.assertIn("field1", B.inputs)
        self.assertIn("field2", B.inputs)

    def test_inputs_subclassing_multiple_inheritance(self) -> None:
        class A(TaskFieldContainer):
            field1 = ConcreteTaskInput("one")

        class B(A):
            field2 = ConcreteTaskInput("two")

        class C(A):
            field3 = ConcreteTaskInput("three")

        class D(C, B):
            field4 = ConcreteTaskInput("four")

        self.assertEqual(list(A.inputs), ["field1"])
        self.assertEqual(list(B.inputs), ["field1", "field2"])
        self.assertEqual(list(C.inputs), ["field1", "field3"])
        self.assertEqual(
            list(D.inputs), ["field1", "field2", "field3", "field4"]
        )

    def test_not_resolved(self) -> None:
        class A(TaskFieldContainer):
            field = ConcreteTaskInput("test")

        a = A()
        with self.assertRaisesRegex(
            TaskConfigError, r"Cannot access unresolved input 'field'"
        ):
            a.field

    def test_default_resolve(self) -> None:
        self.field = ConcreteTaskInput("test")
        resolver_method = self.enterContext(
            mock.patch.object(
                self.field_resolver,
                "resolve_suitearchiveinput",
                return_value=mock.sentinel.result,
            )
        )
        self.assertIs(self.resolve(), mock.sentinel.result)
        resolver_method.assert_called_once_with(self.field)

    def test_get_value(self) -> None:
        class A(TaskFieldContainer):
            field1 = ConcreteTaskInput("one")

        a = A()
        a.__dict__["field1"] = "value"
        self.assertEqual(a.inputs["field1"].get_value(a), "value")

    def test_get_value_not_resolved(self) -> None:
        class A(TaskFieldContainer):
            field1 = ConcreteTaskInput("one")

        a = A()
        with self.assertRaisesRegex(
            TaskConfigError, r"Cannot access unresolved input 'field1'"
        ):
            a.inputs["field1"].get_value(a)

    def test_get_artifact_ids(self) -> None:
        class A(TaskFieldContainer):
            field = ConcreteTaskInput("one")

        a = A()
        a.field = "test"
        self.assertEqual(A.inputs["field"].get_artifact_ids(a.field), [])


class TaskFieldContainerTests(TestCase):
    """Tests for :py:class:`TaskFieldContainer`."""

    @override
    def setUp(self) -> None:
        super().setUp()

        class Container(TaskFieldContainer):
            field = ConcreteTaskInput("test")

        class ChildContainer(TaskFieldContainer):
            child_field = ConcreteTaskInput("test_child")

        self.container = Container()
        self.child_container = ChildContainer()

    def test_field_is_resolved(self) -> None:
        self.assertFalse(self.container.field_is_resolved("field"))
        self.container.field = "foo"
        self.assertTrue(self.container.field_is_resolved("field"))

    def test_iter_all_fields(self) -> None:
        self.assertEqual(
            list(self.container.iter_all_fields()),
            [(self.container, self.container.inputs["field"])],
        )

    def test_iter_all_fields_with_child(self) -> None:
        self.container.child_task_field_container = self.child_container

        self.assertEqual(
            list(self.container.iter_all_fields()),
            [
                (self.container, self.container.inputs["field"]),
                (
                    self.child_container,
                    self.child_container.inputs["child_field"],
                ),
            ],
        )

    def test_save_field_values(self) -> None:
        self.container.field = "field value"

        dynamic_data = BaseDynamicTaskData()
        self.assertNotIn("task_input_fields", dynamic_data.model_fields_set)

        self.container.save_field_values(dynamic_data)

        self.assertEqual(dynamic_data.task_input_fields["field"], "field value")
        self.assertIn("task_input_fields", dynamic_data.model_fields_set)

    def test_save_field_values_with_child(self) -> None:
        self.container.child_task_field_container = self.child_container

        self.container.field = "field value"
        self.child_container.child_field = "child field value"

        dynamic_data = BaseDynamicTaskData()
        self.container.save_field_values(dynamic_data)
        self.assertEqual(dynamic_data.task_input_fields["field"], "field value")
        self.assertEqual(
            dynamic_data.task_input_fields["child_field"], "child field value"
        )

    def test_save_field_values_preserve_existing(self) -> None:
        self.container.field = "field value"

        dynamic_data = BaseDynamicTaskData()
        dynamic_data.task_input_fields["other"] = "other value"

        self.container.save_field_values(dynamic_data)

        self.assertEqual(
            dynamic_data.task_input_fields,
            {
                "field": "field value",
                "other": "other value",
            },
        )

    def test_restore_field_values(self) -> None:
        dynamic_data = BaseDynamicTaskData()
        dynamic_data.task_input_fields["field"] = "field value"
        self.assertFalse(self.container.field_is_resolved("field"))
        self.container.restore_field_values(dynamic_data)
        self.assertTrue(self.container.field_is_resolved("field"))
        self.assertEqual(self.container.field, "field value")

    def test_restore_field_values_with_child(self) -> None:
        self.container.child_task_field_container = self.child_container
        dynamic_data = BaseDynamicTaskData()
        dynamic_data.task_input_fields["field"] = "field value"
        dynamic_data.task_input_fields["child_field"] = "child field value"
        self.assertFalse(self.container.field_is_resolved("field"))
        self.assertFalse(self.child_container.field_is_resolved("child_field"))
        self.container.restore_field_values(dynamic_data)
        self.assertTrue(self.container.field_is_resolved("field"))
        self.assertTrue(self.child_container.field_is_resolved("child_field"))
        self.assertEqual(self.container.field, "field value")
        self.assertEqual(self.child_container.child_field, "child field value")

    def test_restore_field_values_ignores_undefined_fields(self) -> None:
        dynamic_data = BaseDynamicTaskData()
        dynamic_data.task_input_fields["field"] = "field value"
        dynamic_data.task_input_fields["other"] = "other value"
        self.container.restore_field_values(dynamic_data)
        self.assertTrue(self.container.field_is_resolved("field"))
        self.assertEqual(self.container.field, "field value")
        self.assertNotIn("other", self.container.__dict__)


class ConcreteDataFieldInput(DataFieldInput[int, None]):
    """Concrete version of DataFieldInput."""

    serializer = ScalarSerializer(type(None))

    @override
    def resolve(self, field_resolver: FieldResolver) -> None:
        raise NotImplementedError()


class DataFieldInputTests(InputTests[ConcreteDataFieldInput]):
    """Tests for :py:class:`DataFieldInput`."""

    @override
    def setUp(self) -> None:
        super().setUp()

        class Fields(TaskFieldContainer):
            field_default = self.field_class()
            field_test = self.field_class(field="test")

        self.fields = Fields()

    def test_field_default(self) -> None:
        self.assertEqual(
            self.fields.inputs["field_default"].field,  # type: ignore[attr-defined]
            "field_default",
        )

    def test_field_explicit(self) -> None:
        self.assertEqual(
            self.fields.inputs["field_test"].field,  # type: ignore[attr-defined]
            "test",
        )


class ConcreteDataListFieldInput(DataListFieldInput[int, None]):
    """Concrete version of DataListFieldInput."""

    serializer = ScalarSerializer(type(None))

    @override
    def resolve(self, field_resolver: FieldResolver) -> None:
        raise NotImplementedError()


class DataListFieldInputTests(InputTests[ConcreteDataListFieldInput]):
    """Tests for :py:class:`DataListFieldInput`."""

    @override
    def setUp(self) -> None:
        super().setUp()

        class Fields(TaskFieldContainer):
            field_default = self.field_class()
            field_test = self.field_class(field="test")
            field_test_nested = self.field_class(field="test.nested")

        self.fields = Fields()

    def test_field_default(self) -> None:
        self.assertEqual(
            self.fields.inputs["field_default"].field,  # type: ignore[attr-defined]
            "field_default",
        )


class SingleInputTests(InputTests[SingleInput]):
    """Tests for :py:class:`SingleInput`."""

    @override
    def setUp(self) -> None:
        super().setUp()
        self.field = self.field_class()
        self.field.field = "field"

    def test_none(self) -> None:
        self.set_task_data_field_value(None)
        with self.assertRaises(AssertionError):
            self.field.resolve(self.field_resolver)
        self.lookup_single_artifact.assert_not_called()
        self.check_category.assert_not_called()

    def test_has_value(self) -> None:
        self.set_task_data_field_value(1)
        self.lookup_single_artifact.return_value = "RESULT"
        self.assertEqual(self.resolve(), "RESULT")
        self.lookup_single_artifact.assert_called_once_with(1, label="field")
        self.check_category.assert_called_with("RESULT")

    def test_get_artifact_ids(self) -> None:
        self.assertEqual(
            self.field.get_artifact_ids(
                self.artifact_info(artifact_id=100),
            ),
            [100],
        )


class OptionalSingleInputTests(InputTests[OptionalSingleInput]):
    """Tests for :py:class:`OptioanlSingleInput`."""

    @override
    def setUp(self) -> None:
        super().setUp()
        self.field = self.field_class()
        self.field.field = "field"

    def test_none(self) -> None:
        self.set_task_data_field_value(None)
        self.assertEqual(self.resolve(), None)
        self.lookup_single_artifact.assert_not_called()
        self.check_category.assert_not_called()

    def test_has_value(self) -> None:
        self.set_task_data_field_value("LOOKUP")
        self.lookup_single_artifact.return_value = "RESULT"
        self.assertEqual(self.resolve(), "RESULT")
        self.lookup_single_artifact.assert_called_once_with(
            "LOOKUP", label="field"
        )
        self.check_category.assert_called_with("RESULT")

    def test_get_artifact_ids(self) -> None:
        artifact_info = InputArtifactSingle(
            artifact_id=100,
            label="source",
            category=ArtifactCategory.SOURCE_PACKAGE,
            lookup="",
            data=DebianSourcePackage(
                name="hello",
                version="1.0.0",
                type="dpkg",
                dsc_fields={},
            ),
        )

        self.assertEqual(self.field.get_artifact_ids(artifact_info), [100])
        self.assertEqual(self.field.get_artifact_ids(None), [])


class SingleInputListTests(InputTests[SingleInputList]):
    """Tests for :py:class:`SingleInputList`."""

    @override
    def setUp(self) -> None:
        super().setUp()
        self.field = self.field_class()
        self.field.field = "field"

    def test_empty_list(self) -> None:
        self.set_task_data_field_value([])
        self.assertEqual(self.resolve(), [])
        self.lookup_single_artifact.assert_not_called()
        self.check_category.assert_not_called()

    def test_multiple_results(self) -> None:
        self.set_task_data_field_value([1, "two"])
        self.lookup_single_artifact.side_effect = ["foo", "bar"]
        self.assertEqual(self.resolve(), ["foo", "bar"])
        self.lookup_single_artifact.assert_has_calls(
            [mock.call(1, label="field"), mock.call("two", label="field")]
        )
        self.check_category.assert_has_calls(
            [mock.call("foo"), mock.call("bar")]
        )

    def test_get_artifact_ids(self) -> None:
        value = [
            self.artifact_info(artifact_id=123),
            self.artifact_info(artifact_id=456),
        ]
        self.assertEqual(self.field.get_artifact_ids(value), [123, 456])


class MultiInputTests(InputTests[MultiInput]):
    """Tests for :py:class:`MultiInputList`."""

    binary = BaseTaskInputArtifact(
        artifact_id=1,
        category=ArtifactCategory.BINARY_PACKAGE,
        data=DebianBinaryPackage(
            srcpkg_name="unsigned",
            srcpkg_version="1.0",
            deb_fields={
                "Package": "hello",
                "Version": "1.0-1",
                "Architecture": "amd64",
            },
            deb_control_files=[],
        ),
    )

    @override
    def setUp(self) -> None:
        super().setUp()
        self.field = self.field_class()
        self.field.field = "field"

    def test_empty(self) -> None:
        lookup = LookupMultiple((1,))
        result = InputArtifactMultiple(
            lookup=lookup, label="field", artifacts=[]
        )
        self.set_task_data_field_value(lookup)
        self.lookup_multiple_artifacts.return_value = result
        self.assertEqual(self.resolve(), result)
        self.lookup_multiple_artifacts.assert_called_with(lookup, label="field")
        self.check_category.assert_not_called()

    def test_multiple_results(self) -> None:
        lookup = LookupMultiple((1,))
        result = InputArtifactMultiple(
            lookup=lookup, label="field", artifacts=[self.binary]
        )
        self.set_task_data_field_value(lookup)
        self.lookup_multiple_artifacts.return_value = result
        self.assertEqual(
            self.resolve(), self.lookup_multiple_artifacts.return_value
        )
        self.lookup_multiple_artifacts.assert_called_once_with(
            lookup, label="field"
        )
        self.check_category.assert_not_called()

    def test_check_category(self) -> None:
        self.field.categories = (ArtifactCategory.BINARY_PACKAGE,)
        lookup = LookupMultiple((1,))
        result = InputArtifactMultiple(
            lookup=lookup, label="field", artifacts=[self.binary]
        )
        self.set_task_data_field_value(lookup)
        self.lookup_multiple_artifacts.return_value = result
        self.assertEqual(
            self.resolve(), self.lookup_multiple_artifacts.return_value
        )
        self.lookup_multiple_artifacts.assert_called_once_with(
            lookup, label="field"
        )
        self.check_category.assert_called_once_with(self.binary, "field[0]")

    def test_get_artifact_ids(self) -> None:
        lookup = LookupMultiple((1,))
        value = InputArtifactMultiple(
            lookup=lookup, label="field", artifacts=[self.binary]
        )
        self.assertEqual(self.field.get_artifact_ids(value), [1])


class MultiInputListTests(InputTests[MultiInputList]):
    """Tests for :py:class:`MultiInputList`."""

    binary = BaseTaskInputArtifact(
        artifact_id=1,
        category=ArtifactCategory.BINARY_PACKAGE,
        data=DebianBinaryPackage(
            srcpkg_name="unsigned",
            srcpkg_version="1.0",
            deb_fields={
                "Package": "hello",
                "Version": "1.0-1",
                "Architecture": "amd64",
            },
            deb_control_files=[],
        ),
    )

    def test_empty_categories(self) -> None:
        self.field = self.field_class()
        self.field.field = "field"

        lookups = [LookupMultiple((1,))]
        self.set_task_data_field_value(lookups)

        results = [
            InputArtifactMultiple(
                lookup=self.get_field.return_value[0],
                label="field",
                artifacts=[self.binary],
            )
        ]
        self.lookup_multiple_artifacts.return_value = results[0]

        self.assertEqual(self.resolve(), results)

        self.lookup_multiple_artifacts.assert_called_with(
            lookups[0], label="field"
        )
        self.check_category.assert_not_called()

    def test_with_categories(self) -> None:
        self.field = MultiInputList(
            field="test", categories=[ArtifactCategory.BINARY_PACKAGE]
        )

        lookups = [
            LookupMultiple((1,)),
            LookupMultiple((2,)),
        ]
        self.set_task_data_field_value(lookups)

        results = [
            InputArtifactMultiple(
                lookup=self.get_field.return_value[0],
                label="field",
                artifacts=[self.binary],
            ),
            InputArtifactMultiple(
                lookup=self.get_field.return_value[1],
                label="field",
                artifacts=[self.binary, self.binary],
            ),
        ]
        self.lookup_multiple_artifacts.side_effect = results
        self.assertEqual(self.resolve(), results)
        self.check_category.assert_has_calls(
            [
                mock.call(results[0].artifacts[0], "test[0][0]"),
                mock.call(results[1].artifacts[0], "test[1][0]"),
                mock.call(results[1].artifacts[1], "test[1][1]"),
            ]
        )

    def test_get_artifact_ids(self) -> None:
        self.field = MultiInputList(
            field="test", categories=[ArtifactCategory.BINARY_PACKAGE]
        )

        value = [
            InputArtifactMultiple(
                label="label1",
                lookup=LookupMultiple(()),
                artifacts=[
                    BaseTaskInputArtifact(artifact_id=100),
                    BaseTaskInputArtifact(artifact_id=101),
                ],
            ),
            InputArtifactMultiple(
                label="label2",
                lookup=LookupMultiple(()),
                artifacts=[BaseTaskInputArtifact(artifact_id=102)],
            ),
            InputArtifactMultiple(
                label="label3",
                lookup=LookupMultiple(()),
                artifacts=[BaseTaskInputArtifact(artifact_id=None)],
            ),
        ]

        self.assertEqual(
            self.field.get_artifact_ids(value),
            [100, 101, 102],
        )


class UploadArtifactsInputTests(InputTests[UploadArtifactsInput]):
    """Tests for :py:class:`UploadArtifactsInput`."""

    binary = BaseTaskInputArtifact(
        artifact_id=1,
        category=ArtifactCategory.BINARY_PACKAGE,
        data=DebianBinaryPackage(
            srcpkg_name="unsigned",
            srcpkg_version="1.0",
            deb_fields={
                "Package": "hello",
                "Version": "1.0-1",
                "Architecture": "amd64",
            },
            deb_control_files=[],
        ),
    )
    binary1 = BaseTaskInputArtifact(
        artifact_id=1,
        category=ArtifactCategory.BINARY_PACKAGE,
        data=DebianBinaryPackage(
            srcpkg_name="unsigned",
            srcpkg_version="1.0",
            deb_fields={
                "Package": "hello",
                "Version": "1.0-1",
                "Architecture": "amd64",
            },
            deb_control_files=[],
        ),
    )
    binary2 = BaseTaskInputArtifact(
        artifact_id=2,
        category=ArtifactCategory.BINARY_PACKAGE,
        data=DebianBinaryPackage(
            srcpkg_name="unsigned-doc",
            srcpkg_version="1.0",
            deb_fields={
                "Package": "unsigned",
                "Version": "1.0-1",
                "Architecture": "all",
            },
            deb_control_files=[],
        ),
    )
    upload = BaseTaskInputArtifact(
        artifact_id=3,
        category=ArtifactCategory.UPLOAD,
        data=DebianUpload(
            type="dpkg",
            changes_fields={
                "Architecture": "all",
                "Files": [{"name": "unsigned-doc_1.0_all.deb"}],
            },
        ),
    )

    @override
    def setUp(self) -> None:
        super().setUp()
        self.field = self.field_class()
        self.field.field = "field"

    def test_empty(self) -> None:
        self.set_task_data_field_value([])
        self.assertEqual(self.resolve(), self.make_multi())
        self.find_related_artifacts.assert_not_called()

    def test_no_uploads(self) -> None:
        self.set_task_data_field_value(["LOOKUP"])
        self.lookup_multiple_artifacts.return_value = self.make_multi(
            self.binary
        )
        self.assertEqual(self.resolve(), self.make_multi(self.binary))
        self.lookup_multiple_artifacts.assert_called_once_with(
            ["LOOKUP"], label="field"
        )
        self.find_related_artifacts.assert_not_called()

    def test_expand_uploads(self) -> None:
        self.set_task_data_field_value(["LOOKUP"])
        self.lookup_multiple_artifacts.return_value = self.make_multi(
            self.binary1, self.upload
        )
        self.find_related_artifacts.return_value = self.make_multi(self.binary2)
        result = self.resolve()
        self.assertEqual(
            [x.artifact_id for x in result.artifacts],
            [x.artifact_id for x in (self.binary1, self.binary2)],
        )
        self.find_related_artifacts.assert_called_once_with(
            [self.upload.artifact_id],
            target_category=ArtifactCategory.BINARY_PACKAGE,
            relation_type=RelationType.EXTENDS,
            label="field",
        )


class EnvironmentInputTests(InputTests[EnvironmentInput]):
    """Tests for :py:class:`EnvironmentInput`."""

    def test_resolver_method(self) -> None:
        field = self.field_class()
        self.assertEqual(
            field.get_resolver_method(self.field_resolver),
            self.field_resolver.resolve_environmentinput,
        )

    def test_get_artifact_ids(self) -> None:
        self.field = EnvironmentInput(field="field")
        self.assertEqual(
            self.field.get_artifact_ids(self.artifact_info(artifact_id=123)),
            [123],
        )


class OptionalEnvironmentInputTests(InputTests[OptionalEnvironmentInput]):
    """Tests for :py:class:`OptionalEnvironmentInput`."""

    def test_resolver_method(self) -> None:
        field = self.field_class()
        self.assertEqual(
            field.get_resolver_method(self.field_resolver),
            self.field_resolver.resolve_optionalenvironmentinput,
        )


class SuiteArchiveInputTests(InputTests[SuiteArchiveInput]):
    """Tests for :py:class:`SuiteArchiveInput`."""

    def test_resolver_method(self) -> None:
        field = self.field_class()
        self.assertEqual(
            field.get_resolver_method(self.field_resolver),
            self.field_resolver.resolve_suitearchiveinput,
        )


class ExtraRepositoriesInputTests(InputTests[ExtraRepositoriesInput]):
    """Tests for :py:class:`ExtraRepositoriesInput`."""

    def test_resolver_method(self) -> None:
        field = self.field_class()
        self.assertEqual(
            field.get_resolver_method(self.field_resolver),
            self.field_resolver.resolve_extrarepositoriesinput,
        )


class DebusineFQDNInputTests(InputTests[DebusineFQDNInput]):
    """Tests for :py:class:`DebusineFQDNInput`."""

    @override
    def setUp(self) -> None:
        super().setUp()
        self.field = self.field_class()
        self.get_server_setting = self.enterContext(
            mock.patch.object(self.field_resolver, "get_server_setting")
        )

    def test_resolve(self) -> None:
        self.get_server_setting.return_value = "FQDN"
        self.assertEqual(self.resolve(), "FQDN")
        self.get_server_setting.assert_called_once_with("DEBUSINE_FQDN")


class BuildArchitectureInputTests(InputTests[BuildArchitectureInput]):
    """Tests for :py:class:`BuildArchitectureInput`."""

    @override
    def setUp(self) -> None:
        super().setUp()
        self.field = self.field_class(field="field")

    def test_with_value(self) -> None:
        self.set_task_data_field_value("arm64")
        self.assertEqual(self.resolve(), "arm64")
        self.get_fallback_architecture.assert_not_called()

    def test_with_fallback(self) -> None:
        self.set_task_data_field_value(None)
        self.get_fallback_architecture.return_value = (
            mock.sentinel.build_architecture
        )
        self.assertEqual(self.resolve(), mock.sentinel.build_architecture)
        self.get_fallback_architecture.assert_called_once_with()


class UnsignedMultiInputTests(InputTests[UnsignedMultiInput]):
    """Tests for :py:class:`UnsignedMultiInput`."""

    unsigned = BaseTaskInputArtifact(
        artifact_id=1,
        category=ArtifactCategory.BINARY_PACKAGE,
        data=DebianBinaryPackage(
            srcpkg_name="unsigned",
            srcpkg_version="1.0",
            deb_fields={
                "Package": "hello",
                "Version": "1.0-1",
                "Architecture": "amd64",
            },
            deb_control_files=[],
        ),
    )
    signing_input = BaseTaskInputArtifact(
        artifact_id=2,
        category=ArtifactCategory.SIGNING_INPUT,
        data=DebusineSigningInput(),
    )
    signing_output = BaseTaskInputArtifact(
        artifact_id=3,
        category=ArtifactCategory.SIGNING_OUTPUT,
        data=DebusineSigningOutput(
            purpose=KeyPurpose.UEFI, fingerprint="", results=[]
        ),
    )

    @override
    def setUp(self) -> None:
        super().setUp()
        self.field = self.field_class()
        self.field.field = "field"

    def test_empty(self) -> None:
        self.set_task_data_field_value([])
        self.find_related_artifacts.return_value = self.make_multi()
        self.assertEqual(self.resolve(), self.make_multi())
        self.assertEqual(
            self.find_related_artifacts.mock_calls,
            [
                mock.call(
                    set(),
                    target_category=ArtifactCategory.SIGNING_INPUT,
                    relation_type=RelationType.RELATES_TO,
                    label='field',
                ),
                mock.call(
                    set(),
                    target_category=ArtifactCategory.BINARY_PACKAGE,
                    relation_type=RelationType.RELATES_TO,
                    label='field',
                ),
            ],
        )

    def test_signing_input(self) -> None:
        self.set_task_data_field_value(["LOOKUP"])
        self.lookup_multiple_artifacts.return_value = self.make_multi(
            self.signing_output
        )

        def find_related_artifacts(
            artifact_ids: AbcCollection[int],
            *,
            target_category: ArtifactCategory,
            relation_type: RelationType = RelationType.RELATES_TO,
            label: str,  # noqa: ARG001
        ) -> InputArtifactMultiple:
            if (
                set(artifact_ids) == {self.signing_output.artifact_id}
                and target_category == ArtifactCategory.SIGNING_INPUT
                and relation_type == RelationType.RELATES_TO
            ):
                return self.make_multi(self.signing_input)
            return self.make_multi()

        self.find_related_artifacts.side_effect = find_related_artifacts

        result = self.resolve()
        self.assertEqual(
            [x.artifact_id for x in result.artifacts],
            [self.signing_input.artifact_id],
        )
        self.assertEqual(
            self.find_related_artifacts.mock_calls,
            [
                mock.call(
                    {self.signing_output.artifact_id},
                    target_category=ArtifactCategory.SIGNING_INPUT,
                    relation_type=RelationType.RELATES_TO,
                    label='field',
                ),
                mock.call(
                    {self.signing_input.artifact_id},
                    target_category=ArtifactCategory.BINARY_PACKAGE,
                    relation_type=RelationType.RELATES_TO,
                    label='field',
                ),
            ],
        )

    def test_unsigned(self) -> None:
        self.set_task_data_field_value(["LOOKUP"])
        self.lookup_multiple_artifacts.return_value = self.make_multi(
            self.signing_output
        )

        def find_related_artifacts(
            artifact_ids: AbcCollection[int],
            *,
            target_category: ArtifactCategory,
            relation_type: RelationType = RelationType.RELATES_TO,
            label: str,  # noqa: ARG001
        ) -> InputArtifactMultiple:
            if (
                set(artifact_ids) == {self.signing_output.artifact_id}
                and target_category == ArtifactCategory.SIGNING_INPUT
                and relation_type == RelationType.RELATES_TO
            ):
                return self.make_multi(self.signing_input)
            if (
                set(artifact_ids) == {self.signing_input.artifact_id}
                and target_category == ArtifactCategory.BINARY_PACKAGE
                and relation_type == RelationType.RELATES_TO
            ):
                return self.make_multi(self.unsigned)
            raise AssertionError(f"Unexpected artifact_ids: {artifact_ids}")

        self.find_related_artifacts.side_effect = find_related_artifacts
        result = self.resolve()
        self.assertEqual(
            [x.artifact_id for x in result.artifacts],
            sorted(
                [x.artifact_id for x in [self.signing_input, self.unsigned]]
            ),
        )
        self.assertEqual(
            self.find_related_artifacts.mock_calls,
            [
                mock.call(
                    {self.signing_output.artifact_id},
                    target_category=ArtifactCategory.SIGNING_INPUT,
                    relation_type=RelationType.RELATES_TO,
                    label='field',
                ),
                mock.call(
                    {self.signing_input.artifact_id},
                    target_category=ArtifactCategory.BINARY_PACKAGE,
                    relation_type=RelationType.RELATES_TO,
                    label='field',
                ),
            ],
        )
