# 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 debusine.test.django import TestCase
from debusine.web.views.places import (
    BreadcrumbPlace,
    ContainerPlace,
    IndexPlace,
    Place,
    PlaceType,
    ResourcePlace,
)
from debusine.web.views.tests.utils import ViewTestMixin


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

    DEFAULT_TITLE = "title"
    DEFAULT_DESCRIPTION = "description"
    DEFAULT_URL = "/test-url"
    DEFAULT_ICON = "icon"

    place_class: type[P]

    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 make_place(self, **kwargs: Any) -> P:
        """Instantiate the place to test."""
        kwargs.setdefault("title", self.DEFAULT_TITLE)
        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_nav(self) -> None:
        place = self.make_place(icon=None, description=None)
        tree = self.assertHTMLValid(place.as_nav())
        a = self.assertHasElement(tree, "//a")
        self.assertTextContentEqual(a, self.DEFAULT_TITLE)
        self.assertEqual(a.get("href"), self.DEFAULT_URL)
        self.assertNoTooltip(a)
        self.assertNoIcon(a)

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

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

    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=ResourcePlace(title="t", url="u", breadcrumb="parent")
        )
        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)
        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)
        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)
        self.assertEqual(a.get("href"), self.DEFAULT_URL)
        self.assertNoIcon(a)
        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 BreadcrumbPlaceTestBase[BP: BreadcrumbPlace](PlaceTestBase[BP]):
    """Base class for BreadcrumbPlace tests."""

    DEFAULT_BREADCRUMB = "breadcrumb"
    place_class: type[BP]

    @override
    def make_place(self, **kwargs: Any) -> BP:
        kwargs.setdefault("breadcrumb", self.DEFAULT_BREADCRUMB)
        return super().make_place(**kwargs)


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

    place_class = IndexPlace

    def test_get_type(self) -> None:
        place = self.make_place()
        self.assertEqual(place.get_type(), PlaceType.INDEX)

    def test_breadcrumb_no_parent(self) -> None:
        place = self.make_place()
        tree = self.assertHTMLValid(place.as_breadcrumbs())
        self.assertEqual([el.tag for el in tree.body.getchildren()], ["h1"])
        h1 = self.assertHasElement(tree, "//h1")
        self.assertTextContentEqual(h1, self.DEFAULT_TITLE)
        self.assertNoIcon(h1)
        self.assertNoTooltip(h1)

    def test_breadcrumb_parent(self) -> None:
        place = self.make_place(
            parent=ResourcePlace(title="t", url="u", breadcrumb="parent")
        )
        tree = self.assertHTMLValid(place.as_breadcrumbs())
        self.assertEqual(
            [el.tag for el in tree.body.getchildren()], ["a", "span", "h1"]
        )
        h1 = self.assertHasElement(tree, "//h1")
        self.assertTextContentEqual(h1, self.DEFAULT_TITLE)
        self.assertNoIcon(h1)
        self.assertNoTooltip(h1)


class ResourcePlaceTests(
    PageTitleMixin[ResourcePlace], BreadcrumbPlaceTestBase[ResourcePlace]
):
    """Tests for the :py:class:`ResourcePlace` class."""

    place_class = ResourcePlace

    def test_get_type(self) -> None:
        place = self.make_place()
        self.assertEqual(place.get_type(), PlaceType.RESOURCE)

    def test_breadcrumb_no_parent(self) -> None:
        place = self.make_place()
        tree = self.assertHTMLValid(place.as_breadcrumbs())
        self.assertEqual([el.tag for el in tree.body.getchildren()], ["span"])
        span = self.assertHasElement(tree, "//span")
        self.assertTextContentEqual(span, self.DEFAULT_BREADCRUMB)
        self.assertNoIcon(span)
        self.assertHasTooltip(span, self.DEFAULT_DESCRIPTION)

    def test_breadcrumb_parent(self) -> None:
        place = self.make_place(
            parent=ResourcePlace(title="t", url="u", breadcrumb="parent")
        )
        tree = self.assertHTMLValid(place.as_breadcrumbs())
        self.assertEqual(
            [el.tag for el in tree.body.getchildren()],
            ["a", "span", "span"],
        )
        span = tree.body.span[-1]
        self.assertTextContentEqual(span, self.DEFAULT_BREADCRUMB)
        self.assertNoIcon(span)
        self.assertHasTooltip(span, self.DEFAULT_DESCRIPTION)


class ContainerPlaceTests(
    NoPageTitleMixin[ContainerPlace], BreadcrumbPlaceTestBase[ContainerPlace]
):
    """Tests for the :py:class:`ContainerPlace` class."""

    place_class = ContainerPlace

    def test_get_type(self) -> None:
        place = self.make_place()
        self.assertEqual(place.get_type(), PlaceType.CONTAINER)

    def test_breadcrumb_no_parent(self) -> None:
        place = self.make_place()
        tree = self.assertHTMLValid(place.as_breadcrumbs())
        self.assertEqual([el.tag for el in tree.body.getchildren()], ["span"])
        span = self.assertHasElement(tree, "//span")
        self.assertTextContentEqual(span, self.DEFAULT_TITLE)
        self.assertNoIcon(span)
        self.assertHasTooltip(span, self.DEFAULT_DESCRIPTION)

    def test_breadcrumb_parent(self) -> None:
        place = self.make_place(
            parent=ResourcePlace(title="t", url="u", breadcrumb="parent")
        )
        tree = self.assertHTMLValid(place.as_breadcrumbs())
        self.assertEqual(
            [el.tag for el in tree.body.getchildren()],
            ["a", "span", "span"],
        )
        span = tree.body.span[-1]
        self.assertTextContentEqual(span, self.DEFAULT_TITLE)
        self.assertNoIcon(span)
        self.assertHasTooltip(span, self.DEFAULT_DESCRIPTION)


del BreadcrumbPlaceTestBase
del PageTitleMixin
del NoPageTitleMixin
del PlaceTestBase
