#!/usr/bin/env python

import sys
import logging
import logging.config
import os
import shutil
from twisted.internet import reactor

sys.path.append("lib/python")

from asterisk.test_case import TestCase

LOGGER = logging.getLogger(__name__)

class IAX2ACLTest(TestCase):

    # Preps test objects and configuration additions as well as copies TLS keys to test folder.
    def __init__(self):
        TestCase.__init__(self)
        self.test_components = []

        #subtest evaluation
        self.test_index = 0
        self.originate_error = False
        self.phase_evaluation_reached = False
        self.success_conditions = 0
        self.failure_conditions = 0
        self.events = []

        #test1 - No named ACL, calling available only to 127.0.0.1
        self.add_test_component("testiax1@local-1", "allow")
        self.add_test_component("testiax1@local-2", "deny")
        self.add_test_component("testiax1@local-3", "deny")
        self.add_test_component("testiax1@local-4", "deny")

        #test2 - Same permissible addresses as test 1, but while using a named ACL from the local configuration
        self.add_test_component("testiax2@local-1", "allow")
        self.add_test_component("testiax2@local-2", "deny")
        self.add_test_component("testiax2@local-3", "deny")
        self.add_test_component("testiax2@local-4", "deny")

        #test3 - Multiple named ACL rules from local configuration. Only 127.0.0.2 should be allowed to call.
        self.add_test_component("testiax3@local-1", "deny")
        self.add_test_component("testiax3@local-2", "allow")
        self.add_test_component("testiax3@local-3", "deny")
        self.add_test_component("testiax3@local-4", "deny")

        #test4 - An undefined rule is used. All addresses should be rejected from calling.
        self.add_test_component("testiax4@local-1", "deny")
        self.add_test_component("testiax4@local-2", "deny")
        self.add_test_component("testiax4@local-3", "deny")
        self.add_test_component("testiax4@local-4", "deny")

        #test5 - A set of 3 named ACLs stored in realtime is used. Collectively only 127.0.0.3 should be allowed to call.
        self.add_test_component("testiax5@local-1", "deny")
        self.add_test_component("testiax5@local-2", "deny")
        self.add_test_component("testiax5@local-3", "allow")
        self.add_test_component("testiax5@local-4", "deny")

        self.create_asterisk()

    def add_test_component(self, test, expectation):
        this_tuple = test, expectation
        self.test_components.append(this_tuple)

    # Once the AMI Factory connects to the AMI, this function fires.
    def ami_connect(self, ami):
        ami.registerEvent('Hangup', self.evaluate_hangup)
        self.test_start(ami)

    def test_start(self, ami):
        LOGGER.info("Starting test %d - %s:" % (self.test_index, self.test_components[self.test_index][0]))

        # The following variable tracks events that occur as a result of the following originate. We expect two events in all cases.
        # In the case of a successful call, we expect two hangups. If the call is unsuccessful, we expect a hangup and an AMI command error.
        self.events_received = 0
        self.success_conditions = 0
        self.failure_conditions = 0
        self.originate_error = False
        self.phase_evaluation_reached = False

        # Storage for the events (failures and AMI hangups) that came in -- stored in case of something going wrong.
        self.events = []

        # There are callbacks that would lose the AMI, so we need to hold onto it within the test object.
        self.last_active_ami = ami

        ami.originate(channel = "IAX2/%s/s" % self.test_components[self.test_index][0], application = "Echo").addErrback(self.evaluate_originate_error)

    def evaluate_hangup(self, ami, event):
        self.events_received += 1
        self.events.append(event)

        # evaluate if whether the hangup indicates success or failure
        reason = event.get('cause-txt')
        if reason == "Normal Clearing":
            self.success_conditions += 1
        elif reason == "Unknown":
            self.failure_conditions += 1

        # If we've got two events of any kind, the call is finished and we can move on to the evaluation
        if self.events_received == 2:
            self.evaluate_call()

    def evaluate_originate_error(self, reason):
        self.originate_error = True
        self.events.append(reason)
        self.events_received += 1
        self.failure_conditions += 1
        if self.events_received == 2:
            self.evaluate_call()

    def evaluate_call(self):
        self.phase_evaluation_reached = True

        if self.test_components[self.test_index][1] == "allow" and self.success_conditions == 2:
            LOGGER.info("Phase %d - %s: Call Succeeded as Expected." % (self.test_index, self.test_components[self.test_index][0]))
        elif self.test_components[self.test_index][1] == "deny" and self.failure_conditions == 2:
            LOGGER.info("Phase %d - %s: Call Failed as Expected." % (self.test_index, self.test_components[self.test_index][0]))
        else:
            LOGGER.error("Phase %d - %s: Events received don't follow expectations. Test Failed." % (self.test_index, self.test_components[self.test_index][0]))
            LOGGER.error("Failure Triggering Events Received: %s" % self.events)
            self.passed = False
            self.stop_reactor()
            return

        # Go on to the next test.
        self.test_index += 1
        if self.test_index < len(self.test_components):
            self.test_start(self.last_active_ami)
        else:
            LOGGER.info("All tests evaluated as expected. Test Successful.")
            self.passed = True
            self.stop_reactor()
            return

    def report_timeout(self):
        LOGGER.error("Phase %d - Test reached timeout without achieving evaluation conditions for this phase." % self.test_index)

        LOGGER.error("Phase %d - Received the following manager events: %s" % (self.test_index, self.events))

        if self.test_components[self.test_index][1] == "allow":
            LOGGER.error("Phase %d - Two hangup events with cause-txt = 'Normal Clearing' were expected." % self.test_index)
            if self.originate_error:
                LOGGER.error("Phase %d - expected no error conditions and received originate error." % self.test_index)

        elif self.test_components[self.test_index][1] == "deny":
            LOGGER.error("Phase %d - A hangup event with cause-txt = 'Unknown' was expected." % self.test_index)
            if not self.originate_error:
                LOGGER.error("Phase %d - an originate error was expected and not received." % self.test_index)

        else:
            LOGGER.error("Phase %d - Bad test, expectation is '%s' but should be either 'allow' or 'deny'" % (self.test_index,
                self.test_components[self.test_index][1]))

    # Sets up reactor and AMI connection
    def run(self):
        TestCase.run(self)
        self.create_ami_factory()


def main():
    TEST_DIR = os.path.dirname(os.path.realpath(__file__))
    DB_PATH = TEST_DIR + "/realtime.sqlite3"
    TMP_DB_PATH = "/tmp/realtime.sqlite3"
    shutil.copyfile(DB_PATH, TMP_DB_PATH)
    test = IAX2ACLTest()
    reactor.run()
    os.remove(TMP_DB_PATH)

    if not test.phase_evaluation_reached:
        test.report_timeout()

    if test.passed:
        return 0
    return 1

if __name__ == "__main__":
    sys.exit(main() or 0)


# vim:sw=4:ts=4:expandtab:textwidth=79

