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

import json
import re
from typing import Any, override

from django.contrib.messages import get_messages
from django.test import RequestFactory
from django.urls import reverse
from django.utils.html import format_html
from lxml.objectify import ObjectifiedElement
from rest_framework import status

from debusine.artifacts.models import CollectionCategory
from debusine.db.models import Collection, CollectionRelation, User, Workspace
from debusine.db.playground import scenarios
from debusine.test.django import AllowAll, TestCase, override_permission
from debusine.web.views.collection_relations import (
    CollectionRelationCreateView,
    CollectionRelationDeleteView,
    CollectionRelationListView,
    CollectionRelationReorderRequiresView,
    _get_allowed_targets_by_type,
)
from debusine.web.views.tests.utils import ViewTestMixin


class CollectionRelationTestsBase(ViewTestMixin, TestCase):
    """Base class for CollectionRelation view tests."""

    trixie: Collection
    personal: Collection
    user: User

    @override
    @classmethod
    def setUpTestData(cls) -> None:
        super().setUpTestData()
        cls.trixie = cls.playground.create_collection(
            name="trixie", category=CollectionCategory.SUITE
        )
        cls.personal = cls.playground.create_collection(
            name="personal", category=CollectionCategory.SUITE
        )
        cls.user = cls.playground.create_user("foo")

    def grant_user_can_create_relations(
        self, user: User, collection: Collection
    ) -> None:
        owners = self.playground.create_group_role(
            collection.workspace, Workspace.Roles.OWNER
        )
        self.playground.add_user(owners, user)

    def assert_collection_relation_information(
        self,
        modal: ObjectifiedElement,  # noqa: ARG002
        collection_relation: CollectionRelation,  # noqa: ARG002
    ) -> None:
        table = self.assertHasElement(modal, ".//table")
        rows = table.xpath(".//tbody/tr")

        self.assertEqual(len(rows), 4)

        created_at_td = rows[0].xpath("./td")[0]
        self.assertTextContentEqual(
            created_at_td,
            collection_relation.created_at.strftime("%Y-%m-%d %H:%M"),
        )

        created_by_td = rows[1].xpath("./td")[0]
        self.assertTextContentEqual(
            created_by_td, str(collection_relation.created_by_user)
        )

        updated_at_td = rows[2].xpath("./td")[0]
        self.assertTextContentEqual(
            updated_at_td, str(collection_relation.updated_at or "")
        )

        updated_by_td = rows[3].xpath("./td")[0]
        self.assertTextContentEqual(
            updated_by_td, str(collection_relation.updated_by_user or "")
        )

    def assert_table_rows(
        self,
        table: ObjectifiedElement,
        expected_collection_relations: list[CollectionRelation],
        *,
        sorting_table: bool = False,
        can_delete: bool = True,
    ) -> None:
        actual_rows = table.xpath("./tbody/tr")

        self.assertEqual(len(actual_rows), len(expected_collection_relations))

        for row, collection_relation in zip(
            actual_rows, expected_collection_relations
        ):
            tds = row.xpath("./td")

            if sorting_table:
                # First column in a sorting table is the handle
                self.assertHTMLContentsEquivalent(
                    tds[0],
                    '<td class="handle" style="cursor: grab;">'
                    '<span class="bi bi-list"></span></td>',
                )
            else:
                # First column in a non-sorting table is the type of relation
                self.assertTextContentEqual(
                    tds[0],
                    CollectionRelation.Types(collection_relation.type).label,
                )

            self.assertTextContentEqual(tds[1], str(collection_relation.target))
            self.assertTextContentEqual(
                tds[2], str(collection_relation.position or "")
            )
            self.assert_collection_relation_information(
                tds[3], collection_relation
            )
            actions_html = self.normalize_node(tds[4])

            if can_delete:
                delete_url = reverse(
                    "workspaces:collections:collection_relations:delete",
                    kwargs={
                        "wname": collection_relation.source.workspace.name,
                        "ccat": collection_relation.source.category,
                        "cname": collection_relation.source.name,
                        "pk": collection_relation.pk,
                    },
                )

                self.assertHTMLEqual(
                    actions_html,
                    '<td><a class="btn btn-outline-secondary" '
                    f'href="{delete_url}" title="Delete collection relation">'
                    '<span class="bi bi-trash"></span></a></td>',
                )
            else:
                self.assertHTMLEqual(actions_html, "<td></td>")


class CollectionRelationCreateViewTests(CollectionRelationTestsBase):
    """Tests for CollectionRelationCreateView class."""

    scenario = scenarios.DefaultContext(set_current=True)

    def test_create_relation_permission_denied_anonymous(self) -> None:
        response = self.client.post(
            reverse(
                "workspaces:collections:collection_relations:create",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
            {},
        )
        self.assertResponseHTML(response, status_code=status.HTTP_403_FORBIDDEN)

    def test_create_relation_permission_denied_wrong_user(self) -> None:
        self.client.force_login(self.user)
        # It's logged in, but self.user does not have permission to create
        # collection relations in the workspace
        response = self.client.post(
            reverse(
                "workspaces:collections:collection_relations:create",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
            {},
            user=self.user,
        )
        self.assertResponseHTML(response, status_code=status.HTTP_403_FORBIDDEN)

    def test_create_relation_invalid_form(self) -> None:
        self.client.force_login(self.user)
        self.grant_user_can_create_relations(self.user, self.personal)

        self.assertEqual(self.personal.relations.count(), 0)
        # Invalid form data: target ID does not exist
        response = self.client.post(
            reverse(
                "workspaces:collections:collection_relations:create",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
            {"type": "suite-based-on", "target": 0},
        )

        # view is rendered
        self.assertResponseHTML(response)

        # One error in the form (invalid target)
        assert response.context_data is not None
        error = response.context_data["form"].errors["target"][0]

        # The error is displayed
        self.assertInHTML(error, response.content.decode())

        # No relation is created
        self.assertEqual(self.personal.relations.count(), 0)

    def test_create_relation_created(self) -> None:
        self.client.force_login(self.user)
        self.grant_user_can_create_relations(self.user, self.personal)

        for relation_type, expected_position in (
            (CollectionRelation.Types.SUITE_BASED_ON, None),
            (
                CollectionRelation.Types.SUITE_REQUIRES,
                1,
            ),
        ):
            with self.subTest(relation_type=str(relation_type)):
                response = self.client.post(
                    reverse(
                        "workspaces:collections:collection_relations:create",
                        kwargs={
                            "wname": self.personal.workspace.name,
                            "ccat": self.personal.category,
                            "cname": self.personal.name,
                        },
                    ),
                    {"type": relation_type, "target": self.trixie.id},
                )
                self.assertRedirects(
                    response,
                    reverse(
                        "workspaces:collections:collection_relations:list",
                        kwargs={
                            "wname": self.personal.workspace.name,
                            "ccat": self.personal.category,
                            "cname": self.personal.name,
                        },
                    ),
                )

                # The new relation is correct
                new_relation = self.personal.relations.get(type=relation_type)

                self.assertEqual(new_relation.source, self.personal)
                self.assertEqual(new_relation.target, self.trixie)
                self.assertEqual(new_relation.created_by_user, self.user)
                self.assertEqual(new_relation.position, expected_position)

                self.assertIsNone(new_relation.updated_by_user)

                # Response has the message that relation is created
                messages_list = list(get_messages(response.wsgi_request))
                self.assertEqual(len(messages_list), 1)
                self.assertEqual(
                    str(messages_list[0]),
                    format_html(
                        "Relation <code>{relation}</code> created.",
                        relation=new_relation,
                    ),
                )

    def test_get_place(self) -> None:
        view = CollectionRelationCreateView()

        view.request = RequestFactory().get("/")

        view.collection = self.personal
        place = view.get_place()

        self.assertPlaceValueEqual(
            place.title,
            "Create",
            page=f"Create relation for <code>{self.personal}</code>",
        )
        self.assertEqual(
            place.url,
            reverse(
                "workspaces:collections:collection_relations:create",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
        )

    def test_get_context_data(self) -> None:
        # Set workspaces and inheritance
        current_workspace = self.playground.get_default_workspace()
        unrelated_workspace = self.playground.create_workspace(name="Unrelated")
        parent_workspace = self.playground.create_workspace(name="Parent")

        current_workspace.set_inheritance([parent_workspace])

        # Grant permissions on these workspaces so user can view collections
        self.playground.create_group_role(
            current_workspace,
            Workspace.Roles.OWNER,
            users=[self.user],
        )
        self.playground.create_group_role(
            parent_workspace,
            Workspace.Roles.OWNER,
            users=[self.user],
        )
        self.playground.create_group_role(
            unrelated_workspace,
            Workspace.Roles.OWNER,
            users=[self.user],
        )

        self.client.force_login(self.user)
        self.grant_user_can_create_relations(self.user, self.personal)

        suite_1 = self.playground.create_collection(
            name="bookworm",
            category=CollectionCategory.SUITE,
        )
        suite_2 = self.playground.create_collection(
            name="forky",
            category=CollectionCategory.SUITE,
        )

        # suite_3 is in the parent workspace: it's available for selection
        # (inheritance chain)
        suite_3 = self.playground.create_collection(
            name="suite_3",
            category=CollectionCategory.SUITE,
            workspace=parent_workspace,
        )

        # suite_4 is in an unrelated workspace: it's not available for selection
        self.playground.create_collection(
            name="suite_4",
            category=CollectionCategory.SUITE,
            workspace=unrelated_workspace,
        )

        qa_results_1 = self.playground.create_collection(
            name="qa-results-1",
            category=CollectionCategory.QA_RESULTS,
        )
        qa_results_2 = self.playground.create_collection(
            name="qa-results-2",
            category=CollectionCategory.QA_RESULTS,
        )
        qa_results_3 = self.playground.create_collection(
            name="qa-results-3",
            category=CollectionCategory.QA_RESULTS,
            workspace=parent_workspace,
        )
        self.playground.create_collection(
            name="qa-results-4",
            category=CollectionCategory.QA_RESULTS,
            workspace=unrelated_workspace,
        )

        # Existing SUITE_REQUIRES relation: this target must be excluded only
        # from the "suite-requires" entry.
        CollectionRelation.objects.create(
            source=self.personal,
            target=suite_1,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            created_by_user=self.user,
            position=1,
        )

        response = self.client.get(
            reverse(
                "workspaces:collections:collection_relations:create",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
        )

        html = self.assertResponseHTML(response)
        allowed_targets_by_type = self._extract_json_script(
            response.content, "allowed-targets-by-type"
        )

        self.assertEqual(
            allowed_targets_by_type,
            {
                CollectionRelation.Types.SUITE_BASED_ON: [
                    self.trixie.id,
                    suite_1.id,
                    suite_2.id,
                    suite_3.id,
                ],
                CollectionRelation.Types.SUITE_DEFAULT_QA_RESULTS: [
                    qa_results_1.id,
                    qa_results_2.id,
                    qa_results_3.id,
                ],
                CollectionRelation.Types.SUITE_FORKED_FROM: [
                    self.trixie.id,
                    suite_1.id,
                    suite_2.id,
                    suite_3.id,
                ],
                CollectionRelation.Types.SUITE_REQUIRES: [
                    self.trixie.id,
                    suite_2.id,
                    suite_3.id,
                ],
                CollectionRelation.Types.SUITE_TARGETING: [
                    self.trixie.id,
                    suite_1.id,
                    suite_2.id,
                    suite_3.id,
                ],
            },
        )

        # The form is displayed
        self.assertHasElement(html, "//form[@id='form']")

    def test_get_context_data_no_form(self) -> None:
        workspace = self.playground.get_default_workspace()
        # Grant permissions on these workspaces so user can view collections
        self.playground.create_group_role(
            workspace,
            Workspace.Roles.OWNER,
            users=[self.user],
        )

        self.client.force_login(self.user)

        source = self.playground.create_collection(
            name="qa-results", category=CollectionCategory.QA_RESULTS
        )

        self.grant_user_can_create_relations(self.user, source)

        response = self.client.get(
            reverse(
                "workspaces:collections:collection_relations:create",
                kwargs={
                    "wname": workspace.name,
                    "ccat": source.category,
                    "cname": source.name,
                },
            ),
        )

        html = self.assertResponseHTML(response)

        # The form is displayed
        self.assertHasElement(html, "//p[@id='cannot-create-relations']")

    def test_get_form_kwargs(self) -> None:
        source = self.playground.create_collection(
            "trixie-for-kwargs", category=CollectionCategory.SUITE
        )

        view = CollectionRelationCreateView()
        view.request = RequestFactory().get("/")
        view.kwargs = {
            "wname": source.workspace.name,
            "ccat": source.category,
            "cname": source.name,
        }

        self.playground.create_collection(
            "some-suite", category=CollectionCategory.SUITE
        )
        self.playground.create_collection(
            "some-suite-qa", category=CollectionCategory.QA_RESULTS
        )

        with override_permission(Workspace, "can_configure", AllowAll):
            kwargs = view.get_form_kwargs()

        self.assertEqual(kwargs["allowed_types"], set(CollectionRelation.Types))
        self.assertEqual(kwargs["source_collection"], source)

    @staticmethod
    def _extract_json_script(response: bytes, script_id: str) -> Any:
        """Return JSON content from a <script type="application/json"> by id."""
        script_re = re.compile(
            rf'<script[^>]*\bid="{re.escape(script_id)}"[^>]*\btype="application/json"[^>]*>'
            r'(?P<content>.*?)</script>',
            re.DOTALL,
        )

        content = response.decode()

        match = script_re.search(content)
        assert match is not None

        return json.loads(match.group("content"))


class CollectionRelationDeleteViewTests(CollectionRelationTestsBase):
    """Tests for CollectionRelationDeleteView class."""

    def test_delete_relation_permission_denied_anonymous(self) -> None:
        response = self.client.delete(
            reverse(
                "workspaces:collections:collection_relations:delete",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                    "pk": 1,
                },
            ),
        )
        self.assertResponseHTML(response, status_code=status.HTTP_403_FORBIDDEN)

    def test_delete_relation_permission_denied_wrong_user(self) -> None:
        bookworm = self.playground.create_collection(
            name="bookworm", category=CollectionCategory.SUITE
        )
        collection_relation = CollectionRelation.objects.create(
            source=self.trixie,
            target=bookworm,
            type=CollectionRelation.Types.SUITE_BASED_ON,
            created_by_user=self.user,
        )

        new_user = self.playground.create_user("new-user")
        self.client.force_login(new_user)
        # It's logged in, but new_user does not have permission to create
        # collection relations in the workspace

        response = self.client.post(
            reverse(
                "workspaces:collections:collection_relations:delete",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                    "pk": collection_relation.pk,
                },
            ),
            {},
            user=new_user,
        )
        self.assertResponseHTML(response, status_code=status.HTTP_403_FORBIDDEN)

    def test_delete_not_found(self) -> None:
        self.client.force_login(self.user)
        self.grant_user_can_create_relations(self.user, self.personal)

        response = self.client.post(
            reverse(
                "workspaces:collections:collection_relations:delete",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                    "pk": 0,
                },
            ),
            {},
            user=self.user,
        )

        self.assertResponseHTML(response, status_code=status.HTTP_404_NOT_FOUND)

    def test_delete(self) -> None:
        self.client.force_login(self.user)
        self.grant_user_can_create_relations(self.user, self.personal)

        collection_relation = CollectionRelation.objects.create(
            source=self.personal,
            target=self.trixie,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            created_by_user=self.user,
            position=1,
        )

        response = self.client.post(
            reverse(
                "workspaces:collections:collection_relations:delete",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                    "pk": collection_relation.pk,
                },
            ),
            {},
            user=self.user,
        )
        self.assertRedirects(
            response,
            reverse(
                "workspaces:collections:collection_relations:list",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
        )

        self.assertFalse(
            CollectionRelation.objects.filter(
                id=collection_relation.id
            ).exists()
        )

        # Response has the message that relation is deleted
        messages_list = list(get_messages(response.wsgi_request))
        self.assertEqual(len(messages_list), 1)
        self.assertEqual(
            str(messages_list[0]),
            format_html(
                "Relation <code>{relation}</code> deleted.",
                relation=collection_relation,
            ),
        )

    def test_get_place(self) -> None:
        view = CollectionRelationDeleteView()

        collection_relation = CollectionRelation.objects.create(
            source=self.personal,
            target=self.trixie,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            created_by_user=self.user,
            position=1,
        )

        view.request = RequestFactory().get("/")
        view.object = collection_relation

        place = view.get_place()

        self.assertPlaceValueEqual(
            place.title,
            "Delete",
            page=format_html(
                "Delete relation for <code>{relation}</code>",
                relation=collection_relation,
            ),
        )
        self.assertEqual(
            place.url,
            reverse(
                "workspaces:collections:collection_relations:delete",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                    "pk": collection_relation.id,
                },
            ),
        )


class CollectionRelationListViewTests(CollectionRelationTestsBase):
    """Tests for CollectionRelationlistView class."""

    no_relations_xpath: str = '//td[@id="no-relations"]'

    def test_get_place(self) -> None:
        view = CollectionRelationListView()

        view.request = RequestFactory().get("/")

        view.collection = self.personal
        place = view.get_place()

        self.assertPlaceValueEqual(
            place.title,
            "Relations",
            page=format_html(
                "Relations of <code>{collection}</code> collection",
                collection=self.personal,
            ),
        )
        self.assertEqual(
            place.url,
            reverse(
                "workspaces:collections:collection_relations:list",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
        )

    def test_list_with_buttons_create_resort_requires(self) -> None:
        self.grant_user_can_create_relations(self.user, self.personal)

        create_xpath = '//a[@id="create"]'
        reorder_requires_xpath = '//a[@id="show_reorder_requires"]'

        trixie_backports = self.playground.create_collection(
            name="trixie-backports", category=CollectionCategory.SUITE
        )

        # Client not logged in
        response = self.client.get(
            reverse(
                "workspaces:collections:collection_relations:list",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
        )
        tree = self.assertResponseHTML(response)

        # No buttons to create or resort collection relations
        self.assertFalse(tree.xpath(create_xpath))
        self.assertFalse(tree.xpath(reorder_requires_xpath))

        # Logs in client
        self.client.force_login(self.user)
        response = self.client.get(
            reverse(
                "workspaces:collections:collection_relations:list",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
        )
        tree = self.assertResponseHTML(response)

        # Create button, but no re-sorting yet (no collection relationships
        # to resort)
        self.assertTrue(tree.xpath(create_xpath))
        self.assertFalse(tree.xpath(reorder_requires_xpath))

        # Add relationships to have the button
        collection_relation_1 = CollectionRelation.objects.create(
            source=self.personal,
            target=self.trixie,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            position=1,
            created_by_user=self.user,
        )
        collection_relation_2 = CollectionRelation.objects.create(
            source=self.personal,
            target=trixie_backports,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            position=2,
            created_by_user=self.user,
        )

        response = self.client.get(
            reverse(
                "workspaces:collections:collection_relations:list",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
        )
        tree = self.assertResponseHTML(response)
        self.assertHasElement(tree, reorder_requires_xpath)
        self.assertHasElement(tree, create_xpath)

        self.assertFalse(tree.xpath(self.no_relations_xpath))

        table = self.assertHasElement(tree, '//table[@id="relations"]')
        self.assert_table_rows(
            table,
            [collection_relation_1, collection_relation_2],
            can_delete=True,
        )

    def test_list_with_filter(self) -> None:
        CollectionRelation.objects.create(
            source=self.personal,
            target=self.trixie,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            position=1,
            created_by_user=self.user,
        )

        trixie_backports = self.playground.create_collection(
            name="trixie-backports", category=CollectionCategory.SUITE
        )

        collection_relation_targeting = CollectionRelation.objects.create(
            source=self.personal,
            target=trixie_backports,
            type=CollectionRelation.Types.SUITE_TARGETING,
            created_by_user=self.user,
        )

        response = self.client.get(
            reverse(
                "workspaces:collections:collection_relations:list",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            )
            + "?relation_type=suite-targeting",
        )

        tree = self.assertResponseHTML(response)

        relations_of_type_p = self.assertHasElement(
            tree, '//p[@id="relations-of-type"]'
        )
        self.assertTextContentEqual(
            relations_of_type_p, 'Relations of type "Targeting" only. List all.'
        )

        table = self.assertHasElement(tree, '//table[@id="relations"]')
        self.assert_table_rows(
            table, [collection_relation_targeting], can_delete=False
        )

    def test_list_with_invalid_filter(self) -> None:
        response = self.client.get(
            reverse(
                "workspaces:collections:collection_relations:list",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            )
            + "?relation_type=some-label-does-not-exist",
        )

        tree = self.assertResponseHTML(response)

        relations_of_type_p = self.assertHasElement(
            tree, '//p[@id="relations-of-type"]'
        )

        self.assertTextContentEqual(
            relations_of_type_p,
            'Relations of type "some-label-does-not-exist" only. List all.',
        )

        table = self.assertHasElement(tree, '//table[@id="relations"]')
        rows = table.xpath('.//tbody/tr')

        self.assertEqual(len(rows), 1)
        self.assertTextContentEqual(
            rows[0],
            "Collection personal@debian:suite does not have any "
            "relations of type some-label-does-not-exist. "
            f"List all relations for {self.personal}",
        )

    def test_list_no_relations(self) -> None:
        response = self.client.get(
            reverse(
                "workspaces:collections:collection_relations:list",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
        )

        tree = self.assertResponseHTML(response)
        self.assertHasElement(tree, self.no_relations_xpath)


class CollectionRelationReorderRequiresViewTests(CollectionRelationTestsBase):
    """Tests for CollectionRelationReorderRequires."""

    def test_reorder_relations_permission_denied_anonymous(self) -> None:
        response = self.client.post(
            reverse(
                "workspaces:collections:collection_relations:reorder_requires",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
            {},
        )
        self.assertResponseHTML(response, status_code=status.HTTP_403_FORBIDDEN)

    def test_reorder_relations_permission_denied_wrong_user(self) -> None:
        self.client.force_login(self.user)
        # It's logged in, but self.user does not have permission to create
        # collection relations in the workspace
        response = self.client.post(
            reverse(
                "workspaces:collections:collection_relations:reorder_requires",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
            {},
            user=self.user,
        )
        self.assertResponseHTML(response, status_code=status.HTTP_403_FORBIDDEN)

    def test_get_place(self) -> None:
        view = CollectionRelationReorderRequiresView()

        view.request = RequestFactory().get("/")

        view.collection = self.personal
        place = view.get_place()

        self.assertPlaceValueEqual(
            place.title,
            "Reorder",
            page=format_html(
                "Reorder <code>Requires</code> for <code>{collection}</code>",
                collection=self.personal,
            ),
        )
        self.assertEqual(
            place.url,
            reverse(
                "workspaces:collections:collection_relations:reorder_requires",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
        )

    def test_get_list(self) -> None:
        self.client.force_login(self.user)
        self.grant_user_can_create_relations(self.user, self.personal)

        trixie_backports = self.playground.create_collection(
            name="trixie-backports", category=CollectionCategory.SUITE
        )
        collection_relation_1 = CollectionRelation.objects.create(
            source=self.personal,
            target=self.trixie,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            created_by_user=self.user,
            position=1,
        )

        collection_relation_2 = CollectionRelation.objects.create(
            source=self.personal,
            target=trixie_backports,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            created_by_user=self.user,
            position=2,
        )

        response = self.client.get(
            reverse(
                "workspaces:collections:collection_relations:reorder_requires",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
        )

        tree = self.assertResponseHTML(response)

        table = self.assertHasElement(tree, '//table[@id="relations"]')
        self.assert_table_rows(
            table,
            [collection_relation_1, collection_relation_2],
            sorting_table=True,
            can_delete=True,
        )

    def test_post(self) -> None:
        self.client.force_login(self.user)
        self.grant_user_can_create_relations(self.user, self.personal)

        trixie_backports = self.playground.create_collection(
            name="trixie-backports", category=CollectionCategory.SUITE
        )

        relation_1 = CollectionRelation.objects.create(
            source=self.personal,
            target=self.trixie,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            created_by_user=self.user,
            position=1,
        )
        relation_2 = CollectionRelation.objects.create(
            source=self.personal,
            target=trixie_backports,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            created_by_user=self.user,
            position=2,
        )

        response = self.client.post(
            reverse(
                "workspaces:collections:collection_relations:reorder_requires",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
            {"order": f"{relation_2.id},{relation_1.id}"},
        )

        self.assertRedirects(
            response,
            reverse(
                "workspaces:collections:collection_relations:list",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
        )

        relation_1.refresh_from_db()
        relation_2.refresh_from_db()

        # Reordered
        self.assertEqual(relation_1.position, 2)
        self.assertEqual(relation_2.position, 1)

        messages_list = list(get_messages(response.wsgi_request))
        self.assertEqual(len(messages_list), 1)
        self.assertEqual(str(messages_list[0]), "Requires relations reordered.")

    def test_post_without_order_invalid_request(self) -> None:
        self.client.force_login(self.user)
        self.grant_user_can_create_relations(self.user, self.personal)

        response = self.client.post(
            reverse(
                "workspaces:collections:collection_relations:reorder_requires",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
            {},
        )

        self.assertResponse400(response, 'Missing "order" POST parameter')

    def test_post_invalid_order_invalid_request(self) -> None:
        self.client.force_login(self.user)
        self.grant_user_can_create_relations(self.user, self.personal)

        response = self.client.post(
            reverse(
                "workspaces:collections:collection_relations:reorder_requires",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
            {"order": "missing numbers"},
        )

        self.assertResponse400(response, 'Invalid "order" format')

    def test_post_ids_do_not_match_db_invalid_request(self) -> None:
        self.client.force_login(self.user)
        self.grant_user_can_create_relations(self.user, self.personal)

        response = self.client.post(
            reverse(
                "workspaces:collections:collection_relations:reorder_requires",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
            {"order": "1,2,3,4"},
        )

        self.assertResponse400(
            response, "Submitted ids and DB ids do not match"
        )

    def test_get_list_empty(self) -> None:
        self.client.force_login(self.user)
        self.grant_user_can_create_relations(self.user, self.personal)

        response = self.client.get(
            reverse(
                "workspaces:collections:collection_relations:reorder_requires",
                kwargs={
                    "wname": self.personal.workspace.name,
                    "ccat": self.personal.category,
                    "cname": self.personal.name,
                },
            ),
        )

        tree = self.assertResponseHTML(response)

        no_relations = self.assertHasElement(tree, '//td[@id="no-relations"]')

        self.assertTextContentEqual(
            no_relations, "No relations of type Requires."
        )


class HelperFunctionsTests(TestCase):
    """Helper functions tests."""

    scenario = scenarios.DefaultContext(set_current=True)

    collection: Collection

    @override
    @classmethod
    def setUpTestData(cls) -> None:
        super().setUpTestData()

        cls.collection = cls.playground.create_collection(
            "trixie", category=CollectionCategory.SUITE
        )

    def test_get_allowed_targets_by_type_no_collections_available(self) -> None:
        with override_permission(Workspace, "can_configure", AllowAll):
            self.assertEqual(
                _get_allowed_targets_by_type(self.collection),
                {
                    CollectionRelation.Types.SUITE_FORKED_FROM: [],
                    CollectionRelation.Types.SUITE_TARGETING: [],
                    CollectionRelation.Types.SUITE_DEFAULT_QA_RESULTS: [],
                    CollectionRelation.Types.SUITE_REQUIRES: [],
                    CollectionRelation.Types.SUITE_BASED_ON: [],
                },
            )

    def test_get_allowed_targets_by_type_collections_used(self) -> None:
        suite = self.playground.create_collection(
            "bookworm", category=CollectionCategory.SUITE
        )

        qa_results = self.playground.create_collection(
            "qa-results", category=CollectionCategory.QA_RESULTS
        )

        user = self.playground.get_default_user()
        CollectionRelation.objects.create(
            source=self.collection,
            target=suite,
            type=CollectionRelation.Types.SUITE_FORKED_FROM,
            created_by_user=user,
        )
        CollectionRelation.objects.create(
            source=self.collection,
            target=suite,
            type=CollectionRelation.Types.SUITE_TARGETING,
            created_by_user=user,
        )
        CollectionRelation.objects.create(
            source=self.collection,
            target=qa_results,
            type=CollectionRelation.Types.SUITE_DEFAULT_QA_RESULTS,
            created_by_user=user,
        )
        CollectionRelation.objects.create(
            source=self.collection,
            target=suite,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            created_by_user=user,
            position=1,
        )
        CollectionRelation.objects.create(
            source=self.collection,
            target=suite,
            type=CollectionRelation.Types.SUITE_BASED_ON,
            created_by_user=user,
        )

        with override_permission(Workspace, "can_configure", AllowAll):
            self.assertEqual(
                _get_allowed_targets_by_type(self.collection),
                {
                    CollectionRelation.Types.SUITE_FORKED_FROM: [],
                    CollectionRelation.Types.SUITE_TARGETING: [],
                    CollectionRelation.Types.SUITE_DEFAULT_QA_RESULTS: [],
                    CollectionRelation.Types.SUITE_REQUIRES: [],
                    CollectionRelation.Types.SUITE_BASED_ON: [],
                },
            )

    def test_get_allowed_targets_by_type_available_collections(self) -> None:
        suite = self.playground.create_collection(
            "bookworm", category=CollectionCategory.SUITE
        )

        qa_results = self.playground.create_collection(
            "qa-results", category=CollectionCategory.QA_RESULTS
        )

        with override_permission(Workspace, "can_configure", AllowAll):
            self.assertEqual(
                _get_allowed_targets_by_type(self.collection),
                {
                    CollectionRelation.Types.SUITE_FORKED_FROM: [suite.id],
                    CollectionRelation.Types.SUITE_TARGETING: [suite.id],
                    CollectionRelation.Types.SUITE_DEFAULT_QA_RESULTS: [
                        qa_results.id
                    ],
                    CollectionRelation.Types.SUITE_REQUIRES: [suite.id],
                    CollectionRelation.Types.SUITE_BASED_ON: [suite.id],
                },
            )
