"""Tests for timestamp conversion helpers in grpc/_mixins/converters.py."""

from __future__ import annotations

from datetime import UTC, datetime, timedelta, timezone

import pytest
from google.protobuf.timestamp_pb2 import Timestamp

from noteflow.grpc._mixins.converters import (
    datetime_to_epoch_seconds,
    datetime_to_iso_string,
    datetime_to_proto_timestamp,
    epoch_seconds_to_datetime,
    iso_string_to_datetime,
    proto_timestamp_to_datetime,
)

# Test constants for timestamp conversion tests
TEST_YEAR = 2024
TEST_MONTH = 6
TEST_DAY = 15
TEST_HOUR = 14
TEST_MINUTE = 30
TEST_SECOND = 45


class TestDatetimeProtoTimestampConversion:
    """Test datetime <-> protobuf Timestamp conversions."""

    def test_datetime_to_proto_timestamp_computes_correct_epoch(self) -> None:
        """Convert datetime to proto Timestamp with correct epoch seconds."""
        dt = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)
        expected_epoch = 1718461845

        ts = datetime_to_proto_timestamp(dt)

        assert ts.seconds == expected_epoch, (
            f"Expected epoch {expected_epoch}, got {ts.seconds}"
        )

    def test_proto_timestamp_to_datetime_returns_utc(self) -> None:
        """Convert proto Timestamp to datetime with UTC timezone."""
        ts = Timestamp()
        ts.FromDatetime(datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC))

        dt = proto_timestamp_to_datetime(ts)

        assert dt.tzinfo == UTC, "Returned datetime should have UTC timezone"

    def test_datetime_proto_timestamp_roundtrip(self) -> None:
        """Datetime -> proto Timestamp -> datetime roundtrip preserves value."""
        original = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)

        ts = datetime_to_proto_timestamp(original)
        result = proto_timestamp_to_datetime(ts)

        # Protobuf timestamps have nanosecond precision, datetime has microsecond
        assert result.year == original.year, "Year should be preserved"
        assert result.month == original.month, "Month should be preserved"
        assert result.day == original.day, "Day should be preserved"
        assert result.hour == original.hour, "Hour should be preserved"
        assert result.minute == original.minute, "Minute should be preserved"
        assert result.second == original.second, "Second should be preserved"


class TestEpochSecondsConversion:
    """Test epoch seconds <-> datetime conversions."""

    def test_epoch_seconds_to_datetime_returns_utc(self) -> None:
        """Convert epoch seconds to datetime with UTC timezone."""
        # 2024-06-15 14:30:45 UTC
        epoch_seconds = 1718458245.0

        dt = epoch_seconds_to_datetime(epoch_seconds)

        assert dt.tzinfo == UTC, "Returned datetime should have UTC timezone"
        assert dt.year == TEST_YEAR, f"Expected year {TEST_YEAR}, got {dt.year}"

    def test_datetime_to_epoch_seconds_computes_correct_value(self) -> None:
        """Convert datetime to epoch seconds with correct value."""
        dt = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)
        expected_seconds = 1718461845.0

        seconds = datetime_to_epoch_seconds(dt)

        assert seconds == expected_seconds, (
            f"Expected {expected_seconds}, got {seconds}"
        )

    def test_epoch_seconds_roundtrip(self) -> None:
        """Epoch seconds -> datetime -> epoch seconds roundtrip preserves value."""
        original_seconds = 1718458245.5  # Include fractional seconds

        dt = epoch_seconds_to_datetime(original_seconds)
        result_seconds = datetime_to_epoch_seconds(dt)

        # Allow small floating point tolerance
        tolerance = 0.001
        assert abs(result_seconds - original_seconds) < tolerance, (
            f"Expected {original_seconds}, got {result_seconds}"
        )

    @pytest.mark.parametrize(
        ("seconds", "expected_year"),
        [
            pytest.param(0.0, 1970, id="unix_epoch_start"),
            pytest.param(1000000000.0, 2001, id="billion_seconds"),
            pytest.param(1718458245.0, TEST_YEAR, id="recent_date"),
        ],
    )
    def test_epoch_seconds_to_datetime_various_values(
        self, seconds: float, expected_year: int
    ) -> None:
        """Convert various epoch seconds values to datetime."""
        dt = epoch_seconds_to_datetime(seconds)

        assert dt.year == expected_year, f"Expected year {expected_year}, got {dt.year}"


class TestIsoStringConversion:
    """Test ISO 8601 string <-> datetime conversions."""

    def test_iso_string_with_z_suffix_parsed_as_utc(self) -> None:
        """Parse ISO string with Z suffix as UTC datetime."""
        iso_str = "2024-06-15T14:30:45Z"

        dt = iso_string_to_datetime(iso_str)

        assert dt.tzinfo == UTC, "Z suffix should be parsed as UTC"
        assert dt.year == TEST_YEAR, f"Expected year {TEST_YEAR}, got {dt.year}"
        assert dt.month == TEST_MONTH, f"Expected month {TEST_MONTH}, got {dt.month}"
        assert dt.day == TEST_DAY, f"Expected day {TEST_DAY}, got {dt.day}"
        assert dt.hour == TEST_HOUR, f"Expected hour {TEST_HOUR}, got {dt.hour}"
        assert dt.minute == TEST_MINUTE, f"Expected minute {TEST_MINUTE}, got {dt.minute}"
        assert dt.second == TEST_SECOND, f"Expected second {TEST_SECOND}, got {dt.second}"

    def test_iso_string_with_offset_preserved(self) -> None:
        """Parse ISO string with timezone offset."""
        iso_str = "2024-06-15T14:30:45+05:30"

        dt = iso_string_to_datetime(iso_str)

        assert dt.tzinfo is not None, "Timezone should be preserved"
        # +05:30 offset
        expected_offset = timezone(offset=timedelta(hours=5, minutes=30))
        assert dt.utcoffset() == expected_offset.utcoffset(None), (
            "Timezone offset should be preserved"
        )

    def test_iso_string_without_timezone_defaults_to_utc(self) -> None:
        """Parse ISO string without timezone, defaulting to UTC."""
        iso_str = "2024-06-15T14:30:45"

        dt = iso_string_to_datetime(iso_str)

        assert dt.tzinfo == UTC, "Missing timezone should default to UTC"

    def test_datetime_to_iso_string_includes_timezone(self) -> None:
        """Format datetime as ISO string with timezone."""
        dt = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)

        iso_str = datetime_to_iso_string(dt)

        assert f"{TEST_YEAR}-0{TEST_MONTH}-{TEST_DAY}" in iso_str, f"Date should be in output: {iso_str}"
        assert f"{TEST_HOUR}:{TEST_MINUTE}:{TEST_SECOND}" in iso_str, f"Time should be in output: {iso_str}"
        # UTC represented as +00:00 in isoformat
        assert "+00:00" in iso_str or "Z" in iso_str, (
            f"Timezone should be in output: {iso_str}"
        )

    def test_iso_string_roundtrip(self) -> None:
        """Datetime -> ISO string -> datetime roundtrip preserves value."""
        original = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)

        iso_str = datetime_to_iso_string(original)
        result = iso_string_to_datetime(iso_str)

        assert result == original, f"Roundtrip should preserve datetime: {original} -> {iso_str} -> {result}"

    @pytest.mark.parametrize(
        ("iso_str", "expected_hour"),
        [
            pytest.param("2024-06-15T00:00:00Z", 0, id="midnight"),
            pytest.param("2024-06-15T12:00:00Z", 12, id="noon"),
            pytest.param("2024-06-15T23:59:59Z", 23, id="end_of_day"),
        ],
    )
    def test_iso_string_various_times(self, iso_str: str, expected_hour: int) -> None:
        """Parse various ISO string time values."""
        dt = iso_string_to_datetime(iso_str)

        assert dt.hour == expected_hour, f"Expected hour {expected_hour}, got {dt.hour}"
