# 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 the pagination widget helpers."""
from typing import cast
from urllib.parse import urlencode

import lxml
from django.db.models import F
from django.template import Context
from django.test import RequestFactory

from debusine.db.models import User
from debusine.test.django import TestCase
from debusine.web.views.pagination import (
    OrderBys,
    Ordering,
    Pagination,
    order_by_tuple,
)
from debusine.web.views.tests.utils import ViewTestMixin


class PaginationTestCase(ViewTestMixin, TestCase):
    """Common test infrastructure."""

    def _pagination(
        self,
        page_size: int = 3,
        default_ordering: OrderBys | None = "username",
        **kwargs: str,
    ) -> Pagination[User]:
        url = "/"
        if kwargs:
            url += "?" + urlencode(kwargs)
        request = RequestFactory().get(url)
        queryset = User.objects.all()
        return Pagination(
            request,
            queryset,
            page_size=page_size,
            default_ordering=default_ordering,
        )


class OrderByTupleTests(TestCase):
    """Tests for :py:func:`order_by_tuple`."""

    def test_single(self) -> None:
        for val in (
            "name",
            F("name"),
            F("name").asc(),
            ["name"],
            [F("name")],
            [F("name").asc()],
        ):
            with self.subTest(val=val):
                self.assertEqual(
                    str(order_by_tuple(val)),
                    "(OrderBy(F(name), descending=False),)",
                )

    def test_multi(self) -> None:
        for val in (
            ["name1", "name2"],
            [F("name1"), F("name2")],
            [F("name1").asc(), F("name2").asc()],
            cast(OrderBys, ["name1", F("name2").asc()]),
            cast(OrderBys, [F("name1"), "name2"]),
        ):
            with self.subTest(val=val):
                self.assertEqual(
                    str(order_by_tuple(val)),
                    "(OrderBy(F(name1), descending=False),"
                    " OrderBy(F(name2), descending=False))",
                )


class OrderingTests(TestCase):
    """Tests for :py:class:`Ordering`."""

    def test_default_desc(self) -> None:
        """Test inferring descending with a single field."""
        for val in ("name", F("name"), F("name").asc()):
            with self.subTest(val=val):
                o = Ordering(val)
                self.assertEqual(
                    str(o.asc), "(OrderBy(F(name), descending=False),)"
                )
                self.assertEqual(
                    str(o.desc), "(OrderBy(F(name), descending=True),)"
                )

    def test_default_desc_multi(self) -> None:
        """Test inferring descending with a single field."""
        for val in (
            ("name1", "name2"),
            (F("name1"), F("name2")),
            (F("name1").asc(), F("name2").asc()),
        ):
            with self.subTest(val=val):
                o = Ordering(val)
                self.assertEqual(
                    str(o.asc),
                    "(OrderBy(F(name1), descending=False),"
                    " OrderBy(F(name2), descending=False))",
                )
                self.assertEqual(
                    str(o.desc),
                    "(OrderBy(F(name1), descending=True),"
                    " OrderBy(F(name2), descending=True))",
                )

    def test_asc_desc(self) -> None:
        """Test inferring descending with a single field."""
        o = Ordering("name1", "name2")
        self.assertEqual(str(o.asc), "(OrderBy(F(name1), descending=False),)")
        self.assertEqual(str(o.desc), "(OrderBy(F(name2), descending=False),)")

    def test_dash(self) -> None:
        """Test inferring descending with a single field."""
        o = Ordering("name", "-name")
        self.assertEqual(str(o.asc), "(OrderBy(F(name), descending=False),)")
        self.assertEqual(str(o.desc), "(OrderBy(F(name), descending=True),)")


class ColumnTests(PaginationTestCase):
    """Tests for :py:class:`Column`."""

    def test_column(self) -> None:
        """Test a simple use case."""
        pagination = self._pagination()
        col = pagination.add_column("user", "User", Ordering("username"))
        self.assertIs(col.pagination, pagination)
        self.assertEqual(col.name, "user")
        self.assertEqual(col.title, "User")
        self.assertFalse(col.current)
        self.assertEqual(col.query_asc, "order=user&asc=1")
        self.assertEqual(col.query_desc, "order=user&asc=0")
        self.assertHTMLValid(col.render(Context()))

    def test_current(self) -> None:
        """Test the current sorting column."""
        pagination = self._pagination(order="user", foo="bar")
        col = pagination.add_column("user", "User", Ordering("username"))
        self.assertTrue(col.current)
        self.assertEqual(col.query_asc, "order=user&foo=bar&asc=1")
        self.assertEqual(col.query_desc, "order=user&foo=bar&asc=0")
        self.assertHTMLValid(col.render(Context()))


class PageNavigationTests(PaginationTestCase):
    """Tests for :py:class:`PageNavigation`."""

    @classmethod
    def setUpTestData(cls) -> None:
        """Initialize class data."""
        super().setUpTestData()
        for idx in range(10):
            cls.playground.create_user(f"user{idx:02d}")

    def _page_numbers(
        self, tree: lxml.objectify.ObjectifiedElement
    ) -> list[str]:
        items = tree.xpath("//li[contains(@class, 'page-item')]")
        return [self.get_node_text_normalized(li) for li in items]

    def test_render(self) -> None:
        """Test a simple use case."""
        pagination = self._pagination()
        nav = pagination.page_navigation
        self.assertIs(nav.pagination, pagination)
        tree = self.assertHTMLValid(nav.render(Context()))
        current = self.assertHasElement(tree, "//li[@class='page-item active']")
        self.assertTextContentEqual(current, "1")
        self.assertEqual(self._page_numbers(tree), ["1", "2", "3", "4"])

    def test_ellipsis(self) -> None:
        """Test page navigation with ellipses."""
        for idx in range(20):
            self.playground.create_user(f"user1{idx:02d}")
        pagination = self._pagination()
        nav = pagination.page_navigation
        self.assertIs(nav.pagination, pagination)
        tree = self.assertHTMLValid(nav.render(Context()))
        self.assertEqual(
            self._page_numbers(tree), ["1", "2", "3", "4", "…", "10", "11"]
        )


class PaginationTests(PaginationTestCase):
    """Tests for :py:class:`Pagination`."""

    @classmethod
    def setUpTestData(cls) -> None:
        """Initialize class data."""
        super().setUpTestData()
        for idx in range(10):
            cls.playground.create_user(f"user{idx:02d}")

    def test_no_columns(self) -> None:
        """Test with no columns defined."""
        p = self._pagination()
        self.assertIsNone(p.current_column_name)
        self.assertIsNone(p.current_column)
        self.assertTrue(p.asc)
        self.assertQuerySetEqual(
            p.ordered_queryset, list(User.objects.all().order_by("username"))
        )
        self.assertEqual(p.paginator.num_pages, 4)
        self.assertFalse(p.page_obj.has_previous())
        self.assertIs(p.page_navigation.pagination, p)

    def test_column_not_current(self) -> None:
        """Test with no columns defined."""
        p = self._pagination()
        p.add_column("user", "User", Ordering("username"))
        self.assertIsNone(p.current_column_name)
        self.assertIsNone(p.current_column)
        self.assertTrue(p.asc)
        self.assertQuerySetEqual(
            p.ordered_queryset, list(User.objects.all().order_by("username"))
        )

    def test_column_current(self) -> None:
        """Test with no columns defined."""
        p = self._pagination(order="user")
        col = p.add_column("user", "User", Ordering("username"))
        self.assertEqual(p.current_column_name, "user")
        self.assertIs(p.current_column, col)
        self.assertTrue(p.asc)
        self.assertQuerySetEqual(
            p.ordered_queryset, list(User.objects.all().order_by("username"))
        )

    def test_column_current_desc(self) -> None:
        """Test with no columns defined."""
        p = self._pagination(order="user", asc="0")
        col = p.add_column("user", "User", Ordering("username"))
        self.assertEqual(p.current_column_name, "user")
        self.assertIs(p.current_column, col)
        self.assertFalse(p.asc)
        self.assertQuerySetEqual(
            p.ordered_queryset, list(User.objects.all().order_by("-username"))
        )

    def test_no_default_ordering(self) -> None:
        """Test with no default ordering."""
        p = self._pagination(default_ordering=None)
        p.queryset = p.queryset.order_by("-username")
        p.add_column("user", "User", Ordering("username"))
        self.assertIsNone(p.current_column_name)
        self.assertIsNone(p.current_column)
        self.assertTrue(p.asc)
        self.assertQuerySetEqual(
            p.ordered_queryset,
            list(User.objects.all().order_by("-username")),
        )
