# 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 the Place system."""

from typing import Any, override

import lxml
from django.db.models import Model
from django.test import RequestFactory

from debusine.db.models import Scope
from debusine.db.playground import scenarios
from debusine.test.django import TestCase
from debusine.web.views.places import (
    Place,
    PlaceValue,
    ResourceMainPlace,
    ResourcePlace,
    ResourceSettingsPlace,
    ResourceSubPlace,
    SitePlace,
    ValueVariants,
)
from debusine.web.views.tests.utils import ViewTestMixin


class PlaceTestBase[P: Place](ViewTestMixin, TestCase):
    """Common assertions for testing Place objects."""

    scenario = scenarios.DefaultScopeUser()

    DEFAULT_TITLE = "title"
    DEFAULT_TITLE_ACTION = "action"
    DEFAULT_TITLE_BREADCRUMB = "breadcrumb"
    DEFAULT_TITLE_LINK = "link"
    DEFAULT_DESCRIPTION = "description"
    DEFAULT_URL = "/test-url"
    DEFAULT_ICON = "icon"

    place_class: type[P]

    @override
    def setUp(self) -> None:
        super().setUp()
        request = RequestFactory().get("/")
        self.scope_ui = self.scenario.scope.ui(request)

    def assertNoIcon(self, element: lxml.objectify.ObjectifiedElement) -> None:
        """Ensure the element does not have an icon."""
        self.assertFalse(element.xpath("span"))

    def assertHasIcon(
        self,
        element: lxml.objectify.ObjectifiedElement,
        value: str,
        dump_on_error: bool = False,
    ) -> None:
        """Ensure the element contains the given icon."""
        span = self.assertHasElement(
            element, "span", dump_on_error=dump_on_error
        )
        self.assertEqual(span.get("class"), f"bi bi-{value}")
        self.assertTextContentEqual(span, "")

    def assertNoTooltip(
        self, element: lxml.objectify.ObjectifiedElement
    ) -> None:
        """Ensure the element does not contain tooltips."""
        self.assertIsNone(element.get("title"))

    def assertHasTooltip(
        self, element: lxml.objectify.ObjectifiedElement, value: str
    ) -> None:
        """Ensure the element contains the given tooltip."""
        self.assertEqual(element.get("title"), value)

    def assertBreadcrumbElement(self, title_element_type: str) -> None:
        """
        Check breadcrumbs rendering.

        :param title_element_type: expected element type to use for the last
          (current) breadcrumb item.
        """
        with self.subTest(parents=False):
            place = self.make_place()
            tree = self.assertHTMLValid(place.as_breadcrumbs())
            self.assertEqual(
                [el.tag for el in tree.body.getchildren()], [title_element_type]
            )
            title = self.assertHasElement(
                tree, f"//{title_element_type}[last()]"
            )
            self.assertTextContentEqual(title, self.DEFAULT_TITLE)
            self.assertNoIcon(title)
            self.assertHasTooltip(title, self.DEFAULT_DESCRIPTION)

        with self.subTest(parents=True):
            place = self.make_place(
                parent=ResourceMainPlace(
                    ui=self.scope_ui,
                    title=PlaceValue("t", breadcrumb="parent"),
                    url="u",
                )
            )
            tree = self.assertHTMLValid(place.as_breadcrumbs())
            self.assertEqual(
                [el.tag for el in tree.body.getchildren()],
                ["a", "span", title_element_type],
            )
            title = self.assertHasElement(
                tree, f"//{title_element_type}[last()]"
            )
            self.assertTextContentEqual(title, self.DEFAULT_TITLE)
            self.assertNoIcon(title)
            self.assertHasTooltip(title, self.DEFAULT_DESCRIPTION)

    def make_place(self, **kwargs: Any) -> P:
        """Instantiate the place to test."""
        kwargs.setdefault(
            "title",
            PlaceValue(
                self.DEFAULT_TITLE,
                breadcrumb=self.DEFAULT_TITLE_BREADCRUMB,
                action=self.DEFAULT_TITLE_ACTION,
                link=self.DEFAULT_TITLE_LINK,
            ),
        )
        kwargs.setdefault("description", self.DEFAULT_DESCRIPTION)
        kwargs.setdefault("url", self.DEFAULT_URL)
        kwargs.setdefault("icon", self.DEFAULT_ICON)
        return self.place_class(**kwargs)

    def test_as_head_title(self) -> None:
        place = self.make_place()
        tree = self.assertHTMLValid(place.as_head_title())
        title = self.assertHasElement(tree, "//title")
        self.assertTextContentEqual(title, self.DEFAULT_TITLE)
        self.assertNoIcon(title)
        self.assertNoTooltip(title)

    def test_as_head_title_with_parent(self) -> None:
        place = self.make_place(
            parent=ResourceMainPlace(
                ui=self.scope_ui,
                title=PlaceValue("t", breadcrumb="parent"),
                url="u",
            )
        )
        tree = self.assertHTMLValid(place.as_head_title())
        title = self.assertHasElement(tree, "//title")
        self.assertTextContentEqual(title, f"parent - {self.DEFAULT_TITLE}")
        self.assertNoIcon(title)
        self.assertNoTooltip(title)

    def test_as_button(self) -> None:
        place = self.make_place(icon=None, description=None)
        tree = self.assertHTMLValid(place.as_button())
        a = self.assertHasElement(tree, "//a")
        self.assertTextContentEqual(a, self.DEFAULT_TITLE_ACTION)
        self.assertEqual(a.get("href"), self.DEFAULT_URL)
        self.assertNoIcon(a)
        self.assertNoTooltip(a)

    def test_as_button_icon(self) -> None:
        place = self.make_place(description=None)
        tree = self.assertHTMLValid(place.as_button())
        a = self.assertHasElement(tree, "//a")
        self.assertTextContentEqual(a, self.DEFAULT_TITLE_ACTION)
        self.assertEqual(a.get("href"), self.DEFAULT_URL)
        self.assertHasIcon(a, self.DEFAULT_ICON)
        self.assertNoTooltip(a)

    def test_as_button_description(self) -> None:
        place = self.make_place(icon=None)
        tree = self.assertHTMLValid(place.as_button())
        a = self.assertHasElement(tree, "//a")
        self.assertTextContentEqual(a, self.DEFAULT_TITLE_ACTION)
        self.assertEqual(a.get("href"), self.DEFAULT_URL)
        self.assertNoIcon(a)
        self.assertHasTooltip(a, self.DEFAULT_DESCRIPTION)

    def test_as_link_from_title(self) -> None:
        place = self.make_place()
        tree = self.assertHTMLValid(place.as_link())
        a = self.assertHasElement(tree, ".//a")
        self.assertTextContentEqual(a, self.DEFAULT_TITLE_LINK)
        self.assertEqual(a.get("href"), self.DEFAULT_URL)

    def test_as_nav_link(self) -> None:
        place = self.make_place(icon=None, description=None)
        tree = self.assertHTMLValid(place.as_nav_link())
        a = self.assertHasElement(tree, "//a")
        self.assertTextContentEqual(a, self.DEFAULT_TITLE_ACTION)
        self.assertEqual(a.get("href"), self.DEFAULT_URL)
        self.assertNoTooltip(a)
        self.assertNoIcon(a)

    def test_as_nav_link_icon(self) -> None:
        place = self.make_place(description=None)
        tree = self.assertHTMLValid(place.as_nav_link())
        a = self.assertHasElement(tree, "//a")
        self.assertTextContentEqual(a, self.DEFAULT_TITLE_ACTION)
        self.assertEqual(a.get("href"), self.DEFAULT_URL)
        self.assertHasIcon(a, self.DEFAULT_ICON)
        self.assertNoTooltip(a)

    def test_as_nav_link_icon_description(self) -> None:
        place = self.make_place()
        tree = self.assertHTMLValid(place.as_nav_link())
        a = self.assertHasElement(tree, "//a")
        self.assertTextContentEqual(a, self.DEFAULT_TITLE_ACTION)
        self.assertEqual(a.get("href"), self.DEFAULT_URL)
        self.assertHasIcon(a, self.DEFAULT_ICON)
        self.assertHasTooltip(a, self.DEFAULT_DESCRIPTION)

    def test_as_dropdown_item(self) -> None:
        place = self.make_place()
        tree = self.assertHTMLValid(place.as_dropdown_item())
        a = self.assertHasElement(tree, "//a")
        self.assertEqual(a.get("href"), place.url)
        self.assertTextContentEqual(a, self.DEFAULT_TITLE_ACTION)
        self.assertHasTooltip(a, self.DEFAULT_DESCRIPTION)


class NoPageTitleMixin[P: Place](PlaceTestBase[P]):
    """Mixin to add tests for places that do not render page titles."""

    def test_is_page_title_visible(self) -> None:
        place = self.make_place()
        self.assertFalse(place.is_page_title_visible())

    def test_as_page_title_is_empty(self) -> None:
        place = self.make_place(icon=None, description=None)
        self.assertEqual(place.as_page_title(), "")


class PageTitleMixin[P: Place](PlaceTestBase[P]):
    """Mixin to add tests for places that render page titles."""

    def test_is_page_title_visible(self) -> None:
        place = self.make_place()
        self.assertTrue(place.is_page_title_visible())

    def test_as_page_title(self) -> None:
        place = self.make_place(icon=None, description=None)
        tree = self.assertHTMLValid(place.as_page_title())
        h1 = self.assertHasElement(tree, "//h1")
        self.assertTextContentEqual(h1, self.DEFAULT_TITLE)
        self.assertNoIcon(h1)
        self.assertNoTooltip(h1)

    def test_as_page_title_icon(self) -> None:
        place = self.make_place(description=None)
        tree = self.assertHTMLValid(place.as_page_title())
        h1 = self.assertHasElement(tree, "//h1")
        self.assertTextContentEqual(h1, self.DEFAULT_TITLE)
        self.assertNoIcon(h1)
        self.assertNoTooltip(h1)

    def test_as_page_title_icon_title(self) -> None:
        place = self.make_place()
        tree = self.assertHTMLValid(place.as_page_title())
        h1 = self.assertHasElement(tree, "//h1")
        self.assertTextContentEqual(h1, self.DEFAULT_TITLE)
        self.assertNoIcon(h1)
        self.assertNoTooltip(h1)


class SitePlaceTests(NoPageTitleMixin[SitePlace], PlaceTestBase[SitePlace]):
    """Tests for the :py:class:`SitePlace` class."""

    place_class = SitePlace

    def test_breadcrumbs(self) -> None:
        self.assertBreadcrumbElement("h1")


class ResourcePlaceTestBase[R: Model](
    PlaceTestBase[ResourcePlace[R]],
):
    """Tests for the :py:class:`ResourcePlace` class."""

    place_class: type[ResourcePlace[R]]

    @override
    def make_place(self, **kwargs: Any) -> ResourcePlace[R]:
        """Instantiate the place to test."""
        kwargs.setdefault("ui", self.scope_ui)
        return super().make_place(**kwargs)

    def test_type_label_default(self) -> None:
        place = self.make_place()
        self.assertEqual(place.title.get(ValueVariants.TYPE), "Scope")

    def test_current_defaults_when_current(self) -> None:
        place = self.make_place(url="/")
        self.assertTrue(place.current)

    def test_current_defaults_when_not_current(self) -> None:
        place = self.make_place(url="/other")
        self.assertFalse(place.current)


class ResourceMainPlaceTests(
    PageTitleMixin[ResourcePlace[Scope]], ResourcePlaceTestBase[Scope]
):
    """Tests for :py:class:`ResourceMainPlace`."""

    place_class: type[ResourceMainPlace[Scope]] = ResourceMainPlace

    def test_breadcrumbs(self) -> None:
        self.assertBreadcrumbElement("span")


class ResourceSubPlaceTests(
    NoPageTitleMixin[ResourcePlace[Scope]], ResourcePlaceTestBase[Scope]
):
    """Tests for :py:class:`ResourceSubPlace`."""

    place_class: type[ResourceSubPlace[Scope]] = ResourceSubPlace

    def test_breadcrumbs(self) -> None:
        self.assertBreadcrumbElement("h1")


class ResourceSettingsPlaceTests(
    PageTitleMixin[ResourcePlace[Scope]], ResourcePlaceTestBase[Scope]
):
    """Tests for :py:class:`ResourceSettingsPlace`."""

    place_class: type[ResourceSettingsPlace[Scope]] = ResourceSettingsPlace

    def test_breadcrumbs(self) -> None:
        self.assertBreadcrumbElement("span")


del ResourcePlaceTestBase
del PageTitleMixin
del NoPageTitleMixin
del PlaceTestBase
