#!/usr/bin/env python3
""" Stress-test the shared WebDriver. """

import os
import threading
import signal
import http.client
import time
import datetime
import base64
import random
import json
import logging
from collections import defaultdict

import click
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys

from vasl_templates.webapp.webdriver import WebDriver
from vasl_templates.webapp.tests.test_scenario_persistence import load_scenario
from vasl_templates.webapp.tests.utils import wait_for, find_child, find_snippet_buttons, \
    select_tab, select_menu_option, click_dialog_button, set_stored_msg, get_stored_msg

shutdown_event = threading.Event()
thread_count = None

stats_lock = threading.Lock()
stats = defaultdict( lambda: [0,0] ) # nb: [ #runs, total elapsed time ]

# ---------------------------------------------------------------------

@click.command()
@click.option( "--webapp-url", default="http://localhost:5010", help="Webapp server URL." )
@click.option( "--snippet-images", default=1, help="Number of 'snippet image' threads to run." )
@click.option( "--update-vsav", default=1, help="Number of 'update VSAV' threads to run." )
@click.option( "--vsav","vsav_fname", help="VASL scenario file (.vsav) to be updated." )
def main( webapp_url, snippet_images, update_vsav, vsav_fname ):
    """Stress-test the shared WebDriver."""

    # initialize
    logging.disable( logging.CRITICAL )

    # read the VASL scenario file
    vsav_data = None
    if update_vsav > 0:
        with open( vsav_fname, "rb" ) as fp:
            vsav_data = fp.read()

    # prepare the test threads
    threads = []
    for i in range(0,snippet_images):
        threads.append( threading.Thread(
            target = snippet_images_thread,
            name = "snippet-images/{:02d}".format( 1+i ),
            args = ( webapp_url, )
        ) )
    for i in range(0,update_vsav):
        threads.append( threading.Thread(
            target = update_vsav_thread,
            name = "update-vsav/{:02d}".format( 1+i ),
            args = ( webapp_url, vsav_fname, vsav_data )
        ) )

    # launch the test threads
    start_time = time.time()
    global thread_count
    thread_count = len(threads)
    for thread in threads:
        thread.start()

    # wait for Ctrl-C
    def on_sigint( signum, stack ): #pylint: disable=missing-docstring,unused-argument
        print( "\n*** SIGINT received ***\n" )
        shutdown_event.set()
    signal.signal( signal.SIGINT, on_sigint )
    while not shutdown_event.is_set():
        time.sleep( 1 )

    # wait for the test threads to shutdown
    for thread in threads:
        print( "Waiting for thread to finish:", thread )
        thread.join()
    elapsed_time = time.time() - start_time
    print()

    # output stats
    print( "=== STATS ===")
    print()
    print( "Total run time: {}".format( datetime.timedelta( seconds=int(elapsed_time) ) ) )
    for key,val in stats.items():
        print( "- {:<14} {}".format( key+":", val[0] ), end="" )
        if val[0] > 0:
            print( " (avg={:.3f}s)".format( float(val[1])/val[0] ) )
        else:
            print()

# ---------------------------------------------------------------------

def snippet_images_thread( webapp_url ):
    """Test generating snippet images."""

    with WebDriver() as webdriver:

        # initialize
        webdriver = webdriver.driver
        init_webapp( webdriver, webapp_url,
             [ "snippet_image_persistence", "scenario_persistence" ]
        )

        # load a scenario (so that we get some sortable's)
        scenario_data = {
            "SCENARIO_NOTES": [ { "caption": "Scenario note #1"  } ],
            "OB_SETUPS_1": [ { "caption": "OB setup note #1" } ],
            "OB_NOTES_1": [ { "caption": "OB note #1" } ],
            "OB_SETUPS_2": [ { "caption": "OB setup note #2" } ],
            "OB_NOTES_2": [ { "caption": "OB note #2" } ],
        }
        load_scenario( scenario_data, webdriver )

        # locate all the "generate snippet" buttons
        snippet_btns = find_snippet_buttons( webdriver )
        tab_ids = list( snippet_btns.keys() )

        while not shutdown_event.is_set():

            try:
                # clear the return buffer
                ret_buffer = find_child( "#_snippet-image-persistence_", webdriver )
                assert ret_buffer.tag_name == "textarea"
                webdriver.execute_script( "arguments[0].value = arguments[1]", ret_buffer, "" )

                # generate a snippet
                tab_id = random.choice( tab_ids )
                btn = random.choice( snippet_btns[ tab_id ] )
                log( "Getting snippet image: {}", btn.get_attribute("data-id") )
                select_tab( tab_id, webdriver )
                start_time = time.time()
                ActionChains( webdriver ) \
                    .key_down( Keys.SHIFT ) \
                    .click( btn ) \
                    .key_up( Keys.SHIFT ) \
                    .perform()

                # wait for the snippet image to be generated
                wait_for( 10*thread_count, lambda: ret_buffer.get_attribute( "value" ) )
                _, img_data = ret_buffer.get_attribute( "value" ).split( "|", 1 )
                elapsed_time = time.time() - start_time

                # update the stats
                with stats_lock:
                    stats["snippet image"][0] += 1
                    stats["snippet image"][1] += elapsed_time

                # FUDGE! Generating the snippet image for a sortable entry is sometimes interpreted as
                # a request to edit the entry (Selenium problem?) - we dismiss the dialog here and continue.
                dlg = find_child( ".ui-dialog", webdriver )
                if dlg and dlg.is_displayed():
                    click_dialog_button( "Cancel", webdriver )

            except ( ConnectionRefusedError, ConnectionResetError, http.client.RemoteDisconnected ):
                if shutdown_event.is_set():
                    break
                raise

            # check the generated snippet
            img_data = base64.b64decode( img_data )
            log( "Received snippet image: #bytes={}", len(img_data) )
            assert img_data[:6] == b"\x89PNG\r\n"

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

def update_vsav_thread( webapp_url, vsav_fname, vsav_data ):
    """Test updating VASL scenario files."""

    # initialize
    vsav_data_b64 = base64.b64encode( vsav_data ).decode( "utf-8" )

    with WebDriver() as webdriver:

        # initialize
        webdriver = webdriver.driver
        init_webapp( webdriver, webapp_url,
             [ "vsav_persistence", "scenario_persistence" ]
        )

        # load a test scenario
        fname = os.path.join( os.path.split(__file__)[0], "../webapp/tests/fixtures/update-vsav/full.json" )
        with open( fname, "r", encoding="utf-8" ) as fp:
            saved_scenario = json.load( fp )
        load_scenario( saved_scenario, webdriver )

        while not shutdown_event.is_set():

            try:

                # send the VSAV data to the front-end to be updated
                log( "Updating VSAV: {}", vsav_fname )
                set_stored_msg( "_vsav-persistence_", vsav_data_b64, webdriver )
                select_menu_option( "update_vsav", webdriver )
                start_time = time.time()

                # wait for the front-end to receive the data
                wait_for( 2*thread_count,
                    lambda: get_stored_msg( "_vsav-persistence_", webdriver ) == ""
                )

                # wait for the updated data to arrive
                wait_for( 60*thread_count,
                    lambda: get_stored_msg( "_vsav-persistence_", webdriver ) != ""
                )
                elapsed_time = time.time() - start_time

                # get the updated VSAV data
                updated_vsav_data = get_stored_msg( "_vsav-persistence_", webdriver )
                if updated_vsav_data.startswith( "ERROR: " ):
                    raise RuntimeError( updated_vsav_data )
                updated_vsav_data = base64.b64decode( updated_vsav_data )

                # check the updated VSAV
                log( "Received updated VSAV data: #bytes={}", len(updated_vsav_data) )
                assert updated_vsav_data[:2] == b"PK"

                # update the stats
                with stats_lock:
                    stats["update vsav"][0] += 1
                    stats["update vsav"][1] += elapsed_time

            except (ConnectionRefusedError, ConnectionResetError, http.client.RemoteDisconnected):
                if shutdown_event.is_set():
                    break
                raise

# ---------------------------------------------------------------------

def log( fmt, *args, **kwargs ):
    """Log a message."""
    now = time.time()
    msec = now - int(now)
    now = "{}.{:03d}".format( time.strftime("%H:%M:%S",time.localtime(now)), int(msec*1000) )
    msg = fmt.format( *args, **kwargs )
    msg = "{} | {:17} | {}".format( now, threading.current_thread().name, msg )
    print( msg )

# ---------------------------------------------------------------------

def init_webapp( webdriver, webapp_url, options ):
    """Initialize the webapp."""
    log( "Initializing the webapp." )
    url = webapp_url + "?" + "&".join( "{}=1".format(opt) for opt in options )
    url += "&store_msgs=1" # nb: stop notification balloons from building up
    webdriver.get( url )
    wait_for( 5, lambda: find_child("#_page-loaded_",webdriver) is not None )

# ---------------------------------------------------------------------

if __name__ == "__main__":
    main() #pylint: disable=no-value-for-parameter
