"""AZ-279 — WgsConverter helper AC tests.

Verifies the contract at ``_docs/02_document/contracts/shared_helpers/wgs_converter.md`` v1.0.0.
"""

from __future__ import annotations

import ast
import math
from pathlib import Path

import numpy as np
import pytest

from gps_denied_onboard._types.geo import BoundingBox, LatLonAlt
from gps_denied_onboard.helpers import (
    MAX_ZOOM,
    WEB_MERCATOR_MAX_LAT_DEG,
    WgsConversionError,
    WgsConverter,
)


def test_ac1_ecef_roundtrip() -> None:
    # Arrange
    samples = [
        LatLonAlt(50.0, 30.0, 100.0),
        LatLonAlt(-33.9, 151.2, 25.0),
        LatLonAlt(0.0, 0.0, 0.0),
        LatLonAlt(80.0, -120.0, 1500.0),
        LatLonAlt(-60.0, 179.999, -50.0),
    ]
    # Act / Assert
    for p in samples:
        ecef = WgsConverter.latlonalt_to_ecef(p)
        back = WgsConverter.ecef_to_latlonalt(ecef)
        assert math.isclose(back.lat_deg, p.lat_deg, abs_tol=1e-9), (back, p)
        assert math.isclose(back.lon_deg, p.lon_deg, abs_tol=1e-9), (back, p)
        assert math.isclose(back.alt_m, p.alt_m, abs_tol=1e-6), (back, p)


def test_ac2_enu_roundtrip_within_10_km() -> None:
    origin = LatLonAlt(50.0, 30.0, 100.0)
    # ~10 km away in NE direction
    p = LatLonAlt(50.07, 30.10, 250.0)
    enu = WgsConverter.latlonalt_to_local_enu(origin, p)
    back = WgsConverter.local_enu_to_latlonalt(origin, enu)
    horizontal_m = math.hypot(
        (back.lat_deg - p.lat_deg) * 111_320.0,
        (back.lon_deg - p.lon_deg) * 111_320.0 * math.cos(math.radians(p.lat_deg)),
    )
    vertical_m = abs(back.alt_m - p.alt_m)
    assert horizontal_m < 1.0, f"horizontal residual {horizontal_m} m > 1 m"
    assert vertical_m < 0.01, f"vertical residual {vertical_m} m > 1 cm"


def test_ac3_slippy_map_tile_roundtrip_z18_contains_input() -> None:
    zoom, lat, lon = 18, 50.45, 30.52
    x, y = WgsConverter.latlon_to_tile_xy(zoom, lat, lon)
    bounds = WgsConverter.tile_xy_to_latlon_bounds(zoom, x, y)
    assert isinstance(bounds, BoundingBox)
    assert bounds.contains(lat, lon)
    # OSM-pinned reference for (lat=50.45, lon=30.52, z=18); precomputed via
    # the slippy-map formula and matching satellite-provider's on-disk layout.
    assert (x, y) == (153295, 88392)


def test_ac4_web_mercator_latitude_range_guard() -> None:
    with pytest.raises(WgsConversionError, match=r"Web-Mercator"):
        WgsConverter.latlon_to_tile_xy(18, 95.0, 0.0)


def test_ac5_zoom_range_guard() -> None:
    with pytest.raises(WgsConversionError, match=r"zoom"):
        WgsConverter.latlon_to_tile_xy(MAX_ZOOM + 3, 50.0, 30.0)
    with pytest.raises(WgsConversionError, match=r"zoom"):
        WgsConverter.tile_xy_to_latlon_bounds(MAX_ZOOM + 3, 0, 0)


def test_ac6_tile_xy_range_guard() -> None:
    with pytest.raises(WgsConversionError, match=r"tile"):
        WgsConverter.tile_xy_to_latlon_bounds(18, 1 << 18, 0)


def test_ac7_ecef_shape_contract() -> None:
    with pytest.raises(WgsConversionError, match=r"shape"):
        WgsConverter.ecef_to_latlonalt(np.array([1.0, 2.0], dtype=np.float64))


def test_ac8_determinism_byte_equal_outputs() -> None:
    p = LatLonAlt(50.0, 30.0, 100.0)
    first = WgsConverter.latlonalt_to_ecef(p)
    second = WgsConverter.latlonalt_to_ecef(p)
    assert first.tobytes() == second.tobytes()


def test_ac9_no_upward_imports_to_components() -> None:
    module_path = (
        Path(__file__).resolve().parents[2]
        / "src"
        / "gps_denied_onboard"
        / "helpers"
        / "wgs_converter.py"
    )
    tree = ast.parse(module_path.read_text(encoding="utf-8"))
    bad: list[str] = []
    for node in ast.walk(tree):
        if isinstance(node, ast.ImportFrom) and node.module:
            if node.module.startswith("gps_denied_onboard.components"):
                bad.append(node.module)
        elif isinstance(node, ast.Import):
            for alias in node.names:
                if alias.name.startswith("gps_denied_onboard.components"):
                    bad.append(alias.name)
    assert not bad, f"wgs_converter must not import components.*; found: {bad}"


def test_invariant_web_mercator_max_lat_close_to_documented_value() -> None:
    # Sanity bound: documented constant matches the Mercator-projection-valid
    # latitude (arctan(sinh(pi))) within rounding.
    expected = math.degrees(math.atan(math.sinh(math.pi)))
    assert math.isclose(WEB_MERCATOR_MAX_LAT_DEG, expected, abs_tol=1e-9)
