#!/usr/bin/python
# Copyright 2013  Lars Wirzenius
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# =*= License: GPL-3+ =*=


import cliapp
import logging
import os
import re
import shutil
import tempfile
import time
import ttystatus

import cmdtestlib
import yarnlib


class YarnRunner(cliapp.Application):

    def setup(self):
        self.ts = ttystatus.TerminalStatus(period=0.001)
        self.ts.format(
            '%ElapsedTime() %Index(scenario,scenarios): '
            '%String(scenario_name): '
            'step %Index(step,steps): %String(step_name)')

    def process_args(self, args):
        scenarios, implementations = self.parse_scenarios(args)
        self.connect_implementations(scenarios, implementations)

        self.ts['scenarios'] = scenarios
        self.ts['num_scenarios'] = len(scenarios)
        logging.info('Found %d scenarios' % len(scenarios))

        start_time = time.time()
        failed_scenarios = []
        for scenario in scenarios:
            if not self.run_scenario(scenario):
                failed_scenarios.append(scenario)
        duration = time.time() - start_time

        self.ts.clear()
        self.ts.finish()

        if failed_scenarios:
            raise cliapp.AppException(
                'Test suite FAILED in %s scenarios' % len(failed_scenarios))

        print (
            'Scenario test suite PASS, with %d scenarios, in %.1f seconds' %
            (len(scenarios), duration))

    def parse_scenarios(self, filenames):
        mdparser = yarnlib.MarkdownParser()
        for filename in filenames:
            mdparser.parse_file(filename)

        block_parser = yarnlib.BlockParser()
        block_parser.parse_blocks(mdparser.blocks)

        return block_parser.scenarios, block_parser.implementations

    def connect_implementations(self, scenarios, implementations):
        for scenario in scenarios:
            for step in scenario.steps:
                self.connect_implementation(scenario, step, implementations)

    def connect_implementation(self, scenario, step, implementations):
        matching = [i for i in implementations
                    if step.what == i.what and
                       re.match('(%s)$' % i.regexp, step.text, re.I)]

        if len(matching) == 0:
            raise cliapp.AppException(
                'Scenario %s, step "%s %s" has no matching '
                'implementation' %
                (scenario.name, step.what, step.text))
        if len(matching) > 1:
            s = '\n'.join(
                'IMPLEMENTS %s %s' % (i.what, i.regexp)
                for i in matching)
            raise cliapp.AppException(
                'Scenario "%s", step "%s %s" has more than one '
                'matching implementations:\n%s' %
                (scenario.name, step.what, step.text, s))

        assert step.implementation is None
        step.implementation = matching[0]

    def run_scenario(self, scenario):
        logging.info('Running scenario %s' % scenario.name)
        self.ts['scenario'] = scenario
        self.ts['scenario_name'] = scenario.name
        self.ts['steps'] = scenario.steps

        datadir = tempfile.mkdtemp()

        cleanup = [s for s in scenario.steps if s.what == 'FINALLY']
        normal = [s for s in scenario.steps if s not in cleanup]

        ok = True

        for step in normal:
            exit = self.run_step(datadir, scenario, step)
            if exit != 0:
                ok = False
                break

        for step in cleanup:
            exit = self.run_step(datadir, scenario, step)
            if exit != 0:
                ok = False
                break

        shutil.rmtree(datadir)

        return ok

    def run_step(self, datadir, scenario, step):
        logging.info('Running step "%s %s"' % (step.what, step.text))
        logging.info('DATADIR is %s' % datadir)
        self.ts['step'] = step
        self.ts['step_name'] = '%s %s' % (step.what, step.text)

        m = re.match(step.implementation.regexp, step.text)
        assert m is not None
        env = os.environ.copy()
        env['DATADIR'] = datadir
        for i, match in enumerate(m.groups('')):
            env['MATCH_%d' % (i+1)] = match

        exit, stdout, stderr = cliapp.runcmd_unchecked(
            ['sh', '-euc', step.implementation.shell], env=env)

        logging.debug('Exit code: %d' % exit)
        if stdout:
            logging.debug('Standard output:\n%s' % self.indent(stdout))
        else:
            logging.debug('Standard output: empty')
        if stderr:
            logging.debug('Standard error:\n%s' % self.indent(stderr))
        else:
            logging.debug('Standard error: empty')

        if exit != 0:
            self.ts.error(
                'ERROR: In scenario "%s"\nstep "%s %s" failed,\n'
                'with exit code %d:\n'
                'Standard output from shell command:\n%s'
                'Standard error from shell command:\n%s' %
                (scenario.name, step.what, step.text, exit,
                 self.indent(stdout), self.indent(stderr)))

        return exit

    def indent(self, s):
        return ''.join('    %s\n' % line for line in s.splitlines())


YarnRunner(version=cmdtestlib.__version__).run()
