# 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 workflow views."""

from typing import ClassVar, cast
from unittest.mock import patch

import lxml
from django.urls import reverse
from django.utils import timezone

from debusine.db.models import WorkRequest, WorkflowTemplate
from debusine.db.playground import scenarios
from debusine.test.django import TestCase
from debusine.web.forms import WorkflowFilterForm
from debusine.web.views.tests.utils import ViewTestMixin
from debusine.web.views.utils import PaginationMixin
from debusine.web.views.workflows import WorkflowsListView


class WorkflowListViewTests(ViewTestMixin, TestCase):
    """Tests for WorkRequestDetailView class."""

    template: ClassVar[WorkflowTemplate]
    workflow_1: ClassVar[WorkRequest]
    workflow_2: ClassVar[WorkRequest]

    scenario = scenarios.DefaultContext(set_current=True)

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up test data."""
        super().setUpTestData()

        cls.template = cls.playground.create_workflow_template(
            "name-2", "noop", workspace=cls.scenario.workspace
        )

        cls.workflow_1 = WorkRequest.objects.create_workflow(
            template=cls.template,
            data={},
            created_by=cls.scenario.user,
        )
        cls.workflow_2 = WorkRequest.objects.create_workflow(
            template=cls.template,
            data={},
            created_by=cls.scenario.user,
        )

    def assertWorkflowRow(
        self, tr: lxml.objectify.ObjectifiedElement, workflow: WorkRequest
    ) -> None:
        """Ensure the row shows the given work request."""
        self.assertTextContentEqual(tr.td[0], str(workflow.id))
        self.assertEqual(tr.td[0].a.get("href"), workflow.get_absolute_url())

        assert workflow.workflow_display_name_parameters
        self.assertTextContentEqual(
            tr.td[1], workflow.workflow_display_name_parameters
        )

        if workflow.status == WorkRequest.Statuses.RUNNING:
            assert workflow.workflow_runtime_status
            self.assertTextContentEqual(
                tr.td[2],
                "R "
                + WorkRequest.RuntimeStatuses(
                    workflow.workflow_runtime_status
                ).label[0],
            )
        else:
            self.assertTextContentEqual(
                tr.td[2], cast(WorkRequest.Statuses, workflow.status).label[0]
            )

        result = WorkRequest.Results(workflow.result)
        if result != "":
            self.assertTextContentEqual(tr.td[3], result.label[0])
        else:
            self.assertTextContentEqual(tr.td[3], "")

        if workflow.started_at:
            self.assertTextContentEqual(
                tr.td[4], workflow.started_at.strftime("%Y-%m-%d %H:%M")
            )
        else:
            self.assertTextContentEqual(tr.td[4], "")

        if workflow.completed_at:
            self.assertTextContentEqual(
                tr.td[5], workflow.completed_at.strftime("%Y-%m-%d %H:%M")
            )
        else:
            self.assertTextContentEqual(tr.td[5], "")

        self.assertTextContentEqual(
            tr.td[6],
            f"{workflow.workflow_work_requests_success}-"
            f"{workflow.workflow_work_requests_failure} "
            f"{workflow.workflow_work_requests_pending}-"
            f"{workflow.workflow_work_requests_blocked}",
        )

        if last_activity := workflow.workflow_last_activity_at:
            self.assertTextContentEqual(
                tr.td[7],
                last_activity.strftime("%Y-%m-%d %H:%M"),
            )
        else:
            self.assertTextContentEqual(tr.td[7], "")

        self.assertTextContentEqual(tr.td[8], workflow.created_by.username)

    def test_can_display_used(self) -> None:
        """Permissions are used via can_display()."""
        self.client.force_login(self.scenario.user)

        with patch(
            "debusine.db.models.work_requests.WorkRequestQuerySet.can_display"
        ) as mock_can_display:
            mock_can_display.return_value = WorkRequest.objects.all()

            self.client.get(
                reverse(
                    "workspaces:workflows:list",
                    kwargs={"wname": self.scenario.workspace.name},
                ),
            )

        mock_can_display.assert_called_once()

    def test_no_workflows(self) -> None:
        """View details shows "No workflows" if no workflows in the space."""
        WorkRequest.objects.all().delete()

        response = self.client.get(
            reverse(
                "workspaces:workflows:list",
                kwargs={"wname": self.scenario.workspace.name},
            ),
        )

        self.assertContains(response, "<p>No workflows.</p>", html=True)

    def test_filter_form_with_issues(self) -> None:
        """Client submit filter form which is not valid."""
        form_data = {"workflow_templates": ["something-not-valid"]}
        form = WorkflowFilterForm(data=form_data)
        response = self.client.get(
            reverse(
                "workspaces:workflows:list",
                kwargs={"wname": self.scenario.workspace.name},
            ),
            data=form_data,
        )

        self.assertContains(
            response,
            "<p>There were issues with your filter criteria:</p>",
            html=True,
        )
        self.assertContains(
            response,
            f"<li><strong>workflow_templates:</strong> "
            f"{form.errors['workflow_templates'][0]}</li>",
            html=True,
        )

    def test_list_no_filtering_check_workspace(self) -> None:
        """View detail return all workflows for the specific workspace."""
        workspace_unused = self.playground.create_workspace(
            name="unused-workspace", public=True
        )
        # The following workflow is not in the workspace that will be used
        template_unused = self.playground.create_workflow_template(
            "name-1", "noop", workspace=workspace_unused
        )
        WorkRequest.objects.create_workflow(
            template=template_unused,
            data={},
            created_by=self.scenario.user,
        )

        wr = self.workflow_1.create_child("noop")
        wr.started_at = timezone.now()
        wr.save()

        self.workflow_1.started_at = timezone.now()
        self.workflow_1.save()

        self.workflow_2.completed_at = timezone.now()
        self.workflow_2.save()

        response = self.client.get(
            reverse(
                "workspaces:workflows:list",
                kwargs={"wname": self.scenario.workspace.name},
            ),
        )
        link = (
            "https://freexian-team.pages.debian.net/"
            "debusine/explanation/concepts.html#workflows"
        )
        self.assertContains(
            response,
            f'<p>This page lets you monitor '
            f'<a href="{link}">workflows</a> started in the '
            f'{self.scenario.workspace} workspace.</p>',
            html=True,
        )

        tree = self.assertHTMLValid(response)
        table = self.assertHasElement(
            tree, "//table[@id='workflow-list-table']"
        )

        # One work request is in a different workspace
        self.assertEqual(len(table.tbody.tr), 2)

        self.assertWorkflowRow(table.tbody.tr[0], self.workflow_2)
        self.assertWorkflowRow(table.tbody.tr[1], self.workflow_1)

    def assert_filtered(
        self, form: WorkflowFilterForm, workflow: WorkRequest
    ) -> None:
        """Make a request with the form and assert one workflow is listed."""
        self.assertTrue(form.is_valid())

        response = self.client.get(
            reverse(
                "workspaces:workflows:list",
                kwargs={"wname": self.scenario.workspace.name},
            ),
            data=form.cleaned_data,
        )

        tree = self.assertHTMLValid(response)
        table = self.assertHasElement(
            tree, "//table[@id='workflow-list-table']"
        )

        self.assertEqual(len(table.tbody.tr), 1)
        self.assertWorkflowRow(table.tbody.tr[0], workflow)

    def test_list_filtering_started_by(self) -> None:
        """View detail filters by started_by user."""
        user = self.playground.create_user(username="relevant")

        self.workflow_1.created_by = user
        self.workflow_1.save()

        form = WorkflowFilterForm(data={"started_by": [user]})

        self.assert_filtered(form, self.workflow_1)

    def test_list_filtering_status(self) -> None:
        """View detail filters by status."""
        self.workflow_1.status = WorkRequest.Statuses.COMPLETED
        self.workflow_1.save()

        form = WorkflowFilterForm(
            data={"statuses": [WorkRequest.Statuses.COMPLETED]}
        )

        self.assert_filtered(form, self.workflow_1)

    def test_list_filtering_runtime_status(self) -> None:
        """View detail filters by runtime status."""
        self.workflow_1.status = WorkRequest.Statuses.RUNNING
        self.workflow_1.save()

        self.workflow_2.status = WorkRequest.Statuses.RUNNING
        self.workflow_2.save()

        self.workflow_1.create_child(
            "noop",
            status=WorkRequest.Statuses.BLOCKED,
        )

        form = WorkflowFilterForm(data={"statuses": ["running__blocked"]})

        self.assert_filtered(form, self.workflow_1)

    def test_list_filtering_runtime_any(self) -> None:
        """
        Test Workflow with running status is displayed.

        No need for any specific WorkRequest.workflow_running_status.
        """
        self.workflow_1.status = WorkRequest.Statuses.RUNNING
        self.workflow_1.save()

        self.workflow_1.create_child(
            "noop",
            status=WorkRequest.Statuses.BLOCKED,
        )

        form = WorkflowFilterForm(data={"statuses": ["running__any"]})

        self.assert_filtered(form, self.workflow_1)

    def test_list_filtering_result(self) -> None:
        """View detail filters by result."""
        self.workflow_1.result = WorkRequest.Results.SUCCESS
        self.workflow_1.save()

        form = WorkflowFilterForm(
            data={"results": [WorkRequest.Results.SUCCESS]}
        )

        self.assert_filtered(form, self.workflow_1)

    def test_list_filtering_with_failed_work_requests(self) -> None:
        """View detail filters by with failed work requests."""
        wr = self.workflow_1.create_child("noop")
        wr.result = WorkRequest.Results.FAILURE
        wr.save()

        form = WorkflowFilterForm(data={"with_failed_work_requests": True})

        self.assert_filtered(form, self.workflow_1)

    def test_list_filtering_workflow_templates(self) -> None:
        """View detail filters by workflow_templates."""
        self.workflow_2.workflow_data_json["workflow_template_name"] = (
            "filtered-out"
        )
        self.workflow_2.save()

        # Also test last_activity_at is displayed
        self.workflow_1.workflow_last_activity_at = timezone.now()
        self.workflow_1.save()

        form = WorkflowFilterForm(
            data={"workflow_templates": [self.template.name]}
        )

        self.assert_filtered(form, self.workflow_1)

    def test_sorting(self) -> None:
        """Test sorting parameters."""
        for field in [
            "id",
            "workflow_template",
            "status",
            "result",
            "started_at",
            "completed_at",
            "last_activity",
            "started_by",
        ]:
            for asc in ["0", "1"]:
                with self.subTest(f"sorted_by-{field}-{asc}"):
                    response = self.client.get(
                        reverse(
                            "workspaces:workflows:list",
                            kwargs={"wname": self.scenario.workspace.name},
                        )
                        + f"?order={field}&asc={asc}"
                    )

                self.assertEqual(response.context["order"], field)
                self.assertEqual(response.context["asc"], asc)

    def test_sorting_id(self) -> None:
        """Test output of workflows for "id" and "-id" sorting."""
        # Request with order=id&asc=0
        response = self.client.get(
            reverse(
                "workspaces:workflows:list",
                kwargs={"wname": self.scenario.workspace.name},
            )
            + "?order=id&asc=0"
        )
        tree = self.assertHTMLValid(response)
        table = self.assertHasElement(
            tree, "//table[@id='workflow-list-table']"
        )
        # First Workflow.id > Second Workflow.id
        id_1 = table.tbody.tr[0].td[0].a.text
        id_2 = table.tbody.tr[1].td[0].a.text
        assert id_1
        assert id_2

        self.assertGreater(int(id_1), int(id_2))

        # Request asc=1
        response = self.client.get(
            reverse(
                "workspaces:workflows:list",
                kwargs={"wname": self.scenario.workspace.name},
            )
            + "?order=id&asc=1"
        )
        tree = self.assertHTMLValid(response)
        table = self.assertHasElement(
            tree, "//table[@id='workflow-list-table']"
        )

        # First Workflow.id < Second Workflow.id
        id_1 = table.tbody.tr[0].td[0].a.text
        id_2 = table.tbody.tr[1].td[0].a.text
        assert id_1
        assert id_2

        self.assertLess(int(id_1), int(id_2))

    def test_sorting_invalid_field(self) -> None:
        """Test sorting by an invalid field: sorted by id."""
        response = self.client.get(
            reverse(
                "workspaces:workflows:list",
                kwargs={"wname": self.scenario.workspace.name},
            )
            + "?order=something"
        )

        self.assertEqual(response.context["order"], "id")
        self.assertEqual(response.context["asc"], "0")

    def test_pagination(self) -> None:
        """Pagination is enabled and rendered by the template."""
        self.assertTrue(issubclass(WorkflowsListView, PaginationMixin))
        self.assertEqual(WorkflowsListView.paginate_by, 50)

        response = self.client.get(
            reverse(
                "workspaces:workflows:list",
                kwargs={"wname": self.scenario.workspace.name},
            )
        )
        tree = self.assertHTMLValid(response)
        self.assertHasElement(tree, "//ul[@class='pagination']")
