#!/usr/bin/env python3
"""Unit tests for mcrfpy.HeightMap terrain generation methods (#195)

Tests the HeightMap terrain methods: add_hill, dig_hill, add_voronoi,
mid_point_displacement, rain_erosion, dig_bezier, smooth
"""

import sys
import mcrfpy


def test_add_hill_basic():
    """add_hill() creates elevation at center"""
    hmap = mcrfpy.HeightMap((50, 50), fill=0.0)
    hmap.add_hill((25, 25), radius=10.0, height=1.0)

    # Center should have highest value
    center_val = hmap[25, 25]
    edge_val = hmap[0, 0]

    assert center_val > edge_val, f"Center ({center_val}) should be higher than edge ({edge_val})"
    assert center_val > 0.5, f"Center should be significantly elevated, got {center_val}"
    print("PASS: test_add_hill_basic")


def test_add_hill_returns_self():
    """add_hill() returns self for chaining"""
    hmap = mcrfpy.HeightMap((20, 20))
    result = hmap.add_hill((10, 10), 5.0, 1.0)
    assert result is hmap
    print("PASS: test_add_hill_returns_self")


def test_add_hill_flexible_center():
    """add_hill() accepts tuple, list, and Vector for center"""
    hmap1 = mcrfpy.HeightMap((20, 20), fill=0.0)
    hmap2 = mcrfpy.HeightMap((20, 20), fill=0.0)
    hmap3 = mcrfpy.HeightMap((20, 20), fill=0.0)

    hmap1.add_hill((10, 10), 5.0, 1.0)
    hmap2.add_hill([10, 10], 5.0, 1.0)
    hmap3.add_hill(mcrfpy.Vector(10, 10), 5.0, 1.0)

    # All should produce same result
    assert abs(hmap1[10, 10] - hmap2[10, 10]) < 0.001
    assert abs(hmap1[10, 10] - hmap3[10, 10]) < 0.001
    print("PASS: test_add_hill_flexible_center")


def test_dig_hill_basic():
    """dig_hill() creates depression at center using target_height"""
    hmap = mcrfpy.HeightMap((50, 50), fill=0.5)
    # dig_hill constructs a pit with target_height at center
    # Only lowers cells - cells below target_height are unchanged
    hmap.dig_hill((25, 25), radius=15.0, target_height=-0.3)

    # Center should have lowest value (set to the target_height)
    center_val = hmap[25, 25]
    edge_val = hmap[0, 0]

    assert center_val < edge_val, f"Center ({center_val}) should be lower than edge ({edge_val})"
    assert center_val < 0, f"Center should be negative, got {center_val}"
    print("PASS: test_dig_hill_basic")


def test_dig_hill_returns_self():
    """dig_hill() returns self for chaining"""
    hmap = mcrfpy.HeightMap((20, 20))
    result = hmap.dig_hill((10, 10), 5.0, 0.5)
    assert result is hmap
    print("PASS: test_dig_hill_returns_self")


def test_add_voronoi_basic():
    """add_voronoi() modifies heightmap"""
    hmap = mcrfpy.HeightMap((50, 50), fill=0.0)
    min_before, max_before = hmap.min_max()

    hmap.add_voronoi(10, seed=12345)

    min_after, max_after = hmap.min_max()

    # Values should have changed
    assert max_after > max_before or min_after < min_before, "Voronoi should modify values"
    print("PASS: test_add_voronoi_basic")


def test_add_voronoi_with_coefficients():
    """add_voronoi() accepts custom coefficients"""
    hmap = mcrfpy.HeightMap((30, 30), fill=0.0)
    hmap.add_voronoi(5, coefficients=(1.0, -0.5, 0.3), seed=42)

    # Should complete without error
    min_val, max_val = hmap.min_max()
    assert min_val != max_val, "Voronoi should create variation"
    print("PASS: test_add_voronoi_with_coefficients")


def test_add_voronoi_returns_self():
    """add_voronoi() returns self for chaining"""
    hmap = mcrfpy.HeightMap((20, 20))
    result = hmap.add_voronoi(5)
    assert result is hmap
    print("PASS: test_add_voronoi_returns_self")


def test_add_voronoi_invalid_num_points():
    """add_voronoi() raises ValueError for invalid num_points"""
    hmap = mcrfpy.HeightMap((20, 20))

    try:
        hmap.add_voronoi(0)
        print("FAIL: should have raised ValueError for num_points=0")
        sys.exit(1)
    except ValueError:
        pass

    print("PASS: test_add_voronoi_invalid_num_points")


def test_mid_point_displacement_basic():
    """mid_point_displacement() generates terrain"""
    # Use power-of-2+1 size for best results
    hmap = mcrfpy.HeightMap((65, 65), fill=0.0)
    hmap.mid_point_displacement(roughness=0.5, seed=12345)

    min_val, max_val = hmap.min_max()
    assert min_val != max_val, "MPD should create variation"
    print("PASS: test_mid_point_displacement_basic")


def test_mid_point_displacement_returns_self():
    """mid_point_displacement() returns self for chaining"""
    hmap = mcrfpy.HeightMap((33, 33))
    result = hmap.mid_point_displacement()
    assert result is hmap
    print("PASS: test_mid_point_displacement_returns_self")


def test_mid_point_displacement_reproducible():
    """mid_point_displacement() is reproducible with same seed"""
    hmap1 = mcrfpy.HeightMap((33, 33), fill=0.0)
    hmap2 = mcrfpy.HeightMap((33, 33), fill=0.0)

    hmap1.mid_point_displacement(roughness=0.6, seed=99999)
    hmap2.mid_point_displacement(roughness=0.6, seed=99999)

    # Should produce identical results
    assert abs(hmap1[16, 16] - hmap2[16, 16]) < 0.001
    assert abs(hmap1[5, 5] - hmap2[5, 5]) < 0.001
    print("PASS: test_mid_point_displacement_reproducible")


def test_rain_erosion_basic():
    """rain_erosion() modifies terrain"""
    hmap = mcrfpy.HeightMap((50, 50), fill=0.5)
    # Add some hills first to have something to erode
    hmap.add_hill((25, 25), 15.0, 0.5)

    val_before = hmap[25, 25]
    hmap.rain_erosion(1000, seed=12345)
    val_after = hmap[25, 25]

    # Erosion should change values
    # Note: might not change if terrain is completely flat
    # so we check min/max spread
    min_val, max_val = hmap.min_max()
    assert max_val > min_val, "Rain erosion should leave some variation"
    print("PASS: test_rain_erosion_basic")


def test_rain_erosion_returns_self():
    """rain_erosion() returns self for chaining"""
    hmap = mcrfpy.HeightMap((20, 20))
    result = hmap.rain_erosion(100)
    assert result is hmap
    print("PASS: test_rain_erosion_returns_self")


def test_rain_erosion_invalid_drops():
    """rain_erosion() raises ValueError for invalid drops"""
    hmap = mcrfpy.HeightMap((20, 20))

    try:
        hmap.rain_erosion(0)
        print("FAIL: should have raised ValueError for drops=0")
        sys.exit(1)
    except ValueError:
        pass

    print("PASS: test_rain_erosion_invalid_drops")


def test_dig_bezier_basic():
    """dig_bezier() constructs a canal with target heights"""
    hmap = mcrfpy.HeightMap((50, 50), fill=0.5)

    # Construct a canal from corner to corner
    # target_height sets the center height of the canal
    points = ((0, 0), (10, 25), (40, 25), (49, 49))
    hmap.dig_bezier(points, start_radius=5.0, end_radius=5.0,
                    start_height=-0.3, end_height=-0.3)

    # Start and end should be at the target height (lowered)
    assert hmap[0, 0] < 0.5, f"Start should be lowered, got {hmap[0, 0]}"
    assert hmap[0, 0] < 0, f"Start should be at target height, got {hmap[0, 0]}"
    print("PASS: test_dig_bezier_basic")


def test_dig_bezier_returns_self():
    """dig_bezier() returns self for chaining"""
    hmap = mcrfpy.HeightMap((20, 20))
    result = hmap.dig_bezier(((0, 0), (5, 10), (15, 10), (19, 19)),
                             start_radius=2.0, end_radius=2.0,
                             start_height=0.3, end_height=0.3)
    assert result is hmap
    print("PASS: test_dig_bezier_returns_self")


def test_dig_bezier_wrong_point_count():
    """dig_bezier() raises ValueError for wrong number of points"""
    hmap = mcrfpy.HeightMap((20, 20))

    try:
        hmap.dig_bezier(((0, 0), (5, 5), (10, 10)),
                        start_radius=2.0, end_radius=2.0,
                        start_height=0.3, end_height=0.3)  # Only 3 points
        print("FAIL: should have raised ValueError for 3 points")
        sys.exit(1)
    except ValueError as e:
        assert "4" in str(e)

    print("PASS: test_dig_bezier_wrong_point_count")


def test_dig_bezier_accepts_list():
    """dig_bezier() accepts list of points"""
    hmap = mcrfpy.HeightMap((20, 20), fill=0.5)
    points = [[0, 0], [5, 10], [15, 10], [19, 19]]  # List instead of tuple
    hmap.dig_bezier(points, start_radius=2.0, end_radius=2.0,
                    start_height=0.3, end_height=0.3)
    # Should complete without error
    print("PASS: test_dig_bezier_accepts_list")


def test_smooth_basic():
    """smooth() reduces terrain variation"""
    hmap = mcrfpy.HeightMap((30, 30), fill=0.0)
    # Create sharp height differences
    hmap.add_hill((15, 15), 5.0, 1.0)

    # Get slope before smoothing (measure of sharpness)
    slope_before = hmap.get_slope((15, 15))

    hmap.smooth(iterations=3)

    # Smoothing should reduce the slope
    slope_after = hmap.get_slope((15, 15))
    # Note: slope might not always decrease depending on terrain, so we just verify it runs
    min_val, max_val = hmap.min_max()
    assert max_val >= min_val
    print("PASS: test_smooth_basic")


def test_smooth_returns_self():
    """smooth() returns self for chaining"""
    hmap = mcrfpy.HeightMap((20, 20))
    result = hmap.smooth()
    assert result is hmap
    print("PASS: test_smooth_returns_self")


def test_smooth_invalid_iterations():
    """smooth() raises ValueError for invalid iterations"""
    hmap = mcrfpy.HeightMap((20, 20))

    try:
        hmap.smooth(iterations=0)
        print("FAIL: should have raised ValueError for iterations=0")
        sys.exit(1)
    except ValueError:
        pass

    try:
        hmap.smooth(iterations=-5)
        print("FAIL: should have raised ValueError for iterations=-5")
        sys.exit(1)
    except ValueError:
        pass

    print("PASS: test_smooth_invalid_iterations")


def test_add_hill_zero_radius_warning():
    """add_hill() warns on zero/negative radius"""
    import warnings
    hmap = mcrfpy.HeightMap((20, 20), fill=0.5)

    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter("always")
        hmap.add_hill((10, 10), radius=0.0, height=1.0)
        assert len(w) == 1
        assert "radius <= 0" in str(w[0].message)

    # Map should be unchanged
    assert abs(hmap[10, 10] - 0.5) < 0.001
    print("PASS: test_add_hill_zero_radius_warning")


def test_dig_hill_zero_radius_warning():
    """dig_hill() warns on zero/negative radius"""
    import warnings
    hmap = mcrfpy.HeightMap((20, 20), fill=0.5)

    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter("always")
        hmap.dig_hill((10, 10), radius=-1.0, target_height=0.0)
        assert len(w) == 1
        assert "radius <= 0" in str(w[0].message)

    print("PASS: test_dig_hill_zero_radius_warning")


def test_dig_bezier_zero_radius_warning():
    """dig_bezier() warns on zero/negative radius"""
    import warnings
    hmap = mcrfpy.HeightMap((20, 20), fill=0.5)

    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter("always")
        hmap.dig_bezier(((0, 0), (5, 10), (15, 10), (19, 19)),
                        start_radius=0.0, end_radius=2.0,
                        start_height=0.3, end_height=0.3)
        assert len(w) == 1
        assert "radius <= 0" in str(w[0].message)

    print("PASS: test_dig_bezier_zero_radius_warning")


def test_chaining_terrain_methods():
    """Terrain methods can be chained together"""
    hmap = mcrfpy.HeightMap((50, 50), fill=0.0)

    result = (hmap
              .add_hill((25, 25), 10.0, 0.5)
              .add_hill((10, 10), 8.0, 0.3)
              .dig_hill((40, 40), 5.0, 0.2)
              .smooth(iterations=2)
              .normalize(0.0, 1.0))

    assert result is hmap
    min_val, max_val = hmap.min_max()
    assert abs(min_val - 0.0) < 0.001
    assert abs(max_val - 1.0) < 0.001
    print("PASS: test_chaining_terrain_methods")


def test_terrain_pipeline():
    """Complete terrain generation pipeline"""
    hmap = mcrfpy.HeightMap((65, 65), fill=0.0)

    # Generate base terrain
    hmap.mid_point_displacement(roughness=0.6, seed=42)
    hmap.normalize(0.0, 1.0)

    # Add features
    hmap.add_hill((32, 32), 20.0, 0.3)  # Mountain
    # dig_bezier constructs a river valley at target heights
    hmap.dig_bezier(((0, 32), (20, 20), (45, 45), (64, 32)),
                    start_radius=3.0, end_radius=2.0,
                    start_height=-0.3, end_height=-0.2)

    # Apply erosion and smoothing
    hmap.rain_erosion(500, seed=123)
    hmap.smooth()

    # Normalize to 0-1 range
    hmap.normalize(0.0, 1.0)

    min_val, max_val = hmap.min_max()
    assert abs(min_val - 0.0) < 0.001
    assert abs(max_val - 1.0) < 0.001
    print("PASS: test_terrain_pipeline")


def run_all_tests():
    """Run all tests"""
    print("Running HeightMap terrain generation tests (#195)...")
    print()

    test_add_hill_basic()
    test_add_hill_returns_self()
    test_add_hill_flexible_center()
    test_dig_hill_basic()
    test_dig_hill_returns_self()
    test_add_voronoi_basic()
    test_add_voronoi_with_coefficients()
    test_add_voronoi_returns_self()
    test_add_voronoi_invalid_num_points()
    test_mid_point_displacement_basic()
    test_mid_point_displacement_returns_self()
    test_mid_point_displacement_reproducible()
    test_rain_erosion_basic()
    test_rain_erosion_returns_self()
    test_rain_erosion_invalid_drops()
    test_dig_bezier_basic()
    test_dig_bezier_returns_self()
    test_dig_bezier_wrong_point_count()
    test_dig_bezier_accepts_list()
    test_smooth_basic()
    test_smooth_returns_self()
    test_smooth_invalid_iterations()
    test_add_hill_zero_radius_warning()
    test_dig_hill_zero_radius_warning()
    test_dig_bezier_zero_radius_warning()
    test_chaining_terrain_methods()
    test_terrain_pipeline()

    print()
    print("All HeightMap terrain generation tests PASSED!")


# Run tests directly
run_all_tests()
sys.exit(0)
