```# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from datetime import datetime, timedelta, date, time
import itertools as it

from dateutil.tz import tz
from dateutil.parser import isoparser, isoparse

import pytest
import six

UTC = tz.tzutc()

def _generate_tzoffsets(limited):
def _mkoffset(hmtuple, fmt):
h, m = hmtuple
m_td = (-1 if h < 0 else 1) * m

tzo = tz.tzoffset(None, timedelta(hours=h, minutes=m_td))
return tzo, fmt.format(h, m)

out = []
if not limited:
# The subset that's just hours
hm_out_h = [(h, 0) for h in (-23, -5, 0, 5, 23)]
out.extend([_mkoffset(hm, '{:+03d}') for hm in hm_out_h])

# Ones that have hours and minutes
hm_out = [] + hm_out_h
hm_out += [(-12, 15), (11, 30), (10, 2), (5, 15), (-5, 30)]
else:
hm_out = [(-5, -0)]

fmts = ['{:+03d}:{:02d}', '{:+03d}{:02d}']
out += [_mkoffset(hm, fmt) for hm in hm_out for fmt in fmts]

# Also add in UTC and naive
out.append((tz.tzutc(), 'Z'))
out.append((None, ''))

return out

FULL_TZOFFSETS = _generate_tzoffsets(False)
FULL_TZOFFSETS_AWARE = [x for x in FULL_TZOFFSETS if x[1]]
TZOFFSETS = _generate_tzoffsets(True)

DATES = [datetime(1996, 1, 1), datetime(2017, 1, 1)]
@pytest.mark.parametrize('dt', tuple(DATES))
def test_year_only(dt):
dtstr = dt.strftime('%Y')

assert isoparse(dtstr) == dt

DATES += [datetime(2000, 2, 1), datetime(2017, 4, 1)]
@pytest.mark.parametrize('dt', tuple(DATES))
def test_year_month(dt):
fmt   = '%Y-%m'
dtstr = dt.strftime(fmt)

assert isoparse(dtstr) == dt

DATES += [datetime(2016, 2, 29), datetime(2018, 3, 15)]
YMD_FMTS = ('%Y%m%d', '%Y-%m-%d')
@pytest.mark.parametrize('dt', tuple(DATES))
@pytest.mark.parametrize('fmt', YMD_FMTS)
def test_year_month_day(dt, fmt):
dtstr = dt.strftime(fmt)

assert isoparse(dtstr) == dt

def _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset,
microsecond_precision=None):
tzi, offset_str = tzoffset
fmt = date_fmt + 'T' + time_fmt
dt = dt.replace(tzinfo=tzi)
dtstr = dt.strftime(fmt)

if microsecond_precision is not None:
if not fmt.endswith('%f'):
raise ValueError('Time format has no microseconds!')

if microsecond_precision != 6:
dtstr = dtstr[:-(6 - microsecond_precision)]
elif microsecond_precision > 6:
raise ValueError('Precision must be 1-6')

dtstr += offset_str

assert isoparse(dtstr) == dt

DATETIMES = [datetime(1998, 4, 16, 12),
datetime(2019, 11, 18, 23),
datetime(2014, 12, 16, 4)]
@pytest.mark.parametrize('dt', tuple(DATETIMES))
@pytest.mark.parametrize('date_fmt', YMD_FMTS)
@pytest.mark.parametrize('tzoffset', TZOFFSETS)
def test_ymd_h(dt, date_fmt, tzoffset):
_isoparse_date_and_time(dt, date_fmt, '%H', tzoffset)

DATETIMES = [datetime(2012, 1, 6, 9, 37)]
@pytest.mark.parametrize('dt', tuple(DATETIMES))
@pytest.mark.parametrize('date_fmt', YMD_FMTS)
@pytest.mark.parametrize('time_fmt', ('%H%M', '%H:%M'))
@pytest.mark.parametrize('tzoffset', TZOFFSETS)
def test_ymd_hm(dt, date_fmt, time_fmt, tzoffset):
_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)

DATETIMES = [datetime(2003, 9, 2, 22, 14, 2),
datetime(2003, 8, 8, 14, 9, 14),
datetime(2003, 4, 7, 6, 14, 59)]
HMS_FMTS = ('%H%M%S', '%H:%M:%S')
@pytest.mark.parametrize('dt', tuple(DATETIMES))
@pytest.mark.parametrize('date_fmt', YMD_FMTS)
@pytest.mark.parametrize('time_fmt', HMS_FMTS)
@pytest.mark.parametrize('tzoffset', TZOFFSETS)
def test_ymd_hms(dt, date_fmt, time_fmt, tzoffset):
_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)

DATETIMES = [datetime(2017, 11, 27, 6, 14, 30, 123456)]
@pytest.mark.parametrize('dt', tuple(DATETIMES))
@pytest.mark.parametrize('date_fmt', YMD_FMTS)
@pytest.mark.parametrize('time_fmt', (x + '.%f' for x in HMS_FMTS))
@pytest.mark.parametrize('tzoffset', TZOFFSETS)
@pytest.mark.parametrize('precision', list(range(3, 7)))
def test_ymd_hms_micro(dt, date_fmt, time_fmt, tzoffset, precision):
# Truncate the microseconds to the desired precision for the representation
dt = dt.replace(microsecond=int(round(dt.microsecond, precision-6)))

_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, precision)

@pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS)
def test_full_tzoffsets(tzoffset):
dt = datetime(2017, 11, 27, 6, 14, 30, 123456)
date_fmt = '%Y-%m-%d'
time_fmt = '%H:%M:%S.%f'

_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)

@pytest.mark.parametrize('dt_str', [
'2014-04-11T00',
'2014-04-11T24',
'2014-04-11T00:00',
'2014-04-11T24:00',
'2014-04-11T00:00:00',
'2014-04-11T24:00:00',
'2014-04-11T00:00:00.000',
'2014-04-11T24:00:00.000',
'2014-04-11T00:00:00.000000',
'2014-04-11T24:00:00.000000']
)
def test_datetime_midnight(dt_str):
assert isoparse(dt_str) == datetime(2014, 4, 11, 0, 0, 0, 0)

@pytest.mark.parametrize('datestr', [
'2014-01-01',
'20140101',
])
@pytest.mark.parametrize('sep', [' ', 'a', 'T', '_', '-'])
def test_isoparse_sep_none(datestr, sep):
isostr = datestr + sep + '14:33:09'
assert isoparse(isostr) == datetime(2014, 1, 1, 14, 33, 9)

##
# Uncommon date formats
TIME_ARGS = ('time_args',
((None, time(0), None), ) + tuple(('%H:%M:%S.%f', _t, _tz)
for _t, _tz in it.product([time(0), time(9, 30), time(14, 47)],
TZOFFSETS)))

@pytest.mark.parametrize('isocal,dt_expected',[
((2017, 10), datetime(2017, 3, 6)),
((2020, 1), datetime(2019, 12, 30)),    # ISO year != Cal year
((2004, 53), datetime(2004, 12, 27)),   # Only half the week is in 2014
])
def test_isoweek(isocal, dt_expected):
# TODO: Figure out how to parametrize this on formats, too
for fmt in ('{:04d}-W{:02d}', '{:04d}W{:02d}'):
dtstr = fmt.format(*isocal)
assert isoparse(dtstr) == dt_expected

@pytest.mark.parametrize('isocal,dt_expected',[
((2016, 13, 7), datetime(2016, 4, 3)),
((2004, 53, 7), datetime(2005, 1, 2)),      # ISO year != Cal year
((2009, 1, 2), datetime(2008, 12, 30)),     # ISO year < Cal year
((2009, 53, 6), datetime(2010, 1, 2))       # ISO year > Cal year
])
def test_isoweek_day(isocal, dt_expected):
# TODO: Figure out how to parametrize this on formats, too
for fmt in ('{:04d}-W{:02d}-{:d}', '{:04d}W{:02d}{:d}'):
dtstr = fmt.format(*isocal)
assert isoparse(dtstr) == dt_expected

@pytest.mark.parametrize('isoord,dt_expected', [
((2004, 1), datetime(2004, 1, 1)),
((2016, 60), datetime(2016, 2, 29)),
((2017, 60), datetime(2017, 3, 1)),
((2016, 366), datetime(2016, 12, 31)),
((2017, 365), datetime(2017, 12, 31))
])
def test_iso_ordinal(isoord, dt_expected):
for fmt in ('{:04d}-{:03d}', '{:04d}{:03d}'):
dtstr = fmt.format(*isoord)

assert isoparse(dtstr) == dt_expected

###
# Acceptance of bytes
@pytest.mark.parametrize('isostr,dt', [
(b'2014', datetime(2014, 1, 1)),
(b'20140204', datetime(2014, 2, 4)),
(b'2014-02-04', datetime(2014, 2, 4)),
(b'2014-02-04T12', datetime(2014, 2, 4, 12)),
(b'2014-02-04T12:30', datetime(2014, 2, 4, 12, 30)),
(b'2014-02-04T12:30:15', datetime(2014, 2, 4, 12, 30, 15)),
(b'2014-02-04T12:30:15.224', datetime(2014, 2, 4, 12, 30, 15, 224000)),
(b'20140204T123015.224', datetime(2014, 2, 4, 12, 30, 15, 224000)),
(b'2014-02-04T12:30:15.224Z', datetime(2014, 2, 4, 12, 30, 15, 224000,
tz.tzutc())),
(b'2014-02-04T12:30:15.224+05:00',
datetime(2014, 2, 4, 12, 30, 15, 224000,
tzinfo=tz.tzoffset(None, timedelta(hours=5))))])
def test_bytes(isostr, dt):
assert isoparse(isostr) == dt

###
# Invalid ISO strings
@pytest.mark.parametrize('isostr,exception', [
('201', ValueError),                        # ISO string too short
('2012-0425', ValueError),                  # Inconsistent date separators
('201204-25', ValueError),                  # Inconsistent date separators
('20120425T0120:00', ValueError),           # Inconsistent time separators
('20120425T012500-334', ValueError),        # Wrong microsecond separator
('2001-1', ValueError),                     # YYYY-M not valid
('2012-04-9', ValueError),                  # YYYY-MM-D not valid
('201204', ValueError),                     # YYYYMM not valid
('20120411T03:30+', ValueError),            # Time zone too short
('20120411T03:30+1234567', ValueError),     # Time zone too long
('20120411T03:30-25:40', ValueError),       # Time zone invalid
('2012-1a', ValueError),                    # Invalid month
('20120411T03:30+00:60', ValueError),       # Time zone invalid minutes
('20120411T03:30+00:61', ValueError),       # Time zone invalid minutes
('20120411T033030.123456012:00',            # No sign in time zone
ValueError),
('2012-W00', ValueError),                   # Invalid ISO week
('2012-W55', ValueError),                   # Invalid ISO week
('2012-W01-0', ValueError),                 # Invalid ISO week day
('2012-W01-8', ValueError),                 # Invalid ISO week day
('2013-000', ValueError),                   # Invalid ordinal day
('2013-366', ValueError),                   # Invalid ordinal day
('2013366', ValueError),                    # Invalid ordinal day
('2014-03-12Т12:30:14', ValueError),        # Cyrillic T
('2014-04-21T24:00:01', ValueError),        # Invalid use of 24 for midnight
('2014_W01-1', ValueError),                 # Invalid separator
('2014W01-1', ValueError),                  # Inconsistent use of dashes
('2014-W011', ValueError),                  # Inconsistent use of dashes

])
def test_iso_raises(isostr, exception):
with pytest.raises(exception):
isoparse(isostr)

@pytest.mark.parametrize('sep_act,valid_sep', [
('C', 'T'),
('T', 'C')
])
def test_iso_raises_sep(sep_act, valid_sep):
isostr = '2012-04-25' + sep_act + '01:25:00'

@pytest.mark.xfail()
@pytest.mark.parametrize('isostr,exception', [
('20120425T01:2000', ValueError),           # Inconsistent time separators
])
def test_iso_raises_failing(isostr, exception):
# These are test cases where the current implementation is too lenient
# and need to be fixed
with pytest.raises(exception):
isoparse(isostr)

###
# Test ISOParser constructor
@pytest.mark.parametrize('sep', ['  ', '9', '🍛'])
def test_isoparser_invalid_sep(sep):
with pytest.raises(ValueError):
isoparser(sep=sep)

# This only fails on Python 3
@pytest.mark.xfail(six.PY3, reason="Fails on Python 3 only")
def test_isoparser_byte_sep():
dt = datetime(2017, 12, 6, 12, 30, 45)
dt_str = dt.isoformat(sep=str('T'))

dt_rt = isoparser(sep=b'T').isoparse(dt_str)

assert dt == dt_rt

###
# Test parse_tzstr
@pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS)
def test_parse_tzstr(tzoffset):
dt = datetime(2017, 11, 27, 6, 14, 30, 123456)
date_fmt = '%Y-%m-%d'
time_fmt = '%H:%M:%S.%f'

_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)

@pytest.mark.parametrize('tzstr', [
'-00:00', '+00:00', '+00', '-00', '+0000', '-0000'
])
@pytest.mark.parametrize('zero_as_utc', [True, False])
def test_parse_tzstr_zero_as_utc(tzstr, zero_as_utc):
tzi = isoparser().parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
assert tzi == tz.tzutc()
assert (type(tzi) == tz.tzutc) == zero_as_utc

@pytest.mark.parametrize('tzstr,exception', [
('00:00', ValueError),     # No sign
('05:00', ValueError),     # No sign
('_00:00', ValueError),    # Invalid sign
('+25:00', ValueError),    # Offset too large
('00:0000', ValueError),   # String too long
])
def test_parse_tzstr_fails(tzstr, exception):
with pytest.raises(exception):
isoparser().parse_tzstr(tzstr)

###
# Test parse_isodate
def __make_date_examples():
dates_no_day = [
date(1999, 12, 1),
date(2016, 2, 1)
]

if six.PY3:
# strftime does not support dates before 1900 in Python 2
dates_no_day.append(date(1000, 11, 1))

# Only one supported format for dates with no day
o = zip(dates_no_day, it.repeat('%Y-%m'))

dates_w_day = [
date(1969, 12, 31),
date(1900, 1, 1),
date(2016, 2, 29),
date(2017, 11, 14)
]

dates_w_day_fmts = ('%Y%m%d', '%Y-%m-%d')
o = it.chain(o, it.product(dates_w_day, dates_w_day_fmts))

return list(o)

@pytest.mark.parametrize('d,dt_fmt', __make_date_examples())
@pytest.mark.parametrize('as_bytes', [True, False])
def test_parse_isodate(d, dt_fmt, as_bytes):
d_str = d.strftime(dt_fmt)
if isinstance(d_str, six.text_type) and as_bytes:
d_str = d_str.encode('ascii')
elif isinstance(d_str, six.binary_type) and not as_bytes:
d_str = d_str.decode('ascii')

iparser = isoparser()
assert iparser.parse_isodate(d_str) == d

@pytest.mark.parametrize('isostr,exception', [
('243', ValueError),                        # ISO string too short
('2014-0423', ValueError),                  # Inconsistent date separators
('201404-23', ValueError),                  # Inconsistent date separators
('2014日03月14', ValueError),                # Not ASCII
('2013-02-29', ValueError),                 # Not a leap year
('2014/12/03', ValueError),                 # Wrong separators
('2014-04-19T', ValueError),                # Unknown components
])
def test_isodate_raises(isostr, exception):
with pytest.raises(exception):
isoparser().parse_isodate(isostr)

###
# Test parse_isotime
def __make_time_examples():
outputs = []

# HH
time_h = [time(0), time(8), time(22)]
time_h_fmts = ['%H']

outputs.append(it.product(time_h, time_h_fmts))

# HHMM / HH:MM
time_hm = [time(0, 0), time(0, 30), time(8, 47), time(16, 1)]
time_hm_fmts = ['%H%M', '%H:%M']

outputs.append(it.product(time_hm, time_hm_fmts))

# HHMMSS / HH:MM:SS
time_hms = [time(0, 0, 0), time(0, 15, 30),
time(8, 2, 16), time(12, 0), time(16, 2), time(20, 45)]

time_hms_fmts = ['%H%M%S', '%H:%M:%S']

outputs.append(it.product(time_hms, time_hms_fmts))

# HHMMSS.ffffff / HH:MM:SS.ffffff
time_hmsu = [time(0, 0, 0, 0), time(4, 15, 3, 247993),
time(14, 21, 59, 948730),
time(23, 59, 59, 999999)]

time_hmsu_fmts = ['%H%M%S.%f', '%H:%M:%S.%f']

outputs.append(it.product(time_hmsu, time_hmsu_fmts))

outputs = list(map(list, outputs))

# Time zones
ex_naive = list(it.chain.from_iterable(x[0:2] for x in outputs))
o = it.product(ex_naive, TZOFFSETS)    # ((time, fmt), (tzinfo, offsetstr))
o = ((t.replace(tzinfo=tzi), fmt + off_str)
for (t, fmt), (tzi, off_str) in o)

outputs.append(o)

return list(it.chain.from_iterable(outputs))

@pytest.mark.parametrize('time_val,time_fmt', __make_time_examples())
@pytest.mark.parametrize('as_bytes', [True, False])
def test_isotime(time_val, time_fmt, as_bytes):
tstr = time_val.strftime(time_fmt)
if isinstance(time_val, six.text_type) and as_bytes:
tstr = tstr.encode('ascii')
elif isinstance(time_val, six.binary_type) and not as_bytes:
tstr = tstr.decode('ascii')

iparser = isoparser()

assert iparser.parse_isotime(tstr) == time_val

@pytest.mark.parametrize('isostr,exception', [
('3', ValueError),                          # ISO string too short
('14時30分15秒', ValueError),                # Not ASCII
('14_30_15', ValueError),                   # Invalid separators
('1430:15', ValueError),                    # Inconsistent separator use
('14:30:15.3684000309', ValueError),        # Too much us precision
('25', ValueError),                         # Invalid hours
('25:15', ValueError),                      # Invalid hours
('14:60', ValueError),                      # Invalid minutes
('14:59:61', ValueError),                   # Invalid seconds
('14:30:15.3446830500', ValueError),        # No sign in time zone
('14:30:15+', ValueError),                  # Time zone too short
('14:30:15+1234567', ValueError),           # Time zone invalid
('14:59:59+25:00', ValueError),             # Invalid tz hours
('14:59:59+12:62', ValueError),             # Invalid tz minutes
('14:59:30_344583', ValueError),            # Invalid microsecond separator
])
def test_isotime_raises(isostr, exception):
iparser = isoparser()
with pytest.raises(exception):
iparser.parse_isotime(isostr)

@pytest.mark.xfail()
@pytest.mark.parametrize('isostr,exception', [
('14:3015', ValueError),                    # Inconsistent separator use
('201202', ValueError)                      # Invalid ISO format
])
def test_isotime_raises_xfail(isostr, exception):
iparser = isoparser()
with pytest.raises(exception):
iparser.parse_isotime(isostr)
```