#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Copyright (c) 2011-2020, wradlib developers.
# Distributed under the MIT License. See LICENSE.txt for more info.

import doctest
import getopt
import inspect
import io
import os
import sys
import unittest
from multiprocessing import Process, Queue

import coverage
import nbformat
from nbconvert.preprocessors import ExecutePreprocessor
from nbconvert.preprocessors.execute import CellExecutionError

VERBOSE = 2


def create_examples_testsuite():
    # gather information on examples
    # all functions inside the examples starting with 'ex_' or 'recipe_'
    # are considered as tests
    # find example files in examples directory
    root_dir = "examples/"
    files = []
    skip = ["__init__.py"]
    for root, _, filenames in os.walk(root_dir):
        for filename in filenames:
            if filename in skip or filename[-3:] != ".py":
                continue
            if "examples/data" in root:
                continue
            f = os.path.join(root, filename)
            f = f.replace("/", ".")
            f = f[:-3]
            files.append(f)
    # create empty testsuite
    suite = unittest.TestSuite()
    # find matching functions in
    for idx, module in enumerate(files):
        module1, func = module.split(".")
        module = __import__(module)
        func = getattr(module, func)
        funcs = inspect.getmembers(func, inspect.isfunction)
        [
            suite.addTest(unittest.FunctionTestCase(v))
            for k, v in funcs
            if k.startswith(("ex_", "recipe_"))
        ]

    return suite


class NotebookTest(unittest.TestCase):
    def __init__(self, nbfile, cov):
        setattr(self.__class__, nbfile, staticmethod(self._runTest))
        super(NotebookTest, self).__init__(nbfile)
        self.nbfile = nbfile
        self.cov = cov

    def _runTest(self):
        kernel = "python%d" % sys.version_info[0]
        cur_dir = os.path.dirname(self.nbfile)

        with open(self.nbfile) as f:
            nb = nbformat.read(f, as_version=4)
            if self.cov:
                covdict = {
                    "cell_type": "code",
                    "execution_count": 1,
                    "metadata": {"collapsed": True},
                    "outputs": [],
                    "nbsphinx": "hidden",
                    "source": "import coverage\n"
                    "coverage.process_startup()\n"
                    "import sys\n"
                    'sys.path.append("{0}")\n'.format(cur_dir),
                }
                nb["cells"].insert(0, nbformat.from_dict(covdict))

            exproc = ExecutePreprocessor(kernel_name=kernel, timeout=600)

            try:
                run_dir = os.getenv("WRADLIB_BUILD_DIR", cur_dir)
                exproc.preprocess(nb, {"metadata": {"path": run_dir}})
            except CellExecutionError as e:
                raise e

        if self.cov:
            nb["cells"].pop(0)

        with io.open(self.nbfile, "wt") as f:
            nbformat.write(nb, f)

        self.assertTrue(True)


def create_notebooks_testsuite(**kwargs):
    # gather information on notebooks
    # all notebooks in the notebooks folder
    # are considered as tests
    # find notebook files in notebooks directory
    cov = kwargs.pop("cov")
    root_dir = os.getenv("WRADLIB_NOTEBOOKS", "notebooks")
    files = []
    skip = []
    for root, _, filenames in os.walk(root_dir):
        for filename in filenames:
            if filename in skip or filename[-6:] != ".ipynb":
                continue
            # skip checkpoints
            if "/." in root:
                continue
            f = os.path.join(root, filename)
            files.append(f)

    # create one TestSuite per Notebook to treat testrunners
    # memory overconsumption on travis-ci
    suites = []
    for file in files:
        suite = unittest.TestSuite()
        suite.addTest(NotebookTest(file, cov))
        suites.append(suite)

    return suites


def create_doctest_testsuite():
    # gather information on doctests, search in only wradlib folder
    root_dir = "wradlib/"
    files = []
    skip = ["__init__.py", "version.py", "bufr.py", "test_"]
    for root, _, filenames in os.walk(root_dir):
        for filename in filenames:
            if filename in skip or filename[-3:] != ".py":
                continue
            if "wradlib/tests" in root:
                continue
            f = os.path.join(root, filename)
            f = f.replace("/", ".")
            f = f[:-3]
            files.append(f)

    # put modules in doctest suite
    suite = unittest.TestSuite()
    for module in files:
        suite.addTest(doctest.DocTestSuite(module))
    return suite


def create_unittest_testsuite():
    # gather information on tests (unittest etc)
    root_dir = "wradlib/tests/"
    return unittest.defaultTestLoader.discover(root_dir)


def single_suite_process(queue, test, verbosity, **kwargs):
    test_cov = kwargs.pop("coverage", 0)
    test_nb = kwargs.pop("notebooks", 0)
    if test_cov and not test_nb:
        cov = coverage.coverage()
        cov.start()
    all_success = 1
    for ts in test:
        if ts.countTestCases() != 0:
            res = unittest.TextTestRunner(verbosity=verbosity).run(ts)
            all_success = all_success & res.wasSuccessful()
    if test_cov and not test_nb:
        cov.stop()
        cov.save()
    queue.put(all_success)


def keep_tests(suite, arg):
    newsuite = unittest.TestSuite()
    try:
        for tc in suite:
            try:
                if tc.id().find(arg) != -1:
                    newsuite.addTest(tc)
            except AttributeError:
                new = keep_tests(tc, arg)
                if new.countTestCases() != 0:
                    newsuite.addTest(new)
    except TypeError:
        pass
    return newsuite


def main():

    args = sys.argv[1:]

    usage_message = """Usage: python testrunner.py options arg

    If run without options, testrunner displays the usage message.
    If all tests suites should be run, use the -a option.
    If arg is given, only tests containing arg are run.

    options:

        -a
        --all
            Run all tests (examples, test, doctest, notebooks)

        -m
            Run all tests within a single testsuite [default]

        -M
            Run each suite as separate instance

        -e
        --example
            Run only examples tests

        -d
        --doc
            Run only doctests

        -u
        --unit
            Run only unit test

        -n
        --notebook
            Run only notebook test

        -s
        --use-subprocess
            Run every testsuite in a subprocess.

        -c
        --coverage
            Run notebook tests with code coverage

        -v level
            Set the level of verbosity.

            0 - Silent
            1 - Quiet (produces a dot for each succesful test)
            2 - Verbose (default - produces a line of output for each test)

        -h
            Display usage information.

    """

    test_all = 0
    test_examples = 0
    test_docs = 0
    test_notebooks = 0
    test_units = 0
    test_subprocess = 0
    test_cov = 0
    verbosity = VERBOSE

    try:
        options, arg = getopt.getopt(
            args,
            "aednuschv:",
            [
                "all",
                "example",
                "doc",
                "notebook",
                "unit",
                "use-subprocess",
                "coverage",
                "help",
            ],
        )
    except getopt.GetoptError as e:
        err_exit(e.msg)

    if not options:
        err_exit(usage_message)
    for name, value in options:
        if name in ("-a", "--all"):
            test_all = 1
        elif name in ("-e", "--example"):
            test_examples = 1
        elif name in ("-d", "--doc"):
            test_docs = 1
        elif name in ("-n", "--notebook"):
            test_notebooks = 1
        elif name in ("-u", "--unit"):
            test_units = 1
        elif name in ("-s", "--use-subprocess"):
            test_subprocess = 1
        elif name in ("-c", "--coverage"):
            test_cov = 1
        elif name in ("-h", "--help"):
            err_exit(usage_message, 0)
        elif name == "-v":
            verbosity = int(value)
        else:
            err_exit(usage_message)

    if not (test_all or test_examples or test_docs or test_notebooks or test_units):
        err_exit("must specify one of: -a -e -d -n -u")

    testSuite = []

    if test_all:
        testSuite.append(create_examples_testsuite())
        testSuite.append(create_notebooks_testsuite(cov=test_cov))
        testSuite.append(create_doctest_testsuite())
        testSuite.append(create_unittest_testsuite())
    elif test_examples:
        testSuite.append(create_examples_testsuite())
    elif test_notebooks:
        testSuite.append(create_notebooks_testsuite(cov=test_cov))
    elif test_docs:
        testSuite.append(unittest.TestSuite(create_doctest_testsuite()))
    elif test_units:
        testSuite.append(create_unittest_testsuite())

    all_success = 1
    if test_subprocess:
        for test in testSuite:
            if arg:
                test = keep_tests(test, arg[0])
            queue = Queue()
            keywords = {"coverage": test_cov, "notebooks": test_notebooks}
            proc = Process(
                target=single_suite_process,
                args=(queue, test, verbosity),
                kwargs=keywords,
            )
            proc.start()
            result = queue.get()
            proc.join()
            # all_success should be 0 in the end
            all_success = all_success & result
    else:
        if test_cov and not test_notebooks:
            cov = coverage.coverage()
            cov.start()
        for ts in testSuite:
            if arg:
                ts = keep_tests(ts, arg[0])
            for test in ts:
                if test.countTestCases() != 0:
                    result = unittest.TextTestRunner(verbosity=verbosity).run(test)
                    # all_success should be 0 in the end
                    all_success = all_success & result.wasSuccessful()
        if test_cov and not test_notebooks:
            cov.stop()
            cov.save()

    if all_success:
        sys.exit(0)
    else:
        # This will return exit code 1
        sys.exit("At least one test has failed. " "Please see test report for details.")


def err_exit(message, rc=2):
    sys.stderr.write("\n%s\n" % message)
    sys.exit(rc)


if __name__ == "__main__":
    main()