#  Copyright 2008-2015 Nokia Networks
#  Copyright 2016-     Robot Framework Foundation
#
#  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 os
import random
import sys
import time

from robot.errors import DataError, FrameworkError
from robot.output import LOGGER, loggerhelper
from robot.result.keywordremover import KeywordRemover
from robot.result.flattenkeywordmatcher import validate_flatten_keyword
from robot.utils import (abspath, escape, format_time, get_link_path,
                         html_escape, is_list_like, py2to3,
                         split_args_from_name_or_path)

from .gatherfailed import gather_failed_tests, gather_failed_suites


@py2to3
class _BaseSettings(object):
    _cli_opts = {'RPA'              : ('rpa', None),
                 'Name'             : ('name', None),
                 'Doc'              : ('doc', None),
                 'Metadata'         : ('metadata', []),
                 'TestNames'        : ('test', []),
                 'TaskNames'        : ('task', []),
                 'ReRunFailed'      : ('rerunfailed', 'NONE'),
                 'ReRunFailedSuites': ('rerunfailedsuites', 'NONE'),
                 'SuiteNames'       : ('suite', []),
                 'SetTag'           : ('settag', []),
                 'Include'          : ('include', []),
                 'Exclude'          : ('exclude', []),
                 'Critical'         : ('critical', None),
                 'NonCritical'      : ('noncritical', None),
                 'OutputDir'        : ('outputdir', abspath('.')),
                 'Log'              : ('log', 'log.html'),
                 'Report'           : ('report', 'report.html'),
                 'XUnit'            : ('xunit', None),
                 'SplitLog'         : ('splitlog', False),
                 'TimestampOutputs' : ('timestampoutputs', False),
                 'LogTitle'         : ('logtitle', None),
                 'ReportTitle'      : ('reporttitle', None),
                 'ReportBackground' : ('reportbackground',
                                       ('#9e9', '#9e9', '#f66')),
                 'SuiteStatLevel'   : ('suitestatlevel', -1),
                 'TagStatInclude'   : ('tagstatinclude', []),
                 'TagStatExclude'   : ('tagstatexclude', []),
                 'TagStatCombine'   : ('tagstatcombine', []),
                 'TagDoc'           : ('tagdoc', []),
                 'TagStatLink'      : ('tagstatlink', []),
                 'RemoveKeywords'   : ('removekeywords', []),
                 'FlattenKeywords'  : ('flattenkeywords', []),
                 'PreRebotModifiers': ('prerebotmodifier', []),
                 'StatusRC'         : ('statusrc', True),
                 'ConsoleColors'    : ('consolecolors', 'AUTO'),
                 'StdOut'           : ('stdout', None),
                 'StdErr'           : ('stderr', None),
                 'XUnitSkipNonCritical' : ('xunitskipnoncritical', False)}
    _output_opts = ['Output', 'Log', 'Report', 'XUnit', 'DebugFile']

    def __init__(self, options=None, **extra_options):
        self.start_timestamp = format_time(time.time(), '', '-', '')
        self._opts = {}
        self._cli_opts = self._cli_opts.copy()
        self._cli_opts.update(self._extra_cli_opts)
        self._process_cli_opts(dict(options or {}, **extra_options))

    def _process_cli_opts(self, opts):
        for name, (cli_name, default) in self._cli_opts.items():
            value = opts[cli_name] if cli_name in opts else default
            if isinstance(default, list):
                # Copy mutable values and support list values as scalars.
                value = list(value) if is_list_like(value) else [value]
            self[name] = self._process_value(name, value)
        self['TestNames'] += self['ReRunFailed'] + self['TaskNames']
        self['SuiteNames'] += self['ReRunFailedSuites']

    def __setitem__(self, name, value):
        if name not in self._cli_opts:
            raise KeyError("Non-existing option '%s'." % name)
        self._opts[name] = value

    def _process_value(self, name, value):
        if name == 'ReRunFailed':
            return gather_failed_tests(value)
        if name == 'ReRunFailedSuites':
            return gather_failed_suites(value)
        if name == 'LogLevel':
            return self._process_log_level(value)
        if value == self._get_default_value(name):
            return value
        if name == 'Doc':
            return self._escape_as_data(value)
        if name in ['Metadata', 'TagDoc']:
            if name == 'Metadata':
                value = [self._escape_as_data(v) for v in value]
            return [self._process_metadata_or_tagdoc(v) for v in value]
        if name in ['Include', 'Exclude']:
            return [self._format_tag_patterns(v) for v in value]
        if name in self._output_opts and (not value or value.upper() == 'NONE'):
            return None
        if name == 'OutputDir':
            return abspath(value)
        if name in ['SuiteStatLevel', 'ConsoleWidth']:
            return self._convert_to_positive_integer_or_default(name, value)
        if name == 'VariableFiles':
            return [split_args_from_name_or_path(item) for item in value]
        if name == 'ReportBackground':
            return self._process_report_background(value)
        if name == 'TagStatCombine':
            return [self._process_tag_stat_combine(v) for v in value]
        if name == 'TagStatLink':
            return [v for v in [self._process_tag_stat_link(v) for v in value] if v]
        if name == 'Randomize':
            return self._process_randomize_value(value)
        if name == 'MaxErrorLines':
            return self._process_max_error_lines(value)
        if name == 'RemoveKeywords':
            self._validate_remove_keywords(value)
        if name == 'FlattenKeywords':
            self._validate_flatten_keywords(value)
        if name == 'WarnOnSkipped':
            with LOGGER.cache_only:
                LOGGER.warn("Option '--warnonskippedfiles' is deprecated and "
                            "has no effect. Nowadays all skipped files are "
                            "reported.")
        return value

    def _escape_as_data(self, value):
        return value

    def _process_log_level(self, level):
        level, visible_level = self._split_log_level(level.upper())
        self._opts['VisibleLogLevel'] = visible_level
        return level

    def _split_log_level(self, level):
        if ':' in level:
            level, visible_level = level.split(':', 1)
        else:
            visible_level = level
        self._validate_log_level_and_default(level, visible_level)
        return level, visible_level

    def _validate_log_level_and_default(self, log_level, default):
        if log_level not in loggerhelper.LEVELS:
            raise DataError("Invalid log level '%s'" % log_level)
        if default not in loggerhelper.LEVELS:
            raise DataError("Invalid log level '%s'" % default)
        if not loggerhelper.IsLogged(log_level)(default):
            raise DataError("Default visible log level '%s' is lower than "
                            "log level '%s'" % (default, log_level))

    def _process_max_error_lines(self, value):
        if not value or value.upper() == 'NONE':
            return None
        value = self._convert_to_integer('maxerrorlines', value)
        if value < 10:
            raise DataError("Option '--maxerrorlines' expected an integer "
                            "value greater that 10 but got '%s'." % value)
        return value

    def _process_randomize_value(self, original):
        value = original.lower()
        if ':' in value:
            value, seed = value.split(':', 1)
        else:
            seed = random.randint(0, sys.maxsize)
        if value in ('test', 'suite'):
            value += 's'
        if value not in ('tests', 'suites', 'none', 'all'):
            self._raise_invalid_option_value('--randomize', original)
        try:
            seed = int(seed)
        except ValueError:
            self._raise_invalid_option_value('--randomize', original)
        return value, seed

    def _raise_invalid_option_value(self, option_name, given_value):
        raise DataError("Option '%s' does not support value '%s'."
                        % (option_name, given_value))

    def __getitem__(self, name):
        if name not in self._opts:
            raise KeyError("Non-existing option '%s'." % name)
        if name in self._output_opts:
            return self._get_output_file(name)
        return self._opts[name]

    def _get_output_file(self, option):
        """Returns path of the requested output file and creates needed dirs.

        `option` can be 'Output', 'Log', 'Report', 'XUnit' or 'DebugFile'.
        """
        name = self._opts[option]
        if not name:
            return None
        if option == 'Log' and self._output_disabled():
            self['Log'] = None
            LOGGER.error('Log file is not created if output.xml is disabled.')
            return None
        name = self._process_output_name(option, name)
        path = abspath(os.path.join(self['OutputDir'], name))
        self._create_output_dir(os.path.dirname(path), option)
        return path

    def _process_output_name(self, option, name):
        base, ext = os.path.splitext(name)
        if self['TimestampOutputs']:
            base = '%s-%s' % (base, self.start_timestamp)
        ext = self._get_output_extension(ext, option)
        return base + ext

    def _get_output_extension(self, ext, type_):
        if ext != '':
            return ext
        if type_ in ['Output', 'XUnit']:
            return '.xml'
        if type_ in ['Log', 'Report']:
            return '.html'
        if type_ == 'DebugFile':
            return '.txt'
        raise FrameworkError("Invalid output file type: %s" % type_)

    def _create_output_dir(self, path, type_):
        try:
            if not os.path.exists(path):
                os.makedirs(path)
        except EnvironmentError as err:
            raise DataError("Creating %s file directory '%s' failed: %s"
                            % (type_.lower(), path, err.strerror))

    def _process_metadata_or_tagdoc(self, value):
        if ':' in value:
            return value.split(':', 1)
        return value, ''

    def _process_report_background(self, colors):
        if colors.count(':') not in [1, 2]:
            raise DataError("Invalid report background colors '%s'." % colors)
        colors = colors.split(':')
        if len(colors) == 2:
            return colors[0], colors[0], colors[1]
        return tuple(colors)

    def _process_tag_stat_combine(self, pattern):
        if ':' in pattern:
            pattern, title = pattern.rsplit(':', 1)
        else:
            title = ''
        return self._format_tag_patterns(pattern), title

    def _format_tag_patterns(self, pattern):
        for search, replace in [('&', 'AND'), ('AND', ' AND '), ('OR', ' OR '),
                                ('NOT', ' NOT '), ('_', ' ')]:
            if search in pattern:
                pattern = pattern.replace(search, replace)
        while '  ' in pattern:
            pattern = pattern.replace('  ', ' ')
        if pattern.startswith(' NOT'):
            pattern = pattern[1:]
        return pattern

    def _process_tag_stat_link(self, value):
        tokens = value.split(':')
        if len(tokens) >= 3:
            return tokens[0], ':'.join(tokens[1:-1]), tokens[-1]
        raise DataError("Invalid format for option '--tagstatlink'. "
                        "Expected 'tag:link:title' but got '%s'." % value)

    def _convert_to_positive_integer_or_default(self, name, value):
        value = self._convert_to_integer(name, value)
        return value if value > 0 else self._get_default_value(name)

    def _convert_to_integer(self, name, value):
        try:
            return int(value)
        except ValueError:
            raise DataError("Option '--%s' expected integer value but got '%s'."
                            % (name.lower(), value))

    def _get_default_value(self, name):
        return self._cli_opts[name][1]

    def _validate_remove_keywords(self, values):
        for value in values:
            try:
                KeywordRemover(value)
            except DataError as err:
                raise DataError("Invalid value for option '--removekeywords'. %s" % err)

    def _validate_flatten_keywords(self, values):
        try:
            validate_flatten_keyword(values)
        except DataError as err:
            raise DataError("Invalid value for option '--flattenkeywords'. %s" % err)

    def __contains__(self, setting):
        return setting in self._cli_opts

    def __unicode__(self):
        return '\n'.join('%s: %s' % (name, self._opts[name])
                         for name in sorted(self._opts))

    @property
    def output_directory(self):
        return self['OutputDir']

    @property
    def output(self):
        return self['Output']

    @property
    def log(self):
        return self['Log']

    @property
    def report(self):
        return self['Report']

    @property
    def xunit(self):
        return self['XUnit']

    @property
    def log_level(self):
        return self['LogLevel']

    @property
    def split_log(self):
        return self['SplitLog']

    @property
    def status_rc(self):
        return self['StatusRC']

    @property
    def xunit_skip_noncritical(self):
        return self['XUnitSkipNonCritical']

    @property
    def statistics_config(self):
        return {
            'suite_stat_level': self['SuiteStatLevel'],
            'tag_stat_include': self['TagStatInclude'],
            'tag_stat_exclude': self['TagStatExclude'],
            'tag_stat_combine': self['TagStatCombine'],
            'tag_stat_link': self['TagStatLink'],
            'tag_doc': self['TagDoc'],
        }

    @property
    def critical_tags(self):
        return self['Critical']

    @property
    def non_critical_tags(self):
        return self['NonCritical']

    @property
    def remove_keywords(self):
        return self['RemoveKeywords']

    @property
    def flatten_keywords(self):
        return self['FlattenKeywords']

    @property
    def pre_rebot_modifiers(self):
        return self['PreRebotModifiers']

    @property
    def console_colors(self):
        return self['ConsoleColors']

    @property
    def rpa(self):
        return self['RPA']

    @rpa.setter
    def rpa(self, value):
        self['RPA'] = value


class RobotSettings(_BaseSettings):
    _extra_cli_opts = {'Extension'          : ('extension', None),
                       'Output'             : ('output', 'output.xml'),
                       'LogLevel'           : ('loglevel', 'INFO'),
                       'MaxErrorLines'      : ('maxerrorlines', 40),
                       'DryRun'             : ('dryrun', False),
                       'ExitOnFailure'      : ('exitonfailure', False),
                       'ExitOnError'        : ('exitonerror', False),
                       'SkipTeardownOnExit' : ('skipteardownonexit', False),
                       'Randomize'          : ('randomize', 'NONE'),
                       'RunEmptySuite'      : ('runemptysuite', False),
                       'WarnOnSkipped'      : ('warnonskippedfiles', None),
                       'Variables'          : ('variable', []),
                       'VariableFiles'      : ('variablefile', []),
                       'PreRunModifiers'    : ('prerunmodifier', []),
                       'Listeners'          : ('listener', []),
                       'ConsoleType'        : ('console', 'verbose'),
                       'ConsoleTypeDotted'  : ('dotted', False),
                       'ConsoleTypeQuiet'   : ('quiet', False),
                       'ConsoleWidth'       : ('consolewidth', 78),
                       'ConsoleMarkers'     : ('consolemarkers', 'AUTO'),
                       'DebugFile'          : ('debugfile', None)}

    def get_rebot_settings(self):
        settings = RebotSettings()
        settings.start_timestamp = self.start_timestamp
        settings._opts.update(self._opts)
        for name in ['Variables', 'VariableFiles', 'Listeners']:
            del(settings._opts[name])
        for name in ['Include', 'Exclude', 'TestNames', 'SuiteNames', 'Metadata']:
            settings._opts[name] = []
        for name in ['Name', 'Doc']:
            settings._opts[name] = None
        settings._opts['Output'] = None
        settings._opts['LogLevel'] = 'TRACE'
        settings._opts['ProcessEmptySuite'] = self['RunEmptySuite']
        return settings

    def _output_disabled(self):
        return self.output is None

    def _escape_as_data(self, value):
        return escape(value)

    @property
    def listeners(self):
        return self['Listeners']

    @property
    def debug_file(self):
        return self['DebugFile']

    @property
    def suite_config(self):
        return {
            'name': self['Name'],
            'doc': self['Doc'],
            'metadata': dict(self['Metadata']),
            'set_tags': self['SetTag'],
            'include_tags': self['Include'],
            'exclude_tags': self['Exclude'],
            'include_suites': self['SuiteNames'],
            'include_tests': self['TestNames'],
            'empty_suite_ok': self.run_empty_suite,
            'randomize_suites': self.randomize_suites,
            'randomize_tests': self.randomize_tests,
            'randomize_seed': self.randomize_seed,
        }

    @property
    def randomize_seed(self):
        return self['Randomize'][1]

    @property
    def randomize_suites(self):
        return self['Randomize'][0] in ('suites', 'all')

    @property
    def randomize_tests(self):
        return self['Randomize'][0] in ('tests', 'all')

    @property
    def dry_run(self):
        return self['DryRun']
    @property
    def exit_on_failure(self):
        return self['ExitOnFailure']

    @property
    def exit_on_error(self):
        return self['ExitOnError']

    @property
    def skip_teardown_on_exit(self):
        return self['SkipTeardownOnExit']

    @property
    def console_output_config(self):
        return {
            'type':    self.console_type,
            'width':   self.console_width,
            'colors':  self.console_colors,
            'markers': self.console_markers,
            'stdout':  self['StdOut'],
            'stderr':  self['StdErr']
        }

    @property
    def console_type(self):
        if self['ConsoleTypeQuiet']:
            return 'quiet'
        if self['ConsoleTypeDotted']:
            return 'dotted'
        return self['ConsoleType']

    @property
    def console_width(self):
        return self['ConsoleWidth']

    @property
    def console_markers(self):
        return self['ConsoleMarkers']

    @property
    def max_error_lines(self):
        return self['MaxErrorLines']

    @property
    def pre_run_modifiers(self):
        return self['PreRunModifiers']

    @property
    def run_empty_suite(self):
        return self['RunEmptySuite']

    @property
    def variables(self):
        return self['Variables']

    @property
    def variable_files(self):
        return self['VariableFiles']

    @property
    def extension(self):
        return self['Extension']


class RebotSettings(_BaseSettings):
    _extra_cli_opts = {'Output'            : ('output', None),
                       'LogLevel'          : ('loglevel', 'TRACE'),
                       'ProcessEmptySuite' : ('processemptysuite', False),
                       'StartTime'         : ('starttime', None),
                       'EndTime'           : ('endtime', None),
                       'Merge'             : ('merge', False)}

    def _output_disabled(self):
        return False

    @property
    def suite_config(self):
        return {
            'name': self['Name'],
            'doc': self['Doc'],
            'metadata': dict(self['Metadata']),
            'set_tags': self['SetTag'],
            'include_tags': self['Include'],
            'exclude_tags': self['Exclude'],
            'include_suites': self['SuiteNames'],
            'include_tests': self['TestNames'],
            'empty_suite_ok': self.process_empty_suite,
            'remove_keywords': self.remove_keywords,
            'log_level': self['LogLevel'],
            'critical_tags': self.critical_tags,
            'non_critical_tags': self.non_critical_tags,
            'start_time': self['StartTime'],
            'end_time': self['EndTime']
        }

    @property
    def log_config(self):
        if not self.log:
            return {}
        return {
            'rpa': self.rpa,
            'title': html_escape(self['LogTitle'] or ''),
            'reportURL': self._url_from_path(self.log, self.report),
            'splitLogBase': os.path.basename(os.path.splitext(self.log)[0]),
            'defaultLevel': self['VisibleLogLevel']
        }

    @property
    def report_config(self):
        if not self.report:
            return {}
        return {
            'rpa': self.rpa,
            'title': html_escape(self['ReportTitle'] or ''),
            'logURL': self._url_from_path(self.report, self.log),
            'background' : self._resolve_background_colors(),
        }

    def _url_from_path(self, source, destination):
        if not destination:
            return None
        return get_link_path(destination, os.path.dirname(source))

    def _resolve_background_colors(self):
        colors = self['ReportBackground']
        return {'pass': colors[0], 'nonCriticalFail': colors[1], 'fail': colors[2]}

    @property
    def merge(self):
        return self['Merge']

    @property
    def console_output_config(self):
        return {
            'colors':  self.console_colors,
            'stdout':  self['StdOut'],
            'stderr':  self['StdErr']
        }

    @property
    def process_empty_suite(self):
        return self['ProcessEmptySuite']
