'''
Copyright (C) 2022, Sangoma Technologies Corp
Mike Bradeen <mbradeen@sangoma.com>

This program is free software, distributed under the terms of
the GNU General Public License Version 2.

The sipp_iterator is designed to let us start sipp, then perform an ami
action as a cycle of start sipp->generate event(s)->sipp stops, start sipp...
This is for tests that are performing the same test different ways or performing
different iterations of the same test.

An example would be to start a scenario that waits for a NOTIFY, then send the AMI
to generate that NOTIFY, then starts a second scenario, send a second AMI, etc.

There are two types for this, 'single' and 'multi'.  'single' is one-to-one, with each
scenario start triggering the next ami and the scenario stop triggering the next scenario
start.  It is fed a pair of lists, one is a list of scenarios and the other is a list of
AMI actions.  After the final scenario, we add a 'done' indicator to the sipp side and a
final event on the AMI side, examples:

sipp-config:
    type: 'single'
    scenarios:
        # Alice registers, no AMI action
        - { 'scenario': {'Name': 'alice-registers.xml', 'port': '5061', 'target': '127.0.0.1', 'ordered-args': {'-aa'}},
            'action': {'Action': 'none'}}
        # Alice waits for a NOTIFY, generated by MWIUpdate
        - { 'scenario': {'Name': 'alice-is-notified-1.xml', 'port': '5061', 'target': '127.0.0.1'},
            'action': {'Action': 'MWIUpdate', 'Mailbox': 'alice', 'NewMessages':'2', 'OldMessages':'0'}}
        # Alice waits for a NOTIFY, generated by MWIUpdate
        - { 'scenario': {'Name': 'alice-is-notified-2.xml', 'port': '5061', 'target': '127.0.0.1'},
            'action': {'Action': 'MWIUpdate', 'Mailbox': 'alice', 'NewMessages':'1', 'OldMessages':'1'}}
        # Alice waits for a NOTIFY, generated by MWIUpdate
        - { 'scenario': {'Name': 'alice-is-notified-3.xml', 'port': '5061', 'target': '127.0.0.1'},
            'action': {'Action': 'MWIUpdate', 'Mailbox': 'alice', 'NewMessages':'0', 'OldMessages':'2'}}
        # Alice waits for a NOTIFY, generated by MWIDelete
        - { 'scenario': {'Name': 'alice-is-notified-4.xml', 'port': '5061', 'target': '127.0.0.1'},
            'action': {'Action': 'MWIDelete', 'Mailbox': 'alice'}}
        # indicate no more scenarios to run, send testComplete Event
        - { 'scenario': {'Name': 'done'},
            'action': {'Action': 'UserEvent', 'UserEvent': 'testComplete'}}
-or-
    type: 'multi'
    scenarios:
        # Alice and bob register, receive initial NOTIFYs. No actions
        - { 'scenario': {'Name': 'register', 'sequence': [
                {'Name': 'alice-registers.xml', 'port': '5061', 'target': '127.0.0.1', 'ordered-args': {'-aa'}},
                {'Name': 'bob-registers.xml', 'port': '5062', 'target': '127.0.0.1', 'ordered-args': {'-aa'}} ]},
            'action': {'Messages': [
                {'Action': 'none'} ]}}
        # Alice and Bob are both sent NOTIFY messages, triggered by the MWIUpdate
        - { 'scenario': {'Name': 'mailbox_a', 'sequence': [
                {'Name': 'alice-is-notified-1.xml', 'port': '5061', 'target': '127.0.0.1'},
                {'Name': 'bob-is-notified-1.xml', 'port': '5062', 'target': '127.0.0.1'} ]},
            'action': {'Messages': [
                {'Action': 'MWIUpdate', 'Mailbox': 'mailbox_a', 'NewMessages':'2', 'OldMessages':'1'} ]}}
        # Alice and Bob are both sent NOTIFY messages, triggered by the MWIUpdate
        - { 'scenario': {'Name': 'mailbox_b', 'sequence': [
                {'Name': 'alice-is-notified-2.xml', 'port': '5061', 'target': '127.0.0.1'},
                {'Name': 'bob-is-notified-2.xml', 'port': '5062', 'target': '127.0.0.1'} ]},
            'action': {'Messages': [
                {'Action': 'MWIUpdate', 'Mailbox': 'mailbox_b', 'NewMessages':'3', 'OldMessages':'3'} ]}}
        # indicate no more scenarios to run, send testComplete Event
        - { 'scenario': {'Name': 'done'},
            'action': {'Messages': [
                {'Action': 'UserEvent', 'UserEvent': 'testComplete'} ]}}

In the above examples, we end the test with a testComplete event that we then use to trigger the
test stop event in the yaml file, example:
ami-config:
    -
        ami-events:
            conditions:
                match:
                    Event: 'UserEvent'
                    UserEvent: 'testComplete'
            count: 1
        stop_test:

A Name of 'none' for a scenario(list) or an Action of 'none' skips that particular scenario or action,
but not the other. This is mostly to allow one or more AMI events to be triggered before the
corresponding sipp scenario is started but could also allow for intermediate sipp scenarios. Setting
both to 'none' would be functionally the same as waiting for 1 second before going on to the next
iteration.

'''
import sys
import logging

from . import test_suite_utils
from abc import ABCMeta, abstractmethod
from twisted.internet import reactor, defer, protocol, error
from .test_case import TestCase
from .utils_socket import get_available_port
from .test_runner import load_and_parse_module
from .pluggable_registry import PLUGGABLE_EVENT_REGISTRY,\
    PLUGGABLE_ACTION_REGISTRY

from asterisk.sipp import SIPpScenarioSequence
from asterisk.sipp import SIPpScenario
from twisted.internet import reactor

LOGGER = logging.getLogger(__name__)

sipp_terminator = {'Name': 'done'}
empty_scenario = {'Name': 'none'}
empty_action = {'Action': 'none'}
empty_action_list = {'Messages': [{'Action': 'none'}] }

class SIPpIteratorTestCase(TestCase):

    def __init__(self, test_path, test_config):
        super(SIPpIteratorTestCase, self).__init__(test_path, test_config=test_config)

        if not test_config:
            raise ValueError("SIPpIteratorTestCase requires a test config")

        self._test_config = test_config
        self._current_test = 0
        self._connected_amis = 0
        self._actor = None
        self.asterisk_instances = test_config.get('asterisk-instances') or 1
        self.connect_ami = test_config.get('connect-ami') or False
        self.create_asterisk(count=self.asterisk_instances, test_config=test_config)

    def on_reactor_timeout(self):
        """Create a failure token when the test times out"""
        self.create_fail_token("Reactor timed out. Test Failed.")

    def run(self):
        """Override of the run method.

        Create an AMI factory in case anyone wants it
        """
        super(SIPpIteratorTestCase, self).run()

        for a in range(1, self.asterisk_instances):
                self.ast[a].cli_exec('sip set debug on')
                self.ast[a].cli_exec('pjsip set logger on')

        LOGGER.info("creating ami factory")
        if not isinstance(self.connect_ami, dict):
            self.create_ami_factory(count=self.asterisk_instances)
        else:
            self.create_ami_factory(**self.connect_ami)

    def stop_asterisk(self):
        """Kill any remaining SIPp scenarios"""
        if self._actor:
            self._actor.terminate()

    def ami_connect(self, ami):
        """Handler for the AMI connect event"""
        super(SIPpIteratorTestCase, self).ami_connect(ami)

        # keep track of number of connected amis and only start once they
        # are all connected
        self._connected_amis +=1
        if self._connected_amis == self.asterisk_instances:
            self._execute_test()

    def ami_reconnect(self, ami):
        """Handler for the AMI reconnect event"""
        super(SIPpIteratorTestCase, self).ami_reconnect(ami)

        for a in range(1, self.asterisk_instances):
                self.ast[a].cli_exec('sip set debug on')
                self.ast[a].cli_exec('pjsip set logger on')

    def _execute_test(self):
        self._actor = SIPpIteratorActionModule(self, self._test_config)
        self._actor.execute()



class singleIterator(object):

    def __init__(self, test_object, scenarios):
        self.test_object = test_object
        self.scenarios = scenarios
        self.activescenario = None
        self.iteration = 0

    def __iterate(self):

        try:
            scenariocontainer = self.scenarios[self.iteration]
            scenario = scenariocontainer['scenario'] or empty_scenario
            message = scenariocontainer['action'] or empty_action
        except IndexError:
            LOGGER.warning("End of scenario list without proper termination")
            scenario = sipp_terminator
            message = empty_action

        self.iteration += 1
        if scenario['Name'] == 'none':
            # skip ahead to the next iteration but send the AMI
            # action if set. Speed up the iteration.
            self.__sendMessage(message, 0)
            reactor.callLater(1, self.run)
        elif scenario['Name'] != 'done':
            # A scenaro was specified so run it then schedule the
            # AMI event if there is one.
            self.__startScenario(scenario)
            self.__sendMessage(message)
        else:
            # At the final iteration, send any final AMI immediately
            self.__sendMessage(message, 0)

    def __startScenario(self, scenario):
        LOGGER.info("Starting sipp scenario %s" % scenario['Name'])
        self.activescenario = SIPpScenario(self.test_object.test_name,
                                     {'scenario': scenario['Name'],
                                      '-p': scenario['port']},
                                      scenario.get('ordered-args') or [],
                                     target=scenario['target'])
        exiter = self.activescenario.run(self.test_object)
        exiter.addCallback(self.run)

    def __sendMessage(self, message, delay=2):
        if message['Action'] != 'none':
            testami = self.test_object.ami[0]
            LOGGER.info("Scheduling AMI %s" % message['Action'])
            reactor.callLater(delay, testami.sendMessage, message)

    def run(self, junk=None):
        self.__iterate()

    def terminate(self, junk=None):
        """Kill active scenario if still in existence"""
        if not self.activescenario.exited:
            LOGGER.warn("SIPp Scenario %s has not exited; killing" %
                        self.activescenario.name)
            self.activescenario.kill()


class multiIterator(object):

    def __init__(self, test_object, scenariosequences):
        self.test_object = test_object
        self.scenariosequences = scenariosequences
        self.activescenarios = []
        self.iteration = 0
        self.sequencecounter = 1

    def __iterate(self):

        try:
            scenariosequence = self.scenariosequences[self.iteration]
            sippsequence = scenariosequence['scenario'] or empty_scenario
            messagesequence = scenariosequence['action'] or empty_action_list
        except IndexError:
            LOGGER.warning("End of scenario list without proper termination")
            sippsequence = sipp_terminator
            messagesequence = empty_action_list

        self.iteration += 1
        if sippsequence['Name'] == 'none':
            # skip ahead to the next iteration but send any AMI actions if
            # set, one second apart. Run the next iteration based on how far
            # out we've scheduled messages.
            sequencedelay = self.__sendMessages(messagesequence['Messages'], 1)
            reactor.callLater(sequencedelay, self.run)
        elif sippsequence['Name'] != 'done':
            # A scenaro sequence was specified so run it then schedule the
            # AMI event(s) normally. Set the delay equal to how many scenarios
            # were registered (as seconds) to try and scale against complexity
            LOGGER.info("Starting sipp sequence %s" % sippsequence['Name'])
            self.__startScenarios(sippsequence['sequence'])
            self.__sendMessages(messagesequence['Messages'], self.sequencecounter)
        else:
            # At the final iteration, send any final AMI
            sequencedelay = self.__sendMessages(messagesequence['Messages'], 0)

    def __startScenarios(self, sippscenarios):

        sipp_sequence = SIPpScenarioSequence(self.test_object,
                                             fail_on_any=True,
                                             stop_on_done=False)
        self.sequencecounter = 0
        self.activescenarios.clear()
        for scenario in sippscenarios:
            LOGGER.info("Adding scenario %s to sequence" % scenario['Name'])
            sipp_scenario = SIPpScenario(self.test_object.test_name,
                                         {'scenario': scenario['Name'],
                                          '-p': scenario['port']},
                                          scenario.get('ordered-args') or [],
                                         target=scenario['target'])
            sipp_sequence.register_scenario(sipp_scenario)
            # keep track of how many scenarios we register so we don't
            # iterate until we have stop_callback hits for all of them
            self.sequencecounter += 1
            self.activescenarios.append(sipp_scenario)

        sipp_sequence.register_scenario_stop_callback(self.run)
        sipp_sequence.execute()

    def __sendMessages(self, messages, delay = 2):
        testami = self.test_object.ami[0]
        messagedelay = delay
        for message in messages:
            if message['Action'] !='none':
                LOGGER.info("Scheduling AMI %s" % message['Action'])
                reactor.callLater(delay, testami.sendMessage, message)
                # spread out the messages by 2 seconds
                messagedelay += 2
        # return last action's delay + 2, ie when to run the next
        # command if you want it evenly distributed and after the
        # last message scheduled here
        return messagedelay

    def run(self, junk=None):
        # only run the next iteration once there is a call-back for
        # each sipp scenario in the sequence.  Otherwise, decrement
        # until we get there
        if self.sequencecounter == 1:
            self.__iterate()
        else:
            self.sequencecounter -= 1

    def terminate(self, junk=None):
        """Kill any scenarios still in existence"""
        for scenario in self.activescenarios:
            if not scenario.exited:
                LOGGER.warn("SIPp Scenario %s has not exited; killing" %
                            scenario.name)
                scenario.kill()



class SIPpIteratorActionModule(object):
    """."""

    def __init__(self, test_object, config):
        """Initialize iterator module"""

        scenarios = config.get('scenarios')
        if not scenarios:
            LOGGER.error("No registered SIPp scenarios, required for test type.")
            test_object.set_passed(False)
            test_object.stop_reactor()
            return

        type = config.get('type', 'multi')
        if (type == 'multi'):
            self.testrunner = multiIterator(test_object, scenarios)
        elif (type == 'single'):
            self.testrunner = singleIterator(test_object, scenarios)
        else:
            LOGGER.error("Unknown iteration type, must be single or multi")
            test_object.set_passed(False)
            test_object.stop_reactor()
            return

    def run(self, triggered_by, source, extra):
        """Execute specified SIPp scenarios"""
        self.testrunner.run(triggered_by, source, extra)

    def execute(self):
        """Execute specified SIPp scenarios"""
        self.testrunner.run()

    def terminate(self):
        self.testrunner.terminate()


PLUGGABLE_ACTION_REGISTRY.register("sippiterator", SIPpIteratorActionModule)