# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import absolute_import, division, print_function
__metaclass__ = type


import datetime
import sys

import pytest
from freezegun import freeze_time

from ansible.module_utils.common.collections import is_sequence

from ansible_collections.community.crypto.plugins.module_utils.time import (
    add_or_remove_timezone,
    get_now_datetime,
    convert_relative_to_datetime,
    ensure_utc_timezone,
    from_epoch_seconds,
    get_epoch_seconds,
    get_relative_time_option,
    remove_timezone,
    UTC,
)


TIMEZONES = [
    datetime.timedelta(hours=0),
    datetime.timedelta(hours=1),
    datetime.timedelta(hours=2),
    datetime.timedelta(hours=-6),
]


def cartesian_product(list1, list2):
    result = []
    for item1 in list1:
        if not is_sequence(item1):
            item1 = (item1, )
        elif not isinstance(item1, tuple):
            item1 = tuple(item1)
        for item2 in list2:
            if not is_sequence(item2):
                item2 = (item2, )
            elif not isinstance(item2, tuple):
                item2 = tuple(item2)
            result.append(item1 + item2)
    return result


TEST_REMOVE_TIMEZONE = cartesian_product(TIMEZONES, [
    (
        datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=UTC),
        datetime.datetime(2024, 1, 1, 0, 1, 2),
    ),
    (
        datetime.datetime(2024, 1, 1, 0, 1, 2),
        datetime.datetime(2024, 1, 1, 0, 1, 2),
    ),
])

TEST_UTC_TIMEZONE = cartesian_product(TIMEZONES, [
    (
        datetime.datetime(2024, 1, 1, 0, 1, 2),
        datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=UTC),
    ),
    (
        datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=UTC),
        datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=UTC),
    ),
])

TEST_EPOCH_SECONDS = cartesian_product(TIMEZONES, [
    (0, dict(year=1970, day=1, month=1, hour=0, minute=0, second=0, microsecond=0)),
    (1E-6, dict(year=1970, day=1, month=1, hour=0, minute=0, second=0, microsecond=1)),
    (1E-3, dict(year=1970, day=1, month=1, hour=0, minute=0, second=0, microsecond=1000)),
    (3691.2, dict(year=1970, day=1, month=1, hour=1, minute=1, second=31, microsecond=200000)),
])

TEST_EPOCH_TO_SECONDS = cartesian_product(TIMEZONES, [
    (datetime.datetime(1970, 1, 1, 0, 1, 2, 0), 62),
    (datetime.datetime(1970, 1, 1, 0, 1, 2, 0, tzinfo=UTC), 62),
])

TEST_CONVERT_RELATIVE_TO_DATETIME = cartesian_product(TIMEZONES, [
    (
        '+0',
        False,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        datetime.datetime(2024, 1, 1, 0, 0, 0),
    ),
    (
        '+1s',
        False,
        datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC),
        datetime.datetime(2024, 1, 1, 0, 0, 1),
    ),
    (
        '-10w20d30h40m50s',
        False,
        datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC),
        datetime.datetime(2023, 10, 1, 17, 19, 10),
    ),
    (
        '+0',
        True,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC),
    ),
    (
        '+1s',
        True,
        datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC),
        datetime.datetime(2024, 1, 1, 0, 0, 1, tzinfo=UTC),
    ),
    (
        '-10w20d30h40m50s',
        True,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        datetime.datetime(2023, 10, 1, 17, 19, 10, tzinfo=UTC),
    ),
])

TEST_GET_RELATIVE_TIME_OPTION = cartesian_product(TIMEZONES, [
    (
        '+1d2h3m4s',
        'foo',
        'cryptography',
        False,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        datetime.datetime(2024, 1, 2, 2, 3, 4),
    ),
    (
        '-1w10d24h',
        'foo',
        'cryptography',
        False,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        datetime.datetime(2023, 12, 14, 0, 0, 0),
    ),
    (
        '20240102040506Z',
        'foo',
        'cryptography',
        False,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        datetime.datetime(2024, 1, 2, 4, 5, 6),
    ),
    (
        '202401020405Z',
        'foo',
        'cryptography',
        False,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        datetime.datetime(2024, 1, 2, 4, 5, 0),
    ),
    (
        '+1d2h3m4s',
        'foo',
        'cryptography',
        True,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        datetime.datetime(2024, 1, 2, 2, 3, 4, tzinfo=UTC),
    ),
    (
        '-1w10d24h',
        'foo',
        'cryptography',
        True,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        datetime.datetime(2023, 12, 14, 0, 0, 0, tzinfo=UTC),
    ),
    (
        '20240102040506Z',
        'foo',
        'cryptography',
        True,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        datetime.datetime(2024, 1, 2, 4, 5, 6, tzinfo=UTC),
    ),
    (
        '202401020405Z',
        'foo',
        'cryptography',
        True,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        datetime.datetime(2024, 1, 2, 4, 5, 0, tzinfo=UTC),
    ),
    (
        '+1d2h3m4s',
        'foo',
        'pyopenssl',
        False,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        '20240102020304Z',
    ),
    (
        '-1w10d24h',
        'foo',
        'pyopenssl',
        False,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        '20231214000000Z',
    ),
    (
        '20240102040506Z',
        'foo',
        'pyopenssl',
        False,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        '20240102040506Z',
    ),
    (
        '202401020405Z',
        'foo',
        'pyopenssl',
        False,
        datetime.datetime(2024, 1, 1, 0, 0, 0),
        '202401020405Z',
    ),
])


if sys.version_info >= (3, 5):
    ONE_HOUR_PLUS = datetime.timezone(datetime.timedelta(hours=1))

    TEST_REMOVE_TIMEZONE.extend(cartesian_product(TIMEZONES, [
        (
            datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=ONE_HOUR_PLUS),
            datetime.datetime(2023, 12, 31, 23, 1, 2),
        ),
    ]))
    TEST_UTC_TIMEZONE.extend(cartesian_product(TIMEZONES, [
        (
            datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=ONE_HOUR_PLUS),
            datetime.datetime(2023, 12, 31, 23, 1, 2, tzinfo=UTC),
        ),
    ]))
    TEST_EPOCH_TO_SECONDS.extend(cartesian_product(TIMEZONES, [
        (datetime.datetime(1970, 1, 1, 0, 1, 2, 0, tzinfo=ONE_HOUR_PLUS), 62 - 3600),
    ]))
    TEST_GET_RELATIVE_TIME_OPTION.extend(cartesian_product(TIMEZONES, [
        (
            '20240102040506+0100',
            'foo',
            'cryptography',
            False,
            datetime.datetime(2024, 1, 1, 0, 0, 0),
            datetime.datetime(2024, 1, 2, 3, 5, 6),
        ),
        (
            '202401020405+0100',
            'foo',
            'cryptography',
            False,
            datetime.datetime(2024, 1, 1, 0, 0, 0),
            datetime.datetime(2024, 1, 2, 3, 5, 0),
        ),
        (
            '20240102040506+0100',
            'foo',
            'cryptography',
            True,
            datetime.datetime(2024, 1, 1, 0, 0, 0),
            datetime.datetime(2024, 1, 2, 3, 5, 6, tzinfo=UTC),
        ),
        (
            '202401020405+0100',
            'foo',
            'cryptography',
            True,
            datetime.datetime(2024, 1, 1, 0, 0, 0),
            datetime.datetime(2024, 1, 2, 3, 5, 0, tzinfo=UTC),
        ),
        (
            '20240102040506+0100',
            'foo',
            'pyopenssl',
            False,
            datetime.datetime(2024, 1, 1, 0, 0, 0),
            '20240102040506+0100',
        ),
        (
            '202401020405+0100',
            'foo',
            'pyopenssl',
            False,
            datetime.datetime(2024, 1, 1, 0, 0, 0),
            '202401020405+0100',
        ),
    ]))


@pytest.mark.parametrize("timezone, input, expected", TEST_REMOVE_TIMEZONE)
def test_remove_timezone(timezone, input, expected):
    with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
        output_1 = remove_timezone(input)
        assert expected == output_1
        output_2 = add_or_remove_timezone(input, with_timezone=False)
        assert expected == output_2


@pytest.mark.parametrize("timezone, input, expected", TEST_UTC_TIMEZONE)
def test_utc_timezone(timezone, input, expected):
    with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
        output_1 = ensure_utc_timezone(input)
        assert expected == output_1
        output_2 = add_or_remove_timezone(input, with_timezone=True)
        assert expected == output_2


# @pytest.mark.parametrize("timezone", TIMEZONES)
# Due to a bug in freezegun (https://github.com/spulec/freezegun/issues/348, https://github.com/spulec/freezegun/issues/553)
# this only works with timezone = UTC
@pytest.mark.parametrize("timezone", [datetime.timedelta(hours=0)])
def test_get_now_datetime_w_timezone(timezone):
    with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
        output_2 = get_now_datetime(with_timezone=True)
        assert output_2.tzinfo is not None
        assert output_2.tzinfo == UTC
        assert output_2 == datetime.datetime(2024, 2, 3, 4, 5, 6, tzinfo=UTC)


@pytest.mark.parametrize("timezone", TIMEZONES)
def test_get_now_datetime_wo_timezone(timezone):
    with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
        output_1 = get_now_datetime(with_timezone=False)
        assert output_1.tzinfo is None
        assert output_1 == datetime.datetime(2024, 2, 3, 4, 5, 6)


@pytest.mark.parametrize("timezone, seconds, timestamp", TEST_EPOCH_SECONDS)
def test_epoch_seconds(timezone, seconds, timestamp):
    with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
        ts_wo_tz = datetime.datetime(**timestamp)
        assert seconds == get_epoch_seconds(ts_wo_tz)
        timestamp_w_tz = dict(timestamp)
        timestamp_w_tz['tzinfo'] = UTC
        ts_w_tz = datetime.datetime(**timestamp_w_tz)
        assert seconds == get_epoch_seconds(ts_w_tz)
        output_1 = from_epoch_seconds(seconds, with_timezone=False)
        assert ts_wo_tz == output_1
        output_2 = from_epoch_seconds(seconds, with_timezone=True)
        assert ts_w_tz == output_2


@pytest.mark.parametrize("timezone, timestamp, expected_seconds", TEST_EPOCH_TO_SECONDS)
def test_epoch_to_seconds(timezone, timestamp, expected_seconds):
    with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
        assert expected_seconds == get_epoch_seconds(timestamp)


@pytest.mark.parametrize("timezone, relative_time_string, with_timezone, now, expected", TEST_CONVERT_RELATIVE_TO_DATETIME)
def test_convert_relative_to_datetime(timezone, relative_time_string, with_timezone, now, expected):
    with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
        output = convert_relative_to_datetime(relative_time_string, with_timezone=with_timezone, now=now)
        assert expected == output


@pytest.mark.parametrize("timezone, input_string, input_name, backend, with_timezone, now, expected", TEST_GET_RELATIVE_TIME_OPTION)
def test_get_relative_time_option(timezone, input_string, input_name, backend, with_timezone, now, expected):
    with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
        output = get_relative_time_option(input_string, input_name, backend=backend, with_timezone=with_timezone, now=now)
        assert expected == output
