# Problem Description
This is a binary tree transformation problem where we need to modify certain node values in the tree according to specific rules. The function takes a binary tree's root node as input and should return the transformed tree's root node. The transformation needs to be done in-place by manipulating the existing nodes rather than creating a new tree.

# Visual Facts
1. The image shows three different examples of binary tree transformations (before and after).
2. The nodes that change are highlighted in orange in the result trees.
3. Values in the nodes can be:
   - Positive integers (e.g., 3, 4)
   - Negative integers (e.g., -2, -4)
   - Special value "null"
4. The tree structure (topology) remains unchanged after transformation.

# Visual Patterns
1. Value Transformation Pattern:
   - identify the nodes that, one of the child's value is null, the other one is not.
   - change the value of each such node to its negative value
   - keep all the other nodes' values unchanged.
   - no other changes are made to the tree.

2. Special Case Handling:
   - "null" values remain "null" even after transformation

# Solution
class Node:
    def __init__(self, val = 0, left = None, right = None):
        self.left = left
        self.right = right
        self.val = val

def solution(root: Node) -> Node:
    """
    Transforms a binary tree according to the illustrated rule by manipulating its nodes.

    Parameters:
        root (Node): The root node of the input binary tree.

    Returns:
        Node: The root node of the transformed binary tree.
    """
    if root is None:
        return None
    left_val = solution(root.left)
    right_val = solution(root.right)
    # return true only if one of the left or right child is not None
    if left_val or right_val:
        if left_val is None or right_val is None:
            root.val = -root.val

    return root 


# Test Script
class Node:
    def __init__(self, val = 0, left = None, right = None):
        self.left = left
        self.right = right
        self.val = val

def tree_to_list(root):
    def maxDepth(root):
        if not root:
            return 0
        return 1 + max(maxDepth(root.left), maxDepth(root.right))
    li_res = [None for _ in range( 2 ** maxDepth(root) - 1)]
    if not root:
        return []
    def dfs(node, idx):
        if not node:
            return
        li_res[idx] = node.val
        dfs(node.left, 2*idx+1)
        dfs(node.right, 2*idx+2)
    dfs(root, 0)
    return li_res
    

def list_to_tree(lst):
    """
    Converts a list into a binary tree structure and returns the root node.
    Args:
        lst (list): List of elements to form the binary tree.
                   Use `None` for empty nodes.
    Returns:
        Node: The root of the binary tree.
    """
    if not lst:  # If the list is empty, return None
        return None

    # Create a list of nodes, where None in the list corresponds to None in the tree
    nodes = [Node(val = value) if value is not None else None for value in lst]
    
    # Link the nodes to form the tree
    for i in range(len(lst)):
        if nodes[i] is not None:  # Skip None nodes
            left_index = 2 * i + 1  # Left child index
            right_index = 2 * i + 2  # Right child index
            if left_index < len(lst):  # Check if left child exists
                nodes[i].left = nodes[left_index]
            if right_index < len(lst):  # Check if right child exists
                nodes[i].right = nodes[right_index]

    # Return the root node
    return nodes[0]

def is_same_tree(tree1, tree2):
    """
    Checks if two binary trees are identical in structure and values.
    
    Args:
        tree1 (Node): Root of the first binary tree.
        tree2 (Node): Root of the second binary tree.
    
    Returns:
        bool: True if the trees are identical, False otherwise.
    """
    # Both trees are empty
    if not tree1 and not tree2:
        return True
    
    # One of the trees is empty, the other is not
    if not tree1 or not tree2:
        return False
    
    # Compare current node values and recursively check left and right subtrees
    return (tree1.val == tree2.val and
            is_same_tree(tree1.left, tree2.left) and
            is_same_tree(tree1.right, tree2.right))

def test():
    test_cases = [
        ([3,None, 4], [-3,None,4]),
        ([3, -5, 4, None, -4, -7, -2],  [3, 5, 4, None, -4, -7, -2]),
        ([3, -2, 4, 1, -4, None, -2, None, None , 0, None, None, None, 9, None], [3, -2, -4, 1, 4, None, 2, None, None , 0, None, None, None, 9, None]),
        ([1, None, 2, None, None, 4, 5], [-1, None, 2, None, None, 4, 5]),
        ([1, 2, 3, None, None, 4, None, None, None, None, None, 5],[1, 2, -3, None, None, -4, None, None, None, None, None, 5]),
        ([1, 2, 5, None, None, 3, 4, None, None, None, None, None, 5],[1, 2, 5, None, None, -3, 4, None, None, None, None, None, 5]),
    ]

    for input_list, expected_list in test_cases:
        input_tree = list_to_tree(input_list)
        expected_tree = list_to_tree(expected_list)
        
        try:
            output_tree = solution(input_tree)
        except Exception as e:
            error_message = (
                f"An exception occurred while running the test case:\n"
                f"`root`: {input_list} (List representation of input tree)\n"
                f"Tree structure of input:\n{tree_to_list(input_tree)}\n\n"
                f"Exception: '{str(e)}'"
            )
            assert False, error_message

        if not is_same_tree(output_tree, expected_tree):
            error_message = (
                f"The following Test Case failed:\n"
                f"`root`: {input_list} (List representation of input tree)\n"
                f"Tree structure of input:\n{tree_to_list(input_tree)}\n\n"
                f"Actual Output (tree structure): {tree_to_list(output_tree)}\n"
                f"Expected Output (tree structure): {tree_to_list(expected_tree)}"
            )
            assert False, error_message

test()