# 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 views."""
from typing import Any
from unittest import mock

from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.template.context import Context
from django.test import override_settings
from django.urls import reverse
from rest_framework import status

from debusine.db.context import context
from debusine.db.models import (
    Scope,
    WorkRequest,
    WorkflowTemplate,
    default_workspace,
)
from debusine.db.playground import scenarios
from debusine.test.django import ListFilter, TestCase, override_permission
from debusine.web.templatetags import debusine as template_debusine
from debusine.web.views import HomepageView
from debusine.web.views.base import Widget
from debusine.web.views.tests.utils import ViewTestMixin, html_check_icon


class HomepageViewTests(ViewTestMixin, TestCase):
    """Tests for the Homepage class."""

    scenario = scenarios.DefaultScopeUser()

    create_work_request_url = reverse(
        "workspaces:work-requests:create", kwargs={"wname": "System"}
    )
    create_work_request_html = (
        f'<a class="dropdown-item" href="{create_work_request_url}">'
        "Create work request</a>"
    )

    def test_html_check_icon(self) -> None:
        """Test that html_check_icon returns the right results."""
        self.assertIn("green", html_check_icon(True))
        self.assertNotIn("green", html_check_icon(False))
        self.assertIn("red", html_check_icon(False))
        self.assertNotIn("red", html_check_icon(True))

    def test_slash(self) -> None:
        """
        Slash (/) URL and homepage are the same.

        Make sure that the / is handled by the 'homepage' view.
        """
        self.assertEqual(reverse("homepage:homepage"), "/")

    def test_homepage(self) -> None:
        """Homepage view loads."""
        list_your_tokens_url = reverse(
            "user:token-list", kwargs={"username": self.scenario.user.username}
        )
        list_your_tokens_html = (
            f'<a class="dropdown-item" href="{list_your_tokens_url}">Tokens</a>'
        )

        response = self.client.get(reverse("homepage:homepage"))
        tree = self.assertResponseHTML(response)
        h1 = self.assertHasElement(tree, "//h1")
        self.assertTextContentEqual(h1, "Welcome to debusine!")

        title = self.assertHasElement(tree, "//title")
        self.assertTextContentEqual(title, "Debusine - Homepage")

        self.assertNavCommonElements(tree, is_homepage=True)
        self.assertNavNoUser(tree)

        self.assertNotContains(response, list_your_tokens_html, html=True)
        self.assertFalse(tree.xpath("//a[@id='nav-create-artifact']"))
        self.assertFalse(tree.xpath("//a[@id='nav-create-work-request']"))

        ul = self.assertHasElement(tree, "//ul[@id='scope-list']")
        self.assertEqual(ul.li.a.get("href"), reverse("scopes:detail"))
        self.assertTextContentEqual(ul.li.a, "debusine")

    def test_homepage_logged_in(self) -> None:
        """User is logged in: contains "You are authenticated as: username"."""
        self.client.force_login(self.scenario.user)
        response = self.client.get(reverse("homepage:homepage"))
        tree = self.assertResponseHTML(response)

        self.assertNavCommonElements(tree, is_homepage=True)
        self.assertNavHasUser(tree, self.scenario.user)

        self.assertFalse(tree.xpath("//a[@id='nav-create-artifact']"))
        self.assertFalse(tree.xpath("//a[@id='nav-create-work-request']"))

        self.assertContains(
            response,
            f"No work requests created by {self.scenario.user.username}.",
        )

    def test_homepage_logged_in_with_work_requests(self) -> None:
        """User is logged in and contain user's work requests."""
        username = "testuser"

        user = get_user_model().objects.create_user(
            username=username, password="password"
        )

        self.client.force_login(user)

        work_request1 = self.playground.create_work_request(
            task_name="noop", created_by=user, id=11
        )
        work_request2 = self.playground.create_work_request(
            task_name="noop", created_by=user, id=12
        )

        response = self.client.get(reverse("homepage:homepage"))
        tree = self.assertResponseHTML(response)
        table = self.assertHasElement(tree, "//table[@id='work_request-list']")
        self.assertWorkRequestRow(table.tbody.tr[0], work_request2)
        self.assertWorkRequestRow(table.tbody.tr[1], work_request1)

        # latest first
        self.assertEqual(
            response.context["work_requests"].page_obj.object_list[0].id, 12
        )

    def test_homepage_exclude_internal_work_requests(self) -> None:
        """The homepage excludes the user's INTERNAL work requests."""
        username = "testuser"

        user = get_user_model().objects.create_user(
            username=username, password="password"
        )

        self.client.force_login(user)

        template = WorkflowTemplate.objects.create(
            name="test",
            workspace=default_workspace(),
            task_name="noop",
            task_data={},
        )
        root = self.playground.create_workflow(
            template, task_data={}, created_by=user
        )
        WorkRequest.objects.create_synchronization_point(
            parent=root, step="test"
        )

        response = self.client.get(reverse("homepage:homepage"))
        tree = self.assertResponseHTML(response)
        table = self.assertHasElement(tree, "//table[@id='work_request-list']")
        self.assertWorkRequestRow(table.tbody.tr[0], root)
        self.assertEqual(len(table.tbody.tr), 1)

    def test_messages(self) -> None:
        """Messages from django.contrib.messages are displayed."""

        def mocked_get_context_data(
            self: HomepageView, **kwargs: Any
        ) -> dict[str, Any]:
            messages.error(self.request, "Error message")
            return {
                "base_template": HomepageView.base_template,
            }

        with mock.patch(
            "debusine.web.views.views.HomepageView.get_context_data",
            autospec=True,
            side_effect=mocked_get_context_data,
        ):
            response = self.client.get(reverse("homepage:homepage"))

        tree = self.assertResponseHTML(response)
        div = self.assertHasElement(tree, "//div[@id='user-message-container']")
        msgdiv = div.div.div
        self.assertEqual(msgdiv.get("class"), "toast")
        self.assertEqual(msgdiv.get("role"), "alert")
        assert msgdiv.div[1].text is not None
        self.assertEqual(msgdiv.div[1].text.strip(), "Error message")

    def test_homepage_scope_list(self) -> None:
        """Homepage lists visible scopes."""
        scope1 = self.playground.get_or_create_scope("scope1")
        self.playground.get_or_create_scope("scope2")

        with override_permission(
            Scope, "can_display", ListFilter, exclude=[scope1]
        ):
            response = self.client.get(reverse("homepage:homepage"))
        tree = self.assertResponseHTML(response)
        ul = self.assertHasElement(tree, "//ul[@id='scope-list']")
        self.assertEqual(
            [self.get_node_text_normalized(li) for li in ul.li],
            ["debusine", "scope2"],
        )


class WidgetTests(TestCase):
    """Test widget infrastructure."""

    def test_render(self) -> None:
        """Test rendering a widget."""

        class _TestWidget(Widget):
            def render(self, _: Context) -> str:
                return "RENDERED"

        self.assertEqual(
            template_debusine.widget(Context(), _TestWidget()), "RENDERED"
        )

    def test_render_error(self) -> None:
        """Test rendering a widget that raises on render."""

        class _TestWidget(Widget):
            def render(self, _: Context) -> str:
                raise RuntimeError("ERROR")

        with override_settings(DEBUG=False):
            with self.assertLogs("debusine.web") as log:
                self.assertRegex(
                    template_debusine.widget(Context(), _TestWidget()),
                    "<span data-role='debusine-widget-error' "
                    "class='[^']+'>.+UTC: .+failed to render</span>$",
                )
            self.assertIn("failed to render", log.output[0].split("\n")[0])

        with override_settings(DEBUG=True):
            with self.assertRaises(RuntimeError) as e:
                template_debusine.widget(Context(), _TestWidget())
            self.assertEqual(str(e.exception), "ERROR")


class AdminTests(TestCase):
    """Test the admin view."""

    def assertLoginRequired(self, url: str) -> None:
        """Test that a login is required to access the given URL."""
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_302_FOUND)
        # TODO: we apparently have an admin:login view: what does it do? Does
        # it need to be secured? Do we want an admin at all?
        self.assertEqual(
            response["Location"],
            reverse("admin:login") + "?next=" + url,
        )

    def test_admin_url(self) -> None:
        """Test resolving the admin URL."""
        url = reverse("admin:index")
        self.assertTrue(url.startswith("/-/admin"))

    def test_admin_anonymous_user(self) -> None:
        """Test resolving the admin URL."""
        self.assertLoginRequired(reverse("admin:index"))

    def test_admin_ordinary_user(self) -> None:
        """Test resolving the admin URL."""
        self.client.force_login(self.playground.get_default_user())
        self.assertLoginRequired(reverse("admin:index"))

    def test_admin_staff(self) -> None:
        """Test resolving the admin URL."""
        user = self.playground.get_default_user()
        user.is_staff = True
        user.save()
        self.client.force_login(user)
        response = self.client.get(reverse("admin:index"))
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_admin_superuser(self) -> None:
        """Test resolving the admin URL."""
        user = self.playground.get_default_user()
        user.is_superuser = True
        user.save()
        self.client.force_login(user)
        self.assertLoginRequired(reverse("admin:index"))


class LegacyRedirectTests(TestCase):
    """Test the best effort redirects for unscoped URLs."""

    def test_scope_redirect_valid(self) -> None:
        """Test redirection of a valid legacy URL."""
        response = self.client.get("/accounts/foo/bar?baz=true")
        self.assertEqual(
            response.status_code, status.HTTP_301_MOVED_PERMANENTLY
        )
        self.assertEqual(
            response.headers["Location"],
            f"/{settings.DEBUSINE_DEFAULT_SCOPE}/accounts/foo/bar?baz=true",
        )

    def test_scope_redirect_invalid(self) -> None:
        """Test redirection of an invalid legacy URL."""
        # Misspelled url (misses trailing 's')
        response = self.client.get("/account/foo/bar?baz=true")
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

    def test_scope_reproduce_redirect_loop(self) -> None:
        """Reproduce a redirect loop."""
        with context.disable_permission_checks():
            artifact, _ = self.playground.create_artifact()

        response = self.client.get(f"/api/1.0/artifact/{artifact.pk}")
        self.assertEqual(
            response.status_code, status.HTTP_301_MOVED_PERMANENTLY
        )
        self.assertEqual(
            response.headers["Location"], f"/api/1.0/artifact/{artifact.pk}/"
        )

        response = self.client.get(
            f"/{settings.DEBUSINE_DEFAULT_SCOPE}/System/artifact/{artifact.pk}"
        )
        self.assertEqual(
            response.status_code, status.HTTP_301_MOVED_PERMANENTLY
        )
        self.assertEqual(
            response.headers["Location"],
            f"/{settings.DEBUSINE_DEFAULT_SCOPE}"
            f"/System/artifact/{artifact.pk}/",
        )

    # urlconfs are created at Django startup, so changing the setting
    # afterwards has no effect: the redirect replacement pattern has already
    # been created. This test would be pointless, but keeping it here as
    # documentation. I would not subclass RedirectView only to have a more
    # dynamic replacement pattern for use only in tests.
    #
    # @override_settings(DEBUSINE_DEFAULT_SCOPE="debian")
    # def test_redirect_different_scope(self) -> None:
    #     """Redirection uses DEBUSINE_DEFAULT_SCOPE."""
    #     response = self.client.get("/accounts/foo/bar?baz=true")
    #     self.assertEqual(
    #         response.status_code, status.HTTP_301_MOVED_PERMANENTLY
    #     )
    #     self.assertEqual(
    #         response.headers["Location"],
    #         "/debian/accounts/foo/bar?baz=true",
    #     )

    def test_workspace_redirect_valid(self) -> None:
        """Test redirection of a valid legacy workspace URL."""
        response = self.client.get("/debusine/workspace/foo/bar?baz=true")
        self.assertEqual(
            response.status_code, status.HTTP_301_MOVED_PERMANENTLY
        )
        self.assertEqual(
            response.headers["Location"],
            "/debusine/foo/bar?baz=true",
        )

    def test_redirect_task_status(self) -> None:
        """Test redirection on the /task-status/ URL."""
        response = self.client.get("/task-status/foo/bar?baz=true")
        self.assertRedirects(
            response,
            "/-/status/queue/foo/bar?baz=true",
            status_code=status.HTTP_301_MOVED_PERMANENTLY,
            fetch_redirect_response=False,
        )

    def test_redirect_workers(self) -> None:
        """Test redirection on the /workers/ URL."""
        response = self.client.get("/workers/foo/bar?baz=true")
        self.assertRedirects(
            response,
            "/-/status/workers/foo/bar?baz=true",
            status_code=status.HTTP_301_MOVED_PERMANENTLY,
            fetch_redirect_response=False,
        )

    def test_redirect_user(self) -> None:
        """Test redirection on the /user/ URL."""
        response = self.client.get("/user/foo/bar?baz=true")
        self.assertRedirects(
            response,
            "/-/user/foo/bar?baz=true",
            status_code=status.HTTP_301_MOVED_PERMANENTLY,
            fetch_redirect_response=False,
        )

    def test_redirect_signon(self) -> None:
        """Test redirection on the signon URLs."""
        scope = settings.DEBUSINE_DEFAULT_SCOPE
        for path, view_name in (
            ("/accounts/oidc_callback/{name}/", "signon:oidc_callback"),
            ("/accounts/bind_identity/{name}/", "signon:bind_identity"),
            ("/{scope}/accounts/oidc_callback/{name}/", "signon:oidc_callback"),
            ("/{scope}/accounts/bind_identity/{name}/", "signon:bind_identity"),
        ):
            for name in ("foo", "bar"):
                url = path.format(name=name, scope=scope)
                with self.subTest(url=url):
                    response = self.client.get(url)
                    self.assertRedirects(
                        response,
                        reverse(view_name, kwargs={"name": name}),
                        status_code=status.HTTP_301_MOVED_PERMANENTLY,
                        fetch_redirect_response=False,
                    )
