import datetime as pdt
import platform
import re
import struct
import sys

import pyo3_pytests.datetime as rdt
import pytest
from hypothesis import example, given
from hypothesis import strategies as st


# Constants
def _get_utc():
    timezone = getattr(pdt, "timezone", None)
    if timezone:
        return timezone.utc
    else:

        class UTC(pdt.tzinfo):
            def utcoffset(self, dt):
                return pdt.timedelta(0)

            def dst(self, dt):
                return pdt.timedelta(0)

            def tzname(self, dt):
                return "UTC"

        return UTC()


UTC = _get_utc()

MAX_SECONDS = int(pdt.timedelta.max.total_seconds())
MIN_SECONDS = int(pdt.timedelta.min.total_seconds())

MAX_DAYS = pdt.timedelta.max // pdt.timedelta(days=1)
MIN_DAYS = pdt.timedelta.min // pdt.timedelta(days=1)

MAX_MICROSECONDS = int(pdt.timedelta.max.total_seconds() * 1e6)
MIN_MICROSECONDS = int(pdt.timedelta.min.total_seconds() * 1e6)

# The reason we don't use platform.architecture() here is that it's not
# reliable on macOS. See https://stackoverflow.com/a/1405971/823869. Similarly,
# sys.maxsize is not reliable on Windows. See
# https://stackoverflow.com/questions/1405913/how-do-i-determine-if-my-python-shell-is-executing-in-32bit-or-64bit-mode-on-os/1405971#comment6209952_1405971
# and https://stackoverflow.com/a/3411134/823869.
_pointer_size = struct.calcsize("P")
if _pointer_size == 8:
    IS_32_BIT = False
elif _pointer_size == 4:
    IS_32_BIT = True
else:
    raise RuntimeError("unexpected pointer size: " + repr(_pointer_size))
IS_WINDOWS = sys.platform == "win32"

if IS_WINDOWS:
    MIN_DATETIME = pdt.datetime(1970, 1, 1, 0, 0, 0)
    if IS_32_BIT:
        MAX_DATETIME = pdt.datetime(2038, 1, 18, 23, 59, 59)
    else:
        MAX_DATETIME = pdt.datetime(3000, 12, 31, 23, 59, 59)
else:
    if IS_32_BIT:
        # TS ±2147483648 (2**31)
        MIN_DATETIME = pdt.datetime(1901, 12, 13, 20, 45, 52)
        MAX_DATETIME = pdt.datetime(2038, 1, 19, 3, 14, 8)
    else:
        MIN_DATETIME = pdt.datetime(1, 1, 2, 0, 0)
        MAX_DATETIME = pdt.datetime(9999, 12, 31, 18, 59, 59)

PYPY = platform.python_implementation() == "PyPy"


# Tests
def test_date():
    assert rdt.make_date(2017, 9, 1) == pdt.date(2017, 9, 1)


@given(d=st.dates())
def test_date_accessors(d):
    act = rdt.get_date_tuple(d)
    exp = (d.year, d.month, d.day)

    assert act == exp


def test_invalid_date_fails():
    with pytest.raises(ValueError):
        rdt.make_date(2017, 2, 30)


@given(d=st.dates(MIN_DATETIME.date(), MAX_DATETIME.date()))
def test_date_from_timestamp(d):
    try:
        ts = pdt.datetime.timestamp(d)
    except Exception:
        # out of range for timestamp
        return

    try:
        expected = pdt.date.fromtimestamp(ts)
    except Exception as pdt_fail:
        # date from timestamp failed; expect the same from Rust binding
        with pytest.raises(type(pdt_fail)) as exc_info:
            rdt.date_from_timestamp(ts)
        assert str(exc_info.value) == str(pdt_fail)
    else:
        assert rdt.date_from_timestamp(int(ts)) == expected


@pytest.mark.parametrize(
    "args, kwargs",
    [
        ((0, 0, 0, 0, None), {}),
        ((1, 12, 14, 124731), {}),
        ((1, 12, 14, 124731), {"tzinfo": UTC}),
    ],
)
def test_time(args, kwargs):
    act = rdt.make_time(*args, **kwargs)
    exp = pdt.time(*args, **kwargs)

    assert act == exp
    assert act.tzinfo is exp.tzinfo
    assert rdt.get_time_tzinfo(act) == exp.tzinfo


@given(t=st.times())
def test_time_hypothesis(t):
    act = rdt.get_time_tuple(t)
    exp = (t.hour, t.minute, t.second, t.microsecond)

    assert act == exp


@given(t=st.times())
def test_time_tuple_fold(t):
    t_nofold = t.replace(fold=0)
    t_fold = t.replace(fold=1)

    for t in (t_nofold, t_fold):
        act = rdt.get_time_tuple_fold(t)
        exp = (t.hour, t.minute, t.second, t.microsecond, t.fold)

        assert act == exp


@pytest.mark.parametrize("fold", [False, True])
def test_time_with_fold(fold):
    t = rdt.time_with_fold(0, 0, 0, 0, None, fold)
    assert t.fold == fold


@pytest.mark.parametrize(
    "args", [(-1, 0, 0, 0), (0, -1, 0, 0), (0, 0, -1, 0), (0, 0, 0, -1)]
)
def test_invalid_time_fails_overflow(args):
    with pytest.raises(OverflowError):
        rdt.make_time(*args)


@pytest.mark.parametrize(
    "args",
    [
        (24, 0, 0, 0),
        (25, 0, 0, 0),
        (0, 60, 0, 0),
        (0, 61, 0, 0),
        (0, 0, 60, 0),
        (0, 0, 61, 0),
        (0, 0, 0, 1000000),
    ],
)
def test_invalid_time_fails(args):
    with pytest.raises(ValueError):
        rdt.make_time(*args)


@pytest.mark.parametrize(
    "args",
    [
        ("0", 0, 0, 0),
        (0, "0", 0, 0),
        (0, 0, "0", 0),
        (0, 0, 0, "0"),
        (0, 0, 0, 0, "UTC"),
    ],
)
def test_time_typeerror(args):
    with pytest.raises(TypeError):
        rdt.make_time(*args)


@pytest.mark.parametrize(
    "args, kwargs",
    [((2017, 9, 1, 12, 45, 30, 0), {}), ((2017, 9, 1, 12, 45, 30, 0), {"tzinfo": UTC})],
)
def test_datetime(args, kwargs):
    act = rdt.make_datetime(*args, **kwargs)
    exp = pdt.datetime(*args, **kwargs)

    assert act == exp
    assert act.tzinfo is exp.tzinfo
    assert rdt.get_datetime_tzinfo(act) == exp.tzinfo


@given(dt=st.datetimes())
def test_datetime_tuple(dt):
    act = rdt.get_datetime_tuple(dt)
    exp = dt.timetuple()[0:6] + (dt.microsecond,)

    assert act == exp


@given(dt=st.datetimes())
def test_datetime_tuple_fold(dt):
    dt_fold = dt.replace(fold=1)
    dt_nofold = dt.replace(fold=0)

    for dt in (dt_fold, dt_nofold):
        act = rdt.get_datetime_tuple_fold(dt)
        exp = dt.timetuple()[0:6] + (dt.microsecond, dt.fold)

        assert act == exp


def test_invalid_datetime_fails():
    with pytest.raises(ValueError):
        rdt.make_datetime(2011, 1, 42, 0, 0, 0, 0)


def test_datetime_typeerror():
    with pytest.raises(TypeError):
        rdt.make_datetime("2011", 1, 1, 0, 0, 0, 0)


@given(dt=st.datetimes(MIN_DATETIME, MAX_DATETIME))
@example(dt=pdt.datetime(1971, 1, 2, 0, 0))
def test_datetime_from_timestamp(dt):
    try:
        ts = pdt.datetime.timestamp(dt)
    except Exception:
        # out of range for timestamp
        return

    try:
        expected = pdt.datetime.fromtimestamp(ts)
    except Exception as pdt_fail:
        # datetime from timestamp failed; expect the same from Rust binding
        with pytest.raises(type(pdt_fail)) as exc_info:
            rdt.datetime_from_timestamp(ts)
        assert str(exc_info.value) == str(pdt_fail)
    else:
        assert rdt.datetime_from_timestamp(ts) == expected


def test_datetime_from_timestamp_tzinfo():
    d1 = rdt.datetime_from_timestamp(0, tz=UTC)
    d2 = rdt.datetime_from_timestamp(0, tz=UTC)

    assert d1 == d2
    assert d1.tzinfo is d2.tzinfo


@pytest.mark.parametrize(
    "args",
    [
        (0, 0, 0),
        (1, 0, 0),
        (-1, 0, 0),
        (0, 1, 0),
        (0, -1, 0),
        (1, -1, 0),
        (-1, 1, 0),
        (0, 0, 123456),
        (0, 0, -123456),
    ],
)
def test_delta(args):
    act = pdt.timedelta(*args)
    exp = rdt.make_delta(*args)

    assert act == exp


@given(td=st.timedeltas())
def test_delta_accessors(td):
    act = rdt.get_delta_tuple(td)
    exp = (td.days, td.seconds, td.microseconds)

    assert act == exp


@pytest.mark.parametrize(
    "args,err_type",
    [
        ((MAX_DAYS + 1, 0, 0), OverflowError),
        ((MIN_DAYS - 1, 0, 0), OverflowError),
        ((0, MAX_SECONDS + 1, 0), OverflowError),
        ((0, MIN_SECONDS - 1, 0), OverflowError),
        ((0, 0, MAX_MICROSECONDS + 1), OverflowError),
        ((0, 0, MIN_MICROSECONDS - 1), OverflowError),
        (("0", 0, 0), TypeError),
        ((0, "0", 0), TypeError),
        ((0, 0, "0"), TypeError),
    ],
)
def test_delta_err(args, err_type):
    with pytest.raises(err_type):
        rdt.make_delta(*args)


def test_tz_class():
    tzi = rdt.TzClass()

    dt = pdt.datetime(2018, 1, 1, tzinfo=tzi)

    assert dt.tzname() == "+01:00"
    assert dt.utcoffset() == pdt.timedelta(hours=1)
    assert dt.dst() is None


def test_tz_class_introspection():
    tzi = rdt.TzClass()

    assert tzi.__class__ == rdt.TzClass
    # PyPy generates <importlib.bootstrap.TzClass ...> for some reason.
    assert re.match(r"^<[\w\.]*TzClass object at", repr(tzi))
