#!/usr/bin/python3

import sys, os
import subprocess, shlex
import time
import unittest
import random

from Test.TestUtil import PcapComparator, ID2TExecution

# this dictionary holds the generators (functions) for the parameters
# that will be passed to the MembershipMgmtCommAttack
# items need the parameter-name as key and a function that will be called
# without parameters and returns a valid value for that parameter as value
# WARNING: parameters will be passed via command line, make sure your values
# get converted to string correctly
_random_bool = lambda: random.random() < 0.5
ID2T_PARAMETER_GENERATORS = {
    "bots.count": lambda: random.randint(1, 6),
    "hidden_mark": _random_bool,
    "interval.selection.end": lambda: random.randint(100, 1501),  # values are taken from default trace
    "interval.selection.start": lambda: random.randint(0, 1401),
    "interval.selection.strategy": lambda: random.choice(["optimal", "custom", "random"]),
    "ip.reuse.external": lambda: random.uniform(0, 1),
    "ip.reuse.local": lambda: random.uniform(0, 1),
    "ip.reuse.total": lambda: random.uniform(0, 1),
    "multiport": _random_bool,
    "nat.present": _random_bool,
    "packet.padding": lambda: random.randint(0, 100),
    "packets.limit": lambda: random.randint(50, 250),
    "ttl.from.caida": _random_bool,
}


class PcapComparison(unittest.TestCase):
    ID2T_PATH = ".."
    ID2T_LOCATION = ID2T_PATH + "/" + "id2t"

    NUM_ITERATIONS_PER_PARAMS = 3
    NUM_ITERATIONS = 4

    PCAP_ENVIRONMENT_VALUE = "ID2T_SRC_PCAP"
    SEED_ENVIRONMENT_VALUE = "ID2T_SEED"

    DEFAULT_PCAP = "resources/test/Botnet/telnet-raw.pcap"
    DEFAULT_SEED = "42"

    VERBOSE = False

    def __init__(self, *args, **kwargs):
        unittest.TestCase.__init__(self, *args, **kwargs)

        # params to call id2t with, as a list[list[str]]
        # do a round of testing for each list[str] we get
        # if none generate some params itself
        self.id2t_params = None
        self.printed_newline = False

    def set_id2t_params(self, params: "list[list[str]]"):
        self.id2t_params = params

    def setUp(self):
        self.executions = []

    def test_determinism(self):
        self.print_warning("Conducting test for determinism of Membership Management Communication Attack:\n")
        input_pcap = os.environ.get(self.PCAP_ENVIRONMENT_VALUE, self.DEFAULT_PCAP)
        seed = os.environ.get(self.SEED_ENVIRONMENT_VALUE, None)

        if self.id2t_params is None:
            self.id2t_params = self.random_id2t_params()

        use_random_seeds = not bool(seed)

        for i, params in enumerate(self.id2t_params):
            self.print_warning("Test round %d:" % (i+1))
            self.print_warning("=================================")
            if use_random_seeds:
                seed = random.randint(0, 0x7FFFFFFF)
            self.do_test_round(input_pcap, seed, params)
            self.print_warning()

    def do_test_round(self, input_pcap, seed, additional_params):
        generated_pcap = None
        for i in range(self.NUM_ITERATIONS_PER_PARAMS):
            execution = ID2TExecution(input_pcap, seed=seed)
            self.print_warning("The command that gets executed is:", execution.get_run_command(additional_params))
            self.executions.append(execution)

            try:
                execution.run(additional_params)
            except AssertionError as e:
                self.print_warning(execution.get_output())
                self.assertEqual(execution.get_return_code(), 0, "For some reason id2t completed with an error")
                raise e

            self.print_warning(execution.get_output())

            pcap = execution.get_pcap_filename()

            if generated_pcap is not None:
                if "No packets were injected." in pcap or "No packets were injected." in generated_pcap:
                    self.assertEqual(pcap, generated_pcap)
                else:
                    try:
                        self.compare_pcaps(generated_pcap, pcap)
                    except AssertionError as e:
                        execution.keep_file(pcap)
                        for ex in self.executions:
                            try:
                                ex.keep_file(generated_pcap)
                            except ValueError:
                                pass

                        e.args += tuple(("Command was: %s" % execution.get_run_command(additional_params),))
                        e.args += tuple(("Files are: %s, %s" % (generated_pcap, pcap),))
                        raise e
            else:
                generated_pcap = pcap

            self.print_warning()
            time.sleep(1)  # let some time pass between calls because files are based on the time

    def tearDown(self):
        self.print_warning("Cleaning up files generated by the test-calls...")
        for id2t_run in self.executions:
            for file in id2t_run.get_files_for_deletion():
                self.print_warning(file)

            id2t_run.cleanup()

        self.print_warning("Done")

        if any(e.get_kept_files() for e in self.executions):
            self.print_warning("The following files have been kept:")
            for e in self.executions:
                for file in e.get_kept_files():
                    self.print_warning(file)

    def compare_pcaps(self, one: str, other: str):
        PcapComparator().compare_files(self.ID2T_PATH + "/" + one, self.ID2T_PATH + "/" + other)

    def print_warning(self, *text):
        if self.VERBOSE:
            if not self.printed_newline:
                print("\n", file=sys.stderr)
                self.printed_newline = True
            print(*text, file=sys.stderr)

    def random_id2t_params(self):
        """
        :return: A list of parameter-lists for id2t, useful if you want several
        iterations
        """
        param_list = []
        for i in range(self.NUM_ITERATIONS):
            param_list.append(self.random_id2t_param_set())
        return param_list

    def random_id2t_param_set(self):
        """
        Create a list of parameters to call the membersmgmtcommattack with
        :return: a list of command-line parameters
        """
        param = lambda key, val: "%s=%s" % (str(key), str(val))

        number_of_keys = min(random.randint(2, 5), len(ID2T_PARAMETER_GENERATORS))
        keys = random.sample(list(ID2T_PARAMETER_GENERATORS), number_of_keys)

        params = []
        for key in keys:
            generator = ID2T_PARAMETER_GENERATORS[key]
            params.append(param(key, generator()))

        return params


if __name__ == "__main__":
    import sys

    # parameters for this program are interpreted as id2t-parameters
    id2t_args = sys.argv[1:]
    comparison = PcapComparison("test_determinism")
    if id2t_args: comparison.set_id2t_params([id2t_args])

    suite = unittest.TestSuite()
    suite.addTest(comparison)

    unittest.TextTestRunner().run(suite)
