"""Tests for unit conversion functions."""

from __future__ import annotations

import pytest

from therminal._convert import (
    _go_round,
    convert_observation,
    convert_precipitation,
    convert_pressure,
    convert_temp,
    convert_visibility,
    convert_wind,
)
from therminal.models.observation import Observation

# ── convert_temp ───────────────────────────────────────────


def test_temp_32f_to_metric():
    assert convert_temp(32.0, "metric") == 0.0


def test_temp_212f_to_metric():
    assert convert_temp(212.0, "metric") == 100.0


def test_temp_none():
    assert convert_temp(None, "metric") is None


def test_temp_imperial_noop():
    assert convert_temp(50.0, "imperial") == 50.0


def test_temp_raw_noop():
    assert convert_temp(50.0, "raw") == 50.0


# ── convert_wind ───────────────────────────────────────────


def test_wind_metric():
    result = convert_wind(10, "metric")
    assert result == 5.1


def test_wind_imperial():
    result = convert_wind(10, "imperial")
    assert result == 11.5


def test_wind_none():
    assert convert_wind(None, "metric") is None


def test_wind_raw_noop():
    assert convert_wind(10, "raw") == 10


# ── convert_pressure ──────────────────────────────────────


def test_pressure_metric():
    result = convert_pressure(29.92, "metric")
    assert result == pytest.approx(1013.21, abs=0.1)


def test_pressure_none():
    assert convert_pressure(None, "metric") is None


def test_pressure_imperial_noop():
    assert convert_pressure(29.92, "imperial") == 29.92


def test_pressure_raw_noop():
    assert convert_pressure(29.92, "raw") == 29.92


# ── convert_visibility ────────────────────────────────────


def test_visibility_metric():
    result = convert_visibility(10.0, "metric")
    assert result == pytest.approx(16.09, abs=0.01)


def test_visibility_none():
    assert convert_visibility(None, "metric") is None


def test_visibility_raw_noop():
    assert convert_visibility(10.0, "raw") == 10.0


# ── convert_precipitation ─────────────────────────────────


def test_precip_metric():
    result = convert_precipitation(1.0, "metric")
    assert result == 25.4


def test_precip_none():
    assert convert_precipitation(None, "metric") is None


def test_precip_raw_noop():
    assert convert_precipitation(1.0, "raw") == 1.0


# ── Go-matched rounding ──────────────────────────────────


def test_go_round_half_away_from_zero():
    """Go rounds 0.5 away from zero (up), Python rounds to even.
    2.5 -> Go: 3.0, Python round(): 2.0.
    Verify _go_round matches Go behavior."""
    assert _go_round(2.5) == 3.0
    assert _go_round(-0.5) == 0.0
    assert _go_round(0.5) == 1.0


def test_go_round_with_decimals():
    assert _go_round(2.55, 1) == 2.6
    # 1.005 is 1.00499... in IEEE 754, so _go_round correctly produces 1.0
    assert _go_round(1.005, 2) == 1.0
    assert _go_round(1.125, 2) == 1.13


def test_temp_go_rounding_boundary():
    """50.05 F -> (50.05-32)*5/9 = 10.02777... -> rounded to 1 decimal = 10.0"""
    result = convert_temp(50.05, "metric")
    assert result == 10.0


# ── convert_observation ───────────────────────────────────


def _make_raw_observation() -> Observation:
    return Observation(
        station_code="NYC",
        observed_at="2026-03-29T22:00:00Z",
        observation_type="METAR",
        source="awc",
        temp_f=51.1,
        dewpoint_f=30.0,
        relative_humidity=42.0,
        wind_dir_degrees=140,
        wind_speed_kt=6,
        wind_gust_kt=15,
        altimeter_inhg=30.41,
        sea_level_pressure_mb=1029.0,
        sky_cover_1="FEW",
        sky_base_1_ft=12000,
        sky_cover_2=None,
        sky_base_2_ft=None,
        sky_cover_3=None,
        sky_base_3_ft=None,
        sky_cover_4=None,
        sky_base_4_ft=None,
        visibility_miles=10.0,
        weather_codes=None,
        precip_1hr_inches=0.5,
        peak_wind_gust_kt=20,
        peak_wind_dir=180,
        peak_wind_time=None,
        feels_like_f=48.0,
        snow_depth_inches=None,
        raw_metar="METAR KNYC test",
    )


def test_convert_observation_metric():
    obs = _make_raw_observation()
    converted = convert_observation(obs, "metric")

    # Temps converted F->C
    assert converted.temp_f == convert_temp(51.1, "metric")
    assert converted.dewpoint_f == convert_temp(30.0, "metric")
    assert converted.feels_like_f == convert_temp(48.0, "metric")

    # Wind converted kt->m/s
    assert converted.wind_speed_kt == convert_wind(6, "metric")
    assert converted.wind_gust_kt == convert_wind(15, "metric")
    assert converted.peak_wind_gust_kt == convert_wind(20, "metric")

    # Pressure converted inHg->hPa
    assert converted.altimeter_inhg == convert_pressure(30.41, "metric")

    # sea_level_pressure_mb unchanged (already metric)
    assert converted.sea_level_pressure_mb == 1029.0

    # Visibility converted mi->km
    assert converted.visibility_miles == convert_visibility(10.0, "metric")

    # Precipitation converted in->mm
    assert converted.precip_1hr_inches == convert_precipitation(0.5, "metric")

    # Passthrough fields unchanged
    assert converted.station_code == "NYC"
    assert converted.relative_humidity == 42.0
    assert converted.wind_dir_degrees == 140
    assert converted.raw_metar == "METAR KNYC test"


def test_convert_observation_imperial():
    obs = _make_raw_observation()
    converted = convert_observation(obs, "imperial")

    # Temps and pressure are no-op for imperial
    assert converted.temp_f == 51.1
    assert converted.altimeter_inhg == 30.41

    # Wind converts kt->mph
    assert converted.wind_speed_kt == convert_wind(6, "imperial")
    assert converted.wind_gust_kt == convert_wind(15, "imperial")

    # sea_level_pressure_mb still unchanged
    assert converted.sea_level_pressure_mb == 1029.0


def test_convert_observation_raw_returns_same():
    obs = _make_raw_observation()
    result = convert_observation(obs, "raw")
    assert result is obs


# ── Relative humidity (Magnus formula) ─────────────────────


class TestComputeRelativeHumidity:
    def test_typical_values(self):
        from therminal._convert import compute_relative_humidity

        rh = compute_relative_humidity(20.0, 10.0)
        assert rh is not None
        assert 50.0 < rh < 55.0  # ~52.6%

    def test_dewpoint_equals_temp_is_100(self):
        from therminal._convert import compute_relative_humidity

        assert compute_relative_humidity(15.0, 15.0) == 100.0

    def test_dewpoint_exceeds_temp_clamped_to_100(self):
        from therminal._convert import compute_relative_humidity

        rh = compute_relative_humidity(10.0, 20.0)
        assert rh == 100.0

    def test_none_temp(self):
        from therminal._convert import compute_relative_humidity

        assert compute_relative_humidity(None, 10.0) is None

    def test_none_dewpoint(self):
        from therminal._convert import compute_relative_humidity

        assert compute_relative_humidity(20.0, None) is None

    def test_non_numeric_string(self):
        from therminal._convert import compute_relative_humidity

        assert compute_relative_humidity("bad", 10.0) is None

    def test_cross_check_with_iem(self):
        """IEM reports 70.9% for temp_f=48, dewpoint_f=39. Verify."""
        from therminal._convert import compute_relative_humidity

        t_c = (48.0 - 32) * 5 / 9  # 8.889°C
        td_c = (39.0 - 32) * 5 / 9  # 3.889°C
        rh = compute_relative_humidity(t_c, td_c)
        assert rh is not None
        assert abs(rh - 70.9) < 0.5

    def test_extreme_cold_no_crash(self):
        from therminal._convert import compute_relative_humidity

        # Near singularity but should not crash
        rh = compute_relative_humidity(-243.0, -243.0)
        assert rh is None or 0.0 <= rh <= 100.0
