# Copyright 2014 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import optparse

from typ.host import Host


class _Bailout(Exception):
    pass


DEFAULT_COVERAGE_OMIT = ['*/typ/*', '*/site-packages/*']
DEFAULT_STATUS_FORMAT = '[%f/%t] '
DEFAULT_SUFFIXES = ['*_test.py', '*_unittest.py']


class ArgumentParser(argparse.ArgumentParser):

    @staticmethod
    def add_option_group(parser, title, discovery=False,
                         running=False, reporting=False, skip=None):
        # TODO: Get rid of this when telemetry upgrades to argparse.
        ap = ArgumentParser(add_help=False, version=False, discovery=discovery,
                            running=running, reporting=reporting)
        optlist = ap.optparse_options(skip=skip)
        group = optparse.OptionGroup(parser, title)
        group.add_options(optlist)
        parser.add_option_group(group)

    def __init__(self, host=None, add_help=True, version=True, discovery=True,
                 reporting=True, running=True):
        super(ArgumentParser, self).__init__(prog='typ', add_help=add_help)

        self._host = host or Host()
        self.exit_status = None

        self.usage = '%(prog)s [options] [tests...]'

        if version:
            self.add_argument('-V', '--version', action='store_true',
                              help='Print the typ version and exit.')

        if discovery:
            self.add_argument('-f', '--file-list', metavar='FILENAME',
                              action='store',
                              help=('Takes the list of tests from the file '
                                    '(use "-" for stdin).'))
            self.add_argument('--all', action='store_true',
                              help=('Run all the tests, including the ones '
                                    'normally skipped.'))
            self.add_argument('--isolate', metavar='glob', default=[],
                              action='append',
                              help=('Globs of tests to run in isolation '
                                    '(serially).'))
            self.add_argument('--skip', metavar='glob', default=[],
                              action='append',
                              help=('Globs of test names to skip ('
                                    'defaults to %(default)s).'))
            self.add_argument('--suffixes', metavar='glob', default=[],
                              action='append',
                              help=('Globs of test filenames to look for ('
                                    'can specify multiple times; defaults '
                                    'to %s).' % DEFAULT_SUFFIXES))

        if reporting:
            self.add_argument('--builder-name',
                              help=('Builder name to include in the '
                                    'uploaded data.'))
            self.add_argument('-c', '--coverage', action='store_true',
                              help='Reports coverage information.')
            self.add_argument('--coverage-source', action='append',
                              default=[],
                              help=('Directories to include when running and '
                                    'reporting coverage (defaults to '
                                    '--top-level-dir plus --path)'))
            self.add_argument('--coverage-omit', action='append',
                              default=[],
                              help=('Globs to omit when reporting coverage '
                                    '(defaults to %s).' %
                                    DEFAULT_COVERAGE_OMIT))
            self.add_argument('--coverage-annotate', action='store_true',
                              help=('Produce an annotate source report.'))
            self.add_argument('--coverage-show-missing', action='store_true',
                              help=('Show missing line ranges in coverage '
                                    'report.'))
            self.add_argument('--master-name',
                              help=('Buildbot master name to include in the '
                                    'uploaded data.'))
            self.add_argument('--metadata', action='append', default=[],
                              help=('Optional key=value metadata that will '
                                    'be included in the results.'))
            self.add_argument('--test-results-server',
                              help=('If specified, uploads the full results '
                                    'to this server.'))
            self.add_argument('--test-type',
                              help=('Name of test type to include in the '
                                    'uploaded data (e.g., '
                                    '"telemetry_unittests").'))
            self.add_argument('--write-full-results-to', metavar='FILENAME',
                              action='store',
                              help=('If specified, writes the full results to '
                                    'that path.'))
            self.add_argument('--write-trace-to', metavar='FILENAME',
                              action='store',
                              help=('If specified, writes the trace to '
                                    'that path.'))
            self.add_argument('tests', nargs='*', default=[],
                              help=argparse.SUPPRESS)

        if running:
            self.add_argument('-d', '--debugger', action='store_true',
                              help='Runs the tests under the debugger.')
            self.add_argument('-j', '--jobs', metavar='N', type=int,
                              default=self._host.cpu_count(),
                              help=('Runs N jobs in parallel '
                                    '(defaults to %(default)s).'))
            self.add_argument('-l', '--list-only', action='store_true',
                              help='Lists all the test names found and exits.')
            self.add_argument('-n', '--dry-run', action='store_true',
                              help=argparse.SUPPRESS)
            self.add_argument('-q', '--quiet', action='store_true',
                              default=False,
                              help=('Runs as quietly as possible '
                                    '(only prints errors).'))
            self.add_argument('-s', '--status-format',
                              default=self._host.getenv('NINJA_STATUS',
                                                        DEFAULT_STATUS_FORMAT),
                              help=argparse.SUPPRESS)
            self.add_argument('-t', '--timing', action='store_true',
                              help='Prints timing info.')
            self.add_argument('-v', '--verbose', action='count', default=0,
                              help=('Prints more stuff (can specify multiple '
                                    'times for more output).'))
            self.add_argument('--passthrough', action='store_true',
                              default=False,
                              help='Prints all output while running.')
            self.add_argument('--total-shards', default=1, type=int,
                              help=('Total number of shards being used for '
                                    'this test run. (The user of '
                                    'this script is responsible for spawning '
                                    'all of the shards.)'))
            self.add_argument('--shard-index', default=0, type=int,
                              help=('Shard index (0..total_shards-1) of this '
                                    'test run.'))
            self.add_argument('--retry-limit', type=int, default=0,
                              help='Retries each failure up to N times.')
            self.add_argument('--terminal-width', type=int,
                              default=self._host.terminal_width(),
                              help=argparse.SUPPRESS)
            self.add_argument('--overwrite', action='store_true',
                              default=None,
                              help=argparse.SUPPRESS)
            self.add_argument('--no-overwrite', action='store_false',
                              dest='overwrite', default=None,
                              help=argparse.SUPPRESS)

        if discovery or running:
            self.add_argument('-P', '--path', action='append', default=[],
                              help=('Adds dir to sys.path (can specify '
                                    'multiple times).'))
            self.add_argument('--top-level-dir', default=None,
                              help=('Sets the top directory of project '
                                    '(used when running subdirs).'))

    def parse_args(self, args=None, namespace=None):
        try:
            rargs = super(ArgumentParser, self).parse_args(args=args,
                                                           namespace=namespace)
        except _Bailout:
            return None

        for val in rargs.metadata:
            if '=' not in val:
                self._print_message('Error: malformed --metadata "%s"' % val)
                self.exit_status = 2

        if rargs.test_results_server:
            if not rargs.builder_name:
                self._print_message('Error: --builder-name must be specified '
                                    'along with --test-result-server')
                self.exit_status = 2
            if not rargs.master_name:
                self._print_message('Error: --master-name must be specified '
                                    'along with --test-result-server')
                self.exit_status = 2
            if not rargs.test_type:
                self._print_message('Error: --test-type must be specified '
                                    'along with --test-result-server')
                self.exit_status = 2

        if rargs.total_shards < 1:
            self._print_message('Error: --total-shards must be at least 1')
            self.exit_status = 2

        if rargs.shard_index < 0:
            self._print_message('Error: --shard-index must be at least 0')
            self.exit_status = 2

        if rargs.shard_index >= rargs.total_shards:
            self._print_message('Error: --shard-index must be no more than '
                                'the number of shards (%i) minus 1' %
                                rargs.total_shards)
            self.exit_status = 2

        if not rargs.suffixes:
            rargs.suffixes = DEFAULT_SUFFIXES

        if not rargs.coverage_omit:
            rargs.coverage_omit = DEFAULT_COVERAGE_OMIT

        if rargs.debugger:  # pragma: no cover
            rargs.jobs = 1
            rargs.passthrough = True

        if rargs.overwrite is None:
            rargs.overwrite = self._host.stdout.isatty() and not rargs.verbose

        return rargs

    # Redefining built-in 'file' pylint: disable=W0622

    def _print_message(self, msg, file=None):
        self._host.print_(msg=msg, stream=file, end='\n')

    def print_help(self, file=None):
        self._print_message(msg=self.format_help(), file=file)

    def error(self, message, bailout=True):  # pylint: disable=W0221
        self.exit(2, '%s: error: %s\n' % (self.prog, message), bailout=bailout)

    def exit(self, status=0, message=None,  # pylint: disable=W0221
             bailout=True):
        self.exit_status = status
        if message:
            self._print_message(message, file=self._host.stderr)
        if bailout:
            raise _Bailout()

    def optparse_options(self, skip=None):
        skip = skip or []
        options = []
        for action in self._actions:
            args = [flag for flag in action.option_strings if flag not in skip]
            if not args or action.help == '==SUPPRESS==':
                # must either be a positional argument like 'tests'
                # or an option we want to skip altogether.
                continue

            kwargs = {
                'default': action.default,
                'dest': action.dest,
                'help': action.help,
                'metavar': action.metavar,
                'type': action.type,
                'action': _action_str(action)
            }
            options.append(optparse.make_option(*args, **kwargs))
        return options

    def argv_from_args(self, args):
        default_parser = ArgumentParser(host=self._host)
        default_args = default_parser.parse_args([])
        argv = []
        tests = []
        d = vars(args)
        for k in sorted(d.keys()):
            v = d[k]
            argname = _argname_from_key(k)
            action = self._action_for_key(k)
            action_str = _action_str(action)
            if k == 'tests':
                tests = v
                continue
            if getattr(default_args, k) == v:
                # this arg has the default value, so skip it.
                continue

            assert action_str in ['append', 'count', 'store', 'store_true']
            if action_str == 'append':
                for el in v:
                    argv.append(argname)
                    argv.append(el)
            elif action_str == 'count':
                for _ in range(v):
                    argv.append(argname)
            elif action_str == 'store':
                argv.append(argname)
                argv.append(str(v))
            else:
                # action_str == 'store_true'
                argv.append(argname)

        return argv + tests

    def _action_for_key(self, key):
        for action in self._actions:
            if action.dest == key:
                return action

        assert False, ('Could not find an action for %s'  # pragma: no cover
                       % key)


def _action_str(action):
    # Access to a protected member pylint: disable=W0212
    assert action.__class__ in (
        argparse._AppendAction,
        argparse._CountAction,
        argparse._StoreAction,
        argparse._StoreTrueAction
    )

    if isinstance(action, argparse._AppendAction):
        return 'append'
    if isinstance(action, argparse._CountAction):
        return 'count'
    if isinstance(action, argparse._StoreAction):
        return 'store'
    if isinstance(action, argparse._StoreTrueAction):
        return 'store_true'


def _argname_from_key(key):
    return '--' + key.replace('_', '-')
