"""
testflo is a python testing framework that takes an iterator of test
specifier names e.g., <test_module>:<testcase>.<test_method>, and feeds
them through a pipeline of iterators that operate on them and transform them
into Test objects, then pass them on to other objects in the pipeline.

The Discoverer object takes an initial list of directory names, module names,
or test specifier strings and returns an iterator of Test objects representing
all of the tests found.  The rest of the pipeline operates on these Test
objects, which contain a test specifier string, a status indicating
whether the test passed or failed, and captured stderr from the
running of the test.

The only real API function for objects added to the pipline is:

    def get_iter(self, input_iter)

Functions can also be added directly to the pipeline as long as they
take a Test object iterator as an arg and return a Test object iterator.

The get_iter function should expect to receive an iterator over Test objects
and should return an iterator over Test objects.

"""
from __future__ import print_function

import os
import sys
import time
import warnings
import multiprocessing
import atexit
import shutil

from fnmatch import fnmatch, fnmatchcase

import testflo
from testflo.runner import ConcurrentTestRunner
from testflo.printer import ResultPrinter
from testflo.benchmark import BenchmarkWriter
from testflo.summary import ResultSummary
from testflo.deprecations import DeprecationsReport
from testflo.duration import DurationSummary
from testflo.discover import TestDiscoverer
from testflo.filters import TimeFilter, FailFilter

from testflo.util import read_config_file, read_test_file
from testflo.options import get_options
from testflo.qman import get_server_queue

options = get_options()


def dryrun(input_iter):
    """Iterator added to the pipeline when user only wants a dry run, listing specs for all of the
    discovered tests but not actually running them.
    """
    for tests in input_iter:
        for test in tests:
            if test.status is None:
                test.status = 'OK'
            print(test.spec)
            yield test


class DeDuper(object):
    """
    Removes duplicates from the pipeline.
    """

    def get_iter(self, input_iter):
        seen = set()
        for tests in input_iter:
            for test in tests:
                if test.spec not in seen:
                    yield test
                    seen.add(test.spec)


def run_pipeline(source, pipe, disallow_skipped):
    """Run a pipeline of test iteration objects."""

    global _start_time
    _start_time = time.perf_counter()

    iters = [source]

    # give each object the iterator from upstream in the pipeline
    for i,p in enumerate(pipe):
        iters.append(p(iters[i]))

    n_failed = 0
    n_skipped = 0
    # iterate over the last iter in the pipeline and we're done
    for result in iters[-1]:
        if result.status == 'FAIL' and not result.expected_fail:
            n_failed += 1
        elif result.status == 'SKIP':
            n_skipped += 1

    if n_failed > 0:
        return_code = 1
    elif n_skipped > 0 and disallow_skipped:
        return_code = 2
    else:
        return_code = 0

    return return_code


def main(args=None):
    if args is None:
        args = sys.argv[1:]

    options = get_options(args)

    if options.version:
        print("testflo version %s" % testflo.__version__)
        return 0

    nprocs = options.num_procs

    options.skip_dirs = []

    # read user prefs from ~/.testflo file.
    # create one if it doesn't exist
    homedir = os.path.expanduser('~')
    rcfile = os.path.join(homedir, '.testflo')
    if not os.path.isfile(rcfile):
        with open(rcfile, 'w') as f:
            f.write("""[testflo]
skip_dirs=site-packages,
    dist-packages,
    build,
    _build,
    contrib
""")
    read_config_file(rcfile, options)
    if options.cfg:
        read_config_file(options.cfg, options)

    if nprocs is None and options.num_procs is None:
        try:
            options.num_procs = multiprocessing.cpu_count()
        except:
            warnings.warn('CPU count could not be determined. Defaulting to 1')
            options.num_procs = 1

    if nprocs is not None:
        options.num_procs = nprocs

    tests = options.tests
    if options.testfile:
        tests += list(read_test_file(options.testfile))

    if not tests:
        tests = [os.getcwd()]

    def dir_exclude(d):
        base = os.path.basename(d)
        for skip in options.skip_dirs:
            if fnmatch(base, skip):
                return True
        return False

    # set this so code will know when it's running under testflo
    os.environ['TESTFLO_RUNNING'] = '1'

    if options.coverage or options.coveragehtml:
        os.environ['TESTFLO_MAIN_PID'] = str(os.getpid())
        # some coverage files aren't written until atexit of their processes, so put our finalize
        # routine in atexit before their Coverage._atexit methods are registered so ours will be
        # executed *after* theirs.
        def _finalize():
            if os.getpid() == int(os.environ.get('TESTFLO_MAIN_PID', '0')):
                from testflo.cover import finalize_coverage
                finalize_coverage(options)
                # clean up the temporary dir where we store the interim coverage files from all
                # of the processes.
                if os.path.isdir('_covdir'):
                    shutil.rmtree('_covdir')
        atexit.register(_finalize)
        from testflo.cover import setup_coverage
        setup_coverage(options)

    if options.noreport:
        report_file = open(os.devnull, 'a')
    else:
        report_file = open(options.outfile, 'w')

    if not options.test_glob:
        options.test_glob = ['test*']

    if options.benchmark:
        options.num_procs = 1
        options.isolated = True
        discoverer = TestDiscoverer(options, module_pattern='benchmark*.py',
                                    func_match=lambda f: fnmatchcase(f, 'benchmark*'),
                                    dir_exclude=dir_exclude)
        benchmark_file = open(options.benchmarkfile, 'a')
    else:
        def func_matcher(funcname):
            for pattern in options.excludes:
                if fnmatchcase(funcname, pattern):
                    return False

            for pattern in options.test_glob:
                if fnmatchcase(funcname, pattern):
                    return True

            return False

        discoverer = TestDiscoverer(options, dir_exclude=dir_exclude, func_match=func_matcher)
        benchmark_file = open(os.devnull, 'a')

    retval = 0

    if options.isolated or not options.nompi:
        # create a distributed queue and get a proxy to it
        manager, queue = get_server_queue()
    else:
        manager, queue = (None, None)

    with report_file as report, benchmark_file as bdata:
        pipeline = [
            discoverer.get_iter,
        ]

        if options.dryrun:
            pipeline.append(dryrun)
        else:
            if options.pre_announce:
                options.num_procs = 1

            pipeline.append(ConcurrentTestRunner(options, queue).get_iter)

            if options.show_deprecations or options.deprecations_report:
                pipeline.append(DeprecationsReport(options).get_iter)

            if options.benchmark:
                pipeline.append(BenchmarkWriter(stream=bdata).get_iter)

            verbose = -1 if options.compact else int(options.verbose)

            pipeline.append(ResultPrinter(options, verbose=verbose).get_iter)
            if not options.noreport:
                pipeline.append(ResultPrinter(options, report, verbose=1).get_iter)

            # prevent subtests from appearing multiple times in summaries
            pipeline.append(DeDuper().get_iter)

            if options.durations:
                pipeline.append(DurationSummary(options).get_iter)
                if not options.noreport:
                    pipeline.append(DurationSummary(options, stream=report).get_iter)

            pipeline.append(ResultSummary(options).get_iter)
            if not options.noreport:
                pipeline.append(ResultSummary(options, stream=report).get_iter)

        if options.maxtime > 0:
            pipeline.append(TimeFilter(options.maxtime).get_iter)

        if options.save_fails or options.failfile:
            if options.failfile is None:  # user must have specified -f
                options.failfile = 'failtests.in'
            pipeline.append(FailFilter(options.failfile).get_iter)

        retval = run_pipeline(tests, pipeline, options.disallow_skipped)

        if manager is not None:
            manager.shutdown()

    return retval


def run_tests(args=None):
    """This can be executed from within an "if __name__ == '__main__'" block
    to execute the tests found in that module.
    """
    if args is None:
        args = []
    sys.exit(main(list(args) + [__import__('__main__').__file__]))


if __name__ == '__main__':
    sys.exit(main())
