import pytest
from datetime import datetime, date, timezone, timedelta
from zoneinfo import ZoneInfo
from atomsql.orm.models import Model
from atomsql.orm.fields import StringField, DateTimeTZField


class Event(Model):
    name = StringField()
    scheduled_at = DateTimeTZField(nullable=True)


class StrictEvent(Model):
    name = StringField()
    scheduled_at = DateTimeTZField(nullable=False)


def test_datetimetz_field_accepts_timezone_aware_datetime(db):
    """DateTimeTZ should accept timezone-aware datetime objects."""
    db.register(Event)

    # UTC timezone
    dt_utc = datetime(2026, 1, 4, 14, 30, 0, tzinfo=timezone.utc)
    event = Event(name="UTC Event", scheduled_at=dt_utc)
    assert event.scheduled_at == dt_utc
    assert event.scheduled_at.tzinfo is not None


def test_datetimetz_field_accepts_various_timezones(db):
    """DateTimeTZ should accept datetime objects with various timezones."""
    db.register(Event)

    # US Eastern timezone
    dt_eastern = datetime(2026, 1, 4, 14, 30, 0, tzinfo=ZoneInfo("America/New_York"))
    event1 = Event(name="Eastern Event", scheduled_at=dt_eastern)
    assert event1.scheduled_at == dt_eastern

    # Europe/London timezone
    dt_london = datetime(2026, 1, 4, 14, 30, 0, tzinfo=ZoneInfo("Europe/London"))
    event2 = Event(name="London Event", scheduled_at=dt_london)
    assert event2.scheduled_at == dt_london

    # Asia/Tokyo timezone
    dt_tokyo = datetime(2026, 1, 4, 14, 30, 0, tzinfo=ZoneInfo("Asia/Tokyo"))
    event3 = Event(name="Tokyo Event", scheduled_at=dt_tokyo)
    assert event3.scheduled_at == dt_tokyo


def test_datetimetz_field_accepts_custom_timezone_offset(db):
    """DateTimeTZ should accept datetime with custom timezone offset."""
    db.register(Event)

    # Custom timezone offset (+05:30 for India)
    custom_tz = timezone(timedelta(hours=5, minutes=30))
    dt_custom = datetime(2026, 1, 4, 14, 30, 0, tzinfo=custom_tz)
    event = Event(name="Custom TZ Event", scheduled_at=dt_custom)
    assert event.scheduled_at == dt_custom
    assert event.scheduled_at.tzinfo is not None


def test_datetimetz_field_rejects_naive_datetime(db):
    """DateTimeTZ should reject naive datetime objects."""
    db.register(Event)

    naive_dt = datetime(2026, 1, 4, 14, 30, 0)
    with pytest.raises(
        ValueError, match="expected a timezone-aware datetime, got naive datetime"
    ):
        Event(name="Naive Event", scheduled_at=naive_dt)


def test_datetimetz_field_rejects_date_object(db):
    """DateTimeTZ should reject date objects."""
    db.register(Event)

    d = date(2026, 1, 4)
    with pytest.raises(ValueError, match="expected a datetime"):
        Event(name="Date Event", scheduled_at=d)


def test_datetimetz_field_rejects_string(db):
    """DateTimeTZ should reject string values."""
    db.register(Event)

    with pytest.raises(ValueError, match="expected a datetime"):
        Event(name="String Event", scheduled_at="2026-01-04 14:30:00+00:00")


def test_datetimetz_field_rejects_invalid_types(db):
    """DateTimeTZ should reject non-datetime types."""
    db.register(Event)

    with pytest.raises(ValueError, match="expected a datetime"):
        Event(name="Invalid", scheduled_at=12345)

    with pytest.raises(ValueError, match="expected a datetime"):
        Event(name="Invalid", scheduled_at=123.45)

    with pytest.raises(ValueError, match="expected a datetime"):
        Event(name="Invalid", scheduled_at=["2026-01-04"])


def test_datetimetz_field_null_handling_when_nullable(db):
    """DateTimeTZ should accept None when nullable=True."""
    db.register(Event)

    event = Event(name="TBD Event", scheduled_at=None)
    assert event.scheduled_at is None

    # Should persist and retrieve None
    event.save()
    retrieved = Event.objects().filter(name="TBD Event")[0]
    assert retrieved.scheduled_at is None


def test_datetimetz_field_rejects_null_when_not_nullable(db):
    """DateTimeTZ should reject None when nullable=False."""
    db.register(StrictEvent)

    with pytest.raises(ValueError, match="cannot be None"):
        StrictEvent(name="Required Event", scheduled_at=None)


def test_datetimetz_field_persistence_and_retrieval_utc(db):
    """DateTimeTZ should correctly persist and retrieve UTC datetime values."""
    db.register(Event)

    original_dt = datetime(2026, 6, 15, 18, 45, 30, tzinfo=timezone.utc)
    event = Event(name="Product Launch", scheduled_at=original_dt)
    event.save()

    retrieved = Event.objects().filter(name="Product Launch")[0]
    assert retrieved.scheduled_at is not None
    # Check the datetime value (may need to handle timezone conversion)
    if retrieved.scheduled_at.tzinfo is not None:
        # If timezone is preserved, compare directly
        assert (
            retrieved.scheduled_at == original_dt
            or retrieved.scheduled_at.replace(tzinfo=timezone.utc) == original_dt
        )


def test_datetimetz_field_persistence_with_offset(db):
    """DateTimeTZ should persist datetime with timezone offset."""
    db.register(Event)

    # +05:30 timezone
    tz = timezone(timedelta(hours=5, minutes=30))
    dt = datetime(2026, 1, 4, 14, 30, 0, tzinfo=tz)
    event = Event(name="IST Event", scheduled_at=dt)
    event.save()

    retrieved = Event.objects().filter(name="IST Event")[0]
    assert retrieved.scheduled_at is not None


def test_datetimetz_field_filtering_exact_match(db):
    """DateTimeTZ should support exact match filtering."""
    db.register(Event)

    dt1 = datetime(2026, 1, 4, 10, 0, 0, tzinfo=timezone.utc)
    dt2 = datetime(2026, 1, 5, 15, 0, 0, tzinfo=timezone.utc)

    Event(name="Morning Event", scheduled_at=dt1).save()
    Event(name="Afternoon Event", scheduled_at=dt2).save()

    results = list(Event.objects().filter(scheduled_at=dt1))
    assert len(results) >= 1  # Should find at least one
    # Note: Exact timezone matching may vary by backend


def test_datetimetz_field_filtering_comparison_operators(db):
    """DateTimeTZ should support comparison operators in filters."""
    db.register(Event)

    dt1 = datetime(2026, 1, 1, 8, 0, 0, tzinfo=timezone.utc)
    dt2 = datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
    dt3 = datetime(2026, 1, 31, 18, 0, 0, tzinfo=timezone.utc)

    Event(name="Early Event", scheduled_at=dt1).save()
    Event(name="Mid Event", scheduled_at=dt2).save()
    Event(name="Late Event", scheduled_at=dt3).save()

    # Greater than
    later_events = list(Event.objects().filter(scheduled_at__gt=dt1))
    assert len(later_events) >= 2

    # Less than or equal
    earlier_events = list(Event.objects().filter(scheduled_at__lte=dt2))
    assert len(earlier_events) >= 2


def test_datetimetz_field_with_exists(db):
    """DateTimeTZ should work with exists() queries."""
    db.register(Event)

    dt = datetime(2026, 3, 20, 9, 30, 0, tzinfo=timezone.utc)
    Event(name="Workshop", scheduled_at=dt).save()

    exists = Event.objects().filter(scheduled_at=dt).exists()
    # Backend may or may not preserve exact timezone info
    assert exists is True or Event.objects().filter(name="Workshop").exists()


def test_datetimetz_field_default_value(db):
    """DateTimeTZ should support default values."""
    default_dt = datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc)

    class ScheduledTask(Model):
        name = StringField()
        scheduled_at = DateTimeTZField(default=default_dt)

    db.register(ScheduledTask)

    task = ScheduledTask(name="Backup")
    assert task.scheduled_at == default_dt
    assert task.scheduled_at.tzinfo is not None


def test_datetimetz_field_update_value(db):
    """DateTimeTZ should allow updating values."""
    db.register(Event)

    original_dt = datetime(2026, 1, 4, 10, 0, 0, tzinfo=timezone.utc)
    new_dt = datetime(2026, 1, 5, 14, 0, 0, tzinfo=timezone.utc)

    event = Event(name="Rescheduled Event", scheduled_at=original_dt)
    event.save()

    # Update the datetime
    event.scheduled_at = new_dt
    event.save()

    retrieved = Event.objects().filter(name="Rescheduled Event")[0]
    assert retrieved.scheduled_at is not None


def test_datetimetz_field_ordering(db):
    """DateTimeTZ should support ordering."""
    db.register(Event)

    dt1 = datetime(2026, 1, 10, 9, 0, 0, tzinfo=timezone.utc)
    dt2 = datetime(2026, 1, 5, 14, 0, 0, tzinfo=timezone.utc)
    dt3 = datetime(2026, 1, 20, 11, 0, 0, tzinfo=timezone.utc)

    Event(name="Event2", scheduled_at=dt1).save()
    Event(name="Event1", scheduled_at=dt2).save()
    Event(name="Event3", scheduled_at=dt3).save()

    # Order by datetime
    ordered = list(Event.objects().order_by("scheduled_at"))
    assert len(ordered) == 3


def test_datetimetz_field_unique_constraint(db):
    """DateTimeTZ should support unique constraint."""

    class UniqueEvent(Model):
        name = StringField()
        scheduled_at = DateTimeTZField(unique=True, nullable=False)

    db.register(UniqueEvent)

    dt = datetime(2026, 1, 4, 10, 0, 0, tzinfo=timezone.utc)
    event1 = UniqueEvent(name="First", scheduled_at=dt)
    event1.save()


def test_datetimetz_field_with_different_timezones_same_instant(db):
    """DateTimeTZ with different timezones representing same instant."""
    db.register(Event)

    # Same instant in different timezones
    # 10:00 UTC = 05:00 EST (UTC-5)
    dt_utc = datetime(2026, 1, 4, 10, 0, 0, tzinfo=timezone.utc)
    dt_est = datetime(2026, 1, 4, 5, 0, 0, tzinfo=timezone(timedelta(hours=-5)))

    event1 = Event(name="UTC Event", scheduled_at=dt_utc)
    event1.save()

    event2 = Event(name="EST Event", scheduled_at=dt_est)
    event2.save()

    # Both should be saved successfully
    results = list(Event.objects())
    assert len(results) >= 2


def test_datetimetz_field_binary_expressions(db):
    """DateTimeTZ should support binary expression operators."""
    db.register(Event)

    dt = datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc)

    # Test __eq__
    expr_eq = Event.scheduled_at == dt
    assert expr_eq.operator == "="
    assert expr_eq.value == dt

    # Test __ne__
    expr_ne = Event.scheduled_at != dt
    assert expr_ne.operator == "!="

    # Test __lt__
    expr_lt = Event.scheduled_at < dt
    assert expr_lt.operator == "<"

    # Test __le__
    expr_le = Event.scheduled_at <= dt
    assert expr_le.operator == "<="

    # Test __gt__
    expr_gt = Event.scheduled_at > dt
    assert expr_gt.operator == ">"

    # Test __ge__
    expr_ge = Event.scheduled_at >= dt
    assert expr_ge.operator == ">="


def test_datetimetz_field_sql_type(db):
    """DateTimeTZ should return correct SQL type."""
    db.register(Event)

    field = Event._fields["scheduled_at"]
    sql_type = field.get_sql_type()
    assert sql_type == "TEXT"


def test_datetimetz_field_column_name(db):
    """DateTimeTZ should return correct column name."""
    db.register(Event)

    field = Event._fields["scheduled_at"]
    column_name = field.get_column_name()
    assert column_name == "scheduled_at"


def test_datetimetz_field_with_microseconds(db):
    """DateTimeTZ should handle datetime with microseconds."""
    db.register(Event)

    dt = datetime(2026, 1, 4, 14, 30, 0, 123456, tzinfo=timezone.utc)
    event = Event(name="Precise Event", scheduled_at=dt)
    assert event.scheduled_at.microsecond == 123456
    assert event.scheduled_at.tzinfo is not None


def test_datetimetz_field_with_aggregations(db):
    """DateTimeTZ should work with aggregation queries."""
    db.register(Event)

    dt1 = datetime(2026, 1, 1, 10, 0, 0, tzinfo=timezone.utc)
    dt2 = datetime(2026, 1, 15, 14, 0, 0, tzinfo=timezone.utc)
    dt3 = datetime(2026, 1, 31, 18, 0, 0, tzinfo=timezone.utc)

    Event(name="Event1", scheduled_at=dt1).save()
    Event(name="Event2", scheduled_at=dt2).save()
    Event(name="Event3", scheduled_at=dt3).save()

    # Count events
    count = Event.objects().count()
    assert count == 3


def test_datetimetz_field_exclude_filter(db):
    """DateTimeTZ should work with exclude() filters."""
    db.register(Event)

    dt1 = datetime(2026, 1, 10, 9, 0, 0, tzinfo=timezone.utc)
    dt2 = datetime(2026, 1, 20, 15, 0, 0, tzinfo=timezone.utc)

    Event(name="Early", scheduled_at=dt1).save()
    Event(name="Late", scheduled_at=dt2).save()

    # Exclude specific datetime
    results = list(Event.objects().exclude(name="Early"))
    assert len(results) >= 1


def test_datetimetz_field_dst_transition(db):
    """DateTimeTZ should handle DST transition times."""
    db.register(Event)

    # Create datetime during DST transition (if applicable for timezone)
    try:
        dt = datetime(2026, 3, 8, 2, 30, 0, tzinfo=ZoneInfo("America/New_York"))
        event = Event(name="DST Event", scheduled_at=dt)
        assert event.scheduled_at.tzinfo is not None
    except Exception:
        # Some DST times may not exist
        pass


# PostgreSQL-specific tests
@pytest.mark.postgres
def test_datetimetz_postgres_timestamp_with_timezone(db):
    """Test that PostgreSQL stores DateTimeTZ as TIMESTAMP WITH TIME ZONE."""
    # Skip if not PostgreSQL
    if not db.backend.__class__.__name__ == "PostgresBackend":
        pytest.skip("PostgreSQL-only test")

    db.register(Event)

    # Check field type in PostgreSQL
    if hasattr(db.backend, "get_field_type"):
        field = Event._fields["scheduled_at"]
        field_type = db.backend.get_field_type(field)
        # PostgreSQL should use TIMESTAMP WITH TIME ZONE
        assert "TIMESTAMP" in field_type.upper()


@pytest.mark.postgres
def test_datetimetz_postgres_timezone_preservation(db):
    """Test that PostgreSQL preserves timezone information."""
    # Skip if not PostgreSQL
    if not db.backend.__class__.__name__ == "PostgresBackend":
        pytest.skip("PostgreSQL-only test")

    db.register(Event)

    # Use a specific timezone
    dt = datetime(2026, 1, 4, 14, 30, 0, tzinfo=ZoneInfo("America/New_York"))
    event = Event(name="NYC Event", scheduled_at=dt)
    event.save()

    retrieved = Event.objects().filter(name="NYC Event")[0]
    assert retrieved.scheduled_at is not None
    # PostgreSQL should preserve timezone-aware datetime
    if hasattr(retrieved.scheduled_at, "tzinfo"):
        assert retrieved.scheduled_at.tzinfo is not None


@pytest.mark.postgres
def test_datetimetz_postgres_at_time_zone(db):
    """Test PostgreSQL AT TIME ZONE functionality."""
    db.register(Event)

    dt_utc = datetime(2026, 1, 4, 14, 30, 0, tzinfo=timezone.utc)
    event = Event(name="Test", scheduled_at=dt_utc)
    event.save()

    # PostgreSQL should handle timezone conversions
    retrieved = Event.objects().filter(name="Test")[0]
    assert retrieved.scheduled_at is not None


# SQLite-specific tests
@pytest.mark.sqlite
def test_datetimetz_sqlite_storage_format(db):
    """Test that SQLite stores DateTimeTZ as TEXT."""
    db.register(Event)

    # SQLite stores datetime as TEXT
    field = Event._fields["scheduled_at"]
    field_type = db.backend.get_field_type(field)
    assert field_type == "TEXT"


@pytest.mark.sqlite
def test_datetimetz_sqlite_iso_format(db):
    """Test that SQLite stores datetime in ISO format."""
    db.register(Event)

    dt = datetime(2026, 1, 4, 14, 30, 0, tzinfo=timezone.utc)
    event = Event(name="ISO Test", scheduled_at=dt)
    event.save()

    # Check the stored format
    cursor = db.backend.connection.cursor()
    cursor.execute("SELECT scheduled_at FROM event WHERE name = 'ISO Test'")
    result = cursor.fetchone()

    if result and result[0]:
        stored_value = result[0]
        # Should be ISO format string
        assert isinstance(stored_value, str)
        assert "2026-01-04" in stored_value


@pytest.mark.sqlite
def test_datetimetz_sqlite_timezone_handling(db):
    """Test SQLite handling of timezone information."""
    db.register(Event)

    # SQLite may convert to UTC or store offset
    dt = datetime(2026, 1, 4, 14, 30, 0, tzinfo=ZoneInfo("America/New_York"))
    event = Event(name="TZ Test", scheduled_at=dt)
    event.save()

    retrieved = Event.objects().filter(name="TZ Test")[0]
    assert retrieved.scheduled_at is not None
