"""
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

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, _get_parser
from testflo.cover import setup_coverage, finalize_coverage
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


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

    global _start_time
    _start_time = time.time()

    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 > 1 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'

    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*']

    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

    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:
        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

            runner = ConcurrentTestRunner(options, queue)

            pipeline.append(runner.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)

            if options.durations:
                pipeline.append(DurationSummary(options).get_iter)

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

            pipeline.extend([
                ResultPrinter(options, verbose=verbose).get_iter,
                ResultSummary(options).get_iter,
            ])
            if not options.noreport:
                # print verbose results and summary to a report file
                if options.durations:
                    pipeline.append(DurationSummary(options, stream=report).get_iter)

                pipeline.extend([
                    ResultPrinter(options, report, verbose=1).get_iter,
                    ResultSummary(options, stream=report).get_iter,
                ])

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

        if options.save_fails:
            pipeline.append(FailFilter().get_iter)

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

        finalize_coverage(options)

        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())
