"""Tests for shadow depth computation."""

import numpy as np

from gri_utils.conversion import lla_to_xyz
from gri_utils.terrain import get_shadow_depth, lla_to_xyz_sheet


def _make_flat_sheet(
    n: int = 20,
    alt: float = 0.0,
) -> np.ndarray:
    """Build a flat terrain sheet centered near 39.5N, -104.5E."""
    lats = np.linspace(39.0, 40.0, n)
    lons = np.linspace(-105.0, -104.0, n)
    alts = np.full((n, n), alt)
    return lla_to_xyz_sheet(lats, lons, alts)


def test_flat_terrain_all_positive() -> None:
    """Observer above flat terrain: all depths positive (visible)."""
    sheet = _make_flat_sheet()
    observer = lla_to_xyz(np.array([39.5, -104.5, 10000.0]))

    depth = get_shadow_depth(sheet, observer)

    assert depth.shape == (20, 20)
    assert depth.dtype == np.float64
    assert np.all(depth > 0)


def test_flat_terrain_clearance_increases_near_observer() -> None:
    """Points closer to observer have less clearance on flat terrain.

    For flat terrain and high observer, the ray from observer descends
    more steeply to nearby points, so clearance (depth) is largest near
    the observer and decreases toward edges.
    """
    sheet = _make_flat_sheet(n=30)
    observer = lla_to_xyz(np.array([39.5, -104.5, 10000.0]))

    depth = get_shadow_depth(sheet, observer)

    # All positive
    assert np.all(depth > 0)


def test_ridge_blocks_far_side() -> None:
    """Ridge across the middle blocks points on the far side."""
    lats = np.linspace(39.0, 40.0, 30)
    lons = np.linspace(-105.0, -104.0, 30)
    alts = np.zeros((30, 30))
    alts[15, :] = 5000.0  # 5km ridge

    sheet = lla_to_xyz_sheet(lats, lons, alts)

    # Observer at low altitude on near side
    observer = lla_to_xyz(np.array([39.2, -104.5, 1000.0]))

    depth = get_shadow_depth(sheet, observer)

    # Near side (rows 0-14) should be mostly positive
    assert np.mean(depth[:12, :] > 0) > 0.7

    # Far side (rows 20+) should be mostly negative (shadowed)
    assert np.mean(depth[20:, :] < 0) > 0.5


def test_ridge_shadow_depth_magnitude() -> None:
    """Shadow depth behind ridge increases with distance from ridge."""
    lats = np.linspace(39.0, 40.0, 40)
    lons = np.linspace(-105.0, -104.0, 40)
    alts = np.zeros((40, 40))
    alts[15, :] = 5000.0  # Ridge

    sheet = lla_to_xyz_sheet(lats, lons, alts)
    observer = lla_to_xyz(np.array([39.1, -104.5, 500.0]))

    depth = get_shadow_depth(sheet, observer)

    # Points farther behind ridge should have deeper shadow on average
    # (angular shadow cone deepens with distance on flat ground)
    shadowed_near = depth[20, :]
    shadowed_far = depth[30, :]

    # Both should be negative (shadowed)
    # Far points should have deeper (more negative) shadow
    assert np.mean(shadowed_far) < np.mean(shadowed_near)


def test_single_peak_shadow() -> None:
    """Single peak casts a shadow behind it."""
    lats = np.linspace(39.0, 40.0, 40)
    lons = np.linspace(-105.0, -104.0, 40)
    alts = np.zeros((40, 40))
    alts[20, 20] = 4000.0  # Single peak

    sheet = lla_to_xyz_sheet(lats, lons, alts)

    # Observer on one side, looking past the peak
    observer = lla_to_xyz(np.array([39.2, -104.5, 1000.0]))

    depth = get_shadow_depth(sheet, observer)

    # Peak itself should be visible (positive depth)
    assert depth[20, 20] > 0

    # Some points behind peak should be shadowed
    assert np.any(depth[25:, 18:22] < 0)


def test_observer_at_grid_point() -> None:
    """Observer very close to grid gets large positive depth."""
    sheet = _make_flat_sheet(n=10)
    # Observer at roughly the center grid point, slightly above
    observer = lla_to_xyz(np.array([39.5, -104.5, 1.0]))

    depth = get_shadow_depth(sheet, observer)

    assert depth.shape == (10, 10)
    # Should not crash; nearby points get large positive depth


def test_observer_high_altitude_all_visible() -> None:
    """Satellite observer: all flat terrain visible."""
    sheet = _make_flat_sheet(n=15)
    # Observer at 500km altitude
    observer = lla_to_xyz(np.array([39.5, -104.5, 500000.0]))

    depth = get_shadow_depth(sheet, observer)

    assert np.all(depth > 0)


def test_shadow_boundary_at_zero_crossing() -> None:
    """Shadow depth crosses zero at the lit/shadow transition."""
    lats = np.linspace(39.0, 40.0, 50)
    lons = np.linspace(-105.0, -104.0, 50)
    alts = np.zeros((50, 50))
    alts[25, :] = 4000.0  # Ridge

    sheet = lla_to_xyz_sheet(lats, lons, alts)
    observer = lla_to_xyz(np.array([39.2, -104.5, 500.0]))

    depth = get_shadow_depth(sheet, observer)

    # The ridge row itself should have positive depth (visible)
    assert np.mean(depth[25, :] > 0) > 0.5

    # There should be sign changes between ridge (positive) and far side (negative)
    assert np.any(depth > 0)
    assert np.any(depth < 0)


def test_small_grid() -> None:
    """Works correctly on a 5x5 grid."""
    sheet = _make_flat_sheet(n=5)
    observer = lla_to_xyz(np.array([39.5, -104.5, 5000.0]))

    depth = get_shadow_depth(sheet, observer)

    assert depth.shape == (5, 5)
    assert depth.dtype == np.float64
    assert np.all(depth > 0)


def test_symmetry_symmetric_terrain() -> None:
    """Symmetric terrain produces roughly symmetric depths.

    Not exact due to Earth curvature and lat/lon grid distortion,
    but the sign pattern should match.
    """
    lats = np.linspace(39.0, 40.0, 21)
    lons = np.linspace(-105.0, -104.0, 21)
    alts = np.zeros((21, 21))
    # Symmetric ridge perpendicular to observer direction
    alts[10, :] = 3000.0

    sheet = lla_to_xyz_sheet(lats, lons, alts)

    # Observer centered on the grid, on near side
    observer = lla_to_xyz(np.array([39.2, -104.5, 500.0]))

    depth = get_shadow_depth(sheet, observer)

    # Sign pattern should be symmetric about center column
    left_signs = np.sign(depth[:, :10])
    right_signs = np.sign(depth[:, 11:][:, ::-1])
    assert np.array_equal(left_signs, right_signs)
