"""
Duplicate of test_treebeard.py that uses the old, deprecated API for methods that moved
from the Node class to the model manager.

To be removed when backward compatibility is removed in Treebeard 8.
"""

import os
import threading
from contextlib import contextmanager

import pytest
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.timezone import now

from tests import models
from treebeard.al_tree import AL_Node
from treebeard.exceptions import (
    InvalidMoveToDescendant,
    InvalidPosition,
    MissingNodeOrderBy,
    NodeAlreadySaved,
    PathOverflow,
)
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_AddChildHandler,
    MP_AddRootHandler,
    MP_AddSiblingHandler,
    MP_MoveHandler,
    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

# Suppress deprecation warnings
pytestmark = pytest.mark.filterwarnings("ignore::DeprecationWarning")


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.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.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.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(), o.get_children_count()) for o in model.get_tree()]

    def _assert_get_annotated_list(self, model, expected, parent=None):
        results = model.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.find_problems())


@pytest.mark.django_db
class TestEmptyTree(TestTreeBase):
    def test_load_bulk_empty(self, model_without_data):
        ids = model_without_data.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.dump_bulk() == []

    def test_add_root_empty(self, model_without_data):
        model_without_data.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.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.get_first_root_node()
        assert got is None

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

    def test_get_tree(self, model_without_data):
        got = list(model_without_data.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.add_root(desc="1")
        model_without_data.add_root(desc="2")
        model_without_data.add_root(desc="3")
        model_without_data.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.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.get_tree()
        got = [(o.desc, o.get_depth(), o.get_children_count()) for o in nodes]
        assert got == UNCHANGED
        assert all(isinstance(o, model) for o in nodes)

    def test_dump_bulk_all(self, model):
        assert model.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.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.get_tree(node)

        got = [(o.desc, o.get_depth(), o.get_children_count()) 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 == node.get_children_count()

        with django_assert_max_num_queries(1):
            nodes = model.get_tree(node)
        got = [(o.desc, o.get_depth(), o.get_children_count()) 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.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.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.dump_bulk(keep_ids=True)
        model.objects.all().delete()
        model.load_bulk(exp, None, True)
        got = model.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(), o.get_children_count()) for o in model.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.load_bulk(related_data)
        got = related_model.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.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.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.get_last_root_node()
        assert got.desc == "4"
        assert isinstance(got, model)

    def test_add_root(self, model):
        obj = model.add_root(desc="5")
        assert obj.get_depth() == 1
        got = model.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.add_root(instance=obj)
        assert result == obj
        got = model.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.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 = descendant.get_root()
            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 = node.get_parent()
            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 = node.get_parent(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 = node.get_children()
            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 = node.get_children_count()
            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 = node.get_siblings()
            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 = node.get_first_sibling()
            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 = node.get_prev_sibling()
            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 = node.get_next_sibling()
            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 = node.get_last_sibling()
            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 = parent.get_first_child()
            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 = parent.get_last_child()
            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 = descendant.get_ancestors()
            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 = parent.get_descendants()
            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 = parent.get_descendants(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 = parent.get_descendant_count()
            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.get(desc="231").add_child(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.get(desc="2").add_child(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.get(desc="231").add_child(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.add_root(desc="1")
        child1 = model_without_data(desc="11")
        child2 = model_without_data(desc="12")
        assert parent.get_children_count() == 0
        parent.add_child(instance=child1)
        parent.add_child(instance=child2)
        assert list(parent.get_children().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.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):
                    p.add_child(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 parent.get_children_count() == 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.get(desc="2").add_child(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.get(desc="2").add_child(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 = instance.get_parent()
                assert parent.get_descendant_count() == 1

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

            parent.add_child(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.get(desc="231").add_sibling("invalid_pos")

    def test_add_sibling_missing_nodeorderby(self, model):
        node_wchildren = model.objects.get(desc="2")
        with pytest.raises(MissingNodeOrderBy):
            node_wchildren.add_sibling("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 = node_wchildren.add_sibling("last-sibling", desc="5")
        assert obj.get_depth() == 1
        assert node_wchildren.get_last_sibling().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 = node.add_sibling("last-sibling", desc="232")
        assert obj.get_depth() == 3
        assert node.get_last_sibling().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 = node_wchildren.add_sibling("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 = node_wchildren.add_sibling("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 = node_wchildren.add_sibling("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 = node_wchildren.add_sibling("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 = node.add_sibling("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 = node.add_sibling("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 = node_wchildren.add_sibling("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 = node_wchildren.add_sibling("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 = node.add_sibling("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 = node.add_sibling("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 = node_wchildren.add_sibling("last-sibling", instance=obj)
        assert result == obj
        assert obj.get_depth() == 1
        assert node_wchildren.get_last_sibling().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):
            node_wchildren.add_sibling("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.get(desc="2").add_child(instance=child)
        assert result == child


@pytest.mark.django_db
class TestMoveErrors(TestNonEmptyTree):
    def test_move_invalid_pos(self, model):
        node = model.objects.get(desc="231")
        with pytest.raises(InvalidPosition):
            node.move(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):
            node.move(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"):
            node.move(node, pos)

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

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


@pytest.mark.django_db
class TestMoveSortedErrors(TestTreeBase):
    def test_nonsorted_move_in_sorted(self, sorted_model):
        node = sorted_model.add_root(val1=3, val2=3, desc="zxy")
        with pytest.raises(InvalidPosition):
            node.move(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.get(desc="231").move(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.get(desc="231").move(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.get(desc="231").move(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.get(desc="231").move(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.get(desc="231").move(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.get(desc="231").move(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.get(desc="231").move(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.get(desc="231").move(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.get(desc="231").move(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.get(desc="231").move(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.get(desc="231").move(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.get(desc="231").move(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.get(desc="231").move(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.get(desc="4").move(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.get(desc="4").move(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.get(desc="4").move(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.get(desc="4").move(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(desc="2").get_first_sibling()
        with capture_signals() as signals:
            model.objects.get(desc="4").move(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(desc="2").get_last_sibling()
        with capture_signals() as signals:
            model.objects.get(desc="4").move(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.get(desc="4").move(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.get(desc="4").move(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.get(desc="4").move(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.get(desc="4").move(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.get(desc="4").move(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.get(desc="4").move(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(desc="23").get_first_sibling()
        with capture_signals() as signals:
            model.objects.get(desc="4").move(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(desc="23").get_last_sibling()
        with capture_signals() as signals:
            model.objects.get(desc="4").move(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.get(desc="4").move(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:
            node.move(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 target.get_children_count() == 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.get(desc="4").move(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(), o.get_children_count()) for o in sorted_model.get_tree()]

    def test_add_root_sorted(self, sorted_model):
        sorted_model.add_root(val1=3, val2=3, desc="zxy")
        sorted_model.add_root(val1=1, val2=4, desc="bcd")
        sorted_model.add_root(val1=2, val2=5, desc="zxy")
        sorted_model.add_root(val1=3, val2=3, desc="abc")
        sorted_model.add_root(val1=4, val2=1, desc="fgh")
        sorted_model.add_root(val1=3, val2=3, desc="abc")
        sorted_model.add_root(val1=2, val2=2, desc="qwe")
        sorted_model.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.add_root(instance=sorted_model(val1=3, val2=3, desc="zxy"))
        sorted_model.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.add_root(val1=0, val2=0, desc="aaa")
        root.add_child(val1=3, val2=3, desc="zxy")
        root.add_child(val1=1, val2=4, desc="bcd")
        root.add_child(val1=2, val2=5, desc="zxy")
        root.add_child(val1=3, val2=3, desc="abc")
        root.add_child(val1=4, val2=1, desc="fgh")
        root.add_child(val1=3, val2=3, desc="abc")
        root.add_child(val1=2, val2=2, desc="qwe")
        root.add_child(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.add_root(val1=0, val2=0, desc="a").pk
        node_id = get_node(root_id).add_child(val1=0, val2=0, desc="ac").pk
        get_node(root_id).add_child(val1=0, val2=0, desc="aa")
        get_node(root_id).add_child(val1=0, val2=0, desc="av")
        get_node(node_id).add_child(val1=0, val2=0, desc="aca")
        get_node(node_id).add_child(val1=0, val2=0, desc="acc")
        get_node(node_id).add_child(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.add_root(val1=3, val2=3, desc="zxy")
        sorted_model.add_root(val1=1, val2=4, desc="bcd")
        sorted_model.add_root(val1=2, val2=5, desc="zxy")
        sorted_model.add_root(val1=3, val2=3, desc="abc")
        sorted_model.add_root(val1=4, val2=1, desc="fgh")
        sorted_model.add_root(val1=3, val2=3, desc="abc")
        sorted_model.add_root(val1=2, val2=2, desc="qwe")
        sorted_model.add_root(val1=3, val2=2, desc="vcx")
        root_nodes = sorted_model.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.move(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.add_root(val1=3, val2=3, desc="zxy")
        sorted_model.add_root(val1=1, val2=4, desc="bcd")
        sorted_model.add_root(val1=2, val2=5, desc="zxy")
        sorted_model.add_root(val1=3, val2=3, desc="abc")
        sorted_model.add_root(val1=4, val2=1, desc="fgh")
        sorted_model.add_root(val1=3, val2=3, desc="abc")
        sorted_model.add_root(val1=2, val2=2, desc="qwe")
        sorted_model.add_root(val1=3, val2=2, desc="vcx")
        root_nodes = sorted_model.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()
            node.move(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.add_root(desc="1")
        base_model.add_root(desc="2")

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

        base_model.objects.get(desc="21").add_child(desc="211")
        base_model.objects.get(desc="21").add_child(desc="212")
        base_model.objects.get(desc="2").add_child(desc="22")

        node3 = inherited_model(desc="3")
        base_model.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(), o.get_children_count()) for o in inherited_model.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(), o.get_children_count()) for o in inherited_model.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.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.get_first_root_node()
        assert got.desc == "1"

    def test_get_last_root_node(self, inherited_model):
        got = inherited_model.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 node21.get_root().desc == "2"
        assert node3.get_root().desc == "3"

    def test_get_parent(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert node21.get_parent().desc == "2"
        assert node3.get_parent() 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 node21.get_children()] == ["211", "212"]
        assert [node.desc for node in node3.get_children()] == []

    def test_get_children_count(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert node21.get_children_count() == 2
        assert node3.get_children_count() == 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 node21.get_siblings()] == ["21", "22"]
        assert [node.desc for node in node3.get_siblings()] == ["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 node21.get_first_sibling().desc == "21"
        assert node3.get_first_sibling().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 node21.get_prev_sibling() is None
        assert node3.get_prev_sibling().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 node21.get_next_sibling().desc == "22"
        assert node3.get_next_sibling() 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 node21.get_last_sibling().desc == "22"
        assert node3.get_last_sibling().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 node21.get_first_child().desc == "211"
        assert node3.get_first_child() 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 node21.get_last_child().desc == "212"
        assert node3.get_last_child() 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 node21.get_ancestors()] == ["2"]
        assert [node.desc for node in node3.get_ancestors()] == []

    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 node21.get_descendants()] == ["211", "212"]
        assert [node.desc for node in node3.get_descendants()] == []

    def test_get_descendant_count(self, inherited_model):
        node21 = inherited_model.objects.get(desc="21")
        node3 = inherited_model.objects.get(desc="3")
        assert node21.get_descendant_count() == 2
        assert node3.get_descendant_count() == 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 node2.get_descendants()] == ["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")
        node21.move(node1, "first-child")

        assert [node.desc for node in node1.get_children()] == ["21"]
        assert [node.desc for node in node21.get_children()] == ["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.get_descendants_group_count():
            assert node.descendants_count == node.get_descendant_count()

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

    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.add_root(val1=2, val2=3, desc="A")
        inherited_model.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")
        node21.add_child(instance=node213)

        node213.add_child(desc="2131")

        assert [node.desc for node in node213.get_children()] == ["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")
        node3.add_sibling("first-sibling", desc="0")

        assert [node.desc for node in base_model.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")
        node21.add_sibling("first-sibling", desc="20")

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


@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.load_bulk(BASE_DATA)
        for node in model.get_root_nodes():
            model.load_bulk(BASE_DATA, node)
        model.add_root(desc="5")
        return model

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

    def test_descendants_group_count_node(self, helpers_model):
        parent = helpers_model.get_root_nodes().get(desc="2")
        expected = [(o.desc, o.get_descendant_count()) for o in parent.get_children()]
        got = [(o.desc, o.descendants_count) for o in helpers_model.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.add_root(desc="A")
        old_path = node.path
        node.move(node, "sorted-sibling")
        assert node.path == old_path

        node2 = mpsorted_model.add_root(desc="B")
        node.move(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.add_root(desc="B")
        node2 = mpsorted_model.add_root(desc="C")

        # Update node2 sorted field and then move it
        node2.desc = "A"
        node2.save()
        node2.move(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.add_root()
        obj = obj.add_child().add_child().add_child()
        with pytest.raises(PathOverflow):
            obj.add_child()


@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.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.dump_bulk(keep_ids=True)
        mp_model.objects.all().delete()
        mp_model.load_bulk(exp, parent=None, keep_ids=True, bulk_create=True)
        got = mp_model.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(), o.get_children_count()) for o in mp_model.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.load_bulk(related_data, bulk_create=True)
        got = mp_relatedmodel.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.add_root(desc="node1")
        with pytest.warns(RuntimeWarning, match="Received a null value for field 'created'"):
            mpsortedautonow_model.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.add_root(desc="node1", created=now())
        mpsortedautonow_model.add_root(desc="node2", created=now())
        assert list(mpsortedautonow_model.objects.values_list("desc", flat=True)) == ["node1", "node2"]


@pytest.mark.django_db
class TestLT_Insertion(TestTreeBase):
    def test_move_right(self, lt_model):
        with capture_signals() as signals:
            node_a = lt_model.add_root(desc="A")
            node_b = lt_model.add_root(desc="B")
            node_a_a = node_a.add_child(desc="A.A")
            node_a_a.add_child(desc="A.A.A")
            node_b.add_child(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 = node_a_a.add_sibling("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 = node_a_0.add_sibling("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 TestMPHandlersDeprecated(TestTreeBase):
    """
    Check that direct use of deprecated MP_ handlers works.
    """

    def test_move(self, mp_model):
        node = mp_model.objects.get(desc="4")
        target = mp_model.objects.get(desc="23")
        MP_MoveHandler(node, target, "left").process()

        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(mp_model) == expected

    def test_add_child(self, mp_model):
        node = mp_model.objects.get(desc="231")
        MP_AddChildHandler(node, {"desc": "2311"}).process()
        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(mp_model) == expected

    def test_add_sibling(self, mp_model):
        node_wchildren = mp_model.objects.get(desc="2")
        obj = MP_AddSiblingHandler(node_wchildren, "last-sibling", {"desc": "5"}).process()
        assert obj.get_depth() == 1
        assert node_wchildren.get_last_sibling().desc == "5"

    def test_add_root(self, mp_model):
        obj = MP_AddRootHandler(mp_model, desc="5").process()
        assert obj.get_depth() == 1
        got = mp_model.get_last_root_node()
        assert got.desc == "5"
        assert isinstance(got, mp_model)
