#!/usr/bin/env python

__usage__ = 'lvalert_sanityChecksMP [--options] email@address.com another@email.org ...'
__description__ = """\
a series of tests that require humans to confirm that they worked. These mostly focus on sending emails or otherwise communicating with the outside world. Email addresses can be supplied as input arguments and we require at least one.
Note, this script is *interactive* in that it requires humans to answer questions about whether emails were received. This means that, as currently written, it *cannot* be run autonomously by a machine.

More pointed tests are available within lvlaert_libraryTestsMP, which should be able to run autonomously.
"""
__author__ = "Reed Essick (reed.essick@ligo.org)"

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

from lvalertMP.lvalert import lvalertMPutils as utils
from lvalertMP.lvalert import commands
from lvalertMP.lvalert import interactiveQueue

import os
import getpass
import socket

import logging
import traceback

import time

import json

import numpy as np

import multiprocessing as mp
from ConfigParser import SafeConfigParser

from optparse import OptionParser

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

def email_body():
    return """\
This is a test email generated by %(filename)s.
    user : %(user)s
    host : %(host)s
    date : %(date)s\
"""%{'filename':__file__, 'user':getpass.getuser(), 'host':socket.gethostname(), 'date':time.strftime('%c')}

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

parser = OptionParser(usage=__usage__, description=__description__)

### options about verbosity and logging
parser.add_option('-v', '--verbose', default=False, action='store_true')
parser.add_option('-V', '--Verbose', default=False, action='store_true')

parser.add_option('', '--wait', default=1.0, type='float', 
    help='the amount of time we wait after sending messages to child processes. DEFAULT=1.0')

parser.add_option('', '--logLevel', default=5, type='int', 
    help='the level of verbosity printed to the log')
parser.add_option('', '--logDir', default='.', type='string',
    help='the directory into which log files will be written')

### options about interactiveQueue kwargs
parser.add_option('', '--sleep', default=0.01, type='int',
    help='the amount of time slept between epochs of interactiveQueue\'s loop')

parser.add_option('', '--maxComplete', default=5, type='int', 
    help='the largest allowable number of complete QueueItems within queue before triggering cleanup')

parser.add_option('', '--maxFrac', default=0.50, type='int',
    help='the largest fraction of QueueItems in queue that can be complete before triggering cleanup') 

parser.add_option('', '--warnThr', default=10, type='int',
    help='the number of QueueItems in queue beyond which we begin sending warnings about the queue\'s size')

parser.add_option('', '--warnDelay', default=30, type='float',
    help='the number of seconds that must ellapse between warnings about queue\'s size')

parser.add_option('', '--maxWarn', default=3, type='int',
    help='the maximum number of warnings about queue\'s size that will be issued')

parser.add_option('', '--print2stdout', default=False, action='store_true',
    help='passed to interactiveQueue')

### options about what to test
parser.add_option('', '--everything', default=False, action='store_true',
    help='run all tests')

parser.add_option('', '--lvalertMPutils', default=False, action='store_true',
    help='run tests of lvalretMPutils')

parser.add_option('', '--commands', default=False, action='store_true',
    help='run tests of commands')

parser.add_option('', '--interactiveQueue', default=False, action='store_true',
    help='run tests of interactiveQueue')

opts, recipients = parser.parse_args()

opts.verbose = opts.Verbose or opts.verbose

assert recipients, 'Please supply at least one email address as an input argument\n%s'%__usage__
recipientsStr = ', '.join(recipients) ### used repeatedly, so only do this once

if not os.path.exists(opts.logDir):
    os.makedirs(opts.logDir)

if opts.interactiveQueue or opts.everything: ### ensure maxComplete and maxFrac are compatible with separable tests
    assert opts.maxFrac < 1, '--maxFrac must be less than 1'
    assert opts.maxComplete > 1./(1-opts.maxFrac)+1, '--maxComplete must be larger in order to separately test maxFrac and maxComplete'

assert opts.wait > 10*opts.sleep, '--wait must be at least 10 times as big as sleep to ensure child processes have ample time to processes QueueItems'

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

if opts.lvalertMPutils or opts.everything:
    if opts.verbose:
        print( 'testing lvalertMPutils' )

    #--- communication with outside world
    ### sendEmail

    ### set up subject and body of email message
    subject = 'TEST: lvalertMP.lvalert.lvalertMPutils.sendEmail'
    body = email_body()

    ### ensure we get the right type of error when we add in an empty recipient list
    try:
        utils.sendEmail( [], body, subject )
    except Exception as e:
        assert isinstance(e, ValueError), 'unexpected error from sendEmail\n%s'%(traceback.format_exc().strip('\n'))

    ### actually try sending the email
    utils.sendEmail( recipients, body, subject )
    assert raw_input('    sent an email to %s. Was it received? [y/n] '%recipientsStr )=='y', 'email was not received'

    #--- finished
    if opts.verbose:
        print( 'lvalertMPutils passed all tests successfully' )

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

if opts.commands or opts.everything:
    if opts.verbose:
        print( 'testing commands' )

    #--- CommandTask functionality
    ### sendEmail (requires recipients, body, subject)

    ### set up logger so command.sendEmail can write somewhere
    logger = logging.getLogger(__file__)
    logger.setLevel(opts.logLevel)
    logfilename = os.path.join(opts.logDir, os.path.basename(__file__)+'.log')
    if opts.Verbose:
        print( '    setting up logfile : %s'%logfilename )
    logger.addHandler( logging.FileHandler(logfilename) )
    mtime = os.path.getmtime(logfilename) ### the modification time of this log file

    ### set up object
    command = commands.SendEmailTask(
        utils.SortedQueue(), 
        dict(), 
        logTag=__file__, 
        recipients=' '.join(recipients),
        subject='TEST: lvalertMP.lvalert.commands.SendEmailTask',
        body=email_body(),
    )

    command.sendEmail(verbose=False) ### send email without printing to logger
    assert mtime==os.path.getmtime(logfilename), 'logfile : %s modified when it should not have been'%logfilename
    assert raw_input( '    sent an email to %s without writing to logfile. Was it received? [y/n] '%recipientsStr )=='y', 'email was not received'

    command.sendEmail(verbose=True) ### send email and print to logger
    assert mtime<os.path.getmtime(logfilename), 'logfile : %s was not modified when it should have been'%logfilename

    string = 'sending email to %s'%(' '.join(recipients))
    file_obj = open(logfilename, 'r')
    last_line = file_obj.readlines()[-1]
    file_obj.close()
    assert string in last_line, '''\
last line of %s does not match expected string:
    last line : %s
    expected  : %s'''%(logfilename, last_line, string)
    if opts.Verbose:
        print( '    last line of logfile matches expectations' )
        
    assert raw_input( '    sent an email to %s and wrote to logfile. Was it recieved? [y/n] '%recipientsStr )=='y', 'email was not received'
    #--- finsihed
    if opts.verbose:
        print( 'commands passed all tests successfully' )

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

if opts.interactiveQueue or opts.everything:
    if opts.verbose:
        print( 'testing interactiveQueue' )

    ### set up config files
    if opts.Verbose:
        print( '    setting up config files' )
    config = SafeConfigParser() ### a config file containing the minimum amount of information required
    email_filename = os.path.join(opts.logDir, 'email_config.ini')

    if opts.Verbose:
        print( '        '+email_filename )

    ### add in options
    config.add_section('general')
    config.set('general', 'process_type', 'test')
    config.set('general', 'log_directory', opts.logDir)
    config.set('general', 'log_level', '%d'%opts.logLevel)

    ### write configs to disk
    file_obj = open(email_filename, 'w')
    config.write(file_obj)
    file_obj.close()

    ### set-up multiprocessing connection
    email_conn1, email_conn2 = mp.Pipe()

    #--- start processes
    email_proc = mp.Process(
        target=interactiveQueue.interactiveQueue, 
        args=(email_conn2, email_filename, True, opts.sleep, opts.maxComplete, opts.maxFrac, opts.warnThr, recipients, opts.warnDelay, opts.maxWarn, opts.print2stdout),
    )
    email_proc.start()
    email_conn2.close()

    time.sleep(opts.wait)

    ### wrap the rest of this in a big try/except loop to handle graceful termination of child processes
    try:
        #--- reading in config file and setting up logger

        #--- robust against improperly formatted json string
        message = 'bad message format' ### send a bad message

        email_conn1.send( (message, time.time()) )
        time.sleep(opts.wait)
        assert email_proc.is_alive(), 'email process died when we attempted to make parseAlert raise an exception'
        assert raw_input( '    email warning about parsing json packet should have been sent to %s. Was it received? [y/n] '%recipientsStr )=='y', 'email was not received'

        #--- sending emails

        ### when parseAlert raises an exception
        message = json.dumps({'format':'bad'}) ### dict does not have a 'uid' key, which will cause lvalertMP.lvalert.parseAlert.parseAlert to raise a KeyError

        email_conn1.send( (message, time.time()) )
        time.sleep(opts.wait)
        assert email_proc.is_alive(), 'email process died when we attempted to make parseAlert raise an exception'
        assert raw_input( '    email warning about parseAlert raising an exception should have been sent to %s. Was it received? [y/n] '%recipientsStr )=='y', 'email was not received'

        ### when execute raises an exception
        message = commands.RaiseException().write() ### send a command to raise an exception

        email_conn1.send( (message, time.time()) )
        time.sleep(opts.wait)
        assert email_proc.is_alive(), 'email process died when item.execute raised an exception'
        assert raw_input( '    email warning about item.execute raising an exception should have been sent to %s. Was it received? [y/n] '%recipientsStr )=='y', 'email was not recieved'

        ### when the queue gets too big
        if opts.Verbose:
            print( '    generating %d QueueItems to check warnings based on queue\'s size'%(opts.warnThr+1) )

        # send in a bunch of messages, enough to trigger the warning threshold
        for ind in xrange(opts.warnThr+1): ### one more than the wanring threshold
            message = commands.PrintMessage(message='message %d'%ind, sleep=np.infty).write() ### sleep for a long time so these message are not printed immediately
            email_conn1.send( (message, time.time()) )
            time.sleep(opts.wait)

        assert email_proc.is_alive(), 'email process died when we added a bunch of QueueItems'
        assert raw_input( '    email warning about too many QueueItems should have been sent to %s. Was it received? [y/n] '%recipientsStr )=='y', 'email was not recieved'

        ### check when queue shrinks again

        # clear the queue's before the final warning is sent
        message = commands.ClearQueue().write()
        email_conn1.send( (message, time.time()) )
        time.sleep(opts.wait)

        assert email_proc.is_alive(), 'email process died when queue was emptied'
        assert raw_input( '    email recovery notice about reduced number of QueueItems should have been sent to %s. Was it recieved? [y/n] '%recipientsStr )=='y', 'email was not recieved'

        ### ensure the final warning is correctly label
   
        if opts.Verbose:
            print( '    generating %d QueueItems to check warnings\' timeout behavior'%(opts.warnThr+1) ) 
        for ind in xrange(opts.warnThr+1): ### one more than the wanring threshold
            email_conn1.send( (commands.PrintMessage(message='another message %d'%ind, sleep=np.infty).write(), time.time()) )
            time.sleep(opts.wait)

        wait = opts.warnDelay*(opts.maxWarn+2)
        if opts.Verbose:
            print( '    sleeping until final warning (and 2 more after that) should have been issued (%d seconds)'%wait )
        time.sleep(wait) 

        assert raw_input( '    %d email warnings, including a \"final\" warning should have been sent to %s. These emails should be roughly spaced by %d seconds.\n    Were they all received? [y/n] '%(opts.maxWarn, recipientsStr, opts.warnDelay) )=='y', 'emails were not received'

        ### clean up queue's in preparation for next check
        message = commands.ClearQueue().write()
        email_conn1.send( (message, time.time()) )
        time.sleep(opts.wait)

        assert email_proc.is_alive(), 'email process died'
        assert raw_input( '    email recovery notice should have been sent to %s. Was it received? [y/n] '%recipientsStr )=='y', 'email was not received' ### do this just to be safe

        #--- finish
        if opts.verbose:
            print( 'interactiveQueue passed all tests successfully' )

    except Exception as e:
        ### report on error
        print( traceback.format_exc() )

    #--- finsihed without errors
    ### clean up processes
    email_proc.terminate()
