"""Tests for SatelliteDataManager (F04) — SAT-01/02 and mercator utils (H06)."""

import numpy as np
import pytest

from gps_denied.core.satellite import SatelliteDataManager
from gps_denied.schemas import GPSPoint
from gps_denied.utils import mercator

# ---------------------------------------------------------------
# Mercator utils
# ---------------------------------------------------------------

def test_latlon_to_tile():
    lat, lon, zoom = 50.4501, 30.5234, 15
    coords = mercator.latlon_to_tile(lat, lon, zoom)
    assert coords.zoom == 15
    assert coords.x > 0
    assert coords.y > 0


def test_tile_to_latlon():
    gps = mercator.tile_to_latlon(19131, 10927, 15)
    assert 50.0 < gps.lat < 52.0
    assert 30.0 < gps.lon < 31.0


def test_tile_bounds():
    coords = mercator.TileCoords(x=19131, y=10927, zoom=15)
    bounds = mercator.compute_tile_bounds(coords)
    assert bounds.nw.lat > bounds.se.lat
    assert bounds.nw.lon < bounds.se.lon
    assert bounds.gsd > 0


# ---------------------------------------------------------------
# SAT-01: Local tile storage (no HTTP)
# ---------------------------------------------------------------

@pytest.fixture
def satellite_manager(tmp_path):
    return SatelliteDataManager(tile_dir=str(tmp_path / "tiles"))


def test_load_local_tile_missing(satellite_manager):
    """Missing tile returns None — no crash."""
    coords = mercator.TileCoords(x=0, y=0, zoom=12)
    result = satellite_manager.load_local_tile(coords)
    assert result is None


def test_save_and_load_local_tile(satellite_manager):
    """SAT-01: saved tile can be read back from the local directory."""
    coords = mercator.TileCoords(x=19131, y=10927, zoom=15)
    img = np.zeros((256, 256, 3), dtype=np.uint8)
    img[:] = [0, 128, 255]

    ok = satellite_manager.save_local_tile(coords, img)
    assert ok is True

    loaded = satellite_manager.load_local_tile(coords)
    assert loaded is not None
    assert loaded.shape == (256, 256, 3)


def test_mem_cache_hit(satellite_manager):
    """Tile loaded once should be served from memory on second request."""
    coords = mercator.TileCoords(x=1, y=1, zoom=10)
    img = np.ones((256, 256, 3), dtype=np.uint8) * 42
    satellite_manager.save_local_tile(coords, img)

    r1 = satellite_manager.load_local_tile(coords)
    r2 = satellite_manager.load_local_tile(coords)
    assert r1 is r2  # same object = came from mem cache


# ---------------------------------------------------------------
# SAT-02: ESKF ±3σ tile selection
# ---------------------------------------------------------------

def test_select_tiles_small_sigma(satellite_manager):
    """Very tight sigma → single tile covering the position."""
    gps = GPSPoint(lat=50.45, lon=30.52)
    tiles = satellite_manager.select_tiles_for_eskf_position(gps, sigma_h_m=1.0, zoom=18)
    # Should produce at least the center tile
    assert len(tiles) >= 1
    for t in tiles:
        assert t.zoom == 18


def test_select_tiles_large_sigma(satellite_manager):
    """Larger sigma → more tiles returned."""
    gps = GPSPoint(lat=50.45, lon=30.52)
    small = satellite_manager.select_tiles_for_eskf_position(gps, sigma_h_m=10.0, zoom=18)
    large = satellite_manager.select_tiles_for_eskf_position(gps, sigma_h_m=200.0, zoom=18)
    assert len(large) >= len(small)


def test_select_tiles_bounding_box(satellite_manager):
    """Selected tiles must span a bounding box that covers ±3σ."""
    gps = GPSPoint(lat=49.0, lon=32.0)
    sigma = 50.0  # 50 m → 3σ = 150 m
    zoom = 18
    tiles = satellite_manager.select_tiles_for_eskf_position(gps, sigma_h_m=sigma, zoom=zoom)
    assert len(tiles) >= 1
    # All returned tiles must be at the requested zoom
    assert all(t.zoom == zoom for t in tiles)


# ---------------------------------------------------------------
# SAT-01: Mosaic assembly
# ---------------------------------------------------------------

def test_assemble_mosaic_single(satellite_manager):
    """Single tile → mosaic equals that tile (resized)."""
    coords = mercator.TileCoords(x=10, y=10, zoom=15)
    img = np.zeros((256, 256, 3), dtype=np.uint8)
    mosaic, bounds = satellite_manager.assemble_mosaic([(coords, img)], target_size=256)
    assert mosaic.shape == (256, 256, 3)
    assert bounds.center is not None


def test_assemble_mosaic_2x2(satellite_manager):
    """2×2 tile grid assembles into a single mosaic."""
    mercator.TileCoords(x=10, y=10, zoom=15)
    tiles = [
        (mercator.TileCoords(x=10, y=10, zoom=15), np.zeros((256, 256, 3), dtype=np.uint8)),
        (mercator.TileCoords(x=11, y=10, zoom=15), np.zeros((256, 256, 3), dtype=np.uint8)),
        (mercator.TileCoords(x=10, y=11, zoom=15), np.zeros((256, 256, 3), dtype=np.uint8)),
        (mercator.TileCoords(x=11, y=11, zoom=15), np.zeros((256, 256, 3), dtype=np.uint8)),
    ]
    mosaic, bounds = satellite_manager.assemble_mosaic(tiles, target_size=512)
    assert mosaic.shape == (512, 512, 3)


def test_assemble_mosaic_empty(satellite_manager):
    result = satellite_manager.assemble_mosaic([])
    assert result is None


# ---------------------------------------------------------------
# Cache helpers (backward compat)
# ---------------------------------------------------------------

def test_cache_tile_compat(satellite_manager):
    coords = mercator.TileCoords(x=100, y=100, zoom=12)
    img = np.zeros((256, 256, 3), dtype=np.uint8)
    assert satellite_manager.cache_tile("f1", coords, img) is True
    cached = satellite_manager.get_cached_tile("f1", coords)
    assert cached is not None


def test_grid_calculations(satellite_manager):
    center = mercator.TileCoords(x=100, y=100, zoom=15)
    grid = satellite_manager.get_tile_grid(center, 9)
    assert len(grid) == 9
    assert any(c.x == 100 and c.y == 100 for c in grid)
    new_tiles = satellite_manager.expand_search_grid(center, 9, 25)
    assert len(new_tiles) == 16  # 25 - 9
