import unittest
import os

from robot.utils.argumentparser import ArgumentParser
from robot.utils.asserts import (assert_equal, assert_raises,
                                 assert_raises_with_msg, assert_true)
from robot.errors import Information, DataError, FrameworkError
from robot.version import get_full_version


USAGE = """Example Tool -- Stuff before hyphens is considered name

Usage:  robot.py [options] datafile

Version: <VERSION>

Options:
  -d --reportdir dir        Explanation
  -r --reportfile file      This explanation continues ............... 78
       ........... to multiple lines.
       Next line is totally empty.

  -E --escape what:with *      Line below has nothing after '*'. Next line has
          nothing after value and next nothing after option name
  -v --variable name:value *
  -N --name name
  -t -T --toggle   Something
  -h -? --help
  --version  Explanation

  -z   No long option so not an option line.
  --z  No long option here either
  this line doesn't start with a '-' so not an --optionline
  -\\-option     escaped 1
  -o -\\-option  escaped 2
          --ignored  options cannot be this far
          --ignored

* denotes options that can be set multiple times
"""

USAGE2 = """Just Name Here
usage:  robot.py [options] arg1 arg2

options:
  -v --variable name=value
  -x --var-able name=v1,v2   Explanation
  -3 --42
  --help
"""


class TestArgumentParserInit(unittest.TestCase):

    def setUp(self):
        self.ap = ArgumentParser(USAGE)

    def assert_long_opts(self, expected, ap=None):
        expected += ['no' + e for e in expected if not e.endswith('=')]
        long_opts = (ap or self.ap)._long_opts
        assert_equal(sorted(long_opts), sorted(expected))

    def assert_short_opts(self, expected, ap=None):
        assert_equal((ap or self.ap)._short_opts, expected)

    def assert_multi_opts(self, expected, ap=None):
        assert_equal((ap or self.ap)._multi_opts, expected)

    def assert_flag_opts(self, expected, ap=None):
        assert_equal((ap or self.ap)._flag_opts, expected)

    def test_short_options(self):
        self.assert_short_opts('d:r:E:v:N:tTh?')

    def test_long_options(self):
        self.assert_long_opts(['reportdir=', 'reportfile=', 'escape=',
                               'variable=', 'name=', 'toggle', 'help',
                               'version'])

    def test_multi_options(self):
        self.assert_multi_opts(['escape', 'variable'])

    def test_flag_options(self):
        self.assert_flag_opts(['toggle', 'help', 'version'])

    def test_options_must_be_indented_by_1_to_four_spaces(self):
        ap = ArgumentParser('''Name
1234567890
--notin  this option is not indented at all and thus ignored
 --opt1
    --opt2        This option is 4 spaces from left -> included
    -o --opt3 argument  It doesn't matter how far the option gets.
     --notopt     This option is 5 spaces from left -> not included
     -i --ignored
                     --not-in-either
    --included  back in four space indentation''')
        self.assert_long_opts(['opt1', 'opt2', 'opt3=', 'included'], ap)

    def test_case_insensitive_long_options(self):
        ap = ArgumentParser(' -f --foo\n -B --BAR\n')
        self.assert_short_opts('fB', ap)
        self.assert_long_opts(['foo', 'bar'], ap)

    def test_same_option_multiple_times(self):
        for usage in [' --foo\n --foo\n',
                      ' --foo\n -f --Foo\n',
                      ' -x --foo xxx\n -y --Foo yyy\n',
                      ' -f --foo\n -f --bar\n']:
            assert_raises(FrameworkError, ArgumentParser, usage)
        ap = ArgumentParser(' -f --foo\n -F --bar\n')
        self.assert_short_opts('fF', ap)
        self.assert_long_opts(['foo', 'bar'], ap)

    def test_same_option_multiple_times_with_no_prefix(self):
        for usage in [' --foo\n --nofoo\n',
                      ' --nofoo\n --foo\n'
                      ' --nose size\n --se\n']:
            assert_raises(FrameworkError, ArgumentParser, usage)
        ap = ArgumentParser(' --foo value\n --nofoo value\n')
        self.assert_long_opts(['foo=', 'nofoo='], ap)


class TestArgumentParserParseArgs(unittest.TestCase):

    def setUp(self):
        self.ap = ArgumentParser(USAGE)

    def test_missing_argument_file_throws_data_error(self):
        inargs = '--argumentfile missing_argument_file_that_really_is_not_there.txt'.split()
        self.assertRaises(DataError, self.ap.parse_args, inargs)

    def test_single_options(self):
        inargs = '-d reports --reportfile reps.html -T arg'.split()
        opts, args = self.ap.parse_args(inargs)
        assert_equal(opts, {'reportdir': 'reports', 'reportfile': 'reps.html',
                             'variable': [], 'name': None, 'toggle': True})

    def test_multi_options(self):
        inargs = '-v a:1 -v b:2 --name my_name --variable c:3 arg'.split()
        opts, args = self.ap.parse_args(inargs)
        assert_equal(opts, {'variable': ['a:1','b:2','c:3'], 'name':'my_name',
                             'reportdir': None, 'reportfile': None,
                             'toggle': None})
        assert_equal(args, ['arg'])

    def test_flag_options(self):
        for inargs, exp in [('', None),
                            ('--name whatever', None),
                            ('--toggle', True),
                            ('-T', True),
                            ('--toggle --name whatever -t', True),
                            ('-t -T --toggle', True),
                            ('--notoggle', False),
                            ('--notoggle --name xxx --notoggle', False),
                            ('--toggle --notoggle', False),
                            ('-t -t -T -T --toggle -T --notoggle', False),
                            ('--notoggle --toggle --notoggle', False),
                            ('--notoggle --toggle', True),
                            ('--notoggle --notoggle -T', True)]:
            opts, args = self.ap.parse_args(inargs.split() + ['arg'])
            assert_equal(opts['toggle'], exp, inargs)
            assert_equal(args, ['arg'])

    def test_flag_option_with_no_prefix(self):
        ap = ArgumentParser(' -S --nostatusrc\n --name name')
        for inargs, exp in [('', None),
                            ('--name whatever', None),
                            ('--nostatusrc', False),
                            ('-S', False),
                            ('--nostatusrc -S --nostatusrc -S -S', False),
                            ('--statusrc', True),
                            ('--statusrc --statusrc -S', False),
                            ('--nostatusrc --nostatusrc -S --statusrc', True)]:
            opts, args = ap.parse_args(inargs.split() + ['arg'])
            assert_equal(opts['statusrc'], exp, inargs)
            assert_equal(args, ['arg'])

    def test_single_option_multiple_times(self):
        for inargs in ['--name Foo -N Bar arg',
                       '-N Zap --name Foo --name Bar arg',
                       '-N 1 -N 2 -N 3 -t --variable foo -N 4 --name Bar arg']:
            opts, args = self.ap.parse_args(inargs.split())
            assert_equal(opts['name'], 'Bar')
            assert_equal(args, ['arg'])

    def test_case_insensitive_long_options(self):
        opts, args = self.ap.parse_args('--VarIable X:y --TOGGLE arg'.split())
        assert_equal(opts['variable'], ['X:y'])
        assert_equal(opts['toggle'], True)
        assert_equal(args, ['arg'])

    def test_case_insensitive_long_options_with_equal_sign(self):
        opts, args = self.ap.parse_args('--VariAble=X:y --VARIABLE=ZzZ'.split())
        assert_equal(opts['variable'], ['X:y', 'ZzZ'])
        assert_equal(args, [])

    def test_unescape_options(self):
        cli = '--escape quot:Q -E space:SP -E lt:LT -E gt:GT ' \
                + '-N QQQLTmySPfineSPnameGTQQQ sourceSPwithSPspaces'
        opts, args = self.ap.parse_args(cli.split())
        assert_equal(opts['name'], '"""<my fine name>"""')
        assert_equal(args, ['source with spaces'])
        assert_true('escape' not in opts)

    def test_split_pythonpath(self):
        ap = ArgumentParser('ignored')
        data = [(['path'], ['path']),
                (['path1','path2'], ['path1','path2']),
                (['path1:path2'], ['path1','path2']),
                (['p1:p2:p3','p4','.'], ['p1','p2','p3','p4','.'])]
        if os.sep == '\\':
            data += [(['c:\\path'], ['c:\\path']),
                     (['c:\\path','d:\\path'], ['c:\\path','d:\\path']),
                     (['c:\\path:d:\\path'], ['c:\\path','d:\\path']),
                     (['c:/path:x:yy:d:\\path','c','.','x:/xxx'],
                      ['c:\\path', 'x', 'yy', 'd:\\path', 'c', '.', 'x:\\xxx'])]
        for inp, exp in data:
            assert_equal(ap._split_pythonpath(inp), exp)

    def test_get_pythonpath(self):
        ap = ArgumentParser('ignored')
        p1 = os.path.abspath('.')
        p2 = os.path.abspath('..')
        assert_equal(ap._get_pythonpath(p1), [p1])
        assert_equal(ap._get_pythonpath([p1,p2]), [p1,p2])
        assert_equal(ap._get_pythonpath([p1 + ':' + p2]), [p1,p2])
        assert_true(p1 in ap._get_pythonpath(os.path.join(p2,'*')))

    def test_arguments_are_globbed(self):
        _, args = self.ap.parse_args([__file__.replace('test_', '?????')])
        assert_equal(args, [__file__])
        # Needed to ensure that the globbed directory contains files
        globexpr = os.path.join(os.path.dirname(__file__), '*')
        _, args = self.ap.parse_args([globexpr])
        assert_true(len(args) > 1)

    def test_arguments_with_glob_patterns_arent_removed_if_they_dont_match(self):
        _, args = self.ap.parse_args(['*.non.existing', 'non.ex.??'])
        assert_equal(args, ['*.non.existing', 'non.ex.??'])

    def test_special_options_are_removed(self):
        ap = ArgumentParser('''Usage:
 -h --help
 -v --version
 --pythonpath path
 --escape x:y *
 --argumentfile path
 --option
''')
        opts, args = ap.parse_args(['--option'])
        assert_equal(opts, {'option': True})

    def test_special_options_can_be_turned_to_normal_optios(self):
        ap = ArgumentParser('''Usage:
 -h --help
 -v --version
 --pythonpath path
 --escape x:y
 --argumentfile path
''', auto_help=False, auto_version=False, auto_escape=False,
     auto_pythonpath=False, auto_argumentfile=False)
        opts, args = ap.parse_args(['--help', '-v', '--escape', 'xxx'])
        assert_equal(opts, {'help': True, 'version': True, 'pythonpath': None,
                             'escape': 'xxx', 'argumentfile': None})

    def test_non_list_args(self):
        ap = ArgumentParser('''Options:
 -t --toggle
 -v --value value
 -m --multi multi *
''')
        opts, args = ap.parse_args(())
        assert_equal(opts, {'toggle': None,
                             'value': None,
                             'multi': []})
        assert_equal(args, [])
        opts, args = ap.parse_args(('-t', '-v', 'xxx', '-m', '1', '-m2', 'arg'))
        assert_equal(opts, {'toggle': True,
                             'value': 'xxx',
                             'multi': ['1', '2']})
        assert_equal(args, ['arg'])


class TestDefaultsFromEnvironmentVariables(unittest.TestCase):

    def setUp(self):
        os.environ['ROBOT_TEST_OPTIONS'] = '-t --value default -m1 --multi=2'
        self.ap = ArgumentParser('''Options:
 -t --toggle
 -v --value value
 -m --multi multi *
''', env_options='ROBOT_TEST_OPTIONS')

    def tearDown(self):
        os.environ.pop('ROBOT_TEST_OPTIONS')

    def test_flag(self):
        opts, args = self.ap.parse_args([])
        assert_equal(opts['toggle'], True)
        opts, args = self.ap.parse_args(['--toggle'])
        assert_equal(opts['toggle'], True)
        opts, args = self.ap.parse_args(['--notoggle'])
        assert_equal(opts['toggle'], False)

    def test_value(self):
        opts, args = self.ap.parse_args([])
        assert_equal(opts['value'], 'default')
        opts, args = self.ap.parse_args(['--value', 'given'])
        assert_equal(opts['value'], 'given')

    def test_multi_value(self):
        opts, args = self.ap.parse_args([])
        assert_equal(opts['multi'], ['1', '2'])
        opts, args = self.ap.parse_args(['-m3', '--multi', '4'])
        assert_equal(opts['multi'], ['1', '2', '3', '4'])

    def test_arguments(self):
        os.environ['ROBOT_TEST_OPTIONS'] = '-o opt arg1 arg2'
        ap = ArgumentParser('Usage:\n -o --opt value',
                            env_options='ROBOT_TEST_OPTIONS')
        opts, args = ap.parse_args([])
        assert_equal(opts['opt'], 'opt')
        assert_equal(args, ['arg1', 'arg2'])

    def test_environment_variable_not_set(self):
        ap = ArgumentParser('Usage:\n -o --opt value', env_options='NOT_SET')
        opts, args = ap.parse_args(['arg'])
        assert_equal(opts['opt'], None)
        assert_equal(args, ['arg'])


class TestArgumentValidation(unittest.TestCase):

    def test_check_args_with_correct_args(self):
        for arg_limits in [None, (1, 1), 1, (1,)]:
            ap = ArgumentParser(USAGE, arg_limits=arg_limits)
            assert_equal(ap.parse_args(['hello'])[1], ['hello'])

    def test_default_validation(self):
        ap = ArgumentParser(USAGE)
        for args in [(), ('1',), ('m', 'a', 'n', 'y')]:
            assert_equal(ap.parse_args(args)[1], list(args))

    def test_check_args_with_wrong_number_of_args(self):
        for limits in [1, (1, 1), (1, 2)]:
            ap = ArgumentParser('usage', arg_limits=limits)
            for args in [(), ('arg1', 'arg2', 'arg3')]:
                assert_raises(DataError, ap.parse_args, args)

    def test_check_variable_number_of_args(self):
        ap = ArgumentParser('usage:  robot.py [options] args', arg_limits=(1,))
        ap.parse_args(['one_is_ok'])
        ap.parse_args(['two', 'ok'])
        ap.parse_args(['this', 'should', 'also', 'work', '!'])
        assert_raises_with_msg(DataError, "Expected at least 1 argument, got 0.",
                               ap.parse_args, [])

    def test_argument_range(self):
        ap = ArgumentParser('usage:  test.py [options] args', arg_limits=(2,4))
        ap.parse_args(['1', '2'])
        ap.parse_args(['1', '2', '3', '4'])
        assert_raises_with_msg(DataError, "Expected 2 to 4 arguments, got 1.",
                               ap.parse_args, ['one is not enough'])

    def test_no_arguments(self):
        ap = ArgumentParser('usage:  test.py [options]', arg_limits=(0, 0))
        ap.parse_args([])
        assert_raises_with_msg(DataError, "Expected 0 arguments, got 2.",
                               ap.parse_args, ['1', '2'])

    def test_custom_validator_fails(self):
        def validate(options, args):
            raise AssertionError
        ap = ArgumentParser(USAGE2, validator=validate)
        assert_raises(AssertionError, ap.parse_args, [])

    def test_custom_validator_return_value(self):
        def validate(options, args):
            return options, [a.upper() for a in args]
        ap = ArgumentParser(USAGE2, validator=validate)
        opts, args = ap.parse_args(['-v', 'value', 'inp1', 'inp2'])
        assert_equal(opts['variable'], 'value')
        assert_equal(args, ['INP1', 'INP2'])


class TestPrintHelpAndVersion(unittest.TestCase):

    def setUp(self):
        self.ap = ArgumentParser(USAGE, version='1.0 alpha')
        self.ap2 = ArgumentParser(USAGE2)

    def test_print_help(self):
        assert_raises_with_msg(Information, USAGE2,
                               self.ap2.parse_args, ['--help'])

    def test_name_is_got_from_first_line_of_the_usage(self):
        assert_equal(self.ap.name, 'Example Tool')
        assert_equal(self.ap2.name, 'Just Name Here')

    def test_name_and_version_can_be_given(self):
        ap = ArgumentParser(USAGE, name='Kakkonen', version='2')
        assert_equal(ap.name, 'Kakkonen')
        assert_equal(ap.version, '2')

    def test_print_version(self):
        assert_raises_with_msg(Information, 'Example Tool 1.0 alpha',
                               self.ap.parse_args, ['--version'])

    def test_print_version_when_version_not_set(self):
        ap = ArgumentParser(' --version', name='Kekkonen')
        msg = assert_raises(Information, ap.parse_args, ['--version'])
        assert_equal(str(msg), 'Kekkonen %s' % get_full_version())

    def test_version_is_replaced_in_help(self):
        assert_raises_with_msg(Information, USAGE.replace('<VERSION>', '1.0 alpha'),
                               self.ap.parse_args, ['--help'])

    def test_escapes_are_replaced_in_help(self):
        usage = """Name
 --escape x:y      blaa blaa .............................................. end
                   <-----------------------ESCAPES---------------------------->
                   -- next line --
 --help"""
        expected = """Name
 --escape x:y      blaa blaa .............................................. end
                   Available escapes: amp (&), apos ('), at (@), bslash (\),
                   colon (:), comma (,), curly1 ({), curly2 (}), dollar ($),
                   exclam (!), gt (>), hash (#), lt (<), paren1 ((), paren2
                   ()), percent (%), pipe (|), quest (?), quot ("), semic (;),
                   slash (/), space ( ), square1 ([), square2 (]), star (*)
                   -- next line --
 --help"""
        assert_raises_with_msg(Information, expected,
                               ArgumentParser(usage).parse_args, ['--help'])


if __name__ == "__main__":
    unittest.main()
