"""Tests for NMEA sentence data classes and parsing utilities."""

import pytest
from datetime import datetime, timezone

from fishinfo.nmea.sentences import (
    parse_float,
    parse_int,
    parse_latitude,
    parse_longitude,
    parse_time,
    FixQuality,
)


class TestParseLatitude:
    """Tests for latitude parsing."""

    def test_parse_latitude_north(self) -> None:
        """Test parsing northern latitude."""
        result = parse_latitude("4807.038", "N")
        assert result == pytest.approx(48.1173, rel=1e-4)

    def test_parse_latitude_south(self) -> None:
        """Test parsing southern latitude."""
        result = parse_latitude("3345.678", "S")
        assert result == pytest.approx(-33.7613, rel=1e-4)

    def test_parse_latitude_zero(self) -> None:
        """Test parsing latitude at equator."""
        result = parse_latitude("0000.000", "N")
        assert result == pytest.approx(0.0, abs=1e-6)

    def test_parse_latitude_max_north(self) -> None:
        """Test parsing maximum northern latitude."""
        result = parse_latitude("9000.000", "N")
        assert result == pytest.approx(90.0, abs=1e-6)

    def test_parse_latitude_max_south(self) -> None:
        """Test parsing maximum southern latitude."""
        result = parse_latitude("9000.000", "S")
        assert result == pytest.approx(-90.0, abs=1e-6)

    def test_parse_latitude_with_fractional_minutes(self) -> None:
        """Test parsing latitude with fractional minutes."""
        result = parse_latitude("4807.5", "N")
        assert result == pytest.approx(48.125, rel=1e-4)

    def test_parse_latitude_empty_string(self) -> None:
        """Test parsing empty latitude string."""
        result = parse_latitude("", "N")
        assert result is None

    def test_parse_latitude_empty_hemisphere(self) -> None:
        """Test parsing latitude with empty hemisphere."""
        result = parse_latitude("4807.038", "")
        assert result is None

    def test_parse_latitude_invalid_format(self) -> None:
        """Test parsing invalid latitude format."""
        result = parse_latitude("invalid", "N")
        assert result is None

    def test_parse_latitude_single_digit_degrees(self) -> None:
        """Test parsing latitude with single digit degrees."""
        result = parse_latitude("0530.000", "N")
        assert result == pytest.approx(5.5, rel=1e-4)


class TestParseLongitude:
    """Tests for longitude parsing."""

    def test_parse_longitude_east(self) -> None:
        """Test parsing eastern longitude."""
        result = parse_longitude("01131.000", "E")
        assert result == pytest.approx(11.5167, rel=1e-4)

    def test_parse_longitude_west(self) -> None:
        """Test parsing western longitude."""
        result = parse_longitude("12245.123", "W")
        assert result == pytest.approx(-122.7520, rel=1e-4)

    def test_parse_longitude_zero(self) -> None:
        """Test parsing longitude at prime meridian."""
        result = parse_longitude("00000.000", "E")
        assert result == pytest.approx(0.0, abs=1e-6)

    def test_parse_longitude_max_east(self) -> None:
        """Test parsing maximum eastern longitude."""
        result = parse_longitude("18000.000", "E")
        assert result == pytest.approx(180.0, abs=1e-6)

    def test_parse_longitude_max_west(self) -> None:
        """Test parsing maximum western longitude."""
        result = parse_longitude("18000.000", "W")
        assert result == pytest.approx(-180.0, abs=1e-6)

    def test_parse_longitude_with_fractional_minutes(self) -> None:
        """Test parsing longitude with fractional minutes."""
        result = parse_longitude("12030.5", "W")
        assert result == pytest.approx(-120.5083, rel=1e-4)

    def test_parse_longitude_empty_string(self) -> None:
        """Test parsing empty longitude string."""
        result = parse_longitude("", "E")
        assert result is None

    def test_parse_longitude_empty_hemisphere(self) -> None:
        """Test parsing longitude with empty hemisphere."""
        result = parse_longitude("01131.000", "")
        assert result is None

    def test_parse_longitude_invalid_format(self) -> None:
        """Test parsing invalid longitude format."""
        result = parse_longitude("invalid", "E")
        assert result is None

    def test_parse_longitude_three_digit_degrees(self) -> None:
        """Test parsing longitude with three digit degrees."""
        result = parse_longitude("17530.000", "W")
        assert result == pytest.approx(-175.5, rel=1e-4)


class TestParseTime:
    """Tests for time parsing."""

    def test_parse_time_basic(self) -> None:
        """Test parsing basic time."""
        result = parse_time("123519")
        assert result is not None
        assert result.hour == 12
        assert result.minute == 35
        assert result.second == 19
        assert result.tzinfo == timezone.utc

    def test_parse_time_with_fractional_seconds(self) -> None:
        """Test parsing time with fractional seconds."""
        result = parse_time("123519.123")
        assert result is not None
        assert result.hour == 12
        assert result.minute == 35
        assert result.second == 19
        assert result.microsecond == 123000

    def test_parse_time_with_date(self) -> None:
        """Test parsing time with date."""
        result = parse_time("123519", "230394")
        assert result is not None
        assert result.year == 1994
        assert result.month == 3
        assert result.day == 23
        assert result.hour == 12
        assert result.minute == 35
        assert result.second == 19

    def test_parse_time_with_date_2000s(self) -> None:
        """Test parsing time with date in 2000s."""
        result = parse_time("083045", "150623")
        assert result is not None
        assert result.year == 2023
        assert result.month == 6
        assert result.day == 15
        assert result.hour == 8
        assert result.minute == 30
        assert result.second == 45

    def test_parse_time_midnight(self) -> None:
        """Test parsing midnight time."""
        result = parse_time("000000")
        assert result is not None
        assert result.hour == 0
        assert result.minute == 0
        assert result.second == 0

    def test_parse_time_end_of_day(self) -> None:
        """Test parsing end of day time."""
        result = parse_time("235959")
        assert result is not None
        assert result.hour == 23
        assert result.minute == 59
        assert result.second == 59

    def test_parse_time_empty_string(self) -> None:
        """Test parsing empty time string."""
        result = parse_time("")
        assert result is None

    def test_parse_time_invalid_format(self) -> None:
        """Test parsing invalid time format."""
        result = parse_time("invalid")
        assert result is None

    def test_parse_time_too_short(self) -> None:
        """Test parsing time string too short."""
        result = parse_time("1234")
        assert result is None

    def test_parse_time_with_microseconds(self) -> None:
        """Test parsing time with various fractional second lengths."""
        # 1 digit - padded to 6
        result = parse_time("123519.1")
        assert result is not None
        assert result.microsecond == 100000

        # 3 digits - padded to 6
        result = parse_time("123519.123")
        assert result is not None
        assert result.microsecond == 123000

        # 6 digits - exact
        result = parse_time("123519.123456")
        assert result is not None
        assert result.microsecond == 123456

        # More than 6 digits - truncated
        result = parse_time("123519.1234567890")
        assert result is not None
        assert result.microsecond == 123456


class TestParseFloat:
    """Tests for float parsing."""

    def test_parse_float_basic(self) -> None:
        """Test parsing basic float."""
        result = parse_float("123.45")
        assert result == 123.45

    def test_parse_float_integer(self) -> None:
        """Test parsing integer as float."""
        result = parse_float("123")
        assert result == 123.0

    def test_parse_float_negative(self) -> None:
        """Test parsing negative float."""
        result = parse_float("-123.45")
        assert result == -123.45

    def test_parse_float_zero(self) -> None:
        """Test parsing zero."""
        result = parse_float("0")
        assert result == 0.0

    def test_parse_float_very_small(self) -> None:
        """Test parsing very small float."""
        result = parse_float("0.0001")
        assert result == 0.0001

    def test_parse_float_very_large(self) -> None:
        """Test parsing very large float."""
        result = parse_float("999999.999")
        assert result == 999999.999

    def test_parse_float_scientific_notation(self) -> None:
        """Test parsing scientific notation."""
        result = parse_float("1.23e5")
        assert result == 123000.0

    def test_parse_float_empty_string(self) -> None:
        """Test parsing empty string."""
        result = parse_float("")
        assert result is None

    def test_parse_float_invalid(self) -> None:
        """Test parsing invalid float."""
        result = parse_float("invalid")
        assert result is None

    def test_parse_float_with_spaces(self) -> None:
        """Test parsing float with spaces."""
        # Should fail - spaces not allowed
        result = parse_float(" 123.45 ")
        assert result is None


class TestParseInt:
    """Tests for integer parsing."""

    def test_parse_int_basic(self) -> None:
        """Test parsing basic integer."""
        result = parse_int("123")
        assert result == 123

    def test_parse_int_zero(self) -> None:
        """Test parsing zero."""
        result = parse_int("0")
        assert result == 0

    def test_parse_int_negative(self) -> None:
        """Test parsing negative integer."""
        result = parse_int("-123")
        assert result == -123

    def test_parse_int_very_large(self) -> None:
        """Test parsing very large integer."""
        result = parse_int("999999999")
        assert result == 999999999

    def test_parse_int_empty_string(self) -> None:
        """Test parsing empty string."""
        result = parse_int("")
        assert result is None

    def test_parse_int_invalid(self) -> None:
        """Test parsing invalid integer."""
        result = parse_int("invalid")
        assert result is None

    def test_parse_int_float(self) -> None:
        """Test parsing float as integer."""
        result = parse_int("123.45")
        # Should fail - not an integer
        assert result is None

    def test_parse_int_with_spaces(self) -> None:
        """Test parsing integer with spaces."""
        # Should fail - spaces not allowed
        result = parse_int(" 123 ")
        assert result is None


class TestFixQuality:
    """Tests for FixQuality enum."""

    def test_fix_quality_values(self) -> None:
        """Test all FixQuality enum values."""
        assert FixQuality.INVALID.value == 0
        assert FixQuality.GPS_FIX.value == 1
        assert FixQuality.DGPS_FIX.value == 2
        assert FixQuality.PPS_FIX.value == 3
        assert FixQuality.RTK_FIX.value == 4
        assert FixQuality.RTK_FLOAT.value == 5
        assert FixQuality.ESTIMATED.value == 6
        assert FixQuality.MANUAL.value == 7
        assert FixQuality.SIMULATION.value == 8

    def test_fix_quality_from_int(self) -> None:
        """Test creating FixQuality from integer."""
        quality = FixQuality(1)
        assert quality == FixQuality.GPS_FIX

    def test_fix_quality_comparison(self) -> None:
        """Test comparing FixQuality values."""
        assert FixQuality.GPS_FIX == FixQuality.GPS_FIX
        assert FixQuality.GPS_FIX != FixQuality.DGPS_FIX


class TestCoordinateEdgeCases:
    """Tests for edge cases in coordinate parsing."""

    def test_latitude_boundary_conversion(self) -> None:
        """Test latitude conversion at boundaries."""
        # Test exact degree boundaries
        result = parse_latitude("4800.000", "N")
        assert result == pytest.approx(48.0, abs=1e-6)

        # Test exact minute boundaries
        result = parse_latitude("4830.000", "N")
        assert result == pytest.approx(48.5, abs=1e-6)

    def test_longitude_boundary_conversion(self) -> None:
        """Test longitude conversion at boundaries."""
        # Test exact degree boundaries
        result = parse_longitude("12000.000", "W")
        assert result == pytest.approx(-120.0, abs=1e-6)

        # Test exact minute boundaries
        result = parse_longitude("12030.000", "W")
        assert result == pytest.approx(-120.5, abs=1e-6)

    def test_coordinate_precision(self) -> None:
        """Test coordinate parsing maintains precision."""
        # High precision latitude
        lat = parse_latitude("4807.0383333", "N")
        assert lat is not None
        assert lat == pytest.approx(48.1173055, rel=1e-6)

        # High precision longitude
        lon = parse_longitude("01131.0005", "E")
        assert lon is not None
        assert lon == pytest.approx(11.5166750, rel=1e-6)

    def test_coordinate_rounding(self) -> None:
        """Test coordinate parsing handles rounding correctly."""
        # Test values that might have rounding issues
        result = parse_latitude("3333.333", "N")
        assert result is not None
        assert result == pytest.approx(33.5555, rel=1e-4)


class TestTimeEdgeCases:
    """Tests for edge cases in time parsing."""

    def test_time_year_boundary(self) -> None:
        """Test time parsing at year boundary."""
        # Year 49 should be 2049
        result = parse_time("000000", "010149")
        assert result is not None
        assert result.year == 2049

        # Year 50 should be 1950
        result = parse_time("000000", "010150")
        assert result is not None
        assert result.year == 1950

    def test_time_leap_second(self) -> None:
        """Test time parsing with leap second."""
        # NMEA doesn't really handle leap seconds, but test 60 seconds
        result = parse_time("235960")
        # Should be parsed as second 60 (not normalized)
        assert result is not None
        # Python will normalize this to next minute
        assert result.minute == 0 and result.second == 0 or result.second == 60

    def test_time_fractional_precision(self) -> None:
        """Test time parsing maintains fractional precision."""
        result = parse_time("123519.999999")
        assert result is not None
        assert result.microsecond == 999999

    def test_time_invalid_date(self) -> None:
        """Test time parsing with invalid date."""
        # Invalid date (February 30)
        result = parse_time("123519", "300294")
        # Should raise ValueError or return None
        assert result is None or isinstance(result, datetime)
