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

"""Tests for input field widgets."""

from typing import ClassVar, override

import lxml
from django.template import Context, engines
from django.test import RequestFactory

import debusine.web.views.input_fields as input_widgets
from debusine.artifacts.models import CollectionCategory
from debusine.db.models import Artifact, Collection
from debusine.db.tasks import inputs as dbinputs
from debusine.db.tasks.models import (
    DBArtifactInfo,
    DBBaseTaskInputArtifact,
    DBInputArtifactMultiple,
    DBInputCollectionSingle,
)
from debusine.tasks import inputs
from debusine.tasks.models import (
    BaseTaskInputArtifact,
    ExtraExternalRepository,
    InputArtifactSingle,
    InputCollectionSingle,
    LookupMultiple,
)
from debusine.test.django import TestCase


class WidgetTestCase(TestCase):
    """Base test case for testing widgets."""

    @override
    def setUp(self) -> None:
        super().setUp()
        self.request_factory = RequestFactory()
        self.request = self.request_factory.get("/")
        self.context = Context({"request": self.request})
        self.template = engines["django"].from_string("")
        self.context.template = self.template.template  # type: ignore[attr-defined]


class RenderArtifactTests(WidgetTestCase):
    """Test :py:func:`render_artifact`."""

    artifact: ClassVar[Artifact]

    @override
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.artifact = cls.playground.create_minimal_source_package_artifact()

    def assertRendersActive(
        self, tree: lxml.objectify.ObjectifiedElement
    ) -> None:
        root = self.assertHasElement(tree, "body/div")
        a = root.div[0].a
        self.assertEqual(a.get("href"), self.artifact.get_absolute_url())
        shortcuts = root.div[1]
        self.assertEqual(len(shortcuts.a), 2)

    def assertRendersExpired(
        self, tree: lxml.objectify.ObjectifiedElement
    ) -> None:
        root = self.assertHasElement(tree, "body/div")
        self.assertHasElement(root.div[0], "span[@class='expired']")
        # There are no shortcuts
        self.assertEqual(len(root.div), 1)

    def test_render_artifactinfo(self) -> None:
        item = InputArtifactSingle(
            artifact_id=self.artifact.id,
            category=self.artifact.category,
            data=self.artifact.create_data(),
            label="label",
            lookup="lookup",
        )
        tree = self.assertHTMLValid(
            input_widgets.render_artifact(item, self.context)
        )
        self.assertRendersActive(tree)

    def test_render_artifactinfo_expired(self) -> None:
        item = InputArtifactSingle(
            artifact_id=-1,
            category=self.artifact.category,
            data=self.artifact.create_data(),
            label="label",
            lookup="lookup",
        )
        tree = self.assertHTMLValid(
            input_widgets.render_artifact(item, self.context)
        )
        self.assertRendersExpired(tree)

    def test_render_basetaskinputartifact(self) -> None:
        item = BaseTaskInputArtifact(
            artifact_id=self.artifact.id,
            category=self.artifact.category,
            data=self.artifact.create_data(),
        )
        tree = self.assertHTMLValid(
            input_widgets.render_artifact(item, self.context)
        )
        self.assertRendersActive(tree)

    def test_render_basetaskinputartifact_expired(self) -> None:
        item = BaseTaskInputArtifact(
            artifact_id=-1,
            category=self.artifact.category,
            data=self.artifact.create_data(),
        )
        tree = self.assertHTMLValid(
            input_widgets.render_artifact(item, self.context)
        )
        self.assertRendersExpired(tree)

    def test_render_dbartifactinfo(self) -> None:
        item = DBArtifactInfo.from_artifact(
            self.artifact, label="label", lookup="lookup"
        )
        tree = self.assertHTMLValid(
            input_widgets.render_artifact(item, self.context)
        )
        self.assertRendersActive(tree)

    def test_render_dbartifactinfo_unchached(self) -> None:
        item = DBArtifactInfo.from_artifact(
            self.artifact, label="label", lookup="lookup"
        )
        item._artifact = None
        tree = self.assertHTMLValid(
            input_widgets.render_artifact(item, self.context)
        )
        self.assertRendersActive(tree)

    def test_render_dbartifactinfo_expired(self) -> None:
        item = DBArtifactInfo(
            artifact_id=-1,
            category=self.artifact.category,
            data=self.artifact.create_data(),
            label="label",
            lookup="lookup",
        )
        tree = self.assertHTMLValid(
            input_widgets.render_artifact(item, self.context)
        )
        self.assertRendersExpired(tree)

    def test_render_dbbasetaskinputartifact(self) -> None:
        item = DBBaseTaskInputArtifact.from_artifact(self.artifact)
        tree = self.assertHTMLValid(
            input_widgets.render_artifact(item, self.context)
        )
        self.assertRendersActive(tree)

    def test_render_dbbasetaskinputartifact_uncached(self) -> None:
        item = DBBaseTaskInputArtifact.from_artifact(self.artifact)
        item._artifact = None
        tree = self.assertHTMLValid(
            input_widgets.render_artifact(item, self.context)
        )
        self.assertRendersActive(tree)

    def test_render_dbbasetaskinputartifact_expired(self) -> None:
        item = BaseTaskInputArtifact(
            artifact_id=-1,
            category=self.artifact.category,
            data=self.artifact.create_data(),
        )
        tree = self.assertHTMLValid(
            input_widgets.render_artifact(item, self.context)
        )
        self.assertRendersExpired(tree)


class RenderCollectionTests(WidgetTestCase):
    """Test :py:func:`render_collection`."""

    collection: ClassVar[Collection]

    @override
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.collection = cls.playground.create_collection(
            name="sid", category=CollectionCategory.SUITE
        )

    def assertRendersActive(
        self, tree: lxml.objectify.ObjectifiedElement
    ) -> None:
        a = self.assertHasElement(tree, "body/a")
        self.assertEqual(a.get("href"), self.collection.get_absolute_url())

    def assertRendersExpired(
        self, tree: lxml.objectify.ObjectifiedElement
    ) -> None:
        root = self.assertHasElement(tree, "body")
        self.assertTextContentEqual(
            root,
            f"#-1 {self.collection.name}@{self.collection.category}",
        )

    def test_render_inputcollectionsingle(self) -> None:
        item = InputCollectionSingle(
            collection_id=self.collection.id,
            name=self.collection.name,
            category=self.collection.category,
            scope_name=self.collection.workspace.scope.name,
            workspace_name=self.collection.workspace.name,
            data=self.collection.data,
            label="label",
            lookup="lookup",
        )
        tree = self.assertHTMLValid(
            input_widgets.render_collection(item, self.context)
        )
        self.assertRendersActive(tree)

    def test_render_inputcollectionsingle_uncached(self) -> None:
        item = InputCollectionSingle(
            collection_id=self.collection.id,
            name=self.collection.name,
            category=self.collection.category,
            scope_name=self.collection.workspace.scope.name,
            workspace_name=self.collection.workspace.name,
            data=self.collection.data,
            label="label",
            lookup="lookup",
        )
        tree = self.assertHTMLValid(
            input_widgets.render_collection(item, self.context)
        )
        self.assertRendersActive(tree)

    def test_render_inputcollectionsingle_expired(self) -> None:
        item = InputCollectionSingle(
            collection_id=-1,
            name=self.collection.name,
            category=self.collection.category,
            scope_name=self.collection.workspace.scope.name,
            workspace_name=self.collection.workspace.name,
            data=self.collection.data,
            label="label",
            lookup="lookup",
        )
        tree = self.assertHTMLValid(
            input_widgets.render_collection(item, self.context)
        )
        self.assertRendersExpired(tree)

    def test_render_dbinputcollectionsingle(self) -> None:
        item = DBInputCollectionSingle.from_collection(
            self.collection, label="label", lookup="lookup"
        )
        tree = self.assertHTMLValid(
            input_widgets.render_collection(item, self.context)
        )
        self.assertRendersActive(tree)

    def test_render_dbinputcollectionsingle_uncached(self) -> None:
        item = DBInputCollectionSingle(
            collection_id=self.collection.id,
            name=self.collection.name,
            category=self.collection.category,
            scope_name=self.collection.workspace.scope.name,
            workspace_name=self.collection.workspace.name,
            data=self.collection.data,
            label="label",
            lookup="lookup",
        )
        item._collection = None
        tree = self.assertHTMLValid(
            input_widgets.render_collection(item, self.context)
        )
        self.assertRendersActive(tree)

    def test_render_dbinputcollectionsingle_expired(self) -> None:
        item = DBInputCollectionSingle(
            collection_id=-1,
            name=self.collection.name,
            category=self.collection.category,
            scope_name=self.collection.workspace.scope.name,
            workspace_name=self.collection.workspace.name,
            data=self.collection.data,
            label="label",
            lookup="lookup",
        )
        tree = self.assertHTMLValid(
            input_widgets.render_collection(item, self.context)
        )
        self.assertRendersExpired(tree)


class RenderLookupTests(WidgetTestCase):
    """Test :py:func:`render_lookup`."""

    def test_int(self) -> None:
        rendered = input_widgets.render_lookup(42)
        self.assertEqual(rendered, "<code>42</code>")

    def test_str(self) -> None:
        rendered = input_widgets.render_lookup("foo")
        self.assertEqual(rendered, "<code>foo</code>")

    def test_multiple(self) -> None:
        lookup = LookupMultiple((1, 2, 3))
        tree = self.assertHTMLValid(input_widgets.render_lookup(lookup))
        self.assertYAMLContentEqual(tree, [1, 2, 3])

    def test_other(self) -> None:
        tree = self.assertHTMLValid(input_widgets.render_lookup({"foo": "bar"}))  # type: ignore[arg-type]
        self.assertYAMLContentEqual(tree, {"foo": "bar"})


class InputFieldWidgetTests(WidgetTestCase):
    """Test :py:class:`InputFieldWidget`."""

    def test_render_details_no_help_text(self) -> None:
        field = inputs.SingleInput(field="test")
        field.name = "test"
        widget = input_widgets.InputFieldWidget(field, None)
        tree = self.assertHTMLValid(widget.render_details())
        root = self.assertHasElement(tree, "body")
        self.assertFalse(root.xpath("p"))
        dl = self.assertHasElement(root, "dl")
        self.assertTextContentEqual(dl.dt[0], "Defined as:")
        self.assertTextContentEqual(dl.dd[0], "test")
        self.assertEqual(len(dl.dt), 1)

    def test_render_details_append_fullstop(self) -> None:
        field = inputs.SingleInput(field="test", help_text="Help text")
        field.name = "test"
        widget = input_widgets.InputFieldWidget(field, None)
        tree = self.assertHTMLValid(widget.render_details())
        root = self.assertHasElement(tree, "body")
        p = self.assertHasElement(root, "p")
        self.assertTextContentEqual(p, "Help text.")

    def test_render_details_append_fullstop_only_if_missing(self) -> None:
        field = inputs.SingleInput(field="test", help_text="Help text.")
        field.name = "test"
        widget = input_widgets.InputFieldWidget(field, None)
        tree = self.assertHTMLValid(widget.render_details())
        root = self.assertHasElement(tree, "body")
        p = self.assertHasElement(root, "p")
        self.assertTextContentEqual(p, "Help text.")

    def test_render_value_rows(self) -> None:
        artifact = self.playground.create_minimal_source_package_artifact()
        value = DBArtifactInfo.from_artifact(
            artifact, lookup="lookup", label="test"
        )
        field = inputs.SingleInput(field="test")
        field.name = "test"
        widget = input_widgets.InputFieldWidget(field, value)
        rows = list(widget.render_value_rows(self.context))
        self.assertEqual(len(rows), 1)
        tree = self.assertHTMLValid(rows[0])
        self.assertYAMLContentEqual(tree, value.model_dump())


class ArtifactInputFieldWidgetTests(WidgetTestCase):
    """Test :py:class:`ArtifactInputFieldWidget`."""

    artifact: ClassVar[Artifact]

    @override
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.artifact = cls.playground.create_minimal_source_package_artifact()

    @override
    def setUp(self) -> None:
        super().setUp()
        self.value = DBArtifactInfo.from_artifact(
            self.artifact, label="test", lookup=self.artifact.id
        )

        class Container(inputs.TaskFieldContainer):
            test = inputs.OptionalSingleInput()

        self.container = Container()
        self.container.test = self.value

    def test_create(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        self.assertIsInstance(widget, input_widgets.ArtifactInputFieldWidget)

    def test_render(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        tree = self.assertHTMLValid(widget.render(self.context))
        root = self.assertHasElement(tree, "body")
        self.assertEqual(len(root.tr), 1)
        self.assertIsNone(root.tr[0].td[0].get("rowspan"))

    def test_render_none(self) -> None:
        self.container.test = None
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        self.assertEqual(widget.render(self.context), "")

    def test_render_details_rows(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        rows = list(widget.render_details_rows())
        self.assertEqual(
            rows,
            [
                ("Defined as", "<code>test</code>"),
                ("Task input field", "<code>test</code>"),
                ("Lookup", input_widgets.render_lookup(self.artifact.id)),
            ],
        )

    def test_render_details_rows_empty(self) -> None:
        self.container.test = None
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        rows = list(widget.render_details_rows())
        self.assertEqual(rows, [("Defined as", "<code>test</code>")])


class InputArtifactMultipleFieldWidgetTests(WidgetTestCase):
    """Test :py:class:`InputArtifactMultipleFieldWidget`."""

    artifacts: ClassVar[list[Artifact]]

    @override
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.artifacts = [
            cls.playground.create_minimal_source_package_artifact(
                name="hello1"
            ),
            cls.playground.create_minimal_source_package_artifact(
                name="hello2"
            ),
            cls.playground.create_minimal_source_package_artifact(
                name="hello3"
            ),
        ]

    @override
    def setUp(self) -> None:
        super().setUp()
        self.value = DBInputArtifactMultiple(
            label="test",
            lookup=[a.id for a in self.artifacts],
            artifacts=[
                DBBaseTaskInputArtifact.from_artifact(a) for a in self.artifacts
            ],
        )

        class Container(inputs.TaskFieldContainer):
            test = inputs.MultiInput()

        self.container = Container()
        self.container.test = self.value

    def test_create(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        self.assertIsInstance(
            widget, input_widgets.InputArtifactMultipleFieldWidget
        )

    def test_render(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        tree = self.assertHTMLValid(widget.render(self.context))
        root = self.assertHasElement(tree, "body")
        self.assertEqual(len(root.tr), 3)
        self.assertEqual(root.tr[0].td[0].get("rowspan"), "3")

    def test_render_empty(self) -> None:
        self.container.test = DBInputArtifactMultiple(
            label="test", lookup=[a.id for a in self.artifacts], artifacts=[]
        )

        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        self.assertEqual(widget.render(self.context), "")


class ArtifactInputListFieldWidgetTests(WidgetTestCase):
    """Test :py:class:`ArtifactInputListFieldWidget`."""

    artifacts: ClassVar[list[Artifact]]

    @override
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.artifacts = [
            cls.playground.create_minimal_source_package_artifact(
                name="hello1"
            ),
            cls.playground.create_minimal_source_package_artifact(
                name="hello2"
            ),
            cls.playground.create_minimal_source_package_artifact(
                name="hello3"
            ),
        ]

    @override
    def setUp(self) -> None:
        super().setUp()
        self.value: list[InputArtifactSingle] = [
            DBArtifactInfo.from_artifact(a, lookup=a.id, label=f"test[{idx}]")
            for idx, a in enumerate(self.artifacts)
        ]

        class Container(inputs.TaskFieldContainer):
            test = inputs.SingleInputList()

        self.container = Container()
        self.container.test = self.value

    def test_create(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        self.assertIsInstance(
            widget, input_widgets.ArtifactInputListFieldWidget
        )

    def test_render(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        tree = self.assertHTMLValid(widget.render(self.context))
        root = self.assertHasElement(tree, "body")
        self.assertEqual(len(root.tr), 3)
        self.assertEqual(root.tr[0].td[0].get("rowspan"), "3")

    def test_render_empty(self) -> None:
        self.container.test = []
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        self.assertEqual(widget.render(self.context), "")

    def test_render_details_rows(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        rows = list(widget.render_details_rows())
        self.assertEqual(
            rows,
            [
                ("Defined as", "<code>test</code>"),
                (
                    "Task input field <code>test[0]</code>",
                    input_widgets.render_lookup(self.artifacts[0].id),
                ),
                (
                    "Task input field <code>test[1]</code>",
                    input_widgets.render_lookup(self.artifacts[1].id),
                ),
                (
                    "Task input field <code>test[2]</code>",
                    input_widgets.render_lookup(self.artifacts[2].id),
                ),
            ],
        )

    def test_render_details_rows_empty(self) -> None:
        self.container.test = []
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        rows = list(widget.render_details_rows())
        self.assertEqual(rows, [("Defined as", "<code>test</code>")])


class CollectionInputFieldWidgetTests(WidgetTestCase):
    """Test :py:class:`CollectionInputFieldWidget`."""

    collection: ClassVar[Collection]

    @override
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.collection = cls.playground.create_collection(
            name="sid", category=CollectionCategory.SUITE
        )

    @override
    def setUp(self) -> None:
        super().setUp()
        self.value = DBInputCollectionSingle.from_collection(
            self.collection, label="test", lookup=self.collection.id
        )

        class Container(inputs.TaskFieldContainer):
            test = dbinputs.CollectionInput()

        self.container = Container()
        self.container.test = self.value

    def test_create(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        self.assertIsInstance(widget, input_widgets.CollectionInputFieldWidget)

    def test_render(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        tree = self.assertHTMLValid(widget.render(self.context))
        root = self.assertHasElement(tree, "body")
        self.assertEqual(len(root.tr), 1)
        self.assertIsNone(root.tr[0].td[0].get("rowspan"))

    def test_render_none(self) -> None:
        self.container.test = None  # type: ignore[assignment]
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        self.assertEqual(widget.render(self.context), "")

    def test_render_details_rows(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        rows = list(widget.render_details_rows())
        self.assertEqual(
            rows,
            [
                ("Defined as", "<code>test</code>"),
                ("Task input field", "<code>test</code>"),
                ("Lookup", input_widgets.render_lookup(self.collection.id)),
            ],
        )

    def test_render_details_rows_empty(self) -> None:
        self.container.test = None  # type: ignore[assignment]
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        rows = list(widget.render_details_rows())
        self.assertEqual(rows, [("Defined as", "<code>test</code>")])


class ExtraExternalRepositoryFieldWidgetTests(WidgetTestCase):
    """Test :py:class:`ExtraExternalRepositoryFieldWidget`."""

    @override
    def setUp(self) -> None:
        super().setUp()
        self.value: list[ExtraExternalRepository] = [
            ExtraExternalRepository(
                url="http://deb.debian.org/debian",
                suite="sid",
                components=["main", "non-free-firmware"],
            ),
            ExtraExternalRepository(
                url="http://deb.debian.org/debian",
                suite="experimental",
                components=["main"],
            ),
        ]

        class Container(inputs.TaskFieldContainer):
            test = inputs.ExtraRepositoriesInput()

        self.container = Container()
        self.container.test = self.value

    def test_create(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        self.assertIsInstance(
            widget, input_widgets.ExtraExternalRepositoryFieldWidget
        )

    def test_render(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        tree = self.assertHTMLValid(widget.render(self.context))
        root = self.assertHasElement(tree, "body")
        self.assertEqual(len(root.tr), 2)
        self.assertEqual(root.tr[0].td[0].get("rowspan"), "2")

    def test_render_none(self) -> None:
        self.container.test = None
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        self.assertEqual(widget.render(self.context), "")


class FallbackWidgetTests(WidgetTestCase):
    """Test instantiating a widget for an unsupported field."""

    suite: ClassVar[Collection]
    archive: ClassVar[Collection]

    @override
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.suite = cls.playground.create_collection(
            name="sid", category=CollectionCategory.SUITE
        )
        cls.archive = cls.playground.create_singleton_collection(
            category=CollectionCategory.ARCHIVE
        )

    @override
    def setUp(self) -> None:
        super().setUp()
        self.value: tuple[
            InputCollectionSingle, InputCollectionSingle | None
        ] = (
            DBInputCollectionSingle.from_collection(
                self.suite, lookup=self.suite.id, label="test"
            ),
            DBInputCollectionSingle.from_collection(
                self.archive, lookup="_@debian:archive", label="test"
            ),
        )

        class Container(inputs.TaskFieldContainer):
            test = inputs.SuiteArchiveInput()

        self.container = Container()
        self.container.test = self.value

    def test_create(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        self.assertIsInstance(widget, input_widgets.InputFieldWidget)

    def test_render(self) -> None:
        widget = input_widgets.InputFieldWidget.create(
            self.container, self.container.inputs["test"]
        )
        tree = self.assertHTMLValid(widget.render(self.context))
        root = self.assertHasElement(tree, "body")
        self.assertEqual(len(root.tr), 1)
        self.assertIsNone(root.tr[0].td[0].get("rowspan"))
        assert self.value[1] is not None
        self.assertYAMLContentEqual(
            root.tr.td[1],
            [self.value[0].model_dump(), self.value[1].model_dump()],
        )
