# Implement left rotates and right rotates for trees.
#
# Example right rotate on 10:
#
#       10
#      /  \
#     5   15
#    / \
#   3   7
#  /
# 2
#
# Turns into:
#
#        5
#      /  \
#     3   10
#    /   /  \
#   2   7   15
#
# Notice how the right child of 10's left child (7) becomes the left child of 10.
#
# Left rotates are the reverse. See this example on 16:
#     16
#       \
#        33
#       /  \
#     22    55
#
# Turns into:
#
#      33
#     /  \
#    16   55
#     \
#      22
#
# where the left child of 16's right child (22) turns into 16's right child.
#
# Copy this entire file, implement rotate_left and rotate_right,
# and run the tests using `python3 <name-of-file>.py`.


class Node:
    def __init__(self, value, left=None, right=None, parent=None):
        self.value = value
        self.left: Node | None = left
        self.right: Node | None = right

        # required to re-set the child later
        self.parent: Node | None = parent


def rotate_left(node: Node) -> Node:
    if node.right is None:
        return node

    original_parent = node.parent
    new_parent = node.right
    new_right_child = node.right.left

    node.right = new_right_child
    if new_right_child is not None:
        new_right_child.parent = node

    new_parent.left = node
    node.parent = new_parent
    new_parent.parent = original_parent

    if original_parent is not None:
        if original_parent.left == node:
            original_parent.left = new_parent
        elif original_parent.right == node:
            original_parent.right = new_parent

    return new_parent


def rotate_right(node: Node) -> Node:
    if node.left is None:
        return node

    original_parent = node.parent
    new_parent = node.left
    new_left_child = node.left.right

    node.left = new_left_child
    if new_left_child is not None:
        new_left_child.parent = node

    new_parent.right = node
    node.parent = new_parent
    new_parent.parent = original_parent

    if original_parent is not None:
        if original_parent.left == node:
            original_parent.left = new_parent
        elif original_parent.right == node:
            original_parent.right = new_parent

    return new_parent


# --- Tests ---------------------------------------------------------------
# Both rotate functions are expected to return the new root of the rotated
# subtree (the parent would then point its child link at that new root).


def to_tuple(node: Node | None):
    """Turn a tree into nested (value, left, right) tuples for easy comparison."""
    if node is None:
        return None
    return (
        node.value,
        to_tuple(node.left),
        to_tuple(node.right),
        node.parent.value if node.parent is not None else None,
    )


def trees_equal(a: Node | None, b: Node | None) -> bool:
    return to_tuple(a) == to_tuple(b)


if __name__ == "__main__":
    # note that matching is done by value for simplicity here

    def test_rotate_right_example():
        # The worksheet example: right rotate on 10.
        root = Node(
            10,
            left=Node(
                5,
                left=Node(3, left=Node(2, parent=Node(3)), parent=Node(5)),
                right=Node(7, parent=Node(5)),
                parent=Node(10),
            ),
            right=Node(15, parent=Node(10)),
        )
        expected = Node(
            5,
            left=Node(3, left=Node(2, parent=Node(3)), parent=Node(5)),
            right=Node(
                10,
                left=Node(7, parent=Node(10)),
                right=Node(15, parent=Node(10)),
                parent=Node(5),
            ),
        )
        return rotate_right(root), expected


    def test_rotate_left_example():
        # The worksheet example: left rotate on 16.
        root = Node(
            16,
            right=Node(
                33,
                left=Node(22, parent=Node(33)),
                right=Node(55, parent=Node(33)),
                parent=Node(16),
            ),
        )
        expected = Node(
            33,
            left=Node(16, right=Node(22, parent=Node(16)), parent=Node(33)),
            right=Node(55, parent=Node(33)),
        )
        return rotate_left(root), expected


    def test_rotate_right_two_nodes():
        # Smallest case: a root with only a left child.
        return rotate_right(Node(2, left=Node(1, parent=Node(2)))), Node(1, right=Node(2, parent=Node(1)))


    def test_rotate_left_two_nodes():
        # Smallest case: a root with only a right child.
        return rotate_left(Node(1, right=Node(2, parent=Node(1)))), Node(2, left=Node(1, parent=Node(2)))


    def test_round_trip():
        # A left rotate undone by a right rotate restores the original tree.
        original = Node(1, left=Node(0, parent=Node(1)),
                        right=Node(3, left=Node(2, parent=Node(3)), right=Node(4, parent=Node(3)), parent=Node(1)))
        expected = Node(1, left=Node(0, parent=Node(1)),
                        right=Node(3, left=Node(2, parent=Node(3)), right=Node(4, parent=Node(3)), parent=Node(1)))
        return rotate_right(rotate_left(original)), expected


    def test_rotate_with_parent():
        # run it on 33 instead of the root
        root = Node(16, right=Node(33, left=Node(22), right=Node(55)))
        root.right.parent = root
        root.right.left.parent = root.right
        root.right.right.parent = root.right
        original = root.right
        expected = Node(
            55,
            left=Node(33, left=Node(22, parent=Node(33)), parent=Node(55)),
            parent=Node(16),
        )
        return rotate_left(original), expected  # pyright: ignore[reportArgumentType]


    tests = [
        ("rotate_right on worksheet example (10)", test_rotate_right_example),
        ("rotate_left on worksheet example (16)", test_rotate_left_example),
        ("rotate_right on two nodes", test_rotate_right_two_nodes),
        ("rotate_left on two nodes", test_rotate_left_two_nodes),
        ("rotate_left then rotate_right restores the tree", test_round_trip),
        ("rotate_left works when there is a parent", test_rotate_with_parent),
    ]

    passed = 0
    for name, test in tests:
        try:
            result, expected = test()
            if trees_equal(result, expected):
                print(f"PASS: {name}")
                passed += 1
            else:
                print(f"FAIL: {name}")
                print(f"      expected {to_tuple(expected)}")
                print(f"      got      {to_tuple(result)}")
        except Exception as e:  # noqa: BLE001 - surface any bug as a failed test
            print(f"ERROR: {name}: {type(e).__name__}: {e}")

    print(f"\n{passed}/{len(tests)} tests passed")
