#!/usr/bin/python
# encoding: utf-8

import os, sys, getopt, pprint, time, requests, re

#   .--GUI Test------------------------------------------------------------.
#   |                  ____ _   _ ___   _____         _                    |
#   |                 / ___| | | |_ _| |_   _|__  ___| |_                  |
#   |                | |  _| | | || |    | |/ _ \/ __| __|                 |
#   |                | |_| | |_| || |    | |  __/\__ \ |_                  |
#   |                 \____|\___/|___|   |_|\___||___/\__|                 |
#   |                                                                      |
#   +----------------------------------------------------------------------+
#   | Actual code for running the tests                                    |
#   '----------------------------------------------------------------------'

def tests_dir():
    path = omd_root() + "/var/check_mk/guitests/"
    if not os.path.exists(path):
        os.makedirs(path)
    return path


def recording_symlink():
    return tests_dir() + "RECORD"


def run_guitests(args):
    if not args:
        args = names_of_all_tests()

    sys.stdout.write("Going to run %d tests%s...\n" % (
       len(args), opt_repair and " in repair mode" or ""))

    num_successfull = 0
    for test_name in args:
        if opt_repair:
            set_recording_symlink(test_name)
        succeeded = run_guitest(test_name)
        if opt_repair:
            stop_recording()
        if succeeded:
            num_successfull += 1
        else:
            break

    if succeeded:
        sys.stdout.write("%sAll %d tests succeeded.%s\n" % (tty_green, num_successfull, tty_normal))
        sys.exit(0)
    else:
        sys.stdout.write("%d tests succeeded, but last one %sfailed.%s\n" % (num_successfull, tty_red, tty_normal))
        sys.exit(1)


def run_guitest(test_name):
    try:
        sys.stdout.write("    %s%s%s:\n" % (tty_yellow, test_name, tty_normal))
        guitest = load_guitest(test_name)

        for step_nr, step in enumerate(guitest):
            if step_nr < range_from:
                continue
            elif step_nr > range_to:
                break
            sys.stdout.write("       #%02d - %s..." % (step_nr, step_title(step)))
            errors = run_test_step(test_name, step_nr, step, opt_repair)
            if not errors:
                sys.stdout.write(tty_green + "OK\n" + tty_normal)
            else:
                sys.stdout.write(tty_red + "FAILED\n" + tty_normal)
                for error in errors:
                    sys.stdout.write("           %s\n" % error)

                sys.stdout.write("        Replay this step with: %s%s%s\n" % (tty_blue, rule_step_url(test_name, step_nr), tty_normal))
                if opt_interactive:
                    if interactive_repair(test_name, step_nr, step):
                        continue

                if not opt_continue:
                    return False
        sys.stdout.write("\n")
        return True

    except Exception, e:
        if opt_debug:
            raise
        sys.stdout.write("        %sfailed%s (%s)\n" % (tty_red, tty_normal, e))
        return False



def interactive_repair(test_name, step_nr, step):
    answer = None
    while answer not in [ "y", "n" ]:
        answer = read_line("Repair this test step (y/n)?").strip()
    if answer == "y":
        sys.stdout.write("Rerunning this step #%02d in repair mode...\n" % step_nr)
        set_recording_symlink(test_name)
        errors = run_test_step(test_name, step_nr, step, True)
        remove_recording_symlink()
        if errors:
            for error in errors:
                sys.stdout.write("           %s\n" % error)

        return not errors
    else:
        return False


def read_line(prompt, echo=True):
    sys.stderr.write(prompt + " " + tty_bold)
    sys.stderr.flush()
    try:
        if echo:
            answer = sys.stdin.readline().rstrip()
        else:
            answer = getpass.getpass(prompt="")
    except:
        sys.stderr.write("\n")
        answer = None

    sys.stderr.write(tty_normal)
    return answer


def load_guitest(test_name):

    path = tests_dir() + test_name + ".mk"
    if not os.path.exists(path):
        raise Exception("Test not found (missing file %s)" % path)
    guitest = eval(file(path).read())

    global range_from, range_to
    range_from = opt_steps_range[0]
    if opt_steps_range[1] == None:
        range_to = len(guitest) - 1
    else:
        range_to = opt_steps_range[1]

    return guitest


def save_guitest(test_name, guitest):
    file(tests_dir() + test_name + ".mk", "w").write("%s\n" % pprint.pformat(guitest))


def names_of_all_tests():
    return sorted([
        f[:-3] for f in os.listdir(tests_dir()) if f.endswith(".mk")
    ])


def run_test_step(test_name, step_nr, step, repair):
    if "wait" in step:
        do_wait(step["wait"])
        return []

    url = "http://localhost/%s/check_mk/guitest.py" % omd_site()
    response = requests.post(url, data = {
        "test" : test_name,
        "step" : str(step_nr),
        "repair" : repair and "1" or "0"
    })
    try:
        response_text = response.text.decode(response.encoding)
    except:
        response_text = response.text

    if "<pre>\nMOD_PYTHON ERROR\n\n" in response_text:
        end_offset = response_text.find("MODULE CACHE DETAILS")
        if end_offset != -1:
            response_text = response_text[:end_offset]
        return [ "MOD PYTHON ERROR: %s" % response_text ]

    if response.status_code != requests.codes.ok:
        errors = [ "HTTP Response is not %s but %s" % (requests.codes.ok, response.status_code) ]
        errors += response_text.splitlines()
        return errors

    error_offset = response_text.find('[[[GUITEST FAILED]]]')
    if error_offset != -1:
        error_info = response_text[error_offset + 21:]
        errors = hilite_keywords(error_info).splitlines()
        return errors

def do_wait(seconds):
    sys.stdout.write(tty_blue)
    while seconds > 0:
        sys.stdout.write(".")
        sys.stdout.flush()
        time.sleep(min(seconds, 0.2))
        seconds -= 0.2
    return


def hilite_keywords(text):
    text = text.replace(" expected ", tty_green + " expected " + tty_normal) \
               .replace(" got ", tty_red + " got " + tty_normal)
    text = re.sub(r" expected (.*?)\[\[\[([^]]*?)\]\]\]", r" expected \1" + tty_green + r"\2" + tty_normal, text, flags=re.DOTALL)
    text = re.sub(r" got (.*?)\[\[\[(.*?)\]\]\]", r" got \1" + tty_red + r"\2" + tty_normal, text, flags=re.DOTALL)
    return text


def list_guitests(test_names):
    if not test_names:
        test_names = names_of_all_tests()
    for test_name in test_names:
        sys.stdout.write("%s\n" % (tty_yellow + test_name + tty_normal))
        test = load_guitest(test_name)
        for nr, step in enumerate(test):
            sys.stdout.write("    #%02d - %s\n" % (nr, step_title(step)))
        sys.stdout.write("\n")


def step_title(step):
    if "wait" in step:
        return (" " * 41) + "%sWaiting for %d ms%s" % (tty_blue, step["wait"] * 1000.0, tty_normal)

    if "page_title" in step["output"]:
        page_title = step["output"]["page_title"][0]
    else:
        page_title = "(no page title)"

    transid = step["variables"].get("_transid")
    if transid == "valid":
        action_marker = "[%sA%s] " % (tty_red, tty_normal)
    elif transid == "invalid":
        action_marker = "[X] "
    else:
        action_marker = "    "
    return "%s%-9s%s %-26s %s%s" % (tty_cyan, step["user"], tty_normal,
               step["filename"] + ".py", action_marker, tty_bold + page_title + tty_normal)


def rule_step_url(test_name, step_nr):
    return "http://localhost/%s/check_mk/guitest.py?test=%s&step=%d" % (
        omd_site(), test_name, step_nr)


def start_stop_recording(args):
    if recording_symlink_exists():
        stop_recording()
    else:
        start_recording(args[0])


def start_recording(test_name):
    set_recording_symlink(test_name)
    if os.path.exists(recording_symlink()):
        sys.stdout.write("Started recording, appending to existing test %s (%d steps exist).\n" %
          (test_name, len(load_guitest(test_name))))
    else:
        sys.stdout.write("Started recording into new test %s.\n" % test_name)



def set_recording_symlink(test_name):
    if recording_symlink_exists():
        os.remove(recording_symlink())
    os.symlink(test_name + ".mk", recording_symlink())


def recording_symlink_exists():
    return os.path.lexists(recording_symlink())


def remove_recording_symlink():
    os.remove(recording_symlink())


def stop_recording():
    test_name = os.readlink(recording_symlink())[:-3]
    if not os.path.exists(recording_symlink()):
        sys.stdout.write("Aborted recording. Test %s not created.\n" % test_name)
        remove_recording_symlink()
        return

    remove_recording_symlink()
    test = load_guitest(test_name)
    sys.stdout.write("Stopped recording test %s (%d steps).\n" % (test_name, len(test)))


def extract_from_test(args):
    if len(args) < 1:
        bail_out("Missing name of test to extract from.")
    elif len(args) > 2:
        bail_out("You can specify at most one test to extract from.")

    from_test_name = args[0]
    if len(args) > 1:
        to_test_name = args[1]
    else:
        to_test_name = None

    guitest = load_guitest(from_test_name)

    extracted_steps = guitest[range_from : range_to]
    if to_test_name:
        save_guitest(to_test_name, extracted_steps)
    else:
        sys.stdout.write("%s\n" % pprint.pformat(extracted_steps))


def delete_steps_from_test(args):
    if len(args) != 1:
        bail_out("Please specify exactly one test to delete steps from.")

    test_name = args[0]
    guitest = load_guitest(test_name)
    del guitest[range_from:range_to+1]
    save_guitest(test_name, guitest)


def add_wait_step(args):
    if len(args) != 3:
        bail_out("Please specify the test, the step number to add the wait after and the duration in ms")
    test_name = args[2]
    guitest = load_guitest(test_name)
    step_number = int(args[0])
    wait_seconds = int(args[1]) / 1000.0
    guitest[step_number+1:step_number+1] = [ { "wait" : wait_seconds } ]
    save_guitest(test_name, guitest)


def add_reschedule_step(args):
    if len(args) != 2:
        bail_out("Please specify the test and the step number to add the reschdule after")
    test_name = args[1]
    guitest = load_guitest(test_name)
    step_number = int(args[0])
    guitest[step_number+1:step_number+1] = [{
        'elapsed_time': 0.01,
        'filename': 'guitest_reschedule_all',
        'output': {'message': [('message', 'All hosts are checked.\n'),
                               ('message', 'All services are checked.\n')],
                   'page_title': ['Rescheduling and waiting for check results']},
        'user': u'omdadmin',
        'variables': {},
    }]
    save_guitest(test_name, guitest)

#.
#   .-Helpers--------------------------------------------------------------.
#   |                  _   _      _                                        |
#   |                 | | | | ___| |_ __   ___ _ __ ___                    |
#   |                 | |_| |/ _ \ | '_ \ / _ \ '__/ __|                   |
#   |                 |  _  |  __/ | |_) |  __/ |  \__ \                   |
#   |                 |_| |_|\___|_| .__/ \___|_|  |___/                   |
#   |                              |_|                                     |
#   +----------------------------------------------------------------------+
#   | Various helper functions                                             |
#   '----------------------------------------------------------------------'

if sys.stdout.isatty() and not os.name == "nt":
    tty_bold      = '\033[1m'
    tty_normal    = '\033[0m'
    tty_red       = tty_bold + '\033[31m'
    tty_green     = tty_bold + '\033[32m'
    tty_yellow    = tty_bold + '\033[33m'
    tty_cyan      = tty_bold + '\033[36m'
    tty_blue      = tty_bold + '\033[34m'
else:
    tty_bold   = ""
    tty_normal = ""
    tty_red    = ""
    tty_green  = ""
    tty_yellow = ""
    tty_cyan   = ""
    tty_blue   = ""

def omd_root():
    return os.getenv("OMD_ROOT")

def omd_site():
    return os.getenv("OMD_SITE")

def verbose(x):
    if opt_verbose:
        sys.stderr.write("%s\n" % x)

def bail_out(x):
    sys.stderr.write("%s\n" % x)
    sys.exit(1)


def parse_range_argument(a):
    if ":" in a:
        first_text, last_text = a.split(":")
        if first_text:
            first_step = int(first_text)
        else:
            first_step = 0
        if last_text:
            last_step = int(last_text)
        else:
            last_step = None
    else:
        first_step = int(a)
        last_step = first_step

    return first_step, last_step


def usage():
    sys.stderr.write("""
Usage: cmk-guitest [OPTIONS] MODE [ARGS...]

  OPERATION MODES

     (no option) [TESTS...]              Run some or all tests
     -R, --repair                        Run a test and at the same time rerecord it
     -r, --record                        Start/stop recording test TEST
     -l, --list-tests [TEST]             List one or all tests
     -x, --extract RANGE TEST [OUTPUT]   Extract a range of steps from a test to stdout
     -D, --delete RANGE TEST             Remove a range of steps from a test
     -W, --add-wait-step STEP MS TEST    Insert and artificial wait phase into the test
     -E, --add-reschedule-step STEP TEST Insert and artificial "reschedule all checks"
     -h, --help                          Show this crufty help


  OPTIONS

     -v, --verbose                       Output debug information on stderr
         --debug                         Do not catch Python exceptions
     -S, --steps RANGE                   Just run steps START...END (1st step is 0)
     -i, --interactive                   When tests fail ask for automatic repair
     -c, --continue                      Continue after errors


  RANGES

     0    Just the step 0
     2:5  Steps 2, 3, 4 and 5
     :5   All steps from 0 through 5
     2:   All steps from 2 until end of the test

""")

#.
#   .-main-----------------------------------------------------------------.
#   |                                       _                              |
#   |                       _ __ ___   __ _(_)_ __                         |
#   |                      | '_ ` _ \ / _` | | '_ \                        |
#   |                      | | | | | | (_| | | | | |                       |
#   |                      |_| |_| |_|\__,_|_|_| |_|                       |
#   |                                                                      |
#   +----------------------------------------------------------------------+
#   | Main entry point, getopt, etc.                                       |
#   '----------------------------------------------------------------------'

short_options = 'hvrslRS:x:D:icWE'
long_options = [ "help", "debug", "verbose", "list-tests=", "record", "repair",
                 "steps=", "extract=", "delete=", "interactive", "continue",
                 "add-wait-step", "add-reschedule-step" ]

opt_verbose = False
opt_debug = False
opt_repair = False
opt_steps_range = 0, None
opt_interactive = False
opt_continue = False

try:
    opts, args = getopt.getopt(sys.argv[1:], short_options, long_options)
except getopt.GetoptError, err:
    sys.stderr.write("%s\n\n" % err)
    usage()
    sys.exit(1)

mode_function = run_guitests

for o,a in opts:
    # modes
    if o in [ '-h', '--help' ]:
        usage()
        sys.exit(0)
    elif o in [ '-l', '--list-tests' ]:
        mode_function = list_guitests
    elif o in [ '-r', '--record' ]:
        mode_function = start_stop_recording
    elif o in [ '-x', '--extract' ]:
        opt_steps_range = parse_range_argument(a)
        mode_function = extract_from_test
    elif o in [ '-D', '--delete' ]:
        opt_steps_range = parse_range_argument(a)
        mode_function = delete_steps_from_test
    elif o in [ '-W', '--add-wait-step' ]:
        mode_function = add_wait_step
    elif o in [ '-E', '--add-reschedule-step' ]:
        mode_function = add_reschedule_step

    # Modifiers
    elif o in [ '-S', '--steps' ]:
        opt_steps_range = parse_range_argument(a)
    elif o in [ '-v', '--verbose' ]:
        opt_verbose = True
    elif o == '--debug':
        opt_debug = True
    elif o in [ '-R', '--repair' ]:
        opt_repair = True
    elif o in [ '-i', '--interactive' ]:
        opt_interactive = True
    elif o in [ '-c', '--continue' ]:
        opt_continue = True

# Main modes
try:
    test_names = []
    for a in args:
        if a.endswith(".mk"):
            test_name = a[:-3] # Makes guitest wato.* easier
        else:
            test_name = a
        test_names.append(test_name)
    mode_function(test_names)

except Exception, e:
    if opt_debug:
        raise
    bail_out(e)
