"""Unit/Functional tests"""

import json
import os
import threading
from contextlib import contextmanager
from unittest import mock
from unittest.mock import patch

import pytest
from django.apps import apps
from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.messages.storage.fallback import FallbackStorage
from django.core import checks
from django.core.checks.model_checks import check_all_models
from django.core.exceptions import PermissionDenied
from django.db.models import Manager
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.forms import ValidationError
from django.test.client import RequestFactory
from django.utils.timezone import now

from tests import models
from treebeard import numconv
from treebeard.admin import admin_factory
from treebeard.al_tree import AL_Node
from treebeard.exceptions import (
    InvalidMoveToDescendant,
    InvalidPosition,
    MissingNodeOrderBy,
    NodeAlreadySaved,
    PathOverflow,
)
from treebeard.forms import movenodeform_factory
from treebeard.ltree import LT_Node
from treebeard.ltree import nodes_deleted as lt_nodes_deleted
from treebeard.ltree import subtree_moved as lt_subtree_moved
from treebeard.ltree import subtree_moved_right as lt_subtree_moved_right
from treebeard.mp_tree import MP_Node, path_updated
from treebeard.mp_tree import nodes_deleted as mp_nodes_deleted
from treebeard.ns_tree import NS_Node, gap_altered, tree_ids_incremented
from treebeard.ns_tree import nodes_deleted as ns_nodes_deleted
from treebeard.ns_tree import subtree_moved as ns_subtree_moved
from treebeard.templatetags.admin_tree import tree_context

BASE_DATA = [
    {"data": {"desc": "1"}},
    {
        "data": {"desc": "2"},
        "children": [
            {"data": {"desc": "21"}},
            {"data": {"desc": "22"}},
            {
                "data": {"desc": "23"},
                "children": [
                    {"data": {"desc": "231"}},
                ],
            },
            {"data": {"desc": "24"}},
        ],
    },
    {"data": {"desc": "3"}},
    {
        "data": {"desc": "4"},
        "children": [
            {"data": {"desc": "41"}},
        ],
    },
]
UNCHANGED = [
    ("1", 1, 0),
    ("2", 1, 4),
    ("21", 2, 0),
    ("22", 2, 0),
    ("23", 2, 1),
    ("231", 3, 0),
    ("24", 2, 0),
    ("3", 1, 0),
    ("4", 1, 1),
    ("41", 2, 0),
]


@pytest.fixture(scope="function", params=models.BASE_MODELS + models.PROXY_MODELS)
def model(request):
    request.param.objects.load_bulk(BASE_DATA)
    return request.param


@pytest.fixture(scope="function", params=models.BASE_MODELS + models.PROXY_MODELS)
def model_without_data(request):
    return request.param


@pytest.fixture(scope="function", params=models.BASE_MODELS)
def model_without_proxy(request):
    request.param.objects.load_bulk(BASE_DATA)
    return request.param


@pytest.fixture(scope="function", params=models.SORTED_MODELS)
def sorted_model(request):
    return request.param


@pytest.fixture(scope="function", params=models.RELATED_MODELS)
def related_model(request):
    return request.param


@pytest.fixture(scope="function", params=models.MP_MODELS)
def mp_model(request):
    request.param.objects.load_bulk(BASE_DATA)
    return request.param


@pytest.fixture(scope="function", params=[models.MP_TestNodeRelated])
def mp_relatedmodel(request):
    return request.param


@pytest.fixture(scope="function", params=models.MP_SHORTPATH_MODELS)
def mpshort_model(request):
    return request.param


@pytest.fixture(scope="function", params=[models.MP_TestNodeShortPath])
def mpshortnotsorted_model(request):
    return request.param


@pytest.fixture(scope="function", params=[models.MP_TestNodeAlphabet])
def mpalphabet_model(request):
    return request.param


@pytest.fixture(scope="function", params=[models.MP_TestNodeSortedAutoNow])
def mpsortedautonow_model(request):
    return request.param


@pytest.fixture(scope="function", params=[models.MP_TestNodeSmallStep])
def mpsmallstep_model(request):
    return request.param


@pytest.fixture(scope="function", params=[models.MP_TestSortedNodeShortPath])
def mpsorted_model(request):
    return request.param


@pytest.fixture(scope="function", params=[models.NS_TestNode])
def ns_model(request):
    return request.param


@pytest.fixture(scope="function", params=models.LT_BASE_MODELS)
def lt_model(request):
    return request.param


@contextmanager
def capture_signals():
    calls = []

    def path_updated_handler(sender, **kwargs):
        calls.append(("path_updated", sender, kwargs["old_path"], kwargs["new_path"], kwargs["using"]))

    def mp_nodes_deleted_handler(sender, **kwargs):
        calls.append(("nodes_deleted", sender, kwargs["pks_to_remove"], kwargs["paths_to_remove"], kwargs["using"]))

    def gap_altered_handler(sender, **kwargs):
        calls.append(
            ("gap_altered", sender, kwargs["tree_id"], kwargs["start_index"], kwargs["offset"], kwargs["using"])
        )

    def tree_ids_incremented_handler(sender, **kwargs):
        calls.append(("tree_ids_incremented", sender, kwargs["min_tree_id"], kwargs["using"]))

    def ns_subtree_moved_handler(sender, **kwargs):
        calls.append(
            (
                "subtree_moved",
                sender,
                kwargs["tree_id"],
                kwargs["lft"],
                kwargs["rgt"],
                kwargs["target_tree_id"],
                kwargs["index_offset"],
                kwargs["depth_offset"],
                kwargs["using"],
            )
        )

    def ns_nodes_deleted_handler(sender, **kwargs):
        calls.append(("nodes_deleted", sender, kwargs["removed_ranges"], kwargs["using"]))

    def lt_subtree_moved_right_handler(sender, **kwargs):
        calls.append(
            (
                "subtree_moved_right",
                sender,
                str(kwargs["path"]),
                kwargs["using"],
            )
        )

    def lt_subtree_moved_handler(sender, **kwargs):
        calls.append(
            (
                "subtree_moved",
                sender,
                str(kwargs["old_path"]),
                str(kwargs["new_path"]),
                kwargs["using"],
            )
        )

    def lt_nodes_deleted_handler(sender, **kwargs):
        calls.append(
            ("nodes_deleted", sender, sorted([str(path) for path in kwargs["paths_to_remove"]]), kwargs["using"])
        )

    path_updated.connect(path_updated_handler)
    mp_nodes_deleted.connect(mp_nodes_deleted_handler)
    gap_altered.connect(gap_altered_handler)
    tree_ids_incremented.connect(tree_ids_incremented_handler)
    ns_subtree_moved.connect(ns_subtree_moved_handler)
    ns_nodes_deleted.connect(ns_nodes_deleted_handler)
    lt_subtree_moved_right.connect(lt_subtree_moved_right_handler)
    lt_subtree_moved.connect(lt_subtree_moved_handler)
    lt_nodes_deleted.connect(lt_nodes_deleted_handler)
    try:
        yield calls
    finally:
        path_updated.disconnect(path_updated_handler)
        mp_nodes_deleted.disconnect(mp_nodes_deleted_handler)
        gap_altered.disconnect(gap_altered_handler)
        tree_ids_incremented.disconnect(tree_ids_incremented_handler)
        ns_subtree_moved.disconnect(ns_subtree_moved_handler)
        ns_nodes_deleted.disconnect(ns_nodes_deleted_handler)
        lt_subtree_moved_right.disconnect(lt_subtree_moved_right_handler)
        lt_subtree_moved.disconnect(lt_subtree_moved_handler)
        lt_nodes_deleted.disconnect(lt_nodes_deleted_handler)


class TestTreeBase:
    def got(self, model):
        if model in [models.NS_TestNode, models.NS_TestNode_Proxy]:
            # this slows down nested sets tests quite a bit, but it has the
            # advantage that we'll check the node edges are correct
            d = {}
            for tree_id, lft, rgt in model.objects.values_list("tree_id", "lft", "rgt"):
                d.setdefault(tree_id, []).extend([lft, rgt])
            for tree_id, got_edges in d.items():
                assert len(got_edges) == max(got_edges)
                good_edges = list(range(1, len(got_edges) + 1))
                assert sorted(got_edges) == good_edges

        return [(o.desc, o.get_depth(), model.objects.get_children_count(o)) for o in model.objects.get_tree()]

    def _assert_get_annotated_list(self, model, expected, parent=None):
        results = model.objects.get_annotated_list(parent)
        got = [(obj[0].desc, obj[1]["open"], obj[1]["close"], obj[1]["level"]) for obj in results]
        assert expected == got
        assert all(isinstance(obj[0], model) for obj in results)

    def _assert_no_tree_problems(self, model):
        if issubclass(model, (MP_Node, NS_Node)):
            assert not any(model.objects.find_problems())


@pytest.mark.django_db
class TestEmptyTree(TestTreeBase):
    def test_load_bulk_empty(self, model_without_data):
        ids = model_without_data.objects.load_bulk(BASE_DATA)
        got_descs = [obj.desc for obj in model_without_data.objects.filter(pk__in=ids)]
        expected_descs = [x[0] for x in UNCHANGED]
        assert sorted(got_descs) == sorted(expected_descs)
        assert self.got(model_without_data) == UNCHANGED

    def test_dump_bulk_empty(self, model_without_data):
        assert model_without_data.objects.dump_bulk() == []

    def test_add_root_empty(self, model_without_data):
        model_without_data.objects.add_root({"desc": "1"})
        expected = [("1", 1, 0)]
        assert self.got(model_without_data) == expected

    def test_get_root_nodes_empty(self, model_without_data):
        got = model_without_data.objects.get_root_nodes()
        expected = []
        assert [node.desc for node in got] == expected

    def test_get_first_root_node_empty(self, model_without_data):
        got = model_without_data.objects.get_first_root_node()
        assert got is None

    def test_get_last_root_node_empty(self, model_without_data):
        got = model_without_data.objects.get_last_root_node()
        assert got is None

    def test_get_tree(self, model_without_data):
        got = list(model_without_data.objects.get_tree())
        assert got == []

    def test_get_annotated_list(self, model_without_data):
        expected = []
        self._assert_get_annotated_list(model_without_data, expected)

    def test_add_multiple_root_nodes_adds_sibling_leaves(self, model_without_data):
        model_without_data.objects.add_root({"desc": "1"})
        model_without_data.objects.add_root({"desc": "2"})
        model_without_data.objects.add_root({"desc": "3"})
        model_without_data.objects.add_root({"desc": "4"})
        # these are all sibling root nodes (depth=1), and leaf nodes (children=0)
        expected = [("1", 1, 0), ("2", 1, 0), ("3", 1, 0), ("4", 1, 0)]
        assert self.got(model_without_data) == expected


class TestNonEmptyTree(TestTreeBase):
    pass


@pytest.mark.django_db
class TestClassMethods(TestNonEmptyTree):
    def test_load_bulk_existing(self, model):
        # inserting on an existing node
        node = model.objects.get(desc="231")
        ids = model.objects.load_bulk(BASE_DATA, node)
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 4),
            ("1", 4, 0),
            ("2", 4, 4),
            ("21", 5, 0),
            ("22", 5, 0),
            ("23", 5, 1),
            ("231", 6, 0),
            ("24", 5, 0),
            ("3", 4, 0),
            ("4", 4, 1),
            ("41", 5, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        expected_descs = ["1", "2", "21", "22", "23", "231", "24", "3", "4", "41"]
        got_descs = [obj.desc for obj in model.objects.filter(pk__in=ids)]
        assert sorted(got_descs) == sorted(expected_descs)
        assert self.got(model) == expected

    def test_get_tree_all(self, model, django_assert_max_num_queries):
        max_queries = 4 if issubclass(model, AL_Node) else 1
        with django_assert_max_num_queries(max_queries):
            nodes = model.objects.get_tree()
        got = [(o.desc, o.get_depth(), model.objects.get_children_count(o)) for o in nodes]
        assert got == UNCHANGED
        assert all(isinstance(o, model) for o in nodes)

    def test_get_tree_max_depth(self, model, django_assert_max_num_queries):
        # Max depth of 2
        max_queries = 3 if issubclass(model, AL_Node) else 1
        with django_assert_max_num_queries(max_queries):
            nodes = model.objects.get_tree(max_depth=2)
        got = [(o.desc, o.get_depth(), model.objects.get_children_count(o)) for o in nodes]
        assert got == [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]

        # Max depth of 1
        max_queries = 2 if issubclass(model, AL_Node) else 1
        with django_assert_max_num_queries(max_queries):
            nodes = model.objects.get_tree(max_depth=1)
        got = [(o.desc, o.get_depth(), model.objects.get_children_count(o)) for o in nodes]
        assert got == [
            ("1", 1, 0),
            ("2", 1, 4),
            ("3", 1, 0),
            ("4", 1, 1),
        ]

    def test_get_tree_max_depth_relative_to_parent(self, model, django_assert_max_num_queries):
        parent = model.objects.get(desc="2")
        max_queries = 3 if issubclass(model, AL_Node) else 1
        with django_assert_max_num_queries(max_queries):
            nodes = model.objects.get_tree(parent=parent, max_depth=1)
        got = [(o.desc, o.get_depth(), model.objects.get_children_count(o)) for o in nodes]
        assert got == [
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("24", 2, 0),
        ]

    def test_dump_bulk_all(self, model):
        assert model.objects.dump_bulk(keep_ids=False) == BASE_DATA

    def test_get_tree_node(self, model, django_assert_max_num_queries):
        node = model.objects.get(desc="231")
        model.objects.load_bulk(BASE_DATA, node)

        # the tree was modified by load_bulk, so we reload our node object
        node = model.objects.get(pk=node.pk)

        max_queries = 7 if issubclass(model, AL_Node) else 1
        with django_assert_max_num_queries(max_queries):
            nodes = model.objects.get_tree(node)

        got = [(o.desc, o.get_depth(), model.objects.get_children_count(o)) for o in nodes]
        expected = [
            ("231", 3, 4),
            ("1", 4, 0),
            ("2", 4, 4),
            ("21", 5, 0),
            ("22", 5, 0),
            ("23", 5, 1),
            ("231", 6, 0),
            ("24", 5, 0),
            ("3", 4, 0),
            ("4", 4, 1),
            ("41", 5, 0),
        ]
        assert got == expected
        assert all(isinstance(o, model) for o in nodes)

    def test_get_tree_leaf(self, model, django_assert_max_num_queries):
        node = model.objects.get(desc="1")

        assert 0 == model.objects.get_children_count(node)

        with django_assert_max_num_queries(1):
            nodes = model.objects.get_tree(node)
        got = [(o.desc, o.get_depth(), model.objects.get_children_count(o)) for o in nodes]
        expected = [("1", 1, 0)]
        assert got == expected
        assert all(isinstance(o, model) for o in nodes)

    def test_get_annotated_list_all(self, model, django_assert_max_num_queries):
        expected = [
            ("1", True, [], 0),
            ("2", False, [], 0),
            ("21", True, [], 1),
            ("22", False, [], 1),
            ("23", False, [], 1),
            ("231", True, [0], 2),
            ("24", False, [0], 1),
            ("3", False, [], 0),
            ("4", False, [], 0),
            ("41", True, [0, 1], 1),
        ]
        max_queries = 4 if issubclass(model, AL_Node) else 1
        with django_assert_max_num_queries(max_queries):
            self._assert_get_annotated_list(model, expected)

    def test_get_annotated_list_node(self, model, django_assert_max_num_queries):
        node = model.objects.get(desc="2")
        expected = [
            ("2", True, [], 0),
            ("21", True, [], 1),
            ("22", False, [], 1),
            ("23", False, [], 1),
            ("231", True, [0], 2),
            ("24", False, [0, 1], 1),
        ]
        max_queries = 3 if issubclass(model, AL_Node) else 1
        with django_assert_max_num_queries(max_queries):
            self._assert_get_annotated_list(model, expected, node)

    def test_get_annotated_list_leaf(self, model, django_assert_max_num_queries):
        node = model.objects.get(desc="1")
        expected = [("1", True, [0], 0)]
        with django_assert_max_num_queries(1):
            self._assert_get_annotated_list(model, expected, node)

    def test_dump_bulk_node(self, model):
        node = model.objects.get(desc="231")
        model.objects.load_bulk(BASE_DATA, node)

        # the tree was modified by load_bulk, so we reload our node object
        node = model.objects.get(pk=node.pk)

        got = model.objects.dump_bulk(node, False)
        expected = [{"data": {"desc": "231"}, "children": BASE_DATA}]
        assert got == expected

    def test_load_and_dump_bulk_keeping_ids(self, model):
        exp = model.objects.dump_bulk(keep_ids=True)
        model.objects.all().delete()
        model.objects.load_bulk(exp, None, True)
        got = model.objects.dump_bulk(keep_ids=True)
        assert got == exp
        # do we really have an unchanged tree after the dump/delete/load?
        got = [(o.desc, o.get_depth(), model.objects.get_children_count(o)) for o in model.objects.get_tree()]
        assert got == UNCHANGED

    def test_load_and_dump_bulk_with_related_models(self, related_model):
        related = models.RelatedModel.objects.create(desc=f"Test {related_model.__name__}")

        related_data = [
            {"data": {"desc": "1", "related": related.pk, "related_m2m": [related.pk]}},
            {
                "data": {"desc": "2", "related": related.pk, "related_m2m": []},
                "children": [
                    {"data": {"desc": "21", "related": related.pk, "related_m2m": [related.pk]}},
                    {"data": {"desc": "22", "related": related.pk, "related_m2m": [related.pk]}},
                    {
                        "data": {"desc": "23", "related": related.pk, "related_m2m": []},
                        "children": [
                            {"data": {"desc": "231", "related": related.pk, "related_m2m": [related.pk]}},
                        ],
                    },
                    {"data": {"desc": "24", "related": related.pk, "related_m2m": []}},
                ],
            },
            {"data": {"desc": "3", "related": related.pk, "related_m2m": []}},
            {
                "data": {"desc": "4", "related": related.pk, "related_m2m": []},
                "children": [
                    {"data": {"desc": "41", "related": related.pk, "related_m2m": []}},
                ],
            },
        ]
        related_model.objects.load_bulk(related_data)
        got = related_model.objects.dump_bulk(keep_ids=False)
        assert got == related_data

    def test_get_root_nodes(self, model, django_assert_max_num_queries):
        with django_assert_max_num_queries(1):
            got = model.objects.get_root_nodes()
        expected = ["1", "2", "3", "4"]
        assert [node.desc for node in got] == expected
        assert all(isinstance(node, model) for node in got)

    def test_get_first_root_node(self, model, django_assert_max_num_queries):
        with django_assert_max_num_queries(1):
            got = model.objects.get_first_root_node()
        assert got.desc == "1"
        assert isinstance(got, model)

    def test_get_last_root_node(self, model, django_assert_max_num_queries):
        with django_assert_max_num_queries(1):
            got = model.objects.get_last_root_node()
        assert got.desc == "4"
        assert isinstance(got, model)

    def test_add_root(self, model):
        obj = model.objects.add_root({"desc": "5"})
        assert obj.get_depth() == 1
        got = model.objects.get_last_root_node()
        assert got.desc == "5"
        assert isinstance(got, model)

    def test_add_root_with_passed_instance(self, model):
        obj = model(desc="5")
        result = model.objects.add_root(instance=obj)
        assert result == obj
        got = model.objects.get_last_root_node()
        assert got.desc == "5"
        assert isinstance(got, model)

    def test_add_root_with_already_saved_instance(self, model):
        obj = model.objects.get(desc="4")
        with pytest.raises(NodeAlreadySaved):
            model.objects.add_root(instance=obj)


@pytest.mark.django_db
class TestSimpleNodeMethods(TestNonEmptyTree):
    def test_is_root(self, model, django_assert_max_num_queries):
        data = [
            ("2", True),
            ("1", True),
            ("4", True),
            ("21", False),
            ("24", False),
            ("22", False),
            ("231", False),
        ]
        for desc, expected in data:
            node = model.objects.get(desc=desc)
            with django_assert_max_num_queries(0):
                got = node.is_root()
            assert got == expected

    def test_is_leaf(self, model, django_assert_max_num_queries):
        data = [
            ("2", False),
            ("23", False),
            ("231", True),
        ]
        for desc, expected in data:
            node = model.objects.get(desc=desc)
            max_queries = 0 if issubclass(model, (MP_Node, NS_Node)) else 1
            with django_assert_max_num_queries(max_queries):
                got = node.is_leaf()
            assert got == expected

    def test_get_root(self, model, django_assert_max_num_queries):
        data = [
            ("2", "2"),
            ("1", "1"),
            ("4", "4"),
            ("21", "2"),
            ("24", "2"),
            ("22", "2"),
            ("231", "2"),
        ]
        for desc, expected in data:
            descendant = model.objects.get(desc=desc)
            max_queries = 2 if issubclass(model, AL_Node) else 1
            with django_assert_max_num_queries(max_queries):
                node = model.objects.get_root(descendant)
            assert node.desc == expected
            assert isinstance(node, model)

    def test_get_parent(self, model, django_assert_max_num_queries):
        data = [
            ("2", None),
            ("1", None),
            ("4", None),
            ("21", "2"),
            ("24", "2"),
            ("22", "2"),
            ("231", "23"),
        ]
        data = dict(data)
        objs = {}
        for desc, expected in data.items():
            node = model.objects.get(desc=desc)
            with django_assert_max_num_queries(1):
                parent = model.objects.get_parent(node)
            if expected:
                assert parent.desc == expected
                assert isinstance(parent, model)
            else:
                assert parent is None
            objs[desc] = node
            # corrupt the objects' parent cache
            node._parent_obj = "CORRUPTED!!!"

        for desc, expected in data.items():
            node = objs[desc]
            # asking get_parent to not use the parent cache (since we
            # corrupted it in the previous loop)
            parent = model.objects.get_parent(node, True)
            if expected:
                assert parent.desc == expected
                assert isinstance(parent, model)
            else:
                assert parent is None

    def test_get_children(self, model, django_assert_max_num_queries):
        data = [
            ("2", ["21", "22", "23", "24"]),
            ("23", ["231"]),
            ("231", []),
        ]
        for desc, expected in data:
            node = model.objects.get(desc=desc)
            with django_assert_max_num_queries(1):
                children = model.objects.get_children(node)
            assert [node.desc for node in children] == expected
            assert all(isinstance(node, model) for node in children)

    def test_get_children_count(self, model, django_assert_max_num_queries):
        data = [
            ("2", 4),
            ("23", 1),
            ("231", 0),
        ]
        for desc, expected in data:
            node = model.objects.get(desc=desc)
            max_queries = 0 if issubclass(model, MP_Node) else 1
            with django_assert_max_num_queries(max_queries):
                got = model.objects.get_children_count(node)
            assert got == expected

    def test_get_siblings(self, model, django_assert_max_num_queries):
        data = [
            ("2", ["1", "2", "3", "4"]),
            ("21", ["21", "22", "23", "24"]),
            ("231", ["231"]),
        ]
        for desc, expected in data:
            node = model.objects.get(desc=desc)
            with django_assert_max_num_queries(1):
                siblings = model.objects.get_siblings(node)
            assert [node.desc for node in siblings] == expected
            assert all(isinstance(node, model) for node in siblings)

    def test_get_first_sibling(self, model, django_assert_max_num_queries):
        data = [
            ("2", "1"),
            ("1", "1"),
            ("4", "1"),
            ("21", "21"),
            ("24", "21"),
            ("22", "21"),
            ("231", "231"),
        ]
        for desc, expected in data:
            node = model.objects.get(desc=desc)
            max_queries = 2 if issubclass(model, NS_Node) else 1
            with django_assert_max_num_queries(max_queries):
                sibling = model.objects.get_first_sibling(node)
            assert sibling.desc == expected
            assert isinstance(sibling, model)

    def test_get_prev_sibling(self, model, django_assert_max_num_queries):
        data = [
            ("2", "1"),
            ("1", None),
            ("4", "3"),
            ("21", None),
            ("24", "23"),
            ("22", "21"),
            ("231", None),
        ]
        for desc, expected in data:
            node = model.objects.get(desc=desc)
            max_queries = 4 if issubclass(model, NS_Node) else 1
            with django_assert_max_num_queries(max_queries):
                sibling = model.objects.get_prev_sibling(node)
            if expected is None:
                assert sibling is None
            else:
                assert sibling.desc == expected
                assert isinstance(sibling, model)

    def test_get_next_sibling(self, model, django_assert_max_num_queries):
        data = [
            ("2", "3"),
            ("1", "2"),
            ("4", None),
            ("21", "22"),
            ("24", None),
            ("22", "23"),
            ("231", None),
        ]
        for desc, expected in data:
            node = model.objects.get(desc=desc)
            max_queries = 4 if issubclass(model, NS_Node) else 1  # TODO can NS be made more efficient?
            with django_assert_max_num_queries(max_queries):
                sibling = model.objects.get_next_sibling(node)
            if expected is None:
                assert sibling is None
            else:
                assert sibling.desc == expected
                assert isinstance(sibling, model)

    def test_get_last_sibling(self, model, django_assert_max_num_queries):
        data = [
            ("2", "4"),
            ("1", "4"),
            ("4", "4"),
            ("21", "24"),
            ("24", "24"),
            ("22", "24"),
            ("231", "231"),
        ]
        for desc, expected in data:
            node = model.objects.get(desc=desc)
            max_queries = 4 if issubclass(model, NS_Node) else 1  # TODO can NS be made more efficient?
            with django_assert_max_num_queries(max_queries):
                sibling = model.objects.get_last_sibling(node)
            assert sibling.desc == expected
            assert isinstance(sibling, model)

    def test_get_first_child(self, model, django_assert_max_num_queries):
        data = [
            ("2", "21"),
            ("21", None),
            ("23", "231"),
            ("231", None),
        ]
        for desc, expected in data:
            parent = model.objects.get(desc=desc)
            with django_assert_max_num_queries(1):
                node = model.objects.get_first_child(parent)
            if expected is None:
                assert node is None
            else:
                assert node.desc == expected
                assert isinstance(node, model)

    def test_get_last_child(self, model, django_assert_max_num_queries):
        data = [
            ("2", "24"),
            ("21", None),
            ("23", "231"),
            ("231", None),
        ]
        for desc, expected in data:
            parent = model.objects.get(desc=desc)
            with django_assert_max_num_queries(1):
                node = model.objects.get_last_child(parent)
            if expected is None:
                assert node is None
            else:
                assert node.desc == expected
                assert isinstance(node, model)

    def test_get_ancestors(self, model, django_assert_max_num_queries):
        data = [
            ("2", []),
            ("21", ["2"]),
            ("231", ["2", "23"]),
        ]
        for desc, expected in data:
            descendant = model.objects.get(desc=desc)
            max_queries = 2 if issubclass(model, AL_Node) else 1
            with django_assert_max_num_queries(max_queries):
                nodes = model.objects.get_ancestors(descendant)
            assert [node.desc for node in nodes] == expected
            assert all(isinstance(node, model) for node in nodes)

    def test_get_descendants(self, model, django_assert_max_num_queries):
        data = [
            ("2", ["21", "22", "23", "231", "24"]),
            ("23", ["231"]),
            ("231", []),
            ("1", []),
            ("4", ["41"]),
        ]
        for desc, expected in data:
            parent = model.objects.get(desc=desc)
            max_queries = 3 if issubclass(model, AL_Node) else 1
            with django_assert_max_num_queries(max_queries):
                nodes = model.objects.get_descendants(parent)
            assert [node.desc for node in nodes] == expected
            assert all(isinstance(node, model) for node in nodes)

    def test_get_descendants_max_depth(self, model, django_assert_max_num_queries):
        data = [
            ("2", ["21", "22", "23", "24"]),
            ("23", ["231"]),
            ("231", []),
            ("1", []),
            ("4", ["41"]),
        ]
        for desc, expected in data:
            parent = model.objects.get(desc=desc)
            max_queries = 3 if issubclass(model, AL_Node) else 1
            with django_assert_max_num_queries(max_queries):
                nodes = model.objects.get_descendants(parent, max_depth=1)
            assert [node.desc for node in nodes] == expected
            assert all(isinstance(node, model) for node in nodes)

    def test_get_descendants_include_self(self, model, django_assert_max_num_queries):
        data = [
            ("2", ["2", "21", "22", "23", "231", "24"]),
            ("23", ["23", "231"]),
            ("231", ["231"]),
            ("1", ["1"]),
            ("4", ["4", "41"]),
        ]
        for desc, expected in data:
            parent = model.objects.get(desc=desc)
            max_queries = 3 if issubclass(model, AL_Node) else 1
            with django_assert_max_num_queries(max_queries):
                nodes = model.objects.get_descendants(parent, include_self=True)
            assert [node.desc for node in nodes] == expected
            assert all(isinstance(node, model) for node in nodes)

    def test_get_descendant_count(self, model, django_assert_max_num_queries):
        data = [
            ("2", 5),
            ("23", 1),
            ("231", 0),
            ("1", 0),
            ("4", 1),
        ]
        for desc, expected in data:
            parent = model.objects.get(desc=desc)
            max_queries = 3 if issubclass(model, AL_Node) else 1
            with django_assert_max_num_queries(max_queries):
                got = model.objects.get_descendant_count(parent)
            assert got == expected

    def test_get_descendant_count_max_depth(self, model, django_assert_max_num_queries):
        data = [
            ("2", 4),
            ("23", 1),
            ("231", 0),
            ("1", 0),
            ("4", 1),
        ]
        for desc, expected in data:
            parent = model.objects.get(desc=desc)
            max_queries = 3 if issubclass(model, AL_Node) else 1
            with django_assert_max_num_queries(max_queries):
                got = model.objects.get_descendant_count(parent, max_depth=1)
            assert got == expected

    def test_is_sibling_of(self, model, django_assert_max_num_queries):
        data = [
            ("2", "2", True),
            ("2", "1", True),
            ("21", "2", False),
            ("231", "2", False),
            ("22", "23", True),
            ("231", "23", False),
            ("231", "231", True),
        ]
        for desc1, desc2, expected in data:
            node1 = model.objects.get(desc=desc1)
            node2 = model.objects.get(desc=desc2)
            max_queries = 2 if issubclass(model, NS_Node) else 0
            with django_assert_max_num_queries(max_queries):
                assert node1.is_sibling_of(node2) == expected

    def test_is_child_of(self, model, django_assert_max_num_queries):
        data = [
            ("2", "2", False),
            ("2", "1", False),
            ("21", "2", True),
            ("231", "2", False),
            ("231", "23", True),
            ("231", "231", False),
        ]
        for desc1, desc2, expected in data:
            node1 = model.objects.get(desc=desc1)
            node2 = model.objects.get(desc=desc2)
            max_queries = 1 if issubclass(model, NS_Node) else 0
            with django_assert_max_num_queries(max_queries):
                assert node1.is_child_of(node2) == expected

    def test_is_descendant_of(self, model, django_assert_max_num_queries):
        data = [
            ("2", "2", False),
            ("2", "1", False),
            ("21", "2", True),
            ("231", "2", True),
            ("231", "23", True),
            ("231", "231", False),
        ]
        for desc1, desc2, expected in data:
            node1 = model.objects.get(desc=desc1)
            node2 = model.objects.get(desc=desc2)
            max_queries = 3 if issubclass(model, AL_Node) else 0
            with django_assert_max_num_queries(max_queries):
                assert node1.is_descendant_of(node2) == expected


@pytest.mark.django_db
class TestAddChild(TestNonEmptyTree):
    def test_add_child_to_leaf(self, model):
        with capture_signals() as signals:
            model.objects.add_child(model.objects.get(desc="231"), {"desc": "2311"})

        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 1),
            ("2311", 4, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            assert signals == []
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 8, 2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_child_to_node(self, model):
        with capture_signals() as signals:
            model.objects.add_child(model.objects.get(desc="2"), {"desc": "25"})
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("25", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            assert signals == []
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 12, 2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_child_with_passed_instance(self, model):
        child = model(desc="2311")
        with capture_signals() as signals:
            result = model.objects.add_child(model.objects.get(desc="231"), instance=child)
        assert result == child
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 1),
            ("2311", 4, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            assert signals == []
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 8, 2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_child_called_consecutively(self, model_without_data):
        # Regression test for https://github.com/django-treebeard/django-treebeard/issues/307
        parent = model_without_data.objects.add_root({"desc": "1"})
        child1 = model_without_data(desc="11")
        child2 = model_without_data(desc="12")
        assert model_without_data.objects.get_children_count(parent) == 0
        model_without_data.objects.add_child(parent, instance=child1)
        model_without_data.objects.add_child(parent, instance=child2)
        assert list(model_without_data.objects.get_children(parent).values_list("desc", flat=True)) == [
            child1.desc,
            child2.desc,
        ]

    @pytest.mark.django_db(transaction=True)
    @pytest.mark.skipif(
        os.getenv("DATABASE_ENGINE", "sqlite") == "sqlite",
        reason="SQLite doesn't support row-level locking",
    )
    def test_add_child_concurrent(self, model_without_data):
        # 5 threads x 5 add_child each on same parent; no IntegrityError.
        num_threads = 5
        per_thread = 5
        parent = model_without_data.objects.add_root({"desc": "parent"})
        parent_pk = parent.pk
        errors = []

        def add_children(thread_id):
            try:
                p = model_without_data.objects.get(pk=parent_pk)
                for i in range(per_thread):
                    model_without_data.objects.add_child(p, {"desc": f"t{thread_id}-{i}"})
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=add_children, args=(t,)) for t in range(num_threads)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()
        if errors:
            raise errors[0]
        parent.refresh_from_db()
        assert model_without_data.objects.get_children_count(parent) == num_threads * per_thread

    def test_add_child_with_already_saved_instance(self, model):
        child = model.objects.get(desc="21")
        with pytest.raises(NodeAlreadySaved):
            model.objects.add_child(model.objects.get(desc="2"), instance=child)

    def test_add_child_with_pk_set(self, model):
        """
        If the model is using a natural primary key then it will be
        already set when the instance is inserted.
        """
        child = model(pk=999999, desc="natural key")
        result = model.objects.add_child(model.objects.get(desc="2"), instance=child)
        assert result == child

    def test_add_child_post_save(self, model):
        try:

            @receiver(post_save, dispatch_uid="test_add_child_post_save")
            def on_post_save(instance, **kwargs):
                parent = model.objects.get_parent(instance)
                assert model.objects.get_descendant_count(parent) == 1

            # It's important that we're testing a leaf node
            parent = model.objects.get(desc="231")
            assert parent.is_leaf()

            model.objects.add_child(parent, {"desc": "2311"})
        finally:
            post_save.disconnect(dispatch_uid="test_add_child_post_save")


@pytest.mark.django_db
class TestAddSibling(TestNonEmptyTree):
    def test_add_sibling_invalid_pos(self, model):
        with pytest.raises(InvalidPosition):
            model.objects.add_sibling(model.objects.get(desc="231"), "invalid_pos", {})

    def test_add_sibling_missing_nodeorderby(self, model):
        node_wchildren = model.objects.get(desc="2")
        with pytest.raises(MissingNodeOrderBy):
            model.objects.add_sibling(node_wchildren, "sorted-sibling", {"desc": "aaa"})

    def test_add_sibling_last_root(self, model):
        node_wchildren = model.objects.get(desc="2")
        with capture_signals() as signals:
            obj = model.objects.add_sibling(node_wchildren, "last-sibling", {"desc": "5"})
        assert obj.get_depth() == 1
        assert model.objects.get_last_sibling(node_wchildren).desc == "5"
        if issubclass(model, MP_Node):
            assert signals == []
        elif issubclass(model, NS_Node):
            assert signals == []
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_sibling_last(self, model):
        node = model.objects.get(desc="231")
        with capture_signals() as signals:
            obj = model.objects.add_sibling(node, "last-sibling", {"desc": "232"})
        assert obj.get_depth() == 3
        assert model.objects.get_last_sibling(node).desc == "232"
        if issubclass(model, MP_Node):
            assert signals == []
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 9, 2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_sibling_first_root(self, model):
        node_wchildren = model.objects.get(desc="2")
        with capture_signals() as signals:
            obj = model.objects.add_sibling(node_wchildren, "first-sibling", {"desc": "new"})
        assert obj.get_depth() == 1
        expected = [
            ("new", 1, 0),
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "5", "default"),
                    ("path_updated", model, "3", "4", "default"),
                    ("path_updated", model, "2", "3", "default"),
                    ("path_updated", model, "1", "2", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "005", "default"),
                    ("path_updated", model, "003", "004", "default"),
                    ("path_updated", model, "002", "003", "default"),
                    ("path_updated", model, "001", "002", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("tree_ids_incremented", model, 1, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_sibling_first(self, model):
        node_wchildren = model.objects.get(desc="23")
        with capture_signals() as signals:
            obj = model.objects.add_sibling(node_wchildren, "first-sibling", {"desc": "new"})
        assert obj.get_depth() == 2
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("new", 2, 0),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "24", "25", "default"),
                    ("path_updated", model, "23", "24", "default"),
                    ("path_updated", model, "22", "23", "default"),
                    ("path_updated", model, "21", "22", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002004", "002005", "default"),
                    ("path_updated", model, "002003", "002004", "default"),
                    ("path_updated", model, "002002", "002003", "default"),
                    ("path_updated", model, "002001", "002002", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 2, 2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_sibling_left_root(self, model):
        node_wchildren = model.objects.get(desc="2")
        with capture_signals() as signals:
            obj = model.objects.add_sibling(node_wchildren, "left", {"desc": "new"})
        assert obj.get_depth() == 1
        expected = [
            ("1", 1, 0),
            ("new", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "5", "default"),
                    ("path_updated", model, "3", "4", "default"),
                    ("path_updated", model, "2", "3", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "005", "default"),
                    ("path_updated", model, "003", "004", "default"),
                    ("path_updated", model, "002", "003", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("tree_ids_incremented", model, 2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_sibling_left(self, model):
        node_wchildren = model.objects.get(desc="23")
        with capture_signals() as signals:
            obj = model.objects.add_sibling(node_wchildren, "left", {"desc": "new"})
        assert obj.get_depth() == 2
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("21", 2, 0),
            ("22", 2, 0),
            ("new", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "24", "25", "default"),
                    ("path_updated", model, "23", "24", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002004", "002005", "default"),
                    ("path_updated", model, "002003", "002004", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 6, 2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_sibling_left_noleft_root(self, model):
        node = model.objects.get(desc="1")
        with capture_signals() as signals:
            obj = model.objects.add_sibling(node, "left", {"desc": "new"})
        assert obj.get_depth() == 1
        expected = [
            ("new", 1, 0),
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "5", "default"),
                    ("path_updated", model, "3", "4", "default"),
                    ("path_updated", model, "2", "3", "default"),
                    ("path_updated", model, "1", "2", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "005", "default"),
                    ("path_updated", model, "003", "004", "default"),
                    ("path_updated", model, "002", "003", "default"),
                    ("path_updated", model, "001", "002", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("tree_ids_incremented", model, 1, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_sibling_left_noleft(self, model):
        node = model.objects.get(desc="231")
        with capture_signals() as signals:
            obj = model.objects.add_sibling(node, "left", {"desc": "new"})
        assert obj.get_depth() == 3
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 2),
            ("new", 3, 0),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "231", "232", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002003001", "002003002", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 7, 2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_sibling_right_root(self, model):
        node_wchildren = model.objects.get(desc="2")
        with capture_signals() as signals:
            obj = model.objects.add_sibling(node_wchildren, "right", {"desc": "new"})
        assert obj.get_depth() == 1
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("new", 1, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "5", "default"),
                    ("path_updated", model, "3", "4", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "005", "default"),
                    ("path_updated", model, "003", "004", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("tree_ids_incremented", model, 3, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_sibling_right(self, model):
        node_wchildren = model.objects.get(desc="23")
        with capture_signals() as signals:
            obj = model.objects.add_sibling(node_wchildren, "right", {"desc": "new"})
        assert obj.get_depth() == 2
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("new", 2, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "24", "25", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002004", "002005", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 10, 2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_sibling_right_noright_root(self, model):
        node = model.objects.get(desc="4")
        with capture_signals() as signals:
            obj = model.objects.add_sibling(node, "right", {"desc": "new"})
        assert obj.get_depth() == 1
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
            ("new", 1, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            assert signals == []
        elif issubclass(model, NS_Node):
            assert signals == []
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_sibling_right_noright(self, model):
        node = model.objects.get(desc="231")
        with capture_signals() as signals:
            obj = model.objects.add_sibling(node, "right", {"desc": "new"})
        assert obj.get_depth() == 3
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 2),
            ("231", 3, 0),
            ("new", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            assert signals == []
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 9, 2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == []

    def test_add_sibling_with_passed_instance(self, model):
        node_wchildren = model.objects.get(desc="2")
        obj = model(desc="5")
        result = model.objects.add_sibling(node_wchildren, "last-sibling", instance=obj)
        assert result == obj
        assert obj.get_depth() == 1
        assert model.objects.get_last_sibling(node_wchildren).desc == "5"

    def test_add_sibling_already_saved_instance(self, model):
        node_wchildren = model.objects.get(desc="2")
        existing_node = model.objects.get(desc="4")
        with pytest.raises(NodeAlreadySaved):
            model.objects.add_sibling(node_wchildren, "last-sibling", instance=existing_node)

    def test_add_child_with_pk_set(self, model):
        """
        If the model is using a natural primary key then it will be
        already set when the instance is inserted.
        """
        child = model(pk=999999, desc="natural key")
        result = model.objects.add_child(model.objects.get(desc="2"), instance=child)
        assert result == child


@pytest.mark.django_db
class TestDelete(TestTreeBase):
    @staticmethod
    @pytest.fixture(
        scope="function",
        params=models.DEP_MODELS,
        ids=lambda fv: f"base={fv[0].__name__} dep={fv[1].__name__}",
    )
    def delete_dep_model_pair(request):
        base_model, dep_model = request.param
        base_model.objects.load_bulk(BASE_DATA)
        for node in base_model.objects.all():
            dep_model(node=node).save()
        return base_model, dep_model

    def test_delete_leaf(self, delete_dep_model_pair):
        delete_model, dep_model = delete_dep_model_pair
        node = delete_model.objects.get(desc="231")
        with capture_signals() as signals:
            result = node.delete()
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(delete_model) == expected
        assert result == (2, {delete_model._meta.label: 1, dep_model._meta.label: 1})
        if issubclass(delete_model, MP_Node):
            assert signals == [
                ("nodes_deleted", delete_model, [node.pk], [], "default"),
            ]
        elif issubclass(delete_model, NS_Node):
            assert signals == [
                ("nodes_deleted", delete_model, [(2, 7, 8)], "default"),
                ("gap_altered", delete_model, 2, 7, -2, "default"),
            ]
        elif issubclass(delete_model, LT_Node):
            assert signals == [
                ("nodes_deleted", delete_model, ["B.C.A"], "default"),
            ]

    def test_delete_node(self, delete_dep_model_pair):
        delete_model, dep_model = delete_dep_model_pair
        with capture_signals() as signals:
            result = delete_model.objects.get(desc="23").delete()
        expected = [
            ("1", 1, 0),
            ("2", 1, 3),
            ("21", 2, 0),
            ("22", 2, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(delete_model) == expected
        assert result == (4, {delete_model._meta.label: 2, dep_model._meta.label: 2})
        if issubclass(delete_model, MP_Node):
            if delete_model.steplen == 1:
                expected_signals = [
                    ("nodes_deleted", delete_model, [], ["23"], "default"),
                ]
            elif delete_model.steplen == 3:
                expected_signals = [
                    ("nodes_deleted", delete_model, [], ["002003"], "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {delete_model.steplen}"
            assert signals == expected_signals
        elif issubclass(delete_model, NS_Node):
            assert signals == [
                ("nodes_deleted", delete_model, [(2, 6, 9)], "default"),
                ("gap_altered", delete_model, 2, 6, -4, "default"),
            ]
        elif issubclass(delete_model, LT_Node):
            assert signals == [
                ("nodes_deleted", delete_model, ["B.C"], "default"),
            ]

    def test_delete_root(self, delete_dep_model_pair):
        delete_model, dep_model = delete_dep_model_pair
        with capture_signals() as signals:
            result = delete_model.objects.get(desc="2").delete()
        expected = [("1", 1, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0)]
        assert self.got(delete_model) == expected
        assert result == (12, {delete_model._meta.label: 6, dep_model._meta.label: 6})
        if issubclass(delete_model, MP_Node):
            if delete_model.steplen == 1:
                expected_signals = [
                    ("nodes_deleted", delete_model, [], ["2"], "default"),
                ]
            elif delete_model.steplen == 3:
                expected_signals = [
                    ("nodes_deleted", delete_model, [], ["002"], "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {delete_model.steplen}"
            assert signals == expected_signals
        elif issubclass(delete_model, NS_Node):
            assert signals == [
                ("nodes_deleted", delete_model, [(2, 1, 12)], "default"),
            ]
        elif issubclass(delete_model, LT_Node):
            assert signals == [
                ("nodes_deleted", delete_model, ["B"], "default"),
            ]

    def test_delete_filter_root_nodes(self, delete_dep_model_pair):
        delete_model, dep_model = delete_dep_model_pair
        node3 = delete_model.objects.get(desc="3")
        with capture_signals() as signals:
            result = delete_model.objects.filter(desc__in=("2", "3")).delete()
        expected = [("1", 1, 0), ("4", 1, 1), ("41", 2, 0)]
        assert self.got(delete_model) == expected
        assert result == (14, {delete_model._meta.label: 7, dep_model._meta.label: 7})
        if issubclass(delete_model, MP_Node):
            if delete_model.steplen == 1:
                expected_signals = [
                    ("nodes_deleted", delete_model, [node3.pk], ["2"], "default"),
                ]
            elif delete_model.steplen == 3:
                expected_signals = [
                    ("nodes_deleted", delete_model, [node3.pk], ["002"], "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {delete_model.steplen}"
            assert signals == expected_signals
        elif issubclass(delete_model, NS_Node):
            assert signals == [
                ("nodes_deleted", delete_model, [(2, 1, 12), (3, 1, 2)], "default"),
            ]
        elif issubclass(delete_model, LT_Node):
            assert signals == [
                ("nodes_deleted", delete_model, ["B", "C"], "default"),
            ]

    def test_delete_filter_children(self, delete_dep_model_pair):
        delete_model, dep_model = delete_dep_model_pair
        with capture_signals() as signals:
            result = delete_model.objects.filter(desc__in=("2", "23", "231")).delete()
        expected = [("1", 1, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0)]
        assert self.got(delete_model) == expected
        assert result == (12, {delete_model._meta.label: 6, dep_model._meta.label: 6})
        if issubclass(delete_model, MP_Node):
            if delete_model.steplen == 1:
                expected_signals = [
                    ("nodes_deleted", delete_model, [], ["2"], "default"),
                ]
            elif delete_model.steplen == 3:
                expected_signals = [
                    ("nodes_deleted", delete_model, [], ["002"], "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {delete_model.steplen}"
            assert signals == expected_signals
        elif issubclass(delete_model, NS_Node):
            assert signals == [
                ("nodes_deleted", delete_model, [(2, 1, 12)], "default"),
            ]
        elif issubclass(delete_model, LT_Node):
            assert signals == [
                ("nodes_deleted", delete_model, ["B"], "default"),
            ]

    def test_delete_nonexistant_nodes(self, delete_dep_model_pair):
        delete_model, dep_model = delete_dep_model_pair
        with capture_signals() as signals:
            result = delete_model.objects.filter(desc__in=("ZZZ", "XXX")).delete()
        assert self.got(delete_model) == UNCHANGED
        assert result == (0, {})
        if issubclass(delete_model, MP_Node):
            assert signals == []
        elif issubclass(delete_model, NS_Node):
            assert signals == []

    def test_delete_same_node_twice(self, delete_dep_model_pair):
        delete_model, dep_model = delete_dep_model_pair
        with capture_signals() as signals:
            result = delete_model.objects.filter(desc__in=("2", "2")).delete()
        expected = [("1", 1, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0)]
        assert self.got(delete_model) == expected
        assert result == (12, {delete_model._meta.label: 6, dep_model._meta.label: 6})
        if issubclass(delete_model, MP_Node):
            if delete_model.steplen == 1:
                expected_signals = [
                    ("nodes_deleted", delete_model, [], ["2"], "default"),
                ]
            elif delete_model.steplen == 3:
                expected_signals = [
                    ("nodes_deleted", delete_model, [], ["002"], "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {delete_model.steplen}"
            assert signals == expected_signals
        elif issubclass(delete_model, NS_Node):
            assert signals == [
                ("nodes_deleted", delete_model, [(2, 1, 12)], "default"),
            ]
        elif issubclass(delete_model, LT_Node):
            assert signals == [
                ("nodes_deleted", delete_model, ["B"], "default"),
            ]

    def test_delete_all_root_nodes(self, delete_dep_model_pair):
        delete_model, dep_model = delete_dep_model_pair
        node1 = delete_model.objects.get(desc="1")
        node3 = delete_model.objects.get(desc="3")
        with capture_signals() as signals:
            result = delete_model.objects.get_root_nodes().delete()
        assert result == (20, {delete_model._meta.label: 10, dep_model._meta.label: 10})
        assert delete_model.objects.count() == 0
        if issubclass(delete_model, MP_Node):
            if delete_model.steplen == 1:
                expected_signals = [
                    ("nodes_deleted", delete_model, [node1.pk, node3.pk], ["2", "4"], "default"),
                ]
            elif delete_model.steplen == 3:
                expected_signals = [
                    ("nodes_deleted", delete_model, [node1.pk, node3.pk], ["002", "004"], "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {delete_model.steplen}"
            assert signals == expected_signals
        elif issubclass(delete_model, NS_Node):
            assert signals == [
                ("nodes_deleted", delete_model, [(1, 1, 2), (2, 1, 12), (3, 1, 2), (4, 1, 4)], "default"),
            ]
        elif issubclass(delete_model, LT_Node):
            assert signals == [
                ("nodes_deleted", delete_model, ["A", "B", "C", "D"], "default"),
            ]

    def test_delete_all_nodes(self, delete_dep_model_pair):
        delete_model, dep_model = delete_dep_model_pair
        node1 = delete_model.objects.get(desc="1")
        node3 = delete_model.objects.get(desc="3")
        with capture_signals() as signals:
            result = delete_model.objects.all().delete()
        assert result == (20, {delete_model._meta.label: 10, dep_model._meta.label: 10})
        assert delete_model.objects.count() == 0
        if issubclass(delete_model, MP_Node):
            if delete_model.steplen == 1:
                expected_signals = [
                    ("nodes_deleted", delete_model, [node1.pk, node3.pk], ["2", "4"], "default"),
                ]
            elif delete_model.steplen == 3:
                expected_signals = [
                    ("nodes_deleted", delete_model, [node1.pk, node3.pk], ["002", "004"], "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {delete_model.steplen}"
            assert signals == expected_signals
        elif issubclass(delete_model, NS_Node):
            assert signals == [
                ("nodes_deleted", delete_model, [(1, 1, 2), (2, 1, 12), (3, 1, 2), (4, 1, 4)], "default"),
            ]
        elif issubclass(delete_model, LT_Node):
            assert signals == [
                ("nodes_deleted", delete_model, ["A", "B", "C", "D"], "default"),
            ]

    def test_delete_with_prefetch_related(self, related_model):
        # Regression test for https://github.com/django-treebeard/django-treebeard/issues/405
        # If `delete()` is run on a queryset with `prefetch_related()` set, then Treebeard's use
        # of `iterator()` will throw an exception unless the prefetch is cleared.
        related = models.RelatedModel.objects.create(desc=f"Test {related_model.__name__}")
        related_model.objects.add_root({"desc": "A", "related": related})
        num_deleted, _ = related_model.objects.prefetch_related("related_m2m").all().delete()
        assert num_deleted == 1


@pytest.mark.django_db
class TestMoveErrors(TestNonEmptyTree):
    def test_move_invalid_pos(self, model):
        node = model.objects.get(desc="231")
        with pytest.raises(InvalidPosition):
            model.objects.move(node, node, "invalid_pos")

    def test_move_to_descendant(self, model):
        node = model.objects.get(desc="2")
        target = model.objects.get(desc="231")
        with pytest.raises(InvalidMoveToDescendant):
            model.objects.move(node, target, "first-sibling")

    @pytest.mark.parametrize("pos", ("first-child", "last-child"))
    def test_cannot_move_node_to_its_own_child(self, pos, model):
        # Test for non-leaf node
        node = model.objects.get(desc="22")
        with pytest.raises(InvalidMoveToDescendant, match="move node to itself"):
            model.objects.move(node, node, pos)

        # Test for leaf node
        node = model.objects.get(desc="231")
        with pytest.raises(InvalidMoveToDescendant, match="move node to itself"):
            model.objects.move(node, node, pos)

    def test_move_missing_nodeorderby(self, model):
        node = model.objects.get(desc="231")
        with pytest.raises(MissingNodeOrderBy):
            model.objects.move(node, node, "sorted-child")
        with pytest.raises(MissingNodeOrderBy):
            model.objects.move(node, node, "sorted-sibling")


@pytest.mark.django_db
class TestMoveSortedErrors(TestTreeBase):
    def test_nonsorted_move_in_sorted(self, sorted_model):
        node = sorted_model.objects.add_root({"val1": 3, "val2": 3, "desc": "zxy"})
        with pytest.raises(InvalidPosition):
            sorted_model.objects.move(node, node, "left")


@pytest.mark.django_db
class TestMoveLeafRoot(TestNonEmptyTree):
    def test_move_leaf_last_sibling_root(self, model):
        target = model.objects.get(desc="2")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="231"), target, "last-sibling")
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
            ("231", 1, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "231", "5", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002003001", "005", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("subtree_moved", model, 2, 7, 8, 5, -6, -2, "default"),
                ("gap_altered", model, 2, 7, -2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "B.C.A", "E", "default"),
            ]

    def test_move_leaf_first_sibling_root(self, model):
        target = model.objects.get(desc="2")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="231"), target, "first-sibling")
        expected = [
            ("231", 1, 0),
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "5", "default"),
                    ("path_updated", model, "3", "4", "default"),
                    ("path_updated", model, "2", "3", "default"),
                    ("path_updated", model, "1", "2", "default"),
                    ("path_updated", model, "331", "1", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "005", "default"),
                    ("path_updated", model, "003", "004", "default"),
                    ("path_updated", model, "002", "003", "default"),
                    ("path_updated", model, "001", "002", "default"),
                    ("path_updated", model, "003003001", "001", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("tree_ids_incremented", model, 1, "default"),
                ("subtree_moved", model, 3, 7, 8, 1, -6, -2, "default"),
                ("gap_altered", model, 3, 7, -2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "B.C.A", "0", "default"),
            ]

    def test_move_leaf_left_sibling_root(self, model):
        target = model.objects.get(desc="2")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="231"), target, "left")
        expected = [
            ("1", 1, 0),
            ("231", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "5", "default"),
                    ("path_updated", model, "3", "4", "default"),
                    ("path_updated", model, "2", "3", "default"),
                    ("path_updated", model, "331", "2", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "005", "default"),
                    ("path_updated", model, "003", "004", "default"),
                    ("path_updated", model, "002", "003", "default"),
                    ("path_updated", model, "003003001", "002", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("tree_ids_incremented", model, 2, "default"),
                ("subtree_moved", model, 3, 7, 8, 2, -6, -2, "default"),
                ("gap_altered", model, 3, 7, -2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "B.C.A", "A0", "default"),
            ]

    def test_move_leaf_right_sibling_root(self, model):
        target = model.objects.get(desc="2")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="231"), target, "right")
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("231", 1, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "5", "default"),
                    ("path_updated", model, "3", "4", "default"),
                    ("path_updated", model, "231", "3", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "005", "default"),
                    ("path_updated", model, "003", "004", "default"),
                    ("path_updated", model, "002003001", "003", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("tree_ids_incremented", model, 3, "default"),
                ("subtree_moved", model, 2, 7, 8, 3, -6, -2, "default"),
                ("gap_altered", model, 2, 7, -2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "B.C.A", "B0", "default"),
            ]

    def test_move_leaf_last_child_root(self, model):
        target = model.objects.get(desc="2")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="231"), target, "last-child")
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("231", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "231", "25", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002003001", "002005", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 12, 2, "default"),
                ("subtree_moved", model, 2, 7, 8, 2, 5, -1, "default"),
                ("gap_altered", model, 2, 7, -2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "B.C.A", "B.E", "default"),
            ]

    def test_move_leaf_first_child_root(self, model):
        target = model.objects.get(desc="2")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="231"), target, "first-child")
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("231", 2, 0),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "24", "25", "default"),
                    ("path_updated", model, "23", "24", "default"),
                    ("path_updated", model, "22", "23", "default"),
                    ("path_updated", model, "21", "22", "default"),
                    ("path_updated", model, "241", "21", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002004", "002005", "default"),
                    ("path_updated", model, "002003", "002004", "default"),
                    ("path_updated", model, "002002", "002003", "default"),
                    ("path_updated", model, "002001", "002002", "default"),
                    ("path_updated", model, "002004001", "002001", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 2, 2, "default"),
                ("subtree_moved", model, 2, 9, 10, 2, -7, -1, "default"),
                ("gap_altered", model, 2, 9, -2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "B.C.A", "B.0", "default"),
            ]


@pytest.mark.django_db
class TestMoveLeaf(TestNonEmptyTree):
    def test_move_leaf_last_sibling(self, model):
        target = model.objects.get(desc="22")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="231"), target, "last-sibling")
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("231", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "231", "25", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002003001", "002005", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 12, 2, "default"),
                ("subtree_moved", model, 2, 7, 8, 2, 5, -1, "default"),
                ("gap_altered", model, 2, 7, -2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "B.C.A", "B.E", "default"),
            ]

    def test_move_leaf_first_sibling(self, model):
        target = model.objects.get(desc="22")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="231"), target, "first-sibling")
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("231", 2, 0),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "24", "25", "default"),
                    ("path_updated", model, "23", "24", "default"),
                    ("path_updated", model, "22", "23", "default"),
                    ("path_updated", model, "21", "22", "default"),
                    ("path_updated", model, "241", "21", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002004", "002005", "default"),
                    ("path_updated", model, "002003", "002004", "default"),
                    ("path_updated", model, "002002", "002003", "default"),
                    ("path_updated", model, "002001", "002002", "default"),
                    ("path_updated", model, "002004001", "002001", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 2, 2, "default"),
                ("subtree_moved", model, 2, 9, 10, 2, -7, -1, "default"),
                ("gap_altered", model, 2, 9, -2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "B.C.A", "B.0", "default"),
            ]

    def test_move_leaf_left_sibling(self, model):
        target = model.objects.get(desc="22")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="231"), target, "left")
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("21", 2, 0),
            ("231", 2, 0),
            ("22", 2, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "24", "25", "default"),
                    ("path_updated", model, "23", "24", "default"),
                    ("path_updated", model, "22", "23", "default"),
                    ("path_updated", model, "241", "22", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002004", "002005", "default"),
                    ("path_updated", model, "002003", "002004", "default"),
                    ("path_updated", model, "002002", "002003", "default"),
                    ("path_updated", model, "002004001", "002002", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 4, 2, "default"),
                ("subtree_moved", model, 2, 9, 10, 2, -5, -1, "default"),
                ("gap_altered", model, 2, 9, -2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "B.C.A", "B.A0", "default"),
            ]

    def test_move_leaf_right_sibling(self, model):
        target = model.objects.get(desc="22")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="231"), target, "right")
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("21", 2, 0),
            ("22", 2, 0),
            ("231", 2, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "24", "25", "default"),
                    ("path_updated", model, "23", "24", "default"),
                    ("path_updated", model, "241", "23", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002004", "002005", "default"),
                    ("path_updated", model, "002003", "002004", "default"),
                    ("path_updated", model, "002004001", "002003", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 6, 2, "default"),
                ("subtree_moved", model, 2, 9, 10, 2, -3, -1, "default"),
                ("gap_altered", model, 2, 9, -2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "B.C.A", "B.B0", "default"),
            ]

    def test_move_leaf_left_sibling_itself(self, model):
        target = model.objects.get(desc="231")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="231"), target, "left")
        assert self.got(model) == UNCHANGED
        if issubclass(model, MP_Node):
            assert signals == []
        elif issubclass(model, NS_Node):
            assert signals == []
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "B.C.A", "B.C.0", "default"),
            ]

    def test_move_leaf_last_child(self, model):
        target = model.objects.get(desc="22")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="231"), target, "last-child")
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 1),
            ("231", 3, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "231", "221", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002003001", "002002001", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 5, 2, "default"),
                ("subtree_moved", model, 2, 9, 10, 2, -4, 0, "default"),
                ("gap_altered", model, 2, 9, -2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "B.C.A", "B.B.A", "default"),
            ]

    def test_move_leaf_first_child(self, model):
        target = model.objects.get(desc="22")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="231"), target, "first-child")
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 1),
            ("231", 3, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "231", "221", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002003001", "002002001", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 5, 2, "default"),
                ("subtree_moved", model, 2, 9, 10, 2, -4, 0, "default"),
                ("gap_altered", model, 2, 9, -2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "B.C.A", "B.B.A", "default"),
            ]


@pytest.mark.django_db
class TestMoveBranchRoot(TestNonEmptyTree):
    def test_move_branch_first_sibling_root(self, model):
        target = model.objects.get(desc="2")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "first-sibling")
        expected = [
            ("4", 1, 1),
            ("41", 2, 0),
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "6", "default"),
                    ("path_updated", model, "3", "4", "default"),
                    ("path_updated", model, "2", "3", "default"),
                    ("path_updated", model, "1", "2", "default"),
                    ("path_updated", model, "6", "1", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "006", "default"),
                    ("path_updated", model, "003", "004", "default"),
                    ("path_updated", model, "002", "003", "default"),
                    ("path_updated", model, "001", "002", "default"),
                    ("path_updated", model, "006", "001", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("tree_ids_incremented", model, 1, "default"),
                ("subtree_moved", model, 5, 1, 4, 1, 0, 0, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "0", "default"),
            ]

    def test_move_branch_last_sibling_root(self, model):
        target = model.objects.get(desc="2")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "last-sibling")
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            assert signals == []
        elif issubclass(model, NS_Node):
            assert signals == [
                ("subtree_moved", model, 4, 1, 4, 5, 0, 0, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "E", "default"),
            ]

    def test_move_branch_left_sibling_root(self, model):
        target = model.objects.get(desc="2")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "left")
        expected = [
            ("1", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
        ]
        assert self.got(model) == expected
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "6", "default"),
                    ("path_updated", model, "3", "4", "default"),
                    ("path_updated", model, "2", "3", "default"),
                    ("path_updated", model, "6", "2", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "006", "default"),
                    ("path_updated", model, "003", "004", "default"),
                    ("path_updated", model, "002", "003", "default"),
                    ("path_updated", model, "006", "002", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("tree_ids_incremented", model, 2, "default"),
                ("subtree_moved", model, 5, 1, 4, 2, 0, 0, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "A0", "default"),
            ]

    def test_move_branch_right_sibling_root(self, model):
        target = model.objects.get(desc="2")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "right")
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("4", 1, 1),
            ("41", 2, 0),
            ("3", 1, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "6", "default"),
                    ("path_updated", model, "3", "4", "default"),
                    ("path_updated", model, "6", "3", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "006", "default"),
                    ("path_updated", model, "003", "004", "default"),
                    ("path_updated", model, "006", "003", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("tree_ids_incremented", model, 3, "default"),
                ("subtree_moved", model, 5, 1, 4, 3, 0, 0, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "B0", "default"),
            ]

    def test_move_branch_left_noleft_sibling_root(self, model):
        target = model.objects.get_first_sibling(model.objects.get(desc="2"))
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "left")
        expected = [
            ("4", 1, 1),
            ("41", 2, 0),
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "6", "default"),
                    ("path_updated", model, "3", "4", "default"),
                    ("path_updated", model, "2", "3", "default"),
                    ("path_updated", model, "1", "2", "default"),
                    ("path_updated", model, "6", "1", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "006", "default"),
                    ("path_updated", model, "003", "004", "default"),
                    ("path_updated", model, "002", "003", "default"),
                    ("path_updated", model, "001", "002", "default"),
                    ("path_updated", model, "006", "001", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("tree_ids_incremented", model, 1, "default"),
                ("subtree_moved", model, 5, 1, 4, 1, 0, 0, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "0", "default"),
            ]

    def test_move_branch_right_noright_sibling_root(self, model):
        target = model.objects.get_last_sibling(model.objects.get(desc="2"))
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "right")
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            assert signals == []
        elif issubclass(model, NS_Node):
            assert signals == []
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "E", "default"),
            ]

    def test_move_branch_first_child_root(self, model):
        target = model.objects.get(desc="2")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "first-child")
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("4", 2, 1),
            ("41", 3, 0),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "24", "25", "default"),
                    ("path_updated", model, "23", "24", "default"),
                    ("path_updated", model, "22", "23", "default"),
                    ("path_updated", model, "21", "22", "default"),
                    ("path_updated", model, "4", "21", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002004", "002005", "default"),
                    ("path_updated", model, "002003", "002004", "default"),
                    ("path_updated", model, "002002", "002003", "default"),
                    ("path_updated", model, "002001", "002002", "default"),
                    ("path_updated", model, "004", "002001", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 2, 4, "default"),
                ("subtree_moved", model, 4, 1, 4, 2, 1, 1, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "B.0", "default"),
            ]

    def test_move_branch_last_child_root(self, model):
        target = model.objects.get(desc="2")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "last-child")
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("4", 2, 1),
            ("41", 3, 0),
            ("3", 1, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "25", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "002005", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 12, 4, "default"),
                ("subtree_moved", model, 4, 1, 4, 2, 11, 1, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "B.E", "default"),
            ]


@pytest.mark.django_db
class TestMoveBranch(TestNonEmptyTree):
    def test_move_branch_first_sibling(self, model):
        target = model.objects.get(desc="23")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "first-sibling")
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("4", 2, 1),
            ("41", 3, 0),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "24", "25", "default"),
                    ("path_updated", model, "23", "24", "default"),
                    ("path_updated", model, "22", "23", "default"),
                    ("path_updated", model, "21", "22", "default"),
                    ("path_updated", model, "4", "21", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002004", "002005", "default"),
                    ("path_updated", model, "002003", "002004", "default"),
                    ("path_updated", model, "002002", "002003", "default"),
                    ("path_updated", model, "002001", "002002", "default"),
                    ("path_updated", model, "004", "002001", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 2, 4, "default"),
                ("subtree_moved", model, 4, 1, 4, 2, 1, 1, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "B.0", "default"),
            ]

    def test_move_branch_last_sibling(self, model):
        target = model.objects.get(desc="23")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "last-sibling")
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("4", 2, 1),
            ("41", 3, 0),
            ("3", 1, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "25", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "002005", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 12, 4, "default"),
                ("subtree_moved", model, 4, 1, 4, 2, 11, 1, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "B.E", "default"),
            ]

    def test_move_branch_left_sibling(self, model):
        target = model.objects.get(desc="23")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "left")
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("21", 2, 0),
            ("22", 2, 0),
            ("4", 2, 1),
            ("41", 3, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "24", "25", "default"),
                    ("path_updated", model, "23", "24", "default"),
                    ("path_updated", model, "4", "23", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002004", "002005", "default"),
                    ("path_updated", model, "002003", "002004", "default"),
                    ("path_updated", model, "004", "002003", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 6, 4, "default"),
                ("subtree_moved", model, 4, 1, 4, 2, 5, 1, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "B.B0", "default"),
            ]

    def test_move_branch_right_sibling(self, model):
        target = model.objects.get(desc="23")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "right")
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("4", 2, 1),
            ("41", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "24", "25", "default"),
                    ("path_updated", model, "4", "24", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002004", "002005", "default"),
                    ("path_updated", model, "004", "002004", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 10, 4, "default"),
                ("subtree_moved", model, 4, 1, 4, 2, 9, 1, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "B.C0", "default"),
            ]

    def test_move_branch_left_noleft_sibling(self, model):
        target = model.objects.get_first_sibling(model.objects.get(desc="23"))
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "left")
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("4", 2, 1),
            ("41", 3, 0),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "24", "25", "default"),
                    ("path_updated", model, "23", "24", "default"),
                    ("path_updated", model, "22", "23", "default"),
                    ("path_updated", model, "21", "22", "default"),
                    ("path_updated", model, "4", "21", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002004", "002005", "default"),
                    ("path_updated", model, "002003", "002004", "default"),
                    ("path_updated", model, "002002", "002003", "default"),
                    ("path_updated", model, "002001", "002002", "default"),
                    ("path_updated", model, "004", "002001", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 2, 4, "default"),
                ("subtree_moved", model, 4, 1, 4, 2, 1, 1, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "B.0", "default"),
            ]

    def test_move_branch_right_noright_sibling(self, model):
        target = model.objects.get_last_sibling(model.objects.get(desc="23"))
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "right")
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 0),
            ("24", 2, 0),
            ("4", 2, 1),
            ("41", 3, 0),
            ("3", 1, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "25", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "002005", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 12, 4, "default"),
                ("subtree_moved", model, 4, 1, 4, 2, 11, 1, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "B.E", "default"),
            ]

    def test_move_branch_left_itself_sibling(self, model):
        target = model.objects.get(desc="4")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "left")
        assert self.got(model) == UNCHANGED
        if issubclass(model, MP_Node):
            assert signals == []
        elif issubclass(model, NS_Node):
            assert signals == []
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "C0", "default"),
            ]

    def test_move_branch_first_child(self, model):
        target = model.objects.get(desc="23")
        node = model.objects.get(desc="4")
        with capture_signals() as signals:
            model.objects.move(node, target, "first-child")
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 2),
            ("4", 3, 1),
            ("41", 4, 0),
            ("231", 3, 0),
            ("24", 2, 0),
            ("3", 1, 0),
        ]

        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "231", "232", "default"),
                    ("path_updated", model, "4", "231", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "002003001", "002003002", "default"),
                    ("path_updated", model, "004", "002003001", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 7, 4, "default"),
                ("subtree_moved", model, 4, 1, 4, 2, 6, 2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "B.C.0", "default"),
            ]

        # Check that for MP, NS and LT nodes, the depth was updated on the in-memory instances
        assert node.get_depth() == 3
        assert model.objects.get_children_count(target) == 2
        assert self.got(model) == expected

    def test_move_branch_last_child(self, model):
        target = model.objects.get(desc="23")
        with capture_signals() as signals:
            model.objects.move(model.objects.get(desc="4"), target, "last-child")
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 2),
            ("231", 3, 0),
            ("4", 3, 1),
            ("41", 4, 0),
            ("24", 2, 0),
            ("3", 1, 0),
        ]
        assert self.got(model) == expected
        if issubclass(model, MP_Node):
            if model.steplen == 1:
                expected_signals = [
                    ("path_updated", model, "4", "232", "default"),
                ]
            elif model.steplen == 3:
                expected_signals = [
                    ("path_updated", model, "004", "002003002", "default"),
                ]
            else:
                assert False, f"Unexpected steplen: {model.steplen}"
            assert signals == expected_signals
        elif issubclass(model, NS_Node):
            assert signals == [
                ("gap_altered", model, 2, 9, 4, "default"),
                ("subtree_moved", model, 4, 1, 4, 2, 8, 2, "default"),
            ]
        elif issubclass(model, LT_Node):
            assert signals == [
                ("subtree_moved", model, "D", "B.C.B", "default"),
            ]


@pytest.mark.django_db
class TestTreeSorted(TestTreeBase):
    def got(self, sorted_model):
        return [
            (o.val1, o.val2, o.desc, o.get_depth(), sorted_model.objects.get_children_count(o))
            for o in sorted_model.objects.get_tree()
        ]

    def test_add_root_sorted(self, sorted_model):
        sorted_model.objects.add_root({"val1": 3, "val2": 3, "desc": "zxy"})
        sorted_model.objects.add_root({"val1": 1, "val2": 4, "desc": "bcd"})
        sorted_model.objects.add_root({"val1": 2, "val2": 5, "desc": "zxy"})
        sorted_model.objects.add_root({"val1": 3, "val2": 3, "desc": "abc"})
        sorted_model.objects.add_root({"val1": 4, "val2": 1, "desc": "fgh"})
        sorted_model.objects.add_root({"val1": 3, "val2": 3, "desc": "abc"})
        sorted_model.objects.add_root({"val1": 2, "val2": 2, "desc": "qwe"})
        sorted_model.objects.add_root({"val1": 3, "val2": 2, "desc": "vcx"})
        expected = [
            (1, 4, "bcd", 1, 0),
            (2, 2, "qwe", 1, 0),
            (2, 5, "zxy", 1, 0),
            (3, 2, "vcx", 1, 0),
            (3, 3, "zxy", 1, 0),
            (3, 3, "abc", 1, 0),
            (3, 3, "abc", 1, 0),
            (4, 1, "fgh", 1, 0),
        ]
        assert self.got(sorted_model) == expected

    def test_add_root_sorted_with_instances(self, sorted_model):
        sorted_model.objects.add_root(instance=sorted_model(val1=3, val2=3, desc="zxy"))
        sorted_model.objects.add_root(instance=sorted_model(val1=1, val2=4, desc="bcd"))
        expected = [
            (1, 4, "bcd", 1, 0),
            (3, 3, "zxy", 1, 0),
        ]
        assert self.got(sorted_model) == expected

    def test_add_child_root_sorted(self, sorted_model):
        root = sorted_model.objects.add_root({"val1": 0, "val2": 0, "desc": "aaa"})
        sorted_model.objects.add_child(root, {"val1": 3, "val2": 3, "desc": "zxy"})
        sorted_model.objects.add_child(root, {"val1": 1, "val2": 4, "desc": "bcd"})
        sorted_model.objects.add_child(root, {"val1": 2, "val2": 5, "desc": "zxy"})
        sorted_model.objects.add_child(root, {"val1": 3, "val2": 3, "desc": "abc"})
        sorted_model.objects.add_child(root, {"val1": 4, "val2": 1, "desc": "fgh"})
        sorted_model.objects.add_child(root, {"val1": 3, "val2": 3, "desc": "abc"})
        sorted_model.objects.add_child(root, {"val1": 2, "val2": 2, "desc": "qwe"})
        sorted_model.objects.add_child(root, {"val1": 3, "val2": 2, "desc": "vcx"})
        expected = [
            (0, 0, "aaa", 1, 8),
            (1, 4, "bcd", 2, 0),
            (2, 2, "qwe", 2, 0),
            (2, 5, "zxy", 2, 0),
            (3, 2, "vcx", 2, 0),
            (3, 3, "zxy", 2, 0),
            (3, 3, "abc", 2, 0),
            (3, 3, "abc", 2, 0),
            (4, 1, "fgh", 2, 0),
        ]
        assert self.got(sorted_model) == expected

    def test_add_child_nonroot_sorted(self, sorted_model):
        def get_node(node_id):
            return sorted_model.objects.get(pk=node_id)

        root_id = sorted_model.objects.add_root({"val1": 0, "val2": 0, "desc": "a"}).pk
        node_id = sorted_model.objects.add_child(get_node(root_id), {"val1": 0, "val2": 0, "desc": "ac"}).pk
        sorted_model.objects.add_child(get_node(root_id), {"val1": 0, "val2": 0, "desc": "aa"})
        sorted_model.objects.add_child(get_node(root_id), {"val1": 0, "val2": 0, "desc": "av"})
        sorted_model.objects.add_child(get_node(node_id), {"val1": 0, "val2": 0, "desc": "aca"})
        sorted_model.objects.add_child(get_node(node_id), {"val1": 0, "val2": 0, "desc": "acc"})
        sorted_model.objects.add_child(get_node(node_id), {"val1": 0, "val2": 0, "desc": "acb"})

        expected = [
            (0, 0, "a", 1, 3),
            (0, 0, "av", 2, 0),
            (0, 0, "ac", 2, 3),
            (0, 0, "acc", 3, 0),
            (0, 0, "acb", 3, 0),
            (0, 0, "aca", 3, 0),
            (0, 0, "aa", 2, 0),
        ]
        assert self.got(sorted_model) == expected

    def test_move_sorted(self, sorted_model):
        sorted_model.objects.add_root({"val1": 3, "val2": 3, "desc": "zxy"})
        sorted_model.objects.add_root({"val1": 1, "val2": 4, "desc": "bcd"})
        sorted_model.objects.add_root({"val1": 2, "val2": 5, "desc": "zxy"})
        sorted_model.objects.add_root({"val1": 3, "val2": 3, "desc": "abc"})
        sorted_model.objects.add_root({"val1": 4, "val2": 1, "desc": "fgh"})
        sorted_model.objects.add_root({"val1": 3, "val2": 3, "desc": "abc"})
        sorted_model.objects.add_root({"val1": 2, "val2": 2, "desc": "qwe"})
        sorted_model.objects.add_root({"val1": 3, "val2": 2, "desc": "vcx"})
        root_nodes = sorted_model.objects.get_root_nodes()
        target = root_nodes[0]
        for node in root_nodes[1:]:
            # because raw queries don't update django objects
            node = sorted_model.objects.get(pk=node.pk)
            target = sorted_model.objects.get(pk=target.pk)
            sorted_model.objects.move(node, target, "sorted-child")
        expected = [
            (1, 4, "bcd", 1, 7),
            (2, 2, "qwe", 2, 0),
            (2, 5, "zxy", 2, 0),
            (3, 2, "vcx", 2, 0),
            (3, 3, "zxy", 2, 0),
            (3, 3, "abc", 2, 0),
            (3, 3, "abc", 2, 0),
            (4, 1, "fgh", 2, 0),
        ]
        assert self.got(sorted_model) == expected

    def test_move_sortedsibling(self, sorted_model):
        # https://bitbucket.org/tabo/django-treebeard/issue/27
        sorted_model.objects.add_root({"val1": 3, "val2": 3, "desc": "zxy"})
        sorted_model.objects.add_root({"val1": 1, "val2": 4, "desc": "bcd"})
        sorted_model.objects.add_root({"val1": 2, "val2": 5, "desc": "zxy"})
        sorted_model.objects.add_root({"val1": 3, "val2": 3, "desc": "abc"})
        sorted_model.objects.add_root({"val1": 4, "val2": 1, "desc": "fgh"})
        sorted_model.objects.add_root({"val1": 3, "val2": 3, "desc": "abc"})
        sorted_model.objects.add_root({"val1": 2, "val2": 2, "desc": "qwe"})
        sorted_model.objects.add_root({"val1": 3, "val2": 2, "desc": "vcx"})
        root_nodes = sorted_model.objects.get_root_nodes()
        target = root_nodes[0]
        for node in root_nodes[1:]:
            # because raw queries don't update django objects
            node = sorted_model.objects.get(pk=node.pk)
            target = sorted_model.objects.get(pk=target.pk)
            node.val1 -= 2
            node.save()
            sorted_model.objects.move(node, target, "sorted-sibling")
        expected = [
            (0, 2, "qwe", 1, 0),
            (0, 5, "zxy", 1, 0),
            (1, 2, "vcx", 1, 0),
            (1, 3, "zxy", 1, 0),
            (1, 3, "abc", 1, 0),
            (1, 3, "abc", 1, 0),
            (1, 4, "bcd", 1, 0),
            (2, 1, "fgh", 1, 0),
        ]
        assert self.got(sorted_model) == expected


@pytest.mark.django_db
class TestInheritedModels(TestTreeBase):
    @staticmethod
    @pytest.fixture(
        scope="function",
        params=models.INHERITED_MODELS,
        ids=lambda fv: f"base={fv[0].__name__} inherited={fv[1].__name__}",
    )
    def inherited_model(request):
        base_model, inherited_model = request.param
        base_model.objects.add_root({"desc": "1"})
        base_model.objects.add_root({"desc": "2"})

        node21 = inherited_model(desc="21")
        base_model.objects.add_child(base_model.objects.get(desc="2"), instance=node21)

        base_model.objects.add_child(base_model.objects.get(desc="21"), {"desc": "211"})
        base_model.objects.add_child(base_model.objects.get(desc="21"), {"desc": "212"})
        base_model.objects.add_child(base_model.objects.get(desc="2"), {"desc": "22"})

        node3 = inherited_model(desc="3")
        base_model.objects.add_root(instance=node3)
        return inherited_model

    @staticmethod
    @pytest.fixture(
        scope="function",
        params=models.INHERITED_MODELS_WITH_SORT,
        ids=lambda fv: f"base={fv[0].__name__} inherited={fv[1].__name__}",
    )
    def inherited_model_with_sort(request):
        return request.param

    def test_get_tree_all(self, inherited_model):
        got = [
            (o.desc, o.get_depth(), inherited_model.objects.get_children_count(o))
            for o in inherited_model.objects.get_tree()
        ]
        expected = [
            ("1", 1, 0),
            ("2", 1, 2),
            ("21", 2, 2),
            ("211", 3, 0),
            ("212", 3, 0),
            ("22", 2, 0),
            ("3", 1, 0),
        ]
        assert got == expected

    def test_get_tree_node(self, inherited_model):
        node = inherited_model.objects.get(desc="21")

        got = [
            (o.desc, o.get_depth(), inherited_model.objects.get_children_count(o))
            for o in inherited_model.objects.get_tree(node)
        ]
        expected = [
            ("21", 2, 2),
            ("211", 3, 0),
            ("212", 3, 0),
        ]
        assert got == expected

    def test_get_root_nodes(self, inherited_model):
        got = inherited_model.objects.get_root_nodes()
        expected = ["1", "2", "3"]
        assert [node.desc for node in got] == expected

    def test_get_first_root_node(self, inherited_model):
        got = inherited_model.objects.get_first_root_node()
        assert got.desc == "1"

    def test_get_last_root_node(self, inherited_model):
        got = inherited_model.objects.get_last_root_node()
        assert got.desc == "3"

    def test_is_root(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert node21.is_root() is False
        assert node3.is_root() is True

    def test_is_leaf(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert node21.is_leaf() is False
        assert node3.is_leaf() is True

    def test_get_root(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert inherited_model.objects.get_root(node21).desc == "2"
        assert inherited_model.objects.get_root(node3).desc == "3"

    def test_get_parent(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert inherited_model.objects.get_parent(node21).desc == "2"
        assert inherited_model.objects.get_parent(node3) is None

    def test_get_children(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert [node.desc for node in inherited_model.objects.get_children(node21)] == ["211", "212"]
        assert [node.desc for node in inherited_model.objects.get_children(node3)] == []

    def test_get_children_count(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert inherited_model.objects.get_children_count(node21) == 2
        assert inherited_model.objects.get_children_count(node3) == 0

    def test_get_siblings(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert [node.desc for node in inherited_model.objects.get_siblings(node21)] == ["21", "22"]
        assert [node.desc for node in inherited_model.objects.get_siblings(node3)] == ["1", "2", "3"]

    def test_get_first_sibling(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert inherited_model.objects.get_first_sibling(node21).desc == "21"
        assert inherited_model.objects.get_first_sibling(node3).desc == "1"

    def test_get_prev_sibling(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert inherited_model.objects.get_prev_sibling(node21) is None
        assert inherited_model.objects.get_prev_sibling(node3).desc == "2"

    def test_get_next_sibling(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert inherited_model.objects.get_next_sibling(node21).desc == "22"
        assert inherited_model.objects.get_next_sibling(node3) is None

    def test_get_last_sibling(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert inherited_model.objects.get_last_sibling(node21).desc == "22"
        assert inherited_model.objects.get_last_sibling(node3).desc == "3"

    def test_get_first_child(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert inherited_model.objects.get_first_child(node21).desc == "211"
        assert inherited_model.objects.get_first_child(node3) is None

    def test_get_last_child(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert inherited_model.objects.get_last_child(node21).desc == "212"
        assert inherited_model.objects.get_last_child(node3) is None

    def test_get_ancestors(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert [node.desc for node in inherited_model.objects.get_ancestors(node21)] == ["2"]
        assert [node.desc for node in inherited_model.objects.get_ancestors(node3)] == []

    def test_get_descendants(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert [node.desc for node in inherited_model.objects.get_descendants(node21)] == ["211", "212"]
        assert [node.desc for node in inherited_model.objects.get_descendants(node3)] == []

    def test_get_descendant_count(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert inherited_model.objects.get_descendant_count(node21) == 2
        assert inherited_model.objects.get_descendant_count(node3) == 0

    def test_cascading_deletion(self, inherited_model):
        # Deleting a node by calling delete() on the inherited_model class
        # should delete descendants, even if those descendants are not
        # instances of inherited_model
        base_model = inherited_model.__bases__[0]

        node21 = inherited_model.objects.get(desc="21")
        node21.delete()
        node2 = base_model.objects.get(desc="2")
        for desc in ["21", "211", "212"]:
            assert not base_model.objects.filter(desc=desc).exists()
        assert [node.desc for node in inherited_model.objects.get_descendants(node2)] == ["22"]

    def test_move_with_children(self, inherited_model):
        base_model = inherited_model.__bases__[0]

        node1 = base_model.objects.get(desc="1")
        node21 = inherited_model.objects.get(desc="21")
        inherited_model.objects.move(node21, node1, "first-child")

        assert [node.desc for node in inherited_model.objects.get_children(node1)] == ["21"]
        assert [node.desc for node in inherited_model.objects.get_children(node21)] == ["211", "212"]
        self._assert_no_tree_problems(base_model)

    def test_get_descendants_group_count(self, inherited_model):
        base_model = inherited_model.__bases__[0]
        for node in base_model.objects.get_descendants_group_count():
            assert node.descendants_count == inherited_model.objects.get_descendant_count(node)

        for node in inherited_model.objects.get_descendants_group_count():
            assert node.descendants_count == inherited_model.objects.get_descendant_count(node)

    def test_add_root_with_node_order_by(self, inherited_model_with_sort):
        """
        Regression test for https://github.com/django-treebeard/django-treebeard/issues/301

        Ensure that adding a second inherited root node with node_order_by does not delegate
        to the parent class.
        """
        _, inherited_model = inherited_model_with_sort
        inherited_model.objects.add_root({"val1": 2, "val2": 3, "desc": "A"})
        inherited_model.objects.add_root({"val1": 2, "val2": 3, "desc": "B"})
        assert list(inherited_model.objects.values_list("desc", flat=True)) == ["B", "A"]
        self._assert_no_tree_problems(inherited_model)

    def test_add_child(self, inherited_model):
        """
        Calling add_child on an inherited model should update tree-related fields across all
        nodes of the tree, not just ones that match the inherited model.
        """
        base_model = inherited_model.__bases__[0]

        # We need a leaf node of inherited_model to test against, as non-leaves will delegate to add_sibling instead
        node21 = inherited_model.objects.get(desc="21")
        node213 = inherited_model(desc="213")
        inherited_model.objects.add_child(node21, instance=node213)
        inherited_model.objects.add_child(node213, {"desc": "2131"})

        assert [node.desc for node in inherited_model.objects.get_children(node213)] == ["2131"]
        self._assert_no_tree_problems(base_model)

    def test_add_sibling_to_root(self, inherited_model):
        base_model = inherited_model.__bases__[0]

        node3 = inherited_model.objects.get(desc="3")
        inherited_model.objects.add_sibling(node3, "first-sibling", {"desc": "0"})

        assert [node.desc for node in base_model.objects.get_root_nodes()] == ["0", "1", "2", "3"]
        self._assert_no_tree_problems(base_model)

    def test_add_sibling_to_nonroot(self, inherited_model):
        base_model = inherited_model.__bases__[0]

        node21 = inherited_model.objects.get(desc="21")
        inherited_model.objects.add_sibling(node21, "first-sibling", {"desc": "20"})

        assert [node.desc for node in base_model.objects.get_children(base_model.objects.get(desc="2"))] == [
            "20",
            "21",
            "22",
        ]
        self._assert_no_tree_problems(base_model)


@pytest.mark.django_db
class TestMP_TreeAlphabet(TestTreeBase):
    @pytest.mark.skipif(
        not os.getenv("TREEBEARD_TEST_ALPHABET", False),
        reason="TREEBEARD_TEST_ALPHABET env variable not set.",
    )
    def test_alphabet(self, mpalphabet_model):
        """This isn't actually a test, it's an informational routine."""
        basealpha = numconv.BASE85
        got_err = False
        last_good = None

        for alphabetlen in range(3, len(basealpha) + 1):
            alphabet = basealpha[0:alphabetlen]
            assert len(alphabet) >= 3
            expected = [alphabet[0] + char for char in alphabet[1:]]
            expected.extend([alphabet[1] + char for char in alphabet])
            expected.append(alphabet[2] + alphabet[0])

            # remove all nodes
            mpalphabet_model.objects.all().delete()

            # change the model's alphabet
            mpalphabet_model.alphabet = alphabet
            mpalphabet_model.numconv_obj_ = None

            # insert root nodes
            for pos in range(len(alphabet) * 2):
                try:
                    added = mpalphabet_model.objects.add_root(numval=pos)
                except Exception:
                    got_err = True
                    break

                # Check for case-insensitive LIKE that would break querying even if the object was inserted correctly
                if mpalphabet_model.objects.filter(path__startswith=added.path).count() != 1:
                    got_err = True
                    break
            if got_err:
                break

            got = list(mpalphabet_model.objects.values_list("path", flat=True))
            if got != expected:
                break

            last_good = alphabet

        assert False, f"Best BASE85 based alphabet for your setup: {last_good} (base {len(last_good)})"


@pytest.mark.django_db
class TestHelpers(TestTreeBase):
    @staticmethod
    @pytest.fixture(scope="function", params=models.BASE_MODELS + models.PROXY_MODELS)
    def helpers_model(request):
        model = request.param
        model.objects.load_bulk(BASE_DATA)
        for node in model.objects.get_root_nodes():
            model.objects.load_bulk(BASE_DATA, node)
        model.objects.add_root({"desc": "5"})
        return model

    def test_descendants_group_count_root(self, helpers_model):
        expected = [
            (o.desc, helpers_model.objects.get_descendant_count(o)) for o in helpers_model.objects.get_root_nodes()
        ]
        got = [(o.desc, o.descendants_count) for o in helpers_model.objects.get_descendants_group_count()]
        assert got == expected

    def test_descendants_group_count_node(self, helpers_model):
        parent = helpers_model.objects.get_root_nodes().get(desc="2")
        expected = [
            (o.desc, helpers_model.objects.get_descendant_count(o)) for o in helpers_model.objects.get_children(parent)
        ]
        got = [(o.desc, o.descendants_count) for o in helpers_model.objects.get_descendants_group_count(parent)]
        assert got == expected


@pytest.mark.django_db
class TestMP_Tree(TestTreeBase):
    """
    Tests specific to MP_Tree behaviour.
    """

    def test_sorted_sibling_move_noop_path_not_changed(self, mpsorted_model):
        # Regression test for https://github.com/django-treebeard/django-treebeard/issues/389
        # A noop on a sorted-sibling should not increment the path of siblings
        node = mpsorted_model.objects.add_root({"desc": "A"})
        old_path = node.path
        mpsorted_model.objects.move(node, node, "sorted-sibling")
        assert node.path == old_path

        node2 = mpsorted_model.objects.add_root({"desc": "B"})
        mpsorted_model.objects.move(node, node2, "sorted-sibling")
        assert node.path == old_path

    def test_sorted_sibling_path(self, mpsorted_model):
        # Regression test for https://github.com/django-treebeard/django-treebeard/issues/389
        node1 = mpsorted_model.objects.add_root({"desc": "B"})
        node2 = mpsorted_model.objects.add_root({"desc": "C"})

        # Update node2 sorted field and then move it
        node2.desc = "A"
        node2.save()
        mpsorted_model.objects.move(node2, node1, "sorted-sibling")
        assert node2.path == "1"
        assert node1.path == "2"

    def test_short_path(self, mpshortnotsorted_model):
        """Test a tree with a very small path field (max_length=4) and a steplen of 1"""
        obj = mpshortnotsorted_model.objects.add_root({})
        for i in range(0, 3):
            obj = mpshortnotsorted_model.objects.add_child(obj, {})
        with pytest.raises(PathOverflow):
            mpshortnotsorted_model.objects.add_child(obj, {})


@pytest.mark.skipif(os.getenv("DATABASE_ENGINE") in ["mysql", "mssql"], reason="Unsupported database backend")
@pytest.mark.django_db
class TestMP_TreeLoadBulk(TestTreeBase):
    def test_load_bulk_existing_with_bulk_create(self, mp_model, django_assert_max_num_queries):
        # inserting on an existing node
        node = mp_model.objects.get(desc="231")
        with django_assert_max_num_queries(26):
            ids = mp_model.objects.load_bulk(BASE_DATA, node, bulk_create=True)
        expected = [
            ("1", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 1),
            ("231", 3, 4),
            ("1", 4, 0),
            ("2", 4, 4),
            ("21", 5, 0),
            ("22", 5, 0),
            ("23", 5, 1),
            ("231", 6, 0),
            ("24", 5, 0),
            ("3", 4, 0),
            ("4", 4, 1),
            ("41", 5, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        expected_descs = ["1", "2", "21", "22", "23", "231", "24", "3", "4", "41"]
        got_descs = [obj.desc for obj in mp_model.objects.filter(pk__in=ids)]
        assert sorted(got_descs) == sorted(expected_descs)
        assert self.got(mp_model) == expected

    def test_load_bulk_keeping_ids_with_bulk_create(self, mp_model):
        exp = mp_model.objects.dump_bulk(keep_ids=True)
        mp_model.objects.all().delete()
        mp_model.objects.load_bulk(exp, parent=None, keep_ids=True, bulk_create=True)
        got = mp_model.objects.dump_bulk(keep_ids=True)
        assert got == exp
        # do we really have an unchanged tree after the dump/delete/load?
        got = [(o.desc, o.get_depth(), mp_model.objects.get_children_count(o)) for o in mp_model.objects.get_tree()]
        assert got == UNCHANGED

    def test_load_bulk_keeping_ids_with_bulk_create_and_many_to_many(self, mp_relatedmodel):
        related = models.RelatedModel.objects.create(desc=f"Test {related_model.__name__}")

        related_data = [
            {"data": {"desc": "1", "related": related.pk, "related_m2m": [related.pk]}},
            {
                "data": {"desc": "2", "related": related.pk, "related_m2m": []},
                "children": [
                    {"data": {"desc": "21", "related": related.pk, "related_m2m": [related.pk]}},
                    {"data": {"desc": "22", "related": related.pk, "related_m2m": [related.pk]}},
                    {
                        "data": {"desc": "23", "related": related.pk, "related_m2m": []},
                        "children": [
                            {"data": {"desc": "231", "related": related.pk, "related_m2m": []}},
                        ],
                    },
                    {"data": {"desc": "24", "related": related.pk, "related_m2m": []}},
                ],
            },
            {"data": {"desc": "3", "related": related.pk, "related_m2m": []}},
            {
                "data": {"desc": "4", "related": related.pk, "related_m2m": []},
                "children": [
                    {"data": {"desc": "41", "related": related.pk, "related_m2m": []}},
                ],
            },
        ]
        mp_relatedmodel.objects.load_bulk(related_data, bulk_create=True)
        got = mp_relatedmodel.objects.dump_bulk(keep_ids=False)
        assert got == related_data


@pytest.mark.django_db
class TestMP_TreeSortedAutoNow(TestTreeBase):
    """
    Auto-populated fields cannot be used with node_order_by, because we need
    to be able to run queries against the value before creating the object.

    Treebeard should log a warning and skip ordering by that field if no value is found
    on the object.
    """

    def test_sorted_by_autonow_ignores_and_warns(self, mpsortedautonow_model):
        mpsortedautonow_model.objects.add_root({"desc": "node1"})
        with pytest.warns(RuntimeWarning, match="Received a null value for field 'created'"):
            mpsortedautonow_model.objects.add_root({"desc": "node2"})

        # Object was still created, but `created` order isn't respected
        assert list(mpsortedautonow_model.objects.values_list("desc", flat=True)) == ["node2", "node1"]

    def test_sorted_by_autonow_value_added_manually(self, mpsortedautonow_model):
        mpsortedautonow_model.objects.add_root({"desc": "node1", "created": now()})
        mpsortedautonow_model.objects.add_root({"desc": "node2", "created": now()})
        assert list(mpsortedautonow_model.objects.values_list("desc", flat=True)) == ["node1", "node2"]


@pytest.mark.django_db
class TestMP_TreeStepOverflow(TestTreeBase):
    def test_add_root(self, mpsmallstep_model):
        method = mpsmallstep_model.objects.add_root
        for i in range(1, 10):
            method({})
        with pytest.raises(PathOverflow):
            method({})

    def test_add_child(self, mpsmallstep_model):
        root = mpsmallstep_model.objects.add_root({})
        method = mpsmallstep_model.objects.add_child
        for i in range(1, 10):
            method(root, {})
        with pytest.raises(PathOverflow):
            method(root, {})

    def test_add_sibling(self, mpsmallstep_model):
        root = mpsmallstep_model.objects.add_root({})
        for i in range(1, 10):
            mpsmallstep_model.objects.add_child(root, {})
        positions = ("first-sibling", "left", "right", "last-sibling")
        for pos in positions:
            with pytest.raises(PathOverflow):
                mpsmallstep_model.objects.add_sibling(mpsmallstep_model.objects.get_last_child(root), pos, {})

    def test_move(self, mpsmallstep_model):
        root = mpsmallstep_model.objects.add_root({})
        for i in range(1, 10):
            mpsmallstep_model.objects.add_child(root, {})
        newroot = mpsmallstep_model.objects.add_root({})
        targets = [
            (root, ["first-child", "last-child"]),
            (
                mpsmallstep_model.objects.get_first_child(root),
                ["first-sibling", "left", "right", "last-sibling"],
            ),
        ]
        for target, positions in targets:
            for pos in positions:
                with pytest.raises(PathOverflow):
                    mpsmallstep_model.objects.move(newroot, target, pos)


@pytest.mark.django_db
class TestMP_TreeFindProblems(TestTreeBase):
    def test_find_problems(self, mpalphabet_model):
        mpalphabet_model.alphabet = "01234"
        mpalphabet_model(path="01", depth=1, numchild=0, numval=0).save()
        mpalphabet_model(path="1", depth=1, numchild=0, numval=0).save()
        mpalphabet_model(path="111", depth=1, numchild=0, numval=0).save()
        mpalphabet_model(path="abcd", depth=1, numchild=0, numval=0).save()
        mpalphabet_model(path="qa#$%!", depth=1, numchild=0, numval=0).save()
        mpalphabet_model(path="0201", depth=2, numchild=0, numval=0).save()
        mpalphabet_model(path="020201", depth=3, numchild=0, numval=0).save()
        mpalphabet_model(path="03", depth=1, numchild=2, numval=0).save()
        mpalphabet_model(path="0301", depth=2, numchild=0, numval=0).save()
        mpalphabet_model(path="030102", depth=3, numchild=10, numval=0).save()
        mpalphabet_model(path="04", depth=10, numchild=1, numval=0).save()
        mpalphabet_model(path="0401", depth=20, numchild=0, numval=0).save()

        def got(ids):
            return [o.path for o in mpalphabet_model.objects.filter(pk__in=ids)]

        (
            evil_chars,
            bad_steplen,
            orphans,
            wrong_depth,
            wrong_numchild,
        ) = mpalphabet_model.objects.find_problems()
        assert ["abcd", "qa#$%!"] == got(evil_chars)
        assert ["1", "111"] == got(bad_steplen)
        assert ["0201", "020201"] == got(orphans)
        assert ["03", "0301", "030102"] == got(wrong_numchild)
        assert ["04", "0401"] == got(wrong_depth)

    def test_find_problems_with_root_clean(self, mpalphabet_model):
        mpalphabet_model.alphabet = "01234"
        mpalphabet_model(path="01", depth=1, numchild=1, numval=0).save()
        mpalphabet_model(path="0101", depth=2, numchild=0, numval=0).save()
        root = mpalphabet_model.objects.get(path="01")
        result = mpalphabet_model.objects.find_problems(parent=root)
        assert all(not group for group in result)

    def test_find_problems_with_root_scoped_to_single_tree(self, mpalphabet_model):
        """Problems in one tree should not appear when checking another tree."""
        mpalphabet_model.alphabet = "01234"
        # Tree 1: clean
        mpalphabet_model(path="01", depth=1, numchild=1, numval=0).save()
        mpalphabet_model(path="0101", depth=2, numchild=0, numval=0).save()
        # Tree 2: root is valid but children have problems (wrong depth, wrong numchild)
        mpalphabet_model(path="02", depth=1, numchild=5, numval=0).save()
        mpalphabet_model(path="0201", depth=20, numchild=0, numval=0).save()

        root1 = mpalphabet_model.objects.get(path="01")
        root2 = mpalphabet_model.objects.get(path="02")

        # Tree 1 should be clean
        result1 = mpalphabet_model.objects.find_problems(parent=root1)
        assert all(not group for group in result1)

        # Tree 2 should have problems
        result2 = mpalphabet_model.objects.find_problems(parent=root2)
        evil_chars, bad_steplen, orphans, wrong_depth, wrong_numchild = result2
        assert not evil_chars
        assert not bad_steplen
        assert not orphans
        # child "0201" has wrong depth (20 instead of 2)
        assert len(wrong_depth) == 1
        # root "02" reports numchild=5 but only has 1 child
        assert len(wrong_numchild) == 1

    def test_find_problems_with_root_detects_all_problem_types(self, mpalphabet_model):
        """Verify all five problem categories are detected within a single tree."""
        mpalphabet_model.alphabet = "01234"
        # Another clean tree to ensure isolation
        mpalphabet_model(path="01", depth=1, numchild=0, numval=0).save()

        # Tree with various problems rooted at "03"
        mpalphabet_model(path="03", depth=1, numchild=2, numval=0).save()
        mpalphabet_model(path="0301", depth=2, numchild=0, numval=0).save()
        mpalphabet_model(path="030102", depth=3, numchild=10, numval=0).save()

        def got(ids):
            return [o.path for o in mpalphabet_model.objects.filter(pk__in=ids)]

        root = mpalphabet_model.objects.get(path="03")
        evil_chars, bad_steplen, orphans, wrong_depth, wrong_numchild = mpalphabet_model.objects.find_problems(
            parent=root
        )
        # "03" reports numchild=2 but only has 1 direct child -> wrong_numchild
        assert "03" in got(wrong_numchild)
        # "030102" reports numchild=10 which is wrong
        assert "030102" in got(wrong_numchild)


@pytest.mark.django_db
class TestNS_TreeFindProblems(TestTreeBase):
    def test_find_problems_all_ok(self, ns_model):
        ns_model(tree_id=1, lft=1, rgt=8, depth=1).save()
        ns_model(tree_id=1, lft=2, rgt=5, depth=2).save()
        ns_model(tree_id=1, lft=3, rgt=4, depth=3).save()
        ns_model(tree_id=1, lft=6, rgt=7, depth=2).save()
        ns_model(tree_id=2, lft=1, rgt=2, depth=1).save()

        result = ns_model.objects.find_problems()
        assert result == ([], [], [], [])

    def test_find_problems_reversed_lft_rgt(self, ns_model):
        bad_node = ns_model(tree_id=1, lft=2, rgt=1, depth=1)
        bad_node.save()

        result = ns_model.objects.find_problems()
        assert result == ([bad_node.pk], [], [], [])

    def test_find_problems_overlapping_nodes(self, ns_model):
        ns_model(tree_id=1, lft=1, rgt=6, depth=1).save()
        ns_model(tree_id=1, lft=2, rgt=4, depth=2).save()
        bad_node = ns_model(tree_id=1, lft=3, rgt=5, depth=3)
        bad_node.save()

        result = ns_model.objects.find_problems()
        assert result == ([], [bad_node.pk], [], [])

    def test_find_problems_duplicate_root(self, ns_model):
        ns_model(tree_id=1, lft=1, rgt=8, depth=1).save()
        ns_model(tree_id=1, lft=2, rgt=5, depth=2).save()
        ns_model(tree_id=1, lft=3, rgt=4, depth=3).save()
        ns_model(tree_id=1, lft=6, rgt=7, depth=2).save()
        bad_node = ns_model(tree_id=1, lft=9, rgt=12, depth=1)
        bad_node.save()
        ns_model(tree_id=1, lft=10, rgt=11, depth=2).save()

        result = ns_model.objects.find_problems()
        assert result == ([], [], [bad_node.pk], [])

    def test_find_problems_wrong_depth(self, ns_model):
        ns_model(tree_id=1, lft=1, rgt=8, depth=1).save()
        bad_node_1 = ns_model(tree_id=1, lft=2, rgt=5, depth=1)
        bad_node_1.save()
        ns_model(tree_id=1, lft=3, rgt=4, depth=3).save()
        ns_model(tree_id=1, lft=6, rgt=7, depth=2).save()
        bad_node_2 = ns_model(tree_id=2, lft=1, rgt=2, depth=2)
        bad_node_2.save()

        result = ns_model.objects.find_problems()
        assert result == ([], [], [], [bad_node_1.pk, bad_node_2.pk])

    def test_find_problems_within_parent(self, ns_model):
        ns_model(tree_id=1, lft=1, rgt=8, depth=99).save()  # ignored
        parent_node = ns_model(tree_id=1, lft=2, rgt=5, depth=2)
        parent_node.save()
        bad_node = ns_model(tree_id=1, lft=3, rgt=4, depth=99)
        bad_node.save()
        ns_model(tree_id=1, lft=6, rgt=7, depth=2).save()
        ns_model(tree_id=2, lft=1, rgt=2, depth=99).save()  # ignored

        result = ns_model.objects.find_problems(parent=parent_node)
        assert result == ([], [], [], [bad_node.pk])


@pytest.mark.django_db
class TestMP_TreeFix(TestTreeBase):
    expected_no_holes = {
        models.MP_TestNodeShortPath: [
            ("1", "b", 1, 2),
            ("11", "u", 2, 1),
            ("111", "i", 3, 1),
            ("1111", "e", 4, 0),
            ("12", "o", 2, 0),
            ("2", "d", 1, 0),
            ("3", "g", 1, 0),
            ("4", "a", 1, 4),
            ("41", "a", 2, 0),
            ("42", "a", 2, 0),
            ("43", "u", 2, 1),
            ("431", "i", 3, 1),
            ("4311", "e", 4, 0),
            ("44", "o", 2, 0),
        ],
        models.MP_TestSortedNodeShortPath: [
            ("1", "a", 1, 4),
            ("11", "a", 2, 0),
            ("12", "a", 2, 0),
            ("13", "o", 2, 0),
            ("14", "u", 2, 1),
            ("141", "i", 3, 1),
            ("1411", "e", 4, 0),
            ("2", "b", 1, 2),
            ("21", "o", 2, 0),
            ("22", "u", 2, 1),
            ("221", "i", 3, 1),
            ("2211", "e", 4, 0),
            ("3", "d", 1, 0),
            ("4", "g", 1, 0),
        ],
    }
    expected_with_holes = {
        models.MP_TestNodeShortPath: [
            ("1", "b", 1, 2),
            ("13", "u", 2, 1),
            ("134", "i", 3, 1),
            ("1343", "e", 4, 0),
            ("14", "o", 2, 0),
            ("2", "d", 1, 0),
            ("3", "g", 1, 0),
            ("4", "a", 1, 4),
            ("41", "a", 2, 0),
            ("42", "a", 2, 0),
            ("43", "u", 2, 1),
            ("434", "i", 3, 1),
            ("4343", "e", 4, 0),
            ("44", "o", 2, 0),
        ],
        models.MP_TestSortedNodeShortPath: [
            ("1", "b", 1, 2),
            ("13", "u", 2, 1),
            ("134", "i", 3, 1),
            ("1343", "e", 4, 0),
            ("14", "o", 2, 0),
            ("2", "d", 1, 0),
            ("3", "g", 1, 0),
            ("4", "a", 1, 4),
            ("41", "a", 2, 0),
            ("42", "a", 2, 0),
            ("43", "u", 2, 1),
            ("434", "i", 3, 1),
            ("4343", "e", 4, 0),
            ("44", "o", 2, 0),
        ],
    }

    def got(self, model):
        return [(o.path, o.desc, o.get_depth(), model.objects.get_children_count(o)) for o in model.objects.get_tree()]

    def add_broken_test_data(self, model):
        model(path="4", depth=2, numchild=2, desc="a").save()
        model(path="13", depth=1000, numchild=0, desc="u").save()
        model(path="14", depth=4, numchild=500, desc="o").save()
        model(path="134", depth=321, numchild=543, desc="i").save()
        model(path="1343", depth=321, numchild=543, desc="e").save()
        model(path="42", depth=1, numchild=1, desc="a").save()
        model(path="43", depth=1000, numchild=0, desc="u").save()
        model(path="44", depth=4, numchild=500, desc="o").save()
        model(path="434", depth=321, numchild=543, desc="i").save()
        model(path="4343", depth=321, numchild=543, desc="e").save()
        model(path="41", depth=1, numchild=1, desc="a").save()
        model(path="3", depth=221, numchild=322, desc="g").save()
        model(path="1", depth=10, numchild=3, desc="b").save()
        model(path="2", depth=10, numchild=3, desc="d").save()

    def test_fix_tree_no_fix_paths(self, mpshort_model):
        self.add_broken_test_data(mpshort_model)
        mpshort_model.objects.fix_tree(fix_paths=False)
        got = self.got(mpshort_model)
        expected = self.expected_with_holes[mpshort_model]
        assert got == expected
        assert all(not group for group in mpshort_model.objects.find_problems())

    def test_fix_tree_fix_paths(self, mpshort_model):
        self.add_broken_test_data(mpshort_model)
        with capture_signals() as signals:
            mpshort_model.objects.fix_tree(fix_paths=True)
        got = self.got(mpshort_model)
        expected = self.expected_no_holes[mpshort_model]
        assert got == expected
        assert all(not group for group in mpshort_model.objects.find_problems())
        if issubclass(mpshort_model, MP_Node):
            # Confirm that path_updated signals were fired for any path moves -
            # enumerating the exact sequence of updates would be too brittle
            assert any(signal[0] == "path_updated" for signal in signals)

    def test_fix_tree_with_parent(self, mpshort_model):
        """
        Fix the tree only for parent with path 4 and everything inside it.
        All other bad data should be left intact.
        """

        self.add_broken_test_data(mpshort_model)
        mpshort_model.objects.fix_tree(parent=mpshort_model.objects.get(path="4"))
        got = self.got(mpshort_model)
        expected = self.expected_with_holes[mpshort_model]

        assert got[7:] == expected[7:]  # Only compare items from path 4 onwards
        problems = mpshort_model.objects.find_problems()
        assert not any([problems[0], problems[1], problems[2], problems[4]])
        assert set(problems[3]) == set(mpshort_model.objects.exclude(path__startswith="4").values_list("pk", flat=True))

    def test_fix_tree_with_parent_fix_paths(self, mpshort_model):
        """
        Fix the tree only for parent with path 4 and everything inside it.
        All other bad data should be left intact.
        """

        self.add_broken_test_data(mpshort_model)
        parent = mpshort_model.objects.get(path="4")
        # Fix the depth on the parent - we need a valid parent to operate on only part of a tree
        parent.depth = 1
        parent.save()
        with capture_signals() as signals:
            mpshort_model.objects.fix_tree(parent=parent, fix_paths=True)
        got = self.got(mpshort_model)
        expected_partial = {
            models.MP_TestNodeShortPath: [
                ("4", "a", 1, 4),
                ("41", "a", 2, 0),
                ("42", "a", 2, 0),
                ("43", "u", 2, 1),
                ("431", "i", 3, 1),
                ("4311", "e", 4, 0),
                ("44", "o", 2, 0),
            ],
            models.MP_TestSortedNodeShortPath: [
                ("4", "a", 1, 4),
                ("41", "a", 2, 0),
                ("42", "a", 2, 0),
                ("43", "o", 2, 0),
                ("44", "u", 2, 1),
                ("441", "i", 3, 1),
                ("4411", "e", 4, 0),
            ],
        }

        assert got[7:] == expected_partial[mpshort_model]  # Only compare items from path 4 onwards
        problems = mpshort_model.objects.find_problems()
        assert not any([problems[0], problems[1], problems[2], problems[4]])
        assert set(problems[3]) == set(mpshort_model.objects.exclude(path__startswith="4").values_list("pk", flat=True))
        if issubclass(mpshort_model, MP_Node):
            # Confirm that path_updated signals were fired for any path moves -
            # enumerating the exact sequence of updates would be too brittle
            assert any(signal[0] == "path_updated" for signal in signals)


@pytest.mark.django_db
class TestMoveNodeForm:
    def _get_nodes_list(self, nodes):
        return [(str(pk), f"{'&nbsp;' * 4 * (depth - 1)}{_str}") for pk, _str, depth in nodes]

    def _assert_nodes_in_choices(self, form, nodes):
        choices = list(form.fields["treebeard_ref_node"].choices)
        assert choices.pop(0)[0] == ""  # Empty choice
        assert nodes == [(str(choice[0]), choice[1]) for choice in choices]

    def _move_node_helper(self, node, safe_parent_nodes):
        form_class = movenodeform_factory(type(node))
        form = form_class(instance=node)
        assert ["desc", "treebeard_position", "treebeard_ref_node"] == list(form.base_fields.keys())
        got = [choice[0] for choice in form.fields["treebeard_position"].choices]
        assert ["first-child", "left", "right"] == got
        nodes = self._get_nodes_list(safe_parent_nodes)
        self._assert_nodes_in_choices(form, nodes)

    def _get_node_ids_strs_and_depths(self, nodes):
        return [(node.pk, str(node), node.get_depth()) for node in nodes]

    def test_form_root_node(self, model):
        nodes = list(model.objects.get_tree())
        node = nodes.pop(0)
        safe_parent_nodes = self._get_node_ids_strs_and_depths(nodes)
        self._move_node_helper(node, safe_parent_nodes)

    def test_form_admin(self, model):
        request = None
        nodes = list(model.objects.get_tree())
        safe_parent_nodes = self._get_node_ids_strs_and_depths(nodes)
        for node in model.objects.all():
            site = AdminSite()
            form_class = movenodeform_factory(model)
            admin_class = admin_factory(form_class)
            ma = admin_class(model, site)
            got = list(ma.get_form(request).base_fields.keys())
            desc_pos_refnodeid = ["desc", "treebeard_position", "treebeard_ref_node"]
            assert desc_pos_refnodeid == got
            got = ma.get_fieldsets(request)
            expected = [(None, {"fields": desc_pos_refnodeid})]
            assert got == expected
            got = ma.get_fieldsets(request, node)
            assert got == expected
            form = ma.get_form(request)()
            nodes = self._get_nodes_list(safe_parent_nodes)
            self._assert_nodes_in_choices(form, nodes)

    def test_skips_move_if_no_change(self, model_without_data):
        parent = model_without_data.objects.add_root({"desc": "A"})
        child = model_without_data.objects.add_child(parent, {"desc": "B"})
        form_class = movenodeform_factory(model_without_data)
        form = form_class(
            instance=child, data={"desc": "B", "treebeard_position": "first-child", "treebeard_ref_node": parent.pk}
        )
        assert form.is_valid()
        with mock.patch.object(model_without_data.objects, "move") as mock_move:
            form.save()
            mock_move.assert_not_called()

        # Now change the reference node - should trigger a move
        form = form_class(
            instance=child, data={"desc": "B", "treebeard_position": "first-child", "treebeard_ref_node": None}
        )
        assert form.is_valid()
        with mock.patch.object(model_without_data.objects, "move") as mock_move:
            form.save()
            mock_move.assert_called_once()

    def test_skips_move_if_no_change_node_order_by(self, sorted_model):
        parent = sorted_model.objects.add_root({"desc": "A", "val1": 1, "val2": 1})
        child = sorted_model.objects.add_child(parent, {"desc": "B", "val1": 3, "val2": 4})
        form_class = movenodeform_factory(sorted_model)
        form = form_class(
            instance=child,
            data={
                "desc": "B",
                "val1": 3,
                "val2": 4,
                "treebeard_position": "sorted-child",
                "treebeard_ref_node": parent.pk,
            },
        )
        assert form.is_valid()
        with mock.patch.object(sorted_model.objects, "move") as mock_move:
            form.save()
            mock_move.assert_not_called()

        # Now change the reference node - should trigger a move
        form = form_class(
            instance=child,
            data={
                "desc": "B",
                "val1": 10,
                "val2": 4,
                "treebeard_position": "sorted-child",
                "treebeard_ref_node": parent.pk,
            },
        )
        assert form.is_valid()
        with mock.patch.object(sorted_model.objects, "move") as mock_move:
            form.save()
            mock_move.assert_called_once()


@pytest.mark.django_db
class TestSortedForm(TestTreeSorted):
    def test_sorted_form(self, sorted_model):
        sorted_model.objects.add_root({"val1": 3, "val2": 3, "desc": "zxy"})
        sorted_model.objects.add_root({"val1": 1, "val2": 4, "desc": "bcd"})
        sorted_model.objects.add_root({"val1": 2, "val2": 5, "desc": "zxy"})
        sorted_model.objects.add_root({"val1": 3, "val2": 3, "desc": "abc"})
        sorted_model.objects.add_root({"val1": 4, "val2": 1, "desc": "fgh"})
        sorted_model.objects.add_root({"val1": 3, "val2": 3, "desc": "abc"})
        sorted_model.objects.add_root({"val1": 2, "val2": 2, "desc": "qwe"})
        sorted_model.objects.add_root({"val1": 3, "val2": 2, "desc": "vcx"})

        form_class = movenodeform_factory(sorted_model)
        form = form_class()
        assert list(form.fields.keys()) == [
            "desc",
            "val1",
            "val2",
            "treebeard_position",
            "treebeard_ref_node",
        ]

        form = form_class(instance=sorted_model.objects.get(desc="bcd"))
        assert list(form.fields.keys()) == [
            "desc",
            "val1",
            "val2",
            "treebeard_position",
            "treebeard_ref_node",
        ]
        assert "id_treebeard_position" in str(form)
        assert "id_treebeard_ref_node" in str(form)


@pytest.mark.django_db
class TestForm(TestNonEmptyTree):
    def test_form(self, model):
        form_class = movenodeform_factory(model)
        form = form_class()
        assert list(form.fields.keys()) == ["desc", "treebeard_position", "treebeard_ref_node"]

        form = form_class(instance=model.objects.get(desc="1"))
        assert list(form.fields.keys()) == ["desc", "treebeard_position", "treebeard_ref_node"]
        assert "id_treebeard_position" in str(form)
        assert "id_treebeard_ref_node" in str(form)

    def test_move_node_form(self, model):
        form_class = movenodeform_factory(model)

        bad_node = model.objects.add_child(
            model.objects.get(desc="1"), {"desc": 'Benign<script>alert("Compromised");</script>'}
        )

        form = form_class(instance=bad_node)
        rendered_html = form.as_p()
        assert "Benign" in rendered_html
        assert "<script>" not in rendered_html
        assert "&lt;script&gt;" in rendered_html

    def test_get_initial(self, model):
        form_class = movenodeform_factory(model)

        instance_parent = model.objects.get(desc="1")
        form = form_class(instance=instance_parent)
        assert form._get_initial(instance_parent) == {
            "treebeard_position": "first-child",
            "treebeard_ref_node": None,
        }

        instance_child = model.objects.get(desc="21")
        form = form_class(instance=instance_child)
        assert form._get_initial(instance_child) == {
            "treebeard_position": "first-child",
            "treebeard_ref_node": model.objects.get(desc="2"),
        }

        instance_grandchild = model.objects.get(desc="22")
        form = form_class(instance=instance_grandchild)
        assert form._get_initial(instance_grandchild) == {
            "treebeard_position": "right",
            "treebeard_ref_node": model.objects.get(desc="21"),
        }

        instance_grandchild = model.objects.get(desc="231")
        form = form_class(instance=instance_grandchild)
        assert form._get_initial(instance_grandchild) == {
            "treebeard_position": "first-child",
            "treebeard_ref_node": model.objects.get(desc="23"),
        }

    def test_save_edit(self, model):
        instance_parent = model.objects.get(desc="1")
        original_count = model.objects.count()
        form_class = movenodeform_factory(model)
        form = form_class(
            instance=instance_parent,
            data={
                "treebeard_position": "first-child",
                "treebeard_ref_node": model.objects.get(desc="2").pk,
                "desc": instance_parent.desc,
            },
        )
        assert form.is_valid()
        saved_instance = form.save()
        assert original_count == model.objects.all().count()
        assert model.objects.get_children_count(saved_instance) == 0
        assert saved_instance.get_depth() == 2
        assert not saved_instance.is_root()
        assert saved_instance.is_leaf()

        # Return to original state
        form_class = movenodeform_factory(model)
        form = form_class(
            instance=saved_instance,
            data={
                "treebeard_position": "first-child",
                "treebeard_ref_node": "",
                "desc": saved_instance.desc,
            },
        )
        assert form.is_valid()
        restored_instance = form.save()
        assert original_count == model.objects.all().count()
        assert model.objects.get_children_count(restored_instance) == 0
        assert restored_instance.get_depth() == 1
        assert restored_instance.is_root()
        assert restored_instance.is_leaf()

    def test_save_new(self, model):
        original_count = model.objects.all().count()
        assert original_count == 10
        treebeard_position = "first-child"
        form_class = movenodeform_factory(model)
        form = form_class(data={"treebeard_position": treebeard_position, "desc": "New Form Test"})
        assert form.is_valid()
        assert form.save() is not None
        assert original_count < model.objects.all().count()

    def test_save_new_with_pk_set(self, model):
        """
        If the model is using a natural primary key then it will be
        already set when the instance is inserted.
        """
        original_count = model.objects.all().count()
        assert original_count == 10
        treebeard_position = "first-child"
        form_class = movenodeform_factory(model)
        form = form_class(data={"treebeard_position": treebeard_position, "id": 999999, "desc": "New Form Test"})
        assert form.is_valid()
        # Fake a natural key by updating the instance directly, because
        # the model form will have removed the id from cleaned data because
        # it thinks it is an AutoField.
        form.instance.id = 999999
        assert form.save() is not None
        assert original_count < model.objects.all().count()

    def test_save_instance(self, model):
        form_class = movenodeform_factory(model)
        form = form_class(data={"treebeard_position": "first-child", "desc": "Test Instance"})
        assert form.is_valid()
        form.instance.desc = "Modified Instance"
        instance = form.save()
        assert instance.desc == "Modified Instance"


class AdminTestMixin:
    site = AdminSite()

    @staticmethod
    @pytest.fixture(
        scope="function",
        params=models.INHERITED_MODELS,
        ids=lambda fv: f"base={fv[0].__name__} inherited={fv[1].__name__}",
    )
    def inherited_model(request):
        base_model, inherited_model = request.param
        base_model.objects.add_root({"desc": "1"})
        root_inherited = inherited_model.objects.add_root({"desc": "2"})
        # Add two children, one inherited, one not
        inherited_model.objects.add_child(root_inherited, instance=base_model(desc="3"))
        inherited_model.objects.add_child(root_inherited, {"desc": "4"})
        return inherited_model

    def _create_user(self, username, **kwargs):
        return User.objects.create(username=username, **kwargs)

    def _mocked_request(self, data, user=None):
        request = RequestFactory().post("/", data=data)
        request.session = {}
        request.user = user or AnonymousUser()
        request._messages = FallbackStorage(request)
        return request

    def _get_admin_obj(self, model_class):
        form_class = movenodeform_factory(model_class)
        admin_class = admin_factory(form_class)
        return admin_class(model_class, self.site)


@pytest.mark.django_db
class TestAdminTreeContext(TestNonEmptyTree, AdminTestMixin):
    def test_tree_context_num_queries(self, model, django_assert_num_queries):
        user = self._create_user("changelist_tmp", is_superuser=True)
        request = RequestFactory().get("/")
        request.user = user
        request._treebeard_parent_id = None
        admin_obj = self._get_admin_obj(model)

        cl = admin_obj.get_changelist_instance(request)
        with django_assert_num_queries(1):
            tree_ctx = tree_context(cl, request)

        assert len(tree_ctx) == 4


@pytest.mark.django_db
class TestTreeAdmin(TestNonEmptyTree, AdminTestMixin):
    def test_default_fields(self, model):
        site = AdminSite()
        form_class = movenodeform_factory(model)
        admin_class = admin_factory(form_class)
        ma = admin_class(model, site)
        assert list(ma.get_form(None).base_fields.keys()) == [
            "desc",
            "treebeard_position",
            "treebeard_ref_node",
        ]

    def test_changeform_view_rolls_back_on_form_error(self, model_without_proxy):
        admin_obj = self._get_admin_obj(model_without_proxy)
        target = model_without_proxy.objects.get(desc="231")
        user = self._create_user("tmp", is_superuser=True)

        request = self._mocked_request(
            data={"desc": "new node", "treebeard_position": "first-child", "treebeard_ref_node": target.pk},
            user=user,
        )
        # Access the inner function, skipping CSRF checks
        response = admin_obj.changeform_view.__wrapped__(admin_obj, request)
        assert response.status_code == 302
        target.refresh_from_db()
        children = model_without_proxy.objects.get_children(target)
        assert len(children) == 1  # Normal case: form submitted successfully and child added
        assert children[0].desc == "new node"

        request = self._mocked_request(
            data={"desc": "second new node", "treebeard_position": "first-child", "treebeard_ref_node": target.pk},
            user=user,
        )

        def fake_error(*args):
            admin_obj.form.errors = {"__all__": [ValidationError("fake error")]}
            return False

        with mock.patch("django.contrib.admin.options.all_valid", side_effect=fake_error):
            response = admin_obj.changeform_view.__wrapped__(admin_obj, request)
        assert response.status_code == 200
        target.refresh_from_db()
        assert len(model_without_proxy.objects.get_children(target)) == 1  # Second new child should not have been added

    def test_changelist_view_renders_tree_context(self, model):
        user = self._create_user("changelist_tmp", is_superuser=True)
        request = RequestFactory().get("/")
        request.user = user
        admin_obj = self._get_admin_obj(model)
        response = admin_obj.changelist_view(request)
        response.render()
        content = response.content.decode()
        assert '<script id="tree-context" type="application/json">' in content

    def test_get_queryset_root_nodes(self, model):
        admin_obj = self._get_admin_obj(model)
        request = RequestFactory().get("/")
        request.user = self._create_user("test_view_perm", is_superuser=True)
        request._treebeard_parent_id = None
        qs = admin_obj.get_queryset(request)
        assert list(qs.values_list("pk", flat=True)) == list(
            model.objects.get_root_nodes().values_list("pk", flat=True)
        )

    def test_get_queryset_child_nodes(self, model):
        admin_obj = self._get_admin_obj(model)
        parent = model.objects.get(desc="2")
        request = RequestFactory().get("/")
        request.user = self._create_user("test_view_perm", is_superuser=True)
        request._treebeard_parent_id = parent.pk
        qs = admin_obj.get_queryset(request)
        assert list(qs.values_list("pk", flat=True)) == list(
            model.objects.get_children(parent).values_list("pk", flat=True)
        )

    def test_get_queryset_inherited_model(self, inherited_model):
        admin_obj = self._get_admin_obj(inherited_model)
        parent = inherited_model.objects.get(desc="2")
        request = RequestFactory().get("/")
        request.user = self._create_user("test_view_perm", is_superuser=True)
        request._treebeard_parent_id = parent.pk
        qs = admin_obj.get_queryset(request)
        # qs should only return the specific child
        assert list(qs.values_list("desc", flat=True)) == ["4"]

    def test_children_view_requires_view_permission(self, model):
        admin_obj = self._get_admin_obj(model)
        request = RequestFactory().get("/")
        request.user = self._create_user("test_view_perm")
        with pytest.raises(PermissionDenied):
            admin_obj.children_view(request, 1)

    def test_children_view(self, model):
        admin_obj = self._get_admin_obj(model)
        parent = model.objects.get(desc="2")
        request = RequestFactory().get("/")
        request.user = self._create_user("test_get_children", is_superuser=True)
        response = admin_obj.children_view(request, parent_id=parent.pk)
        data = json.loads(response.content)
        tree_ctx = data["tree_context"]

        assert len(tree_ctx) == 4
        expected = [
            {
                "node-id": str(obj.pk),
                "parent-id": str(parent.pk),
                "level": obj.get_depth(),
                "has-children": 0 if obj.is_leaf() else 1,
                "can-change": 1,
            }
            for obj in model.objects.get_children(parent)
        ]
        assert tree_ctx == expected

        assert data["page"] == 1
        assert '<table id="result_list">' in data["result_html"]
        assert request._treebeard_parent == parent

    def test_move_node_validate(self, model):
        admin_obj = self._get_admin_obj(model)
        user = self._create_user("tmp", is_superuser=True)
        request = self._mocked_request(data={}, user=user)
        response = admin_obj.move_node(request)
        assert response.status_code == 400
        assert response.content.decode() == "Invalid form data provided"
        request = self._mocked_request(data={"node": 1}, user=user)
        response = admin_obj.move_node(request)
        assert response.status_code == 400
        assert response.content.decode() == "Invalid form data provided"

    def test_move_validate_to_descendant(self, model):
        node = model.objects.get(desc="2")
        target = model.objects.get(desc="231")
        admin_obj = self._get_admin_obj(model)
        request = self._mocked_request(
            data={"node": node.pk, "target": target.pk, "relation": "sibling"},
            user=self._create_user("tmp", is_superuser=True),
        )
        response = admin_obj.move_node(request)
        assert response.status_code == 400
        assert request._messages._queued_messages[0].message == "Can't move node to a descendant."

    def test_move_requires_change_permission(self, model):
        node = model.objects.get(desc="231")
        target = model.objects.get(desc="2")

        admin_obj = self._get_admin_obj(model)
        request = self._mocked_request(
            data={"node": node.pk, "target": target.pk, "relation": "sibling"},
            user=self._create_user("test_move_perm"),
        )

        with (
            patch.object(admin_obj, "has_change_permission", return_value=False),
            patch.object(admin_obj, "has_view_permission", return_value=True),
        ):
            with pytest.raises(PermissionDenied):
                admin_obj.move_node(request)

        with (
            patch.object(admin_obj, "has_change_permission", return_value=True),
            patch.object(admin_obj, "has_view_permission", return_value=True),
        ):
            response = admin_obj.move_node(request)
            assert response.status_code == 200

    def test_move_left(self, model):
        node = model.objects.get(desc="231")
        target = model.objects.get(desc="2")

        admin_obj = self._get_admin_obj(model)
        request = self._mocked_request(
            data={"node": node.pk, "target": target.pk, "relation": "sibling"},
            user=self._create_user("tmp", is_superuser=True),
        )
        response = admin_obj.move_node(request)
        assert response.status_code == 200
        expected = [
            ("1", 1, 0),
            ("231", 1, 0),
            ("2", 1, 4),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected

    def test_move_last_child(self, model):
        node = model.objects.get(desc="231")
        target = model.objects.get(desc="2")

        admin_obj = self._get_admin_obj(model)
        request = self._mocked_request(
            data={"node": node.pk, "target": target.pk, "relation": "child"},
            user=self._create_user("tmp", is_superuser=True),
        )
        response = admin_obj.move_node(request)
        assert response.status_code == 200
        expected = [
            ("1", 1, 0),
            ("2", 1, 5),
            ("21", 2, 0),
            ("22", 2, 0),
            ("23", 2, 0),
            ("24", 2, 0),
            ("231", 2, 0),
            ("3", 1, 0),
            ("4", 1, 1),
            ("41", 2, 0),
        ]
        assert self.got(model) == expected


@pytest.mark.django_db
class TestMPFormPerformance:
    def test_form_choices_no_of_queries(self, django_assert_num_queries):
        model = models.MP_TestNode
        model.objects.load_bulk(BASE_DATA)
        form_class = movenodeform_factory(model)
        form = form_class()
        with django_assert_num_queries(2):
            list(form.fields["treebeard_ref_node"].choices)


@pytest.mark.django_db
class TestMP_TreeDescendantsPerformance(TestTreeBase):
    def test_get_descendants_no_of_queries(self, django_assert_num_queries):
        model = models.MP_TestNode
        model.objects.load_bulk(BASE_DATA)

        data = [
            ("2", 1),
            ("23", 1),
            ("231", 0),
            ("1", 0),
            ("4", 1),
        ]

        for desc, expected in data:
            node = model.objects.get(desc=desc)
            with django_assert_num_queries(expected):
                # converting to list to force queryset evaluation
                list(model.objects.get_descendants(node))


@pytest.mark.django_db
class TestLT_Insertion(TestTreeBase):
    def test_move_right(self, lt_model):
        with capture_signals() as signals:
            node_a = lt_model.objects.add_root({"desc": "A"})
            node_b = lt_model.objects.add_root({"desc": "B"})
            node_a_a = lt_model.objects.add_child(node_a, {"desc": "A.A"})
            lt_model.objects.add_child(node_a_a, {"desc": "A.A.A"})
            lt_model.objects.add_child(node_b, {"desc": "B.A"})

            expected_paths = ["A", "A.A", "A.A.A", "B", "B.A"]
            actual_paths = [str(node.path) for node in lt_model.objects.all()]
            assert actual_paths == expected_paths

            # A sibling inserted before A.A gets path A.0; other paths are unchanged
            node_a_0 = lt_model.objects.add_sibling(node_a_a, "first-sibling", {"desc": "A.0"})
            expected_paths = ["A", "A.0", "A.A", "A.A.A", "B", "B.A"]
            actual_paths = [str(node.path) for node in lt_model.objects.all()]
            assert actual_paths == expected_paths

        assert signals == []

        # A sibling inserted before A.0 causes existing siblings to be moved right (i.e. 'A' appended to their labels)
        with capture_signals() as signals:
            new_node_a_0 = lt_model.objects.add_sibling(node_a_0, "first-sibling", {"desc": "new A.0"})

        assert str(new_node_a_0.path) == "A.0"
        node_a_0.refresh_from_db()
        assert str(node_a_0.path) == "A.0A"
        node_a_a.refresh_from_db()
        assert str(node_a_a.path) == "A.AA"

        expected_paths = ["A", "A.0", "A.0A", "A.AA", "A.AA.A", "B", "B.A"]
        actual_paths = [str(node.path) for node in lt_model.objects.all()]
        assert actual_paths == expected_paths

        assert signals == [
            ("subtree_moved_right", lt_model, "A.0", "default"),
        ]


@pytest.mark.django_db
class TestChecks:
    def test_checks_warning_if_model_manager_doesnt_subclass_treebeard_manager(self, model, monkeypatch):
        configs = [apps.get_app_config("tests")]
        assert not check_all_models(configs)
        # Monkey-patch default manager
        monkeypatch.setattr(model._meta, "default_manager", Manager())
        errors = check_all_models(configs)
        assert len(errors) == 1
        assert isinstance(errors[0], checks.Error)
        assert "does not subclass treebeard." in errors[0].msg
