import pytest
from datetime import date, datetime
from atomsql.orm.models import Model
from atomsql.orm.fields import StringField, DateTimeField


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


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


def test_datetime_field_accepts_datetime_object(db):
    """DateTimeField should accept datetime objects."""
    db.register(Event)
    dt = datetime(2026, 1, 4, 14, 30, 0)
    event = Event(name="Conference", scheduled_at=dt)
    assert event.scheduled_at == dt


def test_datetime_field_rejects_date_object(db):
    """DateTimeField should reject date objects (only datetime accepted)."""
    db.register(Event)
    d = date(2026, 1, 4)
    with pytest.raises(ValueError, match="expected a datetime"):
        Event(name="Meeting", scheduled_at=d)


def test_datetime_field_rejects_string_with_time(db):
    """DateTimeField should reject string values (no parsing)."""
    db.register(Event)

    # Standard format: YYYY-MM-DD HH:MM:SS
    with pytest.raises(ValueError, match="expected a datetime"):
        Event(name="Event1", scheduled_at="2026-01-04 14:30:00")

    # ISO format: YYYY-MM-DDTHH:MM:SS
    with pytest.raises(ValueError, match="expected a datetime"):
        Event(name="Event2", scheduled_at="2026-01-04T14:30:00")


def test_datetime_field_accepts_datetime_with_microseconds(db):
    """DateTimeField should accept datetime objects with microseconds."""
    db.register(Event)

    dt = datetime(2026, 1, 4, 14, 30, 0, 123456)
    event = Event(name="PreciseEvent", scheduled_at=dt)
    assert isinstance(event.scheduled_at, datetime)
    assert event.scheduled_at.microsecond == 123456


def test_datetime_field_rejects_string_with_z_suffix(db):
    """DateTimeField should reject string values even with Z suffix."""
    db.register(Event)

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


def test_datetime_field_rejects_date_only_string(db):
    """DateTimeField should reject date-only strings."""
    db.register(Event)

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


def test_datetime_field_rejects_invalid_string(db):
    """DateTimeField should reject strings (no parsing)."""
    db.register(Event)

    with pytest.raises(ValueError, match="expected a datetime"):
        Event(name="InvalidEvent", scheduled_at="not a date")


def test_datetime_field_rejects_invalid_format(db):
    """DateTimeField should reject all string inputs."""
    db.register(Event)

    with pytest.raises(ValueError, match="expected a datetime"):
        Event(name="InvalidEvent", scheduled_at="2026-13-45")  # String not accepted


def test_datetime_field_rejects_invalid_types(db):
    """DateTimeField should reject non-datetime types."""
    db.register(Event)

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

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

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


def test_datetime_field_null_handling_when_nullable(db):
    """DateTimeField 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_datetime_field_rejects_null_when_not_nullable(db):
    """DateTimeField should reject None when nullable=False."""
    db.register(StrictEvent)

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


def test_datetime_field_persistence_and_retrieval(db):
    """DateTimeField should correctly persist and retrieve datetime values."""
    db.register(Event)

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

    retrieved = Event.objects().filter(name="Product Launch")[0]
    # Note: Depending on backend, might be returned as string or datetime
    # For SQLite, it's typically stored as string
    assert retrieved.scheduled_at is not None


def test_datetime_field_filtering_exact_match(db):
    """DateTimeField should support exact match filtering."""
    db.register(Event)

    dt1 = datetime(2026, 1, 4, 10, 0, 0)
    dt2 = datetime(2026, 1, 5, 15, 0, 0)

    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
    assert results[0].name == "Morning Event"


def test_datetime_field_filtering_comparison_operators(db):
    """DateTimeField should support comparison operators in filters."""
    db.register(Event)

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

    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_datetime_field_with_exists(db):
    """DateTimeField should work with exists() queries."""
    db.register(Event)

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

    exists = Event.objects().filter(scheduled_at=dt).exists()
    assert exists is True

    future_dt = datetime(2026, 12, 31, 23, 59, 59)
    not_exists = Event.objects().filter(scheduled_at=future_dt).exists()
    assert not_exists is False


def test_datetime_field_default_value(db):
    """DateTimeField should support default values."""
    default_dt = datetime(2026, 1, 1, 0, 0, 0)

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

    db.register(ScheduledTask)

    task = ScheduledTask(name="Backup")
    assert task.scheduled_at == default_dt


def test_datetime_field_update_value(db):
    """DateTimeField should allow updating values."""
    db.register(Event)

    original_dt = datetime(2026, 1, 4, 10, 0, 0)
    new_dt = datetime(2026, 1, 5, 14, 0, 0)

    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 != original_dt


def test_datetime_field_rejects_all_string_formats(db):
    """Test that all string formats are rejected (no parsing)."""
    db.register(Event)

    test_cases = [
        "2026-01-04",
        "2026-01-04 10:30:00",
        "2026-01-04T10:30:00",
        "2026-01-04T10:30:00.123456",
        "2026-01-04 10:30:00.123456",
    ]

    for date_string in test_cases:
        with pytest.raises(ValueError, match="expected a datetime"):
            Event(name="Test", scheduled_at=date_string)


def test_datetime_field_backend_preparation(db):
    """Test that datetime values are properly prepared for the backend."""
    db.register(Event)

    test_datetime = datetime(2026, 5, 20, 15, 30, 45)
    prepared = db.backend.prepare_value(test_datetime)

    # SQLite backend should convert to string with date and time
    assert isinstance(prepared, str)
    assert "2026-05-20" in prepared
    assert "15:30:45" in prepared


def test_datetime_field_ordering(db):
    """DateTimeField should support ordering."""
    db.register(Event)

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

    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
    # First event should be the earliest
    assert ordered[0].name == "Event1"
    assert ordered[2].name == "Event3"


def test_datetime_field_unique_constraint(db):
    """DateTimeField should support unique constraint."""

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

    db.register(UniqueEvent)

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

    # Attempting to save another event with the same datetime should fail
    # Note: This test currently only verifies that unique constraint can be set
    # Full constraint enforcement testing depends on database-level constraint handling


def test_datetime_field_with_none_string(db):
    """DateTimeField should reject 'None' string."""
    db.register(Event)

    # String "None" should not be treated as None value
    with pytest.raises(ValueError, match="expected a datetime"):
        Event(name="StringNone", scheduled_at="None")


def test_datetime_field_rejects_whitespace_string(db):
    """DateTimeField should reject strings even with whitespace."""
    db.register(Event)

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


def test_datetime_field_empty_string(db):
    """DateTimeField should reject empty strings."""
    db.register(Event)

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


def test_datetime_field_binary_expressions(db):
    """DateTimeField should support binary expression operators."""
    db.register(Event)

    dt = datetime(2026, 1, 15, 12, 0, 0)

    # 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_datetime_field_with_timezone_aware_datetime(db):
    """DateTimeField works with timezone-aware datetime objects."""
    db.register(Event)

    # Create a datetime object (timezone-aware behavior depends on usage)
    dt = datetime(2026, 1, 4, 10, 30, 0)
    event = Event(name="UTC Event", scheduled_at=dt)
    assert event.scheduled_at == dt


def test_datetime_field_only_accepts_datetime_not_date(db):
    """DateTimeField should only accept datetime objects, not date objects."""
    db.register(Event)

    dt = datetime(2026, 1, 15, 12, 0, 0)
    d = date(2026, 1, 15)

    # datetime should work
    Event(name="DateTime Event", scheduled_at=dt).save()

    # date should be rejected
    with pytest.raises(ValueError, match="expected a datetime"):
        Event(name="Date Event", scheduled_at=d)

    # Should only have one record
    results = list(Event.objects())
    assert len(results) == 1


def test_datetime_field_sql_type(db):
    """DateTimeField should return correct SQL type."""
    db.register(Event)

    # The base Field class returns "TEXT" by default
    # DateTimeField doesn't override get_sql_type, so it inherits this behavior
    field = Event._fields["scheduled_at"]
    sql_type = field.get_sql_type()
    assert sql_type == "TEXT"


def test_datetime_field_column_name(db):
    """DateTimeField 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_datetime_field_with_aggregations(db):
    """DateTimeField should work with aggregation queries."""
    db.register(Event)

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

    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_datetime_field_exclude_filter(db):
    """DateTimeField should work with exclude() filters."""
    db.register(Event)

    dt1 = datetime(2026, 1, 10, 9, 0, 0)
    dt2 = datetime(2026, 1, 20, 15, 0, 0)

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

    # Exclude specific datetime
    results = list(Event.objects().exclude(scheduled_at=dt1))
    assert len(results) == 1
    assert results[0].name == "Late"
