"""Unit tests for BSP (Binary Space Partitioning) procedural generation.

Tests for issues #202-#206:
- #202: BSP core class with splitting
- #203: BSPNode lightweight node reference
- #204: BSP iteration (leaves, traverse) with Traversal enum
- #205: BSP query methods (find)
- #206: BSP.to_heightmap (returns HeightMap; BSPMap subclass deferred)
"""
import sys
import mcrfpy

def test_bsp_construction():
    """Test BSP construction with pos/size."""
    print("Testing BSP construction...")

    # Basic construction with pos/size kwargs
    bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 80))
    assert bsp is not None, "BSP should be created"

    # Check pos and size properties
    assert bsp.pos == (0, 0), f"pos should be (0, 0), got {bsp.pos}"
    assert bsp.size == (100, 80), f"size should be (100, 80), got {bsp.size}"

    # Check bounds property (combines pos and size)
    bounds = bsp.bounds
    assert bounds == ((0, 0), (100, 80)), f"Bounds should be ((0, 0), (100, 80)), got {bounds}"

    # Check root property
    root = bsp.root
    assert root is not None, "Root should not be None"
    assert root.bounds == ((0, 0), (100, 80)), f"Root bounds mismatch"

    # Construction with offset
    bsp2 = mcrfpy.BSP(pos=(10, 20), size=(50, 40))
    assert bsp2.bounds == ((10, 20), (50, 40)), "Offset bounds not preserved"

    print("  BSP construction: PASS")

def test_bsp_split_once():
    """Test single split operation (#202)."""
    print("Testing BSP split_once...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 80))

    # Before split, root should be a leaf
    assert bsp.root.is_leaf, "Root should be leaf before split"

    # Horizontal split at y=40
    result = bsp.split_once(horizontal=True, position=40)
    assert result is bsp, "split_once should return self for chaining"

    # After split, root should not be a leaf
    root = bsp.root
    assert not root.is_leaf, "Root should not be leaf after split"
    assert root.split_horizontal == True, "Split should be horizontal"
    assert root.split_position == 40, "Split position should be 40"

    # Check children exist
    left = root.left
    right = root.right
    assert left is not None, "Left child should exist"
    assert right is not None, "Right child should exist"
    assert left.is_leaf, "Left child should be leaf"
    assert right.is_leaf, "Right child should be leaf"

    print("  BSP split_once: PASS")

def test_bsp_split_recursive():
    """Test recursive splitting (#202)."""
    print("Testing BSP split_recursive...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))

    # Recursive split with seed for reproducibility
    result = bsp.split_recursive(depth=3, min_size=(8, 8), max_ratio=1.5, seed=42)
    assert result is bsp, "split_recursive should return self"

    # Count leaves
    leaves = list(bsp.leaves())
    assert len(leaves) > 1, f"Should have multiple leaves, got {len(leaves)}"
    assert len(leaves) <= 8, f"Should have at most 2^3=8 leaves, got {len(leaves)}"

    # All leaves should be within bounds
    for leaf in leaves:
        x, y = leaf.bounds[0]
        w, h = leaf.bounds[1]
        assert x >= 0 and y >= 0, f"Leaf position out of bounds: {leaf.bounds}"
        assert x + w <= 80 and y + h <= 60, f"Leaf extends beyond bounds: {leaf.bounds}"
        assert w >= 8 and h >= 8, f"Leaf smaller than min_size: {leaf.bounds}"

    print("  BSP split_recursive: PASS")

def test_bsp_clear():
    """Test clear operation (#202)."""
    print("Testing BSP clear...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 80))
    bsp.split_recursive(depth=4, min_size=(8, 8), seed=42)

    # Should have multiple leaves
    leaves_before = len(list(bsp.leaves()))
    assert leaves_before > 1, "Should have multiple leaves after split"

    # Clear
    result = bsp.clear()
    assert result is bsp, "clear should return self"

    # Should be back to single leaf
    leaves_after = list(bsp.leaves())
    assert len(leaves_after) == 1, f"Should have 1 leaf after clear, got {len(leaves_after)}"

    # Root should be a leaf with original bounds
    assert bsp.root.is_leaf, "Root should be leaf after clear"
    assert bsp.bounds == ((0, 0), (100, 80)), "Bounds should be restored"

    print("  BSP clear: PASS")

def test_bspnode_properties():
    """Test BSPNode properties (#203)."""
    print("Testing BSPNode properties...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 80))
    bsp.split_recursive(depth=3, min_size=(8, 8), seed=42)

    root = bsp.root

    # Root properties
    assert root.level == 0, f"Root level should be 0, got {root.level}"
    assert root.parent is None, "Root parent should be None"
    assert not root.is_leaf, "Split root should not be leaf"

    # Split properties (not leaf)
    assert root.split_horizontal is not None, "Split horizontal should be bool for non-leaf"
    assert root.split_position is not None, "Split position should be int for non-leaf"

    # Navigate to a leaf
    current = root
    while not current.is_leaf:
        current = current.left

    # Leaf properties
    assert current.is_leaf, "Should be a leaf"
    assert current.split_horizontal is None, "Leaf split_horizontal should be None"
    assert current.split_position is None, "Leaf split_position should be None"
    assert current.level > 0, "Leaf level should be > 0"

    # Test center method
    bounds = current.bounds
    cx, cy = current.center()
    expected_cx = bounds[0][0] + bounds[1][0] // 2
    expected_cy = bounds[0][1] + bounds[1][1] // 2
    assert cx == expected_cx, f"Center x mismatch: {cx} != {expected_cx}"
    assert cy == expected_cy, f"Center y mismatch: {cy} != {expected_cy}"

    print("  BSPNode properties: PASS")

def test_bspnode_navigation():
    """Test BSPNode navigation (#203)."""
    print("Testing BSPNode navigation...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 80))
    bsp.split_once(horizontal=True, position=40)

    root = bsp.root
    left = root.left
    right = root.right

    # Parent navigation
    assert left.parent is not None, "Left parent should exist"
    assert left.parent.bounds == root.bounds, "Left parent should be root"

    # Sibling navigation
    assert left.sibling is not None, "Left sibling should exist"
    assert left.sibling.bounds == right.bounds, "Left sibling should be right"
    assert right.sibling is not None, "Right sibling should exist"
    assert right.sibling.bounds == left.bounds, "Right sibling should be left"

    # Root has no parent or sibling
    assert root.parent is None, "Root parent should be None"
    assert root.sibling is None, "Root sibling should be None"

    print("  BSPNode navigation: PASS")

def test_bspnode_contains():
    """Test BSPNode contains method (#203)."""
    print("Testing BSPNode contains...")

    bsp = mcrfpy.BSP(pos=(10, 20), size=(50, 40))  # x: 10-60, y: 20-60

    root = bsp.root

    # Inside
    assert root.contains((30, 40)), "Center should be inside"
    assert root.contains((10, 20)), "Top-left corner should be inside"
    assert root.contains((59, 59)), "Near bottom-right should be inside"

    # Outside
    assert not root.contains((5, 40)), "Left of bounds should be outside"
    assert not root.contains((65, 40)), "Right of bounds should be outside"
    assert not root.contains((30, 15)), "Above bounds should be outside"
    assert not root.contains((30, 65)), "Below bounds should be outside"

    print("  BSPNode contains: PASS")

def test_bsp_leaves_iteration():
    """Test leaves iteration (#204)."""
    print("Testing BSP leaves iteration...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
    bsp.split_recursive(depth=3, min_size=(8, 8), seed=42)

    # Iterate leaves
    leaves = list(bsp.leaves())
    assert len(leaves) > 0, "Should have at least one leaf"

    # All should be leaves
    for leaf in leaves:
        assert leaf.is_leaf, f"Should be leaf: {leaf}"

    # Can convert to list multiple times
    leaves2 = list(bsp.leaves())
    assert len(leaves) == len(leaves2), "Multiple iterations should yield same count"

    print("  BSP leaves iteration: PASS")

def test_bsp_traverse():
    """Test traverse with different orders (#204)."""
    print("Testing BSP traverse...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
    bsp.split_recursive(depth=2, min_size=(8, 8), seed=42)

    # Test all traversal orders
    orders = [
        mcrfpy.Traversal.PRE_ORDER,
        mcrfpy.Traversal.IN_ORDER,
        mcrfpy.Traversal.POST_ORDER,
        mcrfpy.Traversal.LEVEL_ORDER,
        mcrfpy.Traversal.INVERTED_LEVEL_ORDER,
    ]

    for order in orders:
        nodes = list(bsp.traverse(order=order))
        assert len(nodes) > 0, f"Should have nodes for {order}"
        # All traversals should visit same number of nodes
        assert len(nodes) == len(list(bsp.traverse())), f"Node count mismatch for {order}"

    # Default should be LEVEL_ORDER
    default_nodes = list(bsp.traverse())
    level_nodes = list(bsp.traverse(mcrfpy.Traversal.LEVEL_ORDER))
    assert len(default_nodes) == len(level_nodes), "Default should match LEVEL_ORDER"

    # PRE_ORDER: root first
    pre_nodes = list(bsp.traverse(mcrfpy.Traversal.PRE_ORDER))
    assert pre_nodes[0].bounds == bsp.root.bounds, "PRE_ORDER should start with root"

    # POST_ORDER: root last
    post_nodes = list(bsp.traverse(mcrfpy.Traversal.POST_ORDER))
    assert post_nodes[-1].bounds == bsp.root.bounds, "POST_ORDER should end with root"

    print("  BSP traverse: PASS")

def test_bsp_find():
    """Test find method (#205)."""
    print("Testing BSP find...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
    bsp.split_recursive(depth=3, min_size=(8, 8), seed=42)

    # Find a point inside bounds
    node = bsp.find((40, 30))
    assert node is not None, "Should find node for point inside"
    assert node.is_leaf, "Found node should be a leaf (deepest)"
    assert node.contains((40, 30)), "Found node should contain the point"

    # Find at corner
    corner_node = bsp.find((0, 0))
    assert corner_node is not None, "Should find node at corner"
    assert corner_node.contains((0, 0)), "Corner node should contain (0,0)"

    # Find outside bounds
    outside = bsp.find((100, 100))
    assert outside is None, "Should return None for point outside"

    print("  BSP find: PASS")

def test_bsp_to_heightmap():
    """Test to_heightmap conversion (#206)."""
    print("Testing BSP to_heightmap...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(50, 40))
    bsp.split_recursive(depth=2, min_size=(8, 8), seed=42)

    # Basic conversion
    hmap = bsp.to_heightmap()
    assert hmap is not None, "Should create heightmap"
    assert hmap.size == (50, 40), f"Size should match bounds, got {hmap.size}"

    # Check that leaves are filled
    leaves = list(bsp.leaves())
    for leaf in leaves:
        lx, ly = leaf.bounds[0]
        val = hmap[lx, ly]
        assert val == 1.0, f"Leaf interior should be 1.0, got {val}"

    # Test with shrink
    hmap_shrink = bsp.to_heightmap(shrink=2)
    assert hmap_shrink is not None, "Should create with shrink"

    # Test with select='internal'
    hmap_internal = bsp.to_heightmap(select='internal')
    assert hmap_internal is not None, "Should create with select=internal"

    # Test with select='all'
    hmap_all = bsp.to_heightmap(select='all')
    assert hmap_all is not None, "Should create with select=all"

    # Test with custom size
    hmap_sized = bsp.to_heightmap(size=(100, 80))
    assert hmap_sized.size == (100, 80), f"Custom size should work, got {hmap_sized.size}"

    # Test with custom value
    hmap_val = bsp.to_heightmap(value=0.5)
    leaves = list(bsp.leaves())
    leaf = leaves[0]
    lx, ly = leaf.bounds[0]
    val = hmap_val[lx, ly]
    assert val == 0.5, f"Custom value should be 0.5, got {val}"

    print("  BSP to_heightmap: PASS")

def test_traversal_enum():
    """Test Traversal enum (#204)."""
    print("Testing Traversal enum...")

    # Check enum exists
    assert hasattr(mcrfpy, 'Traversal'), "Traversal enum should exist"

    # Check all members
    assert mcrfpy.Traversal.PRE_ORDER.value == 0
    assert mcrfpy.Traversal.IN_ORDER.value == 1
    assert mcrfpy.Traversal.POST_ORDER.value == 2
    assert mcrfpy.Traversal.LEVEL_ORDER.value == 3
    assert mcrfpy.Traversal.INVERTED_LEVEL_ORDER.value == 4

    print("  Traversal enum: PASS")

def test_bsp_chaining():
    """Test method chaining."""
    print("Testing BSP method chaining...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))

    # Chain multiple operations
    result = bsp.split_recursive(depth=2, min_size=(8, 8), seed=42).clear().split_once(True, 30)
    assert result is bsp, "Chaining should return self"

    print("  BSP chaining: PASS")

def test_bsp_repr():
    """Test repr output."""
    print("Testing BSP repr...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
    repr_str = repr(bsp)
    assert "BSP" in repr_str, f"repr should contain BSP: {repr_str}"
    assert "80" in repr_str and "60" in repr_str, f"repr should contain size: {repr_str}"

    bsp.split_recursive(depth=2, min_size=(8, 8), seed=42)
    repr_str2 = repr(bsp)
    assert "leaves" in repr_str2, f"repr should mention leaves: {repr_str2}"

    # BSPNode repr
    node_repr = repr(bsp.root)
    assert "BSPNode" in node_repr, f"BSPNode repr: {node_repr}"

    print("  BSP repr: PASS")

def test_bsp_stale_node_detection():
    """Test that stale nodes are detected after clear()/split_recursive() (#review)."""
    print("Testing BSP stale node detection...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
    bsp.split_recursive(depth=2, min_size=(8, 8), seed=42)

    # Save reference to a node
    old_root = bsp.root
    old_leaf = list(bsp.leaves())[0]

    # Clear invalidates all nodes
    bsp.clear()

    # Accessing stale node should raise RuntimeError
    try:
        _ = old_root.bounds
        assert False, "Accessing stale root bounds should raise RuntimeError"
    except RuntimeError as e:
        assert "stale" in str(e).lower() or "invalid" in str(e).lower(), \
            f"Error should mention staleness: {e}"

    try:
        _ = old_leaf.is_leaf
        assert False, "Accessing stale leaf should raise RuntimeError"
    except RuntimeError as e:
        pass  # Expected

    # split_recursive also invalidates (rebuilds tree)
    bsp2 = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
    bsp2.split_recursive(depth=2, min_size=(8, 8), seed=42)
    saved_node = bsp2.root
    bsp2.split_recursive(depth=3, min_size=(8, 8), seed=99)

    try:
        _ = saved_node.bounds
        assert False, "Accessing node after split_recursive should raise RuntimeError"
    except RuntimeError:
        pass  # Expected

    print("  BSP stale node detection: PASS")

def test_bsp_grid_max_validation():
    """Test GRID_MAX validation (#review)."""
    print("Testing BSP GRID_MAX validation...")

    # Should succeed with valid size
    bsp = mcrfpy.BSP(pos=(0, 0), size=(1000, 1000))
    assert bsp is not None

    # Should fail with size exceeding GRID_MAX (8192)
    try:
        bsp_too_big = mcrfpy.BSP(pos=(0, 0), size=(10000, 100))
        assert False, "Should raise ValueError for size > GRID_MAX"
    except ValueError as e:
        assert "8192" in str(e) or "exceed" in str(e).lower(), f"Error should mention limit: {e}"

    try:
        bsp_too_big = mcrfpy.BSP(pos=(0, 0), size=(100, 10000))
        assert False, "Should raise ValueError for height > GRID_MAX"
    except ValueError as e:
        pass  # Expected

    print("  BSP GRID_MAX validation: PASS")

def test_bsp_depth_cap():
    """Test depth is capped at 16 (#review)."""
    print("Testing BSP depth cap...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(1000, 1000))

    # Should cap depth at 16 or raise ValueError
    try:
        bsp.split_recursive(depth=20, min_size=(1, 1), seed=42)
        # If it succeeds, verify reasonable number of leaves (not 2^20)
        leaves = list(bsp.leaves())
        assert len(leaves) <= 2**16, f"Too many leaves: {len(leaves)}"
    except ValueError as e:
        # It's also acceptable to reject excessive depth
        assert "16" in str(e) or "depth" in str(e).lower(), f"Error should mention depth limit: {e}"

    print("  BSP depth cap: PASS")

def test_bsp_len():
    """Test __len__ returns leaf count (#review)."""
    print("Testing BSP __len__...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
    assert len(bsp) == 1, f"Unsplit BSP should have 1 leaf, got {len(bsp)}"

    bsp.split_once(horizontal=True, position=30)
    assert len(bsp) == 2, f"After one split should have 2 leaves, got {len(bsp)}"

    bsp.clear()
    bsp.split_recursive(depth=3, min_size=(8, 8), seed=42)
    expected = len(list(bsp.leaves()))
    assert len(bsp) == expected, f"len() mismatch: {len(bsp)} != {expected}"

    print("  BSP __len__: PASS")

def test_bsp_iter():
    """Test __iter__ as shorthand for leaves() (#review)."""
    print("Testing BSP __iter__...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
    bsp.split_recursive(depth=2, min_size=(8, 8), seed=42)

    # Direct iteration should yield same results as leaves()
    iter_list = list(bsp)
    leaves_list = list(bsp.leaves())

    assert len(iter_list) == len(leaves_list), \
        f"Iterator count mismatch: {len(iter_list)} != {len(leaves_list)}"

    # All items should be leaves
    for node in bsp:
        assert node.is_leaf, f"Iterator should yield leaves: {node}"

    # Can iterate multiple times
    count1 = sum(1 for _ in bsp)
    count2 = sum(1 for _ in bsp)
    assert count1 == count2, "Should be able to iterate multiple times"

    print("  BSP __iter__: PASS")

def test_bspnode_equality():
    """Test BSPNode __eq__ comparison (#review)."""
    print("Testing BSPNode equality...")

    bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
    bsp.split_recursive(depth=2, min_size=(8, 8), seed=42)

    # Same node should be equal
    root1 = bsp.root
    root2 = bsp.root
    assert root1 == root2, "Same node should be equal"

    # Different nodes should not be equal
    leaves = list(bsp.leaves())
    assert len(leaves) >= 2, "Need at least 2 leaves for comparison"
    assert leaves[0] != leaves[1], "Different nodes should not be equal"

    # Parent vs child should not be equal
    root = bsp.root
    left = root.left
    assert root != left, "Parent and child should not be equal"

    # Not equal to non-BSPNode
    assert not (root == "not a node"), "BSPNode should not equal string"
    assert not (root == 42), "BSPNode should not equal int"

    print("  BSPNode equality: PASS")

def run_all_tests():
    """Run all BSP tests."""
    print("\n=== BSP Unit Tests ===\n")

    try:
        test_bsp_construction()
        test_bsp_split_once()
        test_bsp_split_recursive()
        test_bsp_clear()
        test_bspnode_properties()
        test_bspnode_navigation()
        test_bspnode_contains()
        test_bsp_leaves_iteration()
        test_bsp_traverse()
        test_bsp_find()
        test_bsp_to_heightmap()
        test_traversal_enum()
        test_bsp_chaining()
        test_bsp_repr()
        test_bsp_stale_node_detection()
        test_bsp_grid_max_validation()
        test_bsp_depth_cap()
        test_bsp_len()
        test_bsp_iter()
        test_bspnode_equality()

        print("\n=== ALL BSP TESTS PASSED ===\n")
        sys.exit(0)
    except AssertionError as e:
        print(f"\nTEST FAILED: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"\nUNEXPECTED ERROR: {type(e).__name__}: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)

if __name__ == "__main__":
    run_all_tests()
