# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
# ----------------------------------------------------------------------------
"""Test isodatetime.main."""


import os
from subprocess import PIPE, Popen  # nosec
from unittest.mock import patch

import pytest

import metomi.isodatetime
import metomi.isodatetime.main as isodatetime_main


@patch('builtins.print')
def test_1_version(mock_print):
    """Test print version."""
    for args in [['--version'], ['-V']]:
        isodatetime_main.main(args)
        mock_print.assert_called_with(
            metomi.isodatetime.__version__
        )


@patch('builtins.print')
def test_1_null(mock_print):
    """Test calling usage 1, no or now argument."""
    with patch.object(
        isodatetime_main.DateTimeOperator,
        'process_time_point_str',
        return_value='20190101T1234Z'
    ):
        for arg in ['', 'now']:
            isodatetime_main.main([arg])
            mock_print.assert_called_with('20190101T1234Z')


@patch('builtins.print')
def test_1_good(mock_print):
    """Test calling usage 1, sample good arguments."""
    env_ref = isodatetime_main.DateTimeOperator.ENV_REF
    ref = os.environ.get(env_ref)
    for args, out in [
        (['20200101T00Z'], '20200101T00Z'),
        (['ref'], '20201225T0000Z'),
        # UTC mode
        (['-u', '20200101T00+0100'], '20191231T23+0000'),
        (['--utc', '20200101T00+0100'], '20191231T23+0000'),
        # With offsets
        (['-s', 'P1D', '20191231T00Z'], '20200101T00Z'),
        (['-s', 'P1D', '--offset=PT1H', '20191231T00Z'], '20200101T01Z'),
        # Negative duration
        (['-s', 'P1D', '--offset=-PT1H', '20191231T00Z'], '20191231T23Z'),
        (['-s', 'P1D', '--offset', '\\-PT1H', '20191231T00Z'],
         '20191231T23Z'),
        # Print format
        (['-f', 'CCYY', '20191231T00Z'], '2019'),
        (['--format', 'CCYY', '20191231T00Z'], '2019'),
        (['--print-format', 'CCYY', '20191231T00Z'], '2019'),
        (['20200101T00-1130', '-f', 'CCYYMMDDThhmm+0000'],
         '20200101T1130+0000'),
        # Parse format
        (['--parse-format=%d/%m/%Y', '-f', 'CCYY-MM-DD', '31/12/2019'],
         '2019-12-31'),
        (['-p', '%d/%m/%Y', '-f', 'CCYY-MM-DD', '31/12/2019'],
         '2019-12-31'),
    ]:
        os.environ[env_ref] = '20201225T0000Z'
        try:
            isodatetime_main.main(args)
            mock_print.assert_called_with(out)
        finally:
            if ref is not None:
                os.environ[env_ref] = ref
            else:
                del os.environ[env_ref]


@patch('builtins.print')
def test_1_bad(mock_print):
    """Test calling usage 1, sample bad arguments."""
    for args, out in [
        # Bad time point string
        (['201abc'], 'Invalid ISO 8601 date representation: 201abc'),
        # Bad offset string
        (['-s', 'add-a-year', '2019'], 'add-a-year: bad offset value'),
    ]:
        mock_print.reset_mock()
        with pytest.raises(SystemExit) as ctxmgr:
            isodatetime_main.main(args)
        mock_print.assert_not_called()
        assert str(ctxmgr.value) == out


@patch('builtins.print')
def test_2_good(mock_print):
    """Test calling usage 2, sample good arguments."""
    env_ref = isodatetime_main.DateTimeOperator.ENV_REF
    ref = os.environ.get(env_ref)
    for args, out in [
        # Same
        (['ref', 'ref'], 'P0Y'),
        (['20380119', '20380119'], 'P0Y'),
        # Positive duration
        (['20181225', '20191225'], 'P365D'),
        (['-1', 'PT12H', '-2', 'PT12H', '20181225', '20191225'], 'P365D'),
        # Negative duration
        (['20191225', '20181225'], '-P365D'),
        (['--offset1=-PT6H', '--offset2=-PT6H', '20191225', '20181225'],
         '-P365D'),
        (['--offset1', '\\-PT6H', '--offset2', '\\-PT6H', '20191225',
         '20181225'], '-P365D'),
    ]:
        os.environ[env_ref] = '20201225T0000Z'
        try:
            isodatetime_main.main(args)
            mock_print.assert_called_with(out)
        finally:
            if ref is not None:
                os.environ[env_ref] = ref
            else:
                del os.environ[env_ref]


@patch('builtins.print')
def test_2_bad(mock_print):
    """Test calling usage 2, sample bad arguments."""
    for args, out in [
        # Bad time point string
        (['201abc', '2020'],
         'Invalid ISO 8601 date representation: 201abc'),
        # Bad offset string
        (['-1', 'add-a-year', '2018', '2019'],
         'add-a-year: bad offset value'),
    ]:
        mock_print.reset_mock()
        with pytest.raises(SystemExit) as ctxmgr:
            isodatetime_main.main(args)
        mock_print.assert_not_called()
        assert str(ctxmgr.value) == out


@patch('builtins.print')
def test_3_good(mock_print):
    """Test calling usage 3, sample good arguments."""
    for args, out in [
        # Same
        (['--as-total=s', 'PT1H30M'], 5400),
        (['--as-total=s', 'P1D'], 86400),
        (['--as-total=h', 'P1D'], 24),
    ]:
        isodatetime_main.main(args)
        mock_print.assert_called_with(out)


@patch('builtins.print')
def test_3_bad(mock_print):
    """Test calling usage 3, sample bad arguments."""
    mock_print.reset_mock()
    with pytest.raises(SystemExit) as ctxmgr:
        isodatetime_main.main(['--as-total=s', 'PS4'])
    mock_print.assert_not_called()
    assert str(ctxmgr.value) == (
        'Invalid ISO 8601 duration representation: PS4'
    )


@patch('builtins.print')
def test_4_good(mock_print):
    """Test calling usage 4, sample good arguments."""
    for args, out in [
        # Forward
        (['-u', 'R/2020/P1Y'],
         '\n'.join('%d-01-01T00:00:00Z' % i for i in range(2020, 2030))),
        (['-u', 'R3/2020/P1Y'],
         '\n'.join('%d-01-01T00:00:00Z' % i for i in range(2020, 2023))),
        (['-u', '--max=5', 'R/2020/P1Y'],
         '\n'.join('%d-01-01T00:00:00Z' % i for i in range(2020, 2025))),
        (['-u', '--max=15', 'R/2020/P1Y'],
         '\n'.join('%d-01-01T00:00:00Z' % i for i in range(2020, 2035))),
        (['--print-format=%Y', 'R/2020/P1Y'],
         '\n'.join('%d' % i for i in range(2020, 2030))),
        # Reverse
        (['-u', 'R/P1Y/2020'],
         '\n'.join('%d-01-01T00:00:00Z' % i
                   for i in range(2020, 2010, -1))),
    ]:
        isodatetime_main.main(args)
        mock_print.assert_called_with(out)


@patch('builtins.print')
def test_4_bad(mock_print):
    """Test calling usage 4, sample bad arguments."""
    mock_print.reset_mock()
    with pytest.raises(SystemExit) as ctxmgr:
        isodatetime_main.main(['R-1/2020/2025'])
    mock_print.assert_not_called()
    assert str(ctxmgr.value) == (
        'Invalid ISO 8601 recurrence representation: R-1/2020/2025'
    )


def test_negative_duration():
    """Test arg parsing of negative durations."""
    for variant in ('-P1D', r'\-P1D',):
        args = isodatetime_main.parse_args(['2000', '-s', variant])
        assert args.offsets1 == ['-P1D']
        args = isodatetime_main.parse_args(['2000', rf'-s={variant}'])
        assert args.offsets1 == ['-P1D']


@pytest.mark.parametrize(
    'stdin,args,stdout', [
        ('2000', [], '2000'),
        ('2000\n2001', ['--as-total', 'h'], '8784.0')
    ]
)
def test_pipe(stdin, args, stdout):
    """Test piping args into the command via stdin."""
    assert Popen(
        ['isodatetime', '-'] + args,
        stdout=PIPE, stdin=PIPE
    ).communicate(  # nosec
        stdin.encode()
    )[0].decode().strip() == stdout
