"""
Test module for the geography module.

Authors: Sebastián Rodríguez-Martínez
Contact: srodriguez@mbari.org
Last Update: 2024-03-27
"""

import numpy as np
import pytest

from navlib import (
    deg2m_lat,
    deg2m_lon,
    dms_to_decimal,
    get_local_gravity,
    latlon_to_zone_letter,
    latlon_to_zone_number,
    ll2utm,
    ll2xy,
    utm2ll,
    xy2ll,
)
from navlib.geo import deg2m_lat as deg2m_lat_relative
from navlib.geo import deg2m_lon as deg2m_lon_relative
from navlib.geo import dms_to_decimal as dms_to_decimal_relative
from navlib.geo import get_local_gravity as local_gravity_relative
from navlib.geo import latlon_to_zone_letter as latlon_to_zone_letter_relative
from navlib.geo import latlon_to_zone_number as latlon_to_zone_number_relative
from navlib.geo import ll2utm as ll2utm_relative
from navlib.geo import ll2xy as ll2xy_relative
from navlib.geo import utm2ll as utm2ll_relative
from navlib.geo import xy2ll as xy2ll_relative


def test_imports():
    # Checks that the absolute and relative imports work correctly
    assert deg2m_lat_relative.__code__.co_code == deg2m_lat.__code__.co_code
    assert deg2m_lon_relative.__code__.co_code == deg2m_lon.__code__.co_code
    assert dms_to_decimal_relative.__code__.co_code == dms_to_decimal.__code__.co_code
    assert (
        latlon_to_zone_letter_relative.__code__.co_code
        == latlon_to_zone_letter.__code__.co_code
    )
    assert (
        latlon_to_zone_number_relative.__code__.co_code
        == latlon_to_zone_number.__code__.co_code
    )
    assert ll2utm_relative.__code__.co_code == ll2utm.__code__.co_code
    assert ll2xy_relative.__code__.co_code == ll2xy.__code__.co_code
    assert local_gravity_relative.__code__.co_code == get_local_gravity.__code__.co_code
    assert utm2ll_relative.__code__.co_code == utm2ll.__code__.co_code
    assert xy2ll_relative.__code__.co_code == xy2ll.__code__.co_code


def test_latlon_to_zone_number():
    """
    Test for the latlon_to_zone_number function.
    """
    assert latlon_to_zone_number(37.7749, -122.4194) == 10
    with pytest.raises(ValueError):
        latlon_to_zone_number(100, 200)


def test_latlon_to_zone_letter():
    """
    Test for the latlon_to_zone_letter function.
    """
    assert latlon_to_zone_letter(37.7749) == "S"
    with pytest.raises(ValueError):
        latlon_to_zone_letter(100)


def test_ll2utm():
    """
    Test for the ll2utm function.
    """
    # Test 1: North Hemisphere coordinates (Moss Landing, CA, USA)
    easting, northing, zone, hemisphere = ll2utm(36.804443, -121.786942)
    expected_easting = 608211
    expected_northing = 4073865
    expected_zone = 10
    expected_hemisphere = "north"
    assert round(easting) == expected_easting
    assert round(northing) == expected_northing
    assert zone == expected_zone
    assert hemisphere == expected_hemisphere

    # Test 2: South Hemisphere coordinates (Cape Town, South Africa)
    easting, northing, zone, hemisphere = ll2utm(-33.918861, 18.423300)
    expected_easting = 261791
    expected_northing = 6243850
    expected_zone = 34
    expected_hemisphere = "south"
    assert round(easting) == expected_easting
    assert round(northing) == expected_northing
    assert zone == expected_zone
    assert hemisphere == expected_hemisphere

    # Test 3: Array input (North Hemisphere)
    latitudes = [36.804443] * 2
    longitudes = [-121.786942] * 2
    easting, northing, zone, hemisphere = ll2utm(latitudes, longitudes)
    expected_easting = np.array([608211, 608211])
    expected_northing = np.array([4073865, 4073865])
    expected_zone = 10
    expected_hemisphere = "north"
    assert np.allclose(easting, expected_easting, atol=1)
    assert np.allclose(northing, expected_northing, atol=1)
    assert zone == expected_zone
    assert hemisphere == expected_hemisphere

    # Test 4: Array input (South Hemisphere)
    latitudes = [-33.918861] * 2
    longitudes = [18.423300] * 2
    easting, northing, zone, hemisphere = ll2utm(latitudes, longitudes)
    expected_easting = np.array([261791, 261791])
    expected_northing = np.array([6243850, 6243850])
    expected_zone = 34
    expected_hemisphere = "south"
    assert np.allclose(easting, expected_easting, atol=1)
    assert np.allclose(northing, expected_northing, atol=1)
    assert zone == expected_zone
    assert hemisphere == expected_hemisphere

    # Test 5: Invalid coordinates
    with pytest.raises(TypeError):
        ll2utm("a", "b")

    # Test 6: Mismatched array lengths
    with pytest.raises(ValueError):
        ll2utm([36.804443, 37.7749], [-121.786942])

    # Test 7: Type mismatch
    with pytest.raises(TypeError):
        ll2utm(36.804443, [-121.786942])

    # Test 8: Invalid latitude range
    with pytest.raises(ValueError):
        ll2utm(85.0, -121.786942)

    # Test 9: Invalid longitude range
    with pytest.raises(ValueError):
        ll2utm(36.804443, -190.0)


def test_utm2ll():
    """
    Test for the utm2ll function.
    """
    # Test 1: North Hemisphere coordinates (Moss Landing, CA, USA)
    latitude, longitude = utm2ll(4073865, 608211, "10N")
    expected_latitude = 36.804
    expected_longitude = -121.787
    assert round(latitude, 3) == expected_latitude
    assert round(longitude, 3) == expected_longitude

    # Test 2: South Hemisphere coordinates (Cape Town, South Africa)
    latitude, longitude = utm2ll(6243850, 261791, "34S")
    expected_latitude = -33.919
    expected_longitude = 18.423
    assert round(latitude, 3) == expected_latitude
    assert round(longitude, 3) == expected_longitude

    # Test 3: Array input (North Hemisphere)
    northings = [4073865] * 2
    eastings = [608211] * 2
    latitudes, longitudes = utm2ll(northings, eastings, "10N")
    expected_latitudes = np.array([36.804, 36.804])
    expected_longitudes = np.array([-121.787, -121.787])
    assert np.allclose(latitudes, expected_latitudes, atol=0.001)
    assert np.allclose(longitudes, expected_longitudes, atol=0.001)

    # Test 4: Array input (South Hemisphere)
    northings = [6243850] * 2
    eastings = [261791] * 2
    latitudes, longitudes = utm2ll(northings, eastings, "34S")
    expected_latitudes = np.array([-33.919, -33.919])
    expected_longitudes = np.array([18.423, 18.423])
    assert np.allclose(latitudes, expected_latitudes, atol=0.001)
    assert np.allclose(longitudes, expected_longitudes, atol=0.001)

    # Test 5: Invalid zone string
    with pytest.raises(ValueError):
        utm2ll(4073865, 608211, "100N")

    # Test 6: Mismatched array lengths
    with pytest.raises(ValueError):
        utm2ll([4073865, 4163834], [608211], "10N")

    # Test 7: Type mismatch
    with pytest.raises(TypeError):
        utm2ll(4073865, [608211], "10N")

    # Test 8: Invalid UTM zone format
    with pytest.raises(ValueError):
        utm2ll(4073865, 608211, "10")

    # Test 9: Invalid UTM zone number
    with pytest.raises(ValueError):
        utm2ll(4073865, 608211, "61N")


def test_deg2m_lat():
    """
    Test for the deg2m_lat function.
    """
    # Test 1: Single value input
    assert deg2m_lat(1) == pytest.approx(110567.57991018235)

    # Test 2: Array input
    latitudes = [1, 1, 1]
    expected_results = np.array(
        [110567.57991018235, 110567.57991018235, 110567.57991018235]
    )
    results = deg2m_lat(latitudes)
    assert np.allclose(results, expected_results, atol=1e-5)

    # Test 3: Numpy array input
    latitudes = np.array([1, 1, 1])
    results = deg2m_lat(latitudes)
    assert np.allclose(results, expected_results, atol=1e-5)

    # Test 4: Invalid input type (string)
    with pytest.raises(TypeError):
        deg2m_lat("invalid")

    # Test 5: Invalid input type (mixed list)
    with pytest.raises(TypeError):
        deg2m_lat([1, "invalid"])


def test_deg2m_lon():
    """
    Test for the deg2m_lon function.
    """
    # Test 1: Single value input
    assert deg2m_lon(1) == pytest.approx(111303.86005690244)

    # Test 2: Array input
    longitudes = [1, 1, 1]
    expected_results = np.array(
        [111303.86005690244, 111303.86005690244, 111303.86005690244]
    )
    results = deg2m_lon(longitudes)
    assert np.allclose(results, expected_results, atol=1e-5)

    # Test 3: Numpy array input
    longitudes = np.array([1, 1, 1])
    results = deg2m_lon(longitudes)
    assert np.allclose(results, expected_results, atol=1e-5)

    # Test 4: Invalid input type (string)
    with pytest.raises(TypeError):
        deg2m_lon("invalid")

    # Test 5: Invalid input type (mixed list)
    with pytest.raises(TypeError):
        deg2m_lon([1, "invalid"])


def test_ll2xy():
    """
    Test for the ll2xy function.
    """
    # Test 1: San Francisco, CA, USA (with origin at 0,0)
    x, y = ll2xy(37.7749, -122.4194, 0, 0)
    expected_x = -13627813.30158
    expected_y = 4176666.358726
    assert round(x, 6) == expected_x
    assert round(y, 6) == expected_y

    # Test 2: San Francisco, CA, USA (with origin at 37.7749, -122.4194)
    x, y = ll2xy(37.7749, -122.4194, 37.7749, -122.4194)
    expected_x = 0
    expected_y = 0
    assert x == expected_x
    assert y == expected_y

    # Test 3: Array input (San Francisco and Los Angeles, CA, USA)
    latitudes = [37.7749] * 2
    longitudes = [-122.4194, -122.4194]
    origin_latitude = 37.7749
    origin_longitude = -122.4194
    x, y = ll2xy(latitudes, longitudes, origin_latitude, origin_longitude)
    expected_x = np.array([0, 0])
    expected_y = np.array([0, 0])
    assert np.allclose(x, expected_x, atol=1e-3)
    assert np.allclose(y, expected_y, atol=1e-3)

    # Test 4: Invalid input type (string)
    with pytest.raises(TypeError):
        ll2xy("invalid", -122.4194)

    # Test 5: Invalid input type (mixed list)
    with pytest.raises(TypeError):
        ll2xy([37.7749, "invalid"], [-122.4194, -118.2437])

    # Test 6: Mismatched array lengths
    with pytest.raises(ValueError):
        ll2xy([37.7749, 34.0522], [-122.4194])


def test_xy2ll():
    """
    Test for the xy2ll function.
    """
    # Test 1: San Francisco, CA, USA (with origin at 0,0)
    latitude, longitude = xy2ll(-13627813.30158, 4176666.358726, 0, 0)
    expected_latitude = 37.7749
    expected_longitude = -122.4194
    assert round(latitude, 6) == expected_latitude
    assert round(longitude, 6) == expected_longitude

    # Test 2: San Francisco, CA, USA (with origin at 37.7749, -122.4194)
    latitude, longitude = xy2ll(0, 0, 37.7749, -122.4194)
    expected_latitude = 37.7749
    expected_longitude = -122.4194
    assert round(latitude, 6) == expected_latitude
    assert round(longitude, 6) == expected_longitude

    # Test 3: Array input (San Francisco)
    x_coords = [0, 0]
    y_coords = [0, 0]
    origin_latitude = 37.7749
    origin_longitude = -122.4194
    latitudes, longitudes = xy2ll(x_coords, y_coords, origin_latitude, origin_longitude)
    expected_latitudes = np.array([37.7749, 37.7749])
    expected_longitudes = np.array([-122.4194, -122.4194])
    assert np.allclose(latitudes, expected_latitudes, atol=1e-4)
    assert np.allclose(longitudes, expected_longitudes, atol=1e-4)

    # Test 4: Invalid input type (string)
    with pytest.raises(TypeError):
        xy2ll("invalid", 4176666.358726)

    # Test 5: Invalid input type (mixed list)
    with pytest.raises(TypeError):
        xy2ll([0, "invalid"], [4176666.358726, 374676.0])

    # Test 6: Mismatched array lengths
    with pytest.raises(ValueError):
        xy2ll([0, 374676.0], [4176666.358726])


def test_dms_to_decimal():
    # Test 1: Valid conversion (positive direction)
    result = dms_to_decimal(37, 46, 29.52, "N")
    expected = 37 + 46 / 60 + 29.52 / 3600
    assert result == pytest.approx(expected)

    # Test 2: Valid conversion (negative for South)
    result = dms_to_decimal(37, 46, 29.52, "S")
    expected = -(37 + 46 / 60 + 29.52 / 3600)
    assert result == pytest.approx(expected)

    # Test 3: Type validation - incorrect types
    with pytest.raises(TypeError):
        dms_to_decimal("37", 46, 29.52, "N")
    with pytest.raises(TypeError):
        dms_to_decimal(37, "46", 29.52, "N")
    with pytest.raises(TypeError):
        dms_to_decimal(37, 46, "29.52", "N")

    # Test 4: Value validation - out of range values
    with pytest.raises(ValueError):
        dms_to_decimal(200, 46, 29.52, "N")  # degrees out of range
    with pytest.raises(ValueError):
        dms_to_decimal(37, 60, 29.52, "N")  # minutes out of range
    with pytest.raises(ValueError):
        dms_to_decimal(37, 46, 60, "N")  # seconds out of range

    # Test 5: Invalid direction
    with pytest.raises(ValueError):
        dms_to_decimal(37, 46, 29.52, "X")


def test_local_gravity():
    """
    Test for the local_gravity function.
    """
    # Test 1: Valid calculation at equator (latitude = 0)
    gravity = get_local_gravity(0.0, 0.0)
    expected = 9.780327  # Base gravity at equator, sea level
    assert gravity == pytest.approx(expected, rel=1e-6)

    # Test 2: Valid calculation at north pole (latitude = 90)
    gravity = get_local_gravity(90.0, 0.0)
    # At poles: g0 = 9.780327 * (1 + 0.0053024 * 1 - 0.0000058 * 0) = 9.780327 * 1.0053024
    expected = 9.780327 * 1.0053024
    assert gravity == pytest.approx(expected, rel=1e-6)

    # Test 3: Valid calculation at south pole (latitude = -90)
    gravity = get_local_gravity(-90.0, 0.0)
    expected = 9.780327 * 1.0053024  # Same as north pole due to sin²(-90°) = sin²(90°)
    assert gravity == pytest.approx(expected, rel=1e-6)

    # Test 4: Valid calculation with positive depth
    gravity = get_local_gravity(45.0, 1000.0)
    phi = np.radians(45.0)
    g0 = 9.780327 * (
        1 + 0.0053024 * np.sin(phi) ** 2 - 0.0000058 * np.sin(2 * phi) ** 2
    )
    expected = g0 + 3.086e-6 * 1000.0
    assert gravity == pytest.approx(expected, rel=1e-6)

    # Test 5: Valid calculation with negative depth (should use absolute value)
    gravity_neg = get_local_gravity(45.0, -1000.0)
    gravity_pos = get_local_gravity(45.0, 1000.0)
    assert gravity_neg == gravity_pos

    # Test 6: Default depth parameter (should be 0.0)
    gravity_default = get_local_gravity(30.0)
    gravity_explicit = get_local_gravity(30.0, 0.0)
    assert gravity_default == gravity_explicit

    # Test 7: Integer inputs (should be accepted)
    gravity_int = get_local_gravity(45, 500)
    gravity_float = get_local_gravity(45.0, 500.0)
    assert gravity_int == gravity_float

    # Test 8: Boundary latitude values
    gravity_min = get_local_gravity(-90.0)
    gravity_max = get_local_gravity(90.0)
    assert isinstance(gravity_min, float)
    assert isinstance(gravity_max, float)
    assert gravity_min > 0
    assert gravity_max > 0

    # Test 9: Type validation - invalid latitude type
    with pytest.raises(TypeError):
        get_local_gravity("45.0", 0.0)

    # Test 10: Type validation - invalid depth type
    with pytest.raises(TypeError):
        get_local_gravity(45.0, "1000")

    # Test 11: Type validation - None values
    with pytest.raises(TypeError):
        get_local_gravity(None, 0.0)

    with pytest.raises(TypeError):
        get_local_gravity(45.0, None)

    # Test 12: Value validation - latitude out of range (too high)
    with pytest.raises(ValueError):
        get_local_gravity(91.0, 0.0)

    # Test 13: Value validation - latitude out of range (too low)
    with pytest.raises(ValueError):
        get_local_gravity(-91.0, 0.0)

    # Test 14: Value validation - latitude way out of range
    with pytest.raises(ValueError):
        get_local_gravity(180.0, 0.0)

    with pytest.raises(ValueError):
        get_local_gravity(-180.0, 0.0)

    # Test 15: Zero depth
    gravity_zero = get_local_gravity(0.0, 0.0)
    assert gravity_zero == pytest.approx(9.780327, rel=1e-6)

    # Test 16: Large depth value
    gravity_deep = get_local_gravity(0.0, 10000.0)
    expected_deep = 9.780327 + 3.086e-6 * 10000.0
    assert gravity_deep == pytest.approx(expected_deep, rel=1e-6)
