#!/usr/bin/env python

import glob
import importlib
import os
import inspect
import fnmatch
import re
import sys
from tempfile import mkdtemp
import traceback
import random
import subprocess
import time

from lib import BaseTest
from s3_lib import S3Test
from swift_lib import SwiftTest
from azure_lib import AzureTest
from api_lib import APITest
from fs_endpoint_lib import FileSystemEndpointTest
from testout import TestOut

try:
    from termcolor import colored
except ImportError:
    def colored(s, **kwargs):
        return s


PYTHON_MINIMUM_VERSION = (3, 9)


def natural_key(string_):
    """See https://blog.codinghorror.com/sorting-for-humans-natural-sort-order/"""
    return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_)]


def run(include_long_tests=False, capture_results=False, tests=None, filters=None, coverage_dir=None, coverage_skip=False):
    """
    Run system test.
    """
    print(colored("\n Aptly System Tests\n====================\n", color="green", attrs=["bold"]))

    if not tests:
        tests = sorted(glob.glob("t*_*"), key=natural_key)
    fails = []
    numTests = numFailed = numSkipped = 0
    lastBase = None
    if not coverage_dir and not coverage_skip:
        coverage_dir = mkdtemp(suffix="aptly-coverage")

    failed = False
    for test in tests:
        orig_stdout = sys.stdout
        orig_stderr = sys.stderr

        # importlib.import_module(test)
        for fname in sorted(glob.glob(test + "/*.py"), key=natural_key):
            fname = os.path.splitext(os.path.basename(fname))[0]
            if fname == "__init__":
                continue

            testout = TestOut()
            sys.stdout = testout
            sys.stderr = testout

            try:
                testModule = importlib.import_module(test + "." + fname)
            except Exception as exc:
                orig_stdout.write(f"error importing: {test + '.' + fname}: {exc}\n")
                continue

            testignore = []
            if hasattr(testModule, "TEST_IGNORE"):
                testignore = testModule.TEST_IGNORE
            for name in sorted(dir(testModule), key=natural_key):
                if name in testignore:
                    continue

                o = getattr(testModule, name)
                if not (inspect.isclass(o) and issubclass(o, BaseTest) and o is not BaseTest and
                        o is not SwiftTest and o is not S3Test and o is not AzureTest and
                        o is not APITest and o is not FileSystemEndpointTest):
                    continue

                testout.clear()
                start_time = time.time()

                newBase = o.__bases__[0]
                if lastBase is not None and lastBase is not newBase:
                    lastBase.shutdown_class()

                lastBase = newBase

                if filters:
                    matches = False

                    for filt in filters:
                        if fnmatch.fnmatch(o.__name__, filt):
                            matches = True
                            break

                    if not matches:
                        continue

                orig_stdout.write("· %-13s ➔ %-48s ... " % (test, colored(o.__name__, color="yellow", attrs=["bold"])))
                orig_stdout.flush()

                t = o()

                if t.longTest and not include_long_tests or not t.fixture_available() or t.skipTest:
                    numSkipped += 1
                    msg = 'SKIP'
                    if t.skipTest and t.skipTest is not True:
                        # If we have a reason to skip, print it
                        msg += ': ' + t.skipTest
                    orig_stdout.write(colored(msg + "\n", color="yellow"))
                    continue

                numTests += 1

                failed = False
                t.captureResults = capture_results
                t.coverage_dir = coverage_dir
                typ = None
                val = None
                tb = None
                try:
                    t.test()
                except Exception:
                    typ, val, tb = sys.exc_info()
                    failed = True

                end_time = time.time()
                execution_time = int(end_time - start_time) + 1
                minutes = execution_time // 60
                seconds = execution_time % 60
                if minutes > 0:
                    minutes = f"{minutes}m"
                    if seconds < 10:
                        seconds = f"0{seconds}"
                else:
                    minutes = "  "
                    if seconds < 10:
                        seconds = f" {seconds}"
                duration = f"{minutes}{seconds}s"

                if failed:
                    numFailed += 1
                    fails.append((test, t, typ, val, tb, testModule))
                    orig_stdout.write(colored("\b\b\b\bFAIL", color="red", attrs=["bold"]) + f"   {duration}\n")

                    orig_stdout.write(testout.get_contents())
                    traceback.print_exception(typ, val, tb, file=orig_stdout)
                else:
                    orig_stdout.write(colored("\b\b\b\bOK", color="green", attrs=["bold"]) + f"   {duration}\n")

                t.shutdown()

                if failed:
                    break
            if failed:
                break

        sys.stdout = orig_stdout
        sys.stderr = orig_stderr
        if failed:
            break

    if lastBase is not None:
        lastBase.shutdown_class()

    print("\nCOVERAGE_RESULTS: %s" % coverage_dir)

    print(f"TESTS: {numTests}    ",
          colored(f"SUCCESS: {numTests - numFailed}    ", color="green", attrs=["bold"]) if numFailed == 0 else
          f"SUCCESS: {numTests - numFailed}    ",
          colored(f"FAIL: {numFailed}    ", color="red", attrs=["bold"]) if numFailed > 0 else "FAIL: 0    ",
          colored(f"SKIP: {numSkipped}", color="yellow", attrs=["bold"]) if numSkipped > 0 else "SKIP: 0")
    print()

    if len(fails) > 0:
        print(colored("FAILURES (%d):" % (len(fails), ), color="red", attrs=["bold"]))

        for (test, t, typ, val, tb, testModule) in fails:
            doc = t.__doc__ or ''
            print(" - %s: %s %s" % (test, colored(t.__class__.__name__, color="yellow", attrs=["bold"]),
                                    testModule.__name__ + ": " + doc.strip()))
        print()
        sys.exit(1)


if __name__ == "__main__":
    try:
        os.environ['APTLY_VERSION'] = os.popen("make -s version").read().strip()
    except BaseException as e:
        print("Failed to capture current version: ", e)

    if sys.version_info < PYTHON_MINIMUM_VERSION:
        raise RuntimeError(f'Tests require Python {PYTHON_MINIMUM_VERSION} or higher.')

    output = subprocess.check_output(['gpg', '--version'], text=True)
    if not output.startswith('gpg (GnuPG) 2'):
        raise RuntimeError('Tests require gpg v2')

    output = subprocess.check_output(['gpgv', '--version'], text=True)
    if not output.startswith('gpgv (GnuPG) 2'):
        raise RuntimeError('Tests require gpgv v2')

    os.chdir(os.path.realpath(os.path.dirname(sys.argv[0])))
    random.seed()
    include_long_tests = False
    capture_results = False
    coverage_dir = None
    coverage_skip = False
    tests = None
    args = sys.argv[1:]

    while len(args) > 0 and args[0].startswith("--"):
        if args[0] == "--long":
            include_long_tests = True
        elif args[0] == "--capture":
            capture_results = True
        elif args[0] == "--coverage-dir":
            coverage_dir = args[1]
            args = args[1:]
        elif args[0] == "--coverage-skip":
            coverage_skip = True

        args = args[1:]

    tests = []
    filters = []

    for arg in args:
        if arg.startswith('t'):
            tests.append(arg)
        else:
            filters.append(arg)

    run(include_long_tests, capture_results, tests, filters, coverage_dir, coverage_skip)
