from __future__ import absolute_import, unicode_literals

from datetime import datetime, timedelta, tzinfo

import pytest
import pytz
from case import Mock, patch
from pytz import AmbiguousTimeError

from celery.utils.iso8601 import parse_iso8601
from celery.utils.time import (LocalTimezone, delta_resolution, ffwd,
                               get_exponential_backoff_interval,
                               humanize_seconds, localize, make_aware,
                               maybe_iso8601, maybe_make_aware,
                               maybe_timedelta, rate, remaining, timezone,
                               utcoffset)


class test_LocalTimezone:

    def test_daylight(self, patching):
        time = patching('celery.utils.time._time')
        time.timezone = 3600
        time.daylight = False
        x = LocalTimezone()
        assert x.STDOFFSET == timedelta(seconds=-3600)
        assert x.DSTOFFSET == x.STDOFFSET
        time.daylight = True
        time.altzone = 3600
        y = LocalTimezone()
        assert y.STDOFFSET == timedelta(seconds=-3600)
        assert y.DSTOFFSET == timedelta(seconds=-3600)

        assert repr(y)

        y._isdst = Mock()
        y._isdst.return_value = True
        assert y.utcoffset(datetime.now())
        assert not y.dst(datetime.now())
        y._isdst.return_value = False
        assert y.utcoffset(datetime.now())
        assert not y.dst(datetime.now())

        assert y.tzname(datetime.now())


class test_iso8601:

    def test_parse_with_timezone(self):
        d = datetime.utcnow().replace(tzinfo=pytz.utc)
        assert parse_iso8601(d.isoformat()) == d
        # 2013-06-07T20:12:51.775877+00:00
        iso = d.isoformat()
        iso1 = iso.replace('+00:00', '-01:00')
        d1 = parse_iso8601(iso1)
        assert d1.tzinfo._minutes == -60
        iso2 = iso.replace('+00:00', '+01:00')
        d2 = parse_iso8601(iso2)
        assert d2.tzinfo._minutes == +60
        iso3 = iso.replace('+00:00', 'Z')
        d3 = parse_iso8601(iso3)
        assert d3.tzinfo == pytz.UTC


@pytest.mark.parametrize('delta,expected', [
    (timedelta(days=2), datetime(2010, 3, 30, 0, 0)),
    (timedelta(hours=2), datetime(2010, 3, 30, 11, 0)),
    (timedelta(minutes=2), datetime(2010, 3, 30, 11, 50)),
    (timedelta(seconds=2), None),
])
def test_delta_resolution(delta, expected):
    dt = datetime(2010, 3, 30, 11, 50, 58, 41065)
    assert delta_resolution(dt, delta) == expected or dt


@pytest.mark.parametrize('seconds,expected', [
    (4 * 60 * 60 * 24, '4.00 days'),
    (1 * 60 * 60 * 24, '1.00 day'),
    (4 * 60 * 60, '4.00 hours'),
    (1 * 60 * 60, '1.00 hour'),
    (4 * 60, '4.00 minutes'),
    (1 * 60, '1.00 minute'),
    (4, '4.00 seconds'),
    (1, '1.00 second'),
    (4.3567631221, '4.36 seconds'),
    (0, 'now'),
])
def test_humanize_seconds(seconds, expected):
    assert humanize_seconds(seconds) == expected


def test_humanize_seconds__prefix():
    assert humanize_seconds(4, prefix='about ') == 'about 4.00 seconds'


def test_maybe_iso8601_datetime():
    now = datetime.now()
    assert maybe_iso8601(now) is now


@pytest.mark.parametrize('arg,expected', [
    (30, timedelta(seconds=30)),
    (30.6, timedelta(seconds=30.6)),
    (timedelta(days=2), timedelta(days=2)),
])
def test_maybe_timedelta(arg, expected):
    assert maybe_timedelta(arg) == expected


def test_remaining_relative():
    remaining(datetime.utcnow(), timedelta(hours=1), relative=True)


class test_timezone:

    def test_get_timezone_with_pytz(self):
        assert timezone.get_timezone('UTC')

    def test_tz_or_local(self):
        assert timezone.tz_or_local() == timezone.local
        assert timezone.tz_or_local(timezone.utc)

    def test_to_local(self):
        assert timezone.to_local(make_aware(datetime.utcnow(), timezone.utc))
        assert timezone.to_local(datetime.utcnow())

    def test_to_local_fallback(self):
        assert timezone.to_local_fallback(
            make_aware(datetime.utcnow(), timezone.utc))
        assert timezone.to_local_fallback(datetime.utcnow())


class test_make_aware:

    def test_tz_without_localize(self):
        tz = tzinfo()
        assert not hasattr(tz, 'localize')
        wtz = make_aware(datetime.utcnow(), tz)
        assert wtz.tzinfo == tz

    def test_when_has_localize(self):

        class tzz(tzinfo):
            raises = False

            def localize(self, dt, is_dst=None):
                self.localized = True
                if self.raises and is_dst is None:
                    self.raised = True
                    raise AmbiguousTimeError()
                return 1  # needed by min() in Python 3 (None not hashable)

        tz = tzz()
        make_aware(datetime.utcnow(), tz)
        assert tz.localized

        tz2 = tzz()
        tz2.raises = True
        make_aware(datetime.utcnow(), tz2)
        assert tz2.localized
        assert tz2.raised

    def test_maybe_make_aware(self):
        aware = datetime.utcnow().replace(tzinfo=timezone.utc)
        assert maybe_make_aware(aware)
        naive = datetime.utcnow()
        assert maybe_make_aware(naive)
        assert maybe_make_aware(naive).tzinfo is pytz.utc

        tz = pytz.timezone('US/Eastern')
        eastern = datetime.utcnow().replace(tzinfo=tz)
        assert maybe_make_aware(eastern).tzinfo is tz
        utcnow = datetime.utcnow()
        assert maybe_make_aware(utcnow, 'UTC').tzinfo is pytz.utc


class test_localize:

    def test_tz_without_normalize(self):
        class tzz(tzinfo):

            def utcoffset(self, dt):
                return None  # Mock no utcoffset specified

        tz = tzz()
        assert not hasattr(tz, 'normalize')
        assert localize(make_aware(datetime.utcnow(), tz), tz)

    def test_when_has_normalize(self):

        class tzz(tzinfo):
            raises = None

            def utcoffset(self, dt):
                return None

            def normalize(self, dt, **kwargs):
                self.normalized = True
                if self.raises and kwargs and kwargs.get('is_dst') is None:
                    self.raised = True
                    raise self.raises
                return 1  # needed by min() in Python 3 (None not hashable)

        tz = tzz()
        localize(make_aware(datetime.utcnow(), tz), tz)
        assert tz.normalized

        tz2 = tzz()
        tz2.raises = AmbiguousTimeError()
        localize(make_aware(datetime.utcnow(), tz2), tz2)
        assert tz2.normalized
        assert tz2.raised

        tz3 = tzz()
        tz3.raises = TypeError()
        localize(make_aware(datetime.utcnow(), tz3), tz3)
        assert tz3.normalized
        assert tz3.raised

    def test_localize_changes_utc_dt(self):
        now_utc_time = datetime.now(tz=pytz.utc)
        local_tz = pytz.timezone('US/Eastern')
        localized_time = localize(now_utc_time, local_tz)
        assert localized_time == now_utc_time

    def test_localize_aware_dt_idempotent(self):
        t = (2017, 4, 23, 21, 36, 59, 0)
        local_zone = pytz.timezone('America/New_York')
        local_time = datetime(*t)
        local_time_aware = datetime(*t, tzinfo=local_zone)
        alternate_zone = pytz.timezone('America/Detroit')
        localized_time = localize(local_time_aware, alternate_zone)
        assert localized_time == local_time_aware
        assert local_zone.utcoffset(
            local_time) == alternate_zone.utcoffset(local_time)
        localized_utc_offset = localized_time.tzinfo.utcoffset(local_time)
        assert localized_utc_offset == alternate_zone.utcoffset(local_time)
        assert localized_utc_offset == local_zone.utcoffset(local_time)


@pytest.mark.parametrize('s,expected', [
    (999, 999),
    (7.5, 7.5),
    ('2.5/s', 2.5),
    ('1456/s', 1456),
    ('100/m', 100 / 60.0),
    ('10/h', 10 / 60.0 / 60.0),
    (0, 0),
    (None, 0),
    ('0/m', 0),
    ('0/h', 0),
    ('0/s', 0),
    ('0.0/s', 0),
])
def test_rate_limit_string(s, expected):
    assert rate(s) == expected


class test_ffwd:

    def test_repr(self):
        x = ffwd(year=2012)
        assert repr(x)

    def test_radd_with_unknown_gives_NotImplemented(self):
        x = ffwd(year=2012)
        assert x.__radd__(object()) == NotImplemented


class test_utcoffset:

    def test_utcoffset(self, patching):
        _time = patching('celery.utils.time._time')
        _time.daylight = True
        assert utcoffset(time=_time) is not None
        _time.daylight = False
        assert utcoffset(time=_time) is not None


class test_get_exponential_backoff_interval:

    @patch('random.randrange', lambda n: n - 2)
    def test_with_jitter(self):
        assert get_exponential_backoff_interval(
            factor=4,
            retries=3,
            maximum=100,
            full_jitter=True
        ) == 4 * (2 ** 3) - 1

    def test_without_jitter(self):
        assert get_exponential_backoff_interval(
            factor=4,
            retries=3,
            maximum=100,
            full_jitter=False
        ) == 4 * (2 ** 3)

    def test_bound_by_maximum(self):
        maximum_boundary = 100
        assert get_exponential_backoff_interval(
            factor=40,
            retries=3,
            maximum=maximum_boundary
        ) == maximum_boundary

    @patch('random.randrange', lambda n: n - 1)
    def test_negative_values(self):
        assert get_exponential_backoff_interval(
            factor=-40,
            retries=3,
            maximum=100
        ) == 0