#!@PYTHONEXE@
#    This file is part of GNUnet.
#    (C) 2010, 2017, 2018 Christian Grothoff (and other contributing authors)
#
#    GNUnet is free software: you can redistribute it and/or modify it
#    under the terms of the GNU Affero General Public License as published
#    by the Free Software Foundation, either version 3 of the License,
#    or (at your option) any later version.
#
#    GNUnet is distributed in the hope that it will be useful, but
#    WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#    Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#    SPDX-License-Identifier: AGPL3.0-or-later
#
# Functions for integration testing

import os
import subprocess
import sys
import shutil
import time
from gnunet_pyexpect import pexpect
import logging

logger = logging.getLogger()
handler = logging.StreamHandler()
formatter = logging.Formatter(
    '%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)


class Check(object):
    def __init__(self, test):
        self.fulfilled = False
        self.conditions = list()
        self.test = test

    def add(self, condition):
        self.conditions.append(condition)

    def run(self):
        fulfilled = True
        pos = 0
        neg = 0
        for c in self.conditions:
            if (False == c.check()):
                fulfilled = False
                neg += 1
            else:
                pos += 1
        return fulfilled

    def run_blocking(self, timeout, pos_cont, neg_cont):
        execs = 0
        res = False
        while ((False == res) and (execs < timeout)):
            res = self.run()
            time.sleep(1)
            execs += 1
        if ((False == res) and (execs >= timeout)):
            logger.debug('Check had timeout after %s seconds', str(timeout))
            neg_cont(self)
        elif ((False == res) and (execs < timeout)):
            if (None != neg_cont):
                neg_cont(self)
        else:
            if (None != pos_cont):
                pos_cont(self)
        return res

    def run_once(self, pos_cont, neg_cont):
        execs = 0
        res = False
        res = self.run()
        if ((res == False) and (neg_cont != None)):
            neg_cont(self)
        if ((res == True) and (pos_cont != None)):
            pos_cont(self)
        return res

    def evaluate(self, failed_only):
        pos = 0
        neg = 0
        for c in self.conditions:
            if (False == c.evaluate(failed_only)):
                neg += 1
            else:
                pos += 1
        logger.debug(
            '%s out of %s conditions fulfilled', str(pos), str(pos + neg)
        )
        return self.fulfilled

    def reset(self):
        self.fulfilled = False
        for c in self.conditions:
            c.fulfilled = False


class Condition(object):
    def __init__(self):
        self.fulfilled = False
        self.type = 'generic'

    def __init__(self, type):
        self.fulfilled = False
        self.type = type

    def check(self):
        return False

    def evaluate(self, failed_only):
        if ((self.fulfilled == False) and (failed_only == True)):
            logger.debug(
                '%s condition for was %s', str(self.type), str(self.fulfilled)
            )
        elif (failed_only == False):
            logger.debug(
                '%s condition for was %s', str(self.type), str(self.fulfilled)
            )
        return self.fulfilled


class FileExistCondition(Condition):
    def __init__(self, file):
        self.fulfilled = False
        self.type = 'file'
        self.file = file

    def check(self):
        if (self.fulfilled == False):
            res = os.path.isfile(self.file)
            if (res == True):
                self.fulfilled = True
                return True
            else:
                return False
        else:
            return True

    def evaluate(self, failed_only):
        if ((self.fulfilled == False) and (failed_only == True)):
            logger.debug(
                '%s confition for file %s was %s', str(self.type), self.file,
                str(self.fulfilled)
            )
        elif (failed_only == False):
            logger.debug(
                '%s confition for file %s was %s', str(self.type), self.file,
                str(self.fulfilled)
            )
        return self.fulfilled


class StatisticsCondition(Condition):
    def __init__(self, peer, subsystem, name, value):
        self.fulfilled = False
        self.type = 'statistics'
        self.peer = peer
        self.subsystem = subsystem
        self.name = name
        self.value = str(value)
        self.result = -1

    def check(self):
        if (self.fulfilled == False):
            self.result = self.peer.get_statistics_value(
                self.subsystem, self.name
            )
            if (self.result == self.value):
                self.fulfilled = True
                return True
            else:
                return False
        else:
            return True

    def evaluate(self, failed_only):
        if (self.fulfilled == False):
            fail = " FAIL!"
            op = " != "
        else:
            fail = ""
            op = " == "
        if (((self.fulfilled == False) and (failed_only == True))
            or (failed_only == False)):
            logger.debug(
                '%s %s condition in subsystem %s: %s: (expected/real value) %s %s %s %s',
                self.peer.id[:4].decode("utf-8"), self.peer.cfg,
                self.subsystem.ljust(12), self.name.ljust(30), self.value, op,
                self.result, fail
            )
        return self.fulfilled


# Specify two statistic values and check if they are equal
class EqualStatisticsCondition(Condition):
    def __init__(self, peer, subsystem, name, peer2, subsystem2, name2):
        self.fulfilled = False
        self.type = 'equalstatistics'
        self.peer = peer
        self.subsystem = subsystem
        self.name = name
        self.result = -1
        self.peer2 = peer2
        self.subsystem2 = subsystem2
        self.name2 = name2
        self.result2 = -1

    def check(self):
        if (self.fulfilled == False):
            self.result = self.peer.get_statistics_value(
                self.subsystem, self.name
            )
            self.result2 = self.peer2.get_statistics_value(
                self.subsystem2, self.name2
            )
            if (self.result == self.result2):
                self.fulfilled = True
                return True
            else:
                return False
        else:
            return True

    def evaluate(self, failed_only):
        if (((self.fulfilled == False) and (failed_only == True))
            or (failed_only == False)):
            logger.debug(
                '%s %s %s == %s %s %s %s %s', self.peer.id[:4],
                self.subsystem.ljust(12), self.name.ljust(30), self.result,
                self.peer2.id[:4], self.subsystem2.ljust(12),
                self.name2.ljust(30), self.result2
            )
        return self.fulfilled


class Test(object):
    def __init__(self, testname, verbose):
        self.peers = list()
        self.verbose = verbose
        self.name = testname
        srcdir = "../.."
        gnunet_pyexpect_dir = os.path.join(srcdir, "contrib/scripts")
        if gnunet_pyexpect_dir not in sys.path:
            sys.path.append(gnunet_pyexpect_dir)
        self.gnunetarm = ''
        self.gnunetstatistics = ''
        if os.name == 'posix':
            self.gnunetarm = 'gnunet-arm'
            self.gnunetstatistics = 'gnunet-statistics'
            self.gnunetpeerinfo = 'gnunet-peerinfo'
        elif os.name == 'nt':
            self.gnunetarm = 'gnunet-arm.exe'
            self.gnunetstatistics = 'gnunet-statistics.exe'
            self.gnunetpeerinfo = 'gnunet-peerinfo.exe'
        if os.name == "nt":
            shutil.rmtree(os.path.join(os.getenv("TEMP"), testname), True)
        else:
            shutil.rmtree("/tmp/" + testname, True)

    def add_peer(self, peer):
        self.peers.append(peer)

    def p(self, msg):
        if (self.verbose == True):
            print(msg)


class Peer(object):
    def __init__(self, test, cfg_file):
        if (False == os.path.isfile(cfg_file)):
            # print(("Peer cfg " + cfg_file + ": FILE NOT FOUND"))
            logger.debug('Peer cfg %s : FILE NOT FOUND', cfg_file)
        self.id = "<NaN>"
        self.test = test
        self.started = False
        self.cfg = cfg_file

    def __del__(self):
        if (self.started == True):
            # print('ERROR! Peer using cfg ' + self.cfg + ' was not stopped')
            logger.debug('ERROR! Peer using cfg %s was not stopped', self.cfg)
            ret = self.stop()
            if (False == ret):
                # print('ERROR! Peer using cfg ' +
                #       self.cfg +
                #       ' could not be stopped')
                logger.debug(
                    'ERROR! Peer using cfg %s could not be stopped', self.cfg
                )
                self.started = False
            return ret
        else:
            return False

    def start(self):
        os.unsetenv("XDG_CONFIG_HOME")
        os.unsetenv("XDG_DATA_HOME")
        os.unsetenv("XDG_CACHE_HOME")
        self.test.p("Starting peer using cfg " + self.cfg)
        try:
            server = subprocess.Popen([
                self.test.gnunetarm, '-sq', '-c', self.cfg
            ])
            server.communicate()
        except OSError:
            # print("Can not start peer")
            logger.debug('Can not start peer')
            self.started = False
            return False
        self.started = True
        test = ''
        try:
            server = pexpect()
            server.spawn(
                None, [self.test.gnunetpeerinfo, '-c', self.cfg, '-s'],
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT
            )
            test = server.read("stdout", 1024)
        except OSError:
            # print("Can not get peer identity")
            logger.debug('Can not get peer identity')
        test = (test.split(b'`')[1])
        self.id = test.split(b'\'')[0]
        return True

    def stop(self):
        if (self.started == False):
            return False
        self.test.p("Stopping peer using cfg " + self.cfg)
        try:
            server = subprocess.Popen([
                self.test.gnunetarm, '-eq', '-c', self.cfg
            ])
            server.communicate()
        except OSError:
            # print("Can not stop peer")
            logger.debug('Can not stop peer')
            return False
        self.started = False
        return True

    def get_statistics_value(self, subsystem, name):
        server = pexpect()
        server.spawn(
            None, [
                self.test.gnunetstatistics, '-c', self.cfg, '-q', '-n', name,
                '-s', subsystem
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT
        )
        # server.expect ("stdout", re.compile (r""))
        test = server.read("stdout", 10240)
        tests = test.partition(b'\n')
        # On W32 GNUnet outputs with \r\n, rather than \n
        if os.name == 'nt' and tests[1] == b'\n' and tests[0][-1] == b'\r':
            tests = (tests[0][:-1], tests[1], tests[2])
        tests = tests[0]
        result = tests.decode("utf-8").strip()
        logger.debug(
            'running gnunet-statistics %s for %s "/" %s yields %s', self.cfg,
            name, subsystem, result
        )
        if (result.isdigit() == True):
            return result
        else:
            logger.debug(
                'Invalid statistics value: %s is not a number!', result
            )
            return -1
