""" :mod: ProcessPoolTests
  =======================

  .. module: ProcessPoolTests
  :synopsis: unit tests for ProcessPool
  .. moduleauthor:: Krzysztof.Ciba@NOSPAMgmail.com

  unit tests for ProcessPool
"""

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import os
import sys
import random
import time
import threading

import pytest

from DIRAC import gLogger
from DIRAC.Core.Utilities.ProcessPool import ProcessPool

# Mark this entire module as slow
pytestmark = pytest.mark.slow


@pytest.fixture(autouse=True)
def capture_wrap():
    """Avoid https://github.com/pytest-dev/pytest/issues/5502"""
    sys.stderr.close = lambda *args: None
    sys.stdout.close = lambda *args: None
    yield


def ResultCallback(task, taskResult):
    """dummy result callback"""
    print("callback for %s result is %s" % (task.getTaskID(), taskResult))


def ExceptionCallback(task, exec_info):
    """dummy exception callback"""
    print("callback for %s exception is %s" % (task.getTaskID(), exec_info))


def CallableFunc(taskID, timeWait, raiseException=False):
    """global function to be executed in task"""
    print("pid=%s task=%s will sleep for %s s" % (os.getpid(), taskID, timeWait))
    time.sleep(timeWait)
    if raiseException:
        raise Exception("testException")
    return timeWait


class CallableClass(object):
    """callable class to be executed in task"""

    def __init__(self, taskID, timeWait, raiseException=False):
        self.log = gLogger.getSubLogger(self.__class__.__name__ + "/%s" % taskID)
        self.taskID = taskID
        self.timeWait = timeWait
        self.raiseException = raiseException

    def __call__(self):
        self.log.always("pid=%s task=%s will sleep for %s s" % (os.getpid(), self.taskID, self.timeWait))
        time.sleep(self.timeWait)
        if self.raiseException:
            raise Exception("testException")
        return self.timeWait


# global locked lock
gLock = threading.Lock()
# make sure it is locked
gLock.acquire()


# dummy callable locked class
class LockedCallableClass(object):
    """callable and locked class"""

    def __init__(self, taskID, timeWait, raiseException=False):
        self.log = gLogger.getSubLogger(self.__class__.__name__ + "/%s" % taskID)
        self.taskID = taskID
        self.log.always("pid=%s task=%s I'm locked" % (os.getpid(), self.taskID))
        gLock.acquire()
        self.log.always("you can't see that line, object is stuck by gLock")
        self.timeWait = timeWait
        self.raiseException = raiseException
        gLock.release()

    def __call__(self):
        self.log.always("If you see this line, miracle had happened!")
        self.log.always("will sleep for %s" % self.timeWait)
        time.sleep(self.timeWait)
        if self.raiseException:
            raise Exception("testException")
        return self.timeWait


########################################################################
@pytest.fixture()
def processPool():
    gLogger.showHeaders(True)
    log = gLogger.getSubLogger("TaskCallbacksTests")
    processPool = ProcessPool(4, 8, 8)
    processPool.daemonize()
    yield processPool


def test_TaskCallbacks_CallableClass(processPool):
    """CallableClass and task callbacks test"""
    i = 0
    while True:
        if processPool.getFreeSlots() > 0:
            timeWait = random.randint(0, 5)
            raiseException = False
            if not timeWait:
                raiseException = True
            result = processPool.createAndQueueTask(
                CallableClass,
                taskID=i,
                args=(i, timeWait, raiseException),
                callback=ResultCallback,
                exceptionCallback=ExceptionCallback,
                blocking=True,
            )
            if result["OK"]:
                print("CallableClass enqueued to task %s" % i)
                i += 1
            else:
                continue
        if i == 10:
            break
    processPool.finalize(2)


def test_TaskCallbacks_CallableFunc(processPool):
    """CallableFunc and task callbacks test"""
    i = 0
    while True:
        if processPool.getFreeSlots() > 0:
            timeWait = random.randint(0, 5)
            raiseException = False
            if not timeWait:
                raiseException = True
            result = processPool.createAndQueueTask(
                CallableFunc,
                taskID=i,
                args=(i, timeWait, raiseException),
                callback=ResultCallback,
                exceptionCallback=ExceptionCallback,
                blocking=True,
            )
            if result["OK"]:
                print("CallableClass enqueued to task %s" % i)
                i += 1
            else:
                continue
        if i == 10:
            break
    processPool.finalize(2)


########################################################################
@pytest.fixture()
def processPoolWithCallbacks():
    gLogger.showHeaders(True)
    log = gLogger.getSubLogger("ProcessPoolCallbacksTests")
    processPoolWithCallbacks = ProcessPool(
        4,
        8,
        8,
        poolCallback=lambda taskID, taskResult: log.always("callback result for %s is %s" % (taskID, taskResult)),
        poolExceptionCallback=lambda taskID, taskException: log.always(
            "callback exception for %s is %s" % (taskID, taskException)
        ),
    )
    processPoolWithCallbacks.daemonize()
    yield processPoolWithCallbacks


def test_ProcessPoolCallbacks_CallableClass(processPoolWithCallbacks):
    """CallableClass and pool callbacks test"""
    i = 0
    while True:
        if processPoolWithCallbacks.getFreeSlots() > 0:
            timeWait = random.randint(0, 5)
            raiseException = False
            if not timeWait:
                raiseException = True
            result = processPoolWithCallbacks.createAndQueueTask(
                CallableClass,
                taskID=i,
                args=(i, timeWait, raiseException),
                usePoolCallbacks=True,
                blocking=True,
            )
            if result["OK"]:
                print("CallableClass enqueued to task %s" % i)
                i += 1
            else:
                continue
        if i == 10:
            break
    processPoolWithCallbacks.finalize(2)


def test_ProcessPoolCallbacks_CallableFunc(processPoolWithCallbacks):
    """CallableFunc and pool callbacks test"""
    i = 0
    while True:
        if processPoolWithCallbacks.getFreeSlots() > 0:
            timeWait = random.randint(0, 5)
            raiseException = False
            if not timeWait:
                raiseException = True
            result = processPoolWithCallbacks.createAndQueueTask(
                CallableFunc,
                taskID=i,
                args=(i, timeWait, raiseException),
                usePoolCallbacks=True,
                blocking=True,
            )
            if result["OK"]:
                print("CallableFunc enqueued to task %s" % i)
                i += 1
            else:
                continue
        if i == 10:
            break
    processPoolWithCallbacks.finalize(2)


########################################################################
@pytest.fixture()
def processPoolWithCallbacks2():
    gLogger.showHeaders(True)
    log = gLogger.getSubLogger("TaskTimeOutTests")
    processPoolWithCallbacks2 = ProcessPool(
        2,
        4,
        8,
        poolCallback=lambda taskID, taskResult: log.always("callback result for %s is %s" % (taskID, taskResult)),
        poolExceptionCallback=lambda taskID, taskException: log.always(
            "callback exception for %s is %s" % (taskID, taskException)
        ),
    )
    processPoolWithCallbacks2.daemonize()
    yield processPoolWithCallbacks2


def test_TaskTimeOut_CallableClass(processPoolWithCallbacks2):
    """CallableClass and task time out test"""
    i = 0
    while True:
        if processPoolWithCallbacks2.getFreeSlots() > 0:
            timeWait = random.randint(0, 5) * 1
            raiseException = False
            if not timeWait:
                raiseException = True
            result = processPoolWithCallbacks2.createAndQueueTask(
                CallableClass,
                taskID=i,
                args=(i, timeWait, raiseException),
                timeOut=1.5,
                usePoolCallbacks=True,
                blocking=True,
            )
            if result["OK"]:
                print("CallableClass enqueued to task %s timeWait=%s exception=%s" % (i, timeWait, raiseException))
                i += 1
            else:
                continue
        if i == 16:
            break
    processPoolWithCallbacks2.finalize(2)


def test_TaskTimeOut_CallableFunc(processPoolWithCallbacks2):
    """CallableFunc and task timeout test"""
    i = 0
    while True:
        if processPoolWithCallbacks2.getFreeSlots() > 0:
            timeWait = random.randint(0, 5) * 0.5
            raiseException = False
            if not timeWait:
                raiseException = True
            result = processPoolWithCallbacks2.createAndQueueTask(
                CallableFunc,
                taskID=i,
                args=(i, timeWait, raiseException),
                timeOut=1.5,
                usePoolCallbacks=True,
                blocking=True,
            )
            if result["OK"]:
                print("CallableFunc enqueued to task %s timeWait=%s exception=%s" % (i, timeWait, raiseException))
                i += 1
            else:
                continue
        if i == 16:
            break
    processPoolWithCallbacks2.finalize(2)


def test_TaskTimeOut_LockedClass(processPoolWithCallbacks2):
    """LockedCallableClass and task time out test"""
    for loop in range(2):
        print("loop %s" % loop)
        i = 0
        while i < 16:
            if processPoolWithCallbacks2.getFreeSlots() > 0:
                timeWait = random.randint(0, 5) * 0.5
                raiseException = False
                if timeWait == 0.5:
                    raiseException = True
                klass = CallableClass
                if timeWait >= 2.0:
                    klass = LockedCallableClass
                result = processPoolWithCallbacks2.createAndQueueTask(
                    klass,
                    taskID=i,
                    args=(i, timeWait, raiseException),
                    timeOut=1.5,
                    usePoolCallbacks=True,
                    blocking=True,
                )
                if result["OK"]:
                    print(
                        "%s enqueued to task %s timeWait=%s exception=%s"
                        % (klass.__name__, i, timeWait, raiseException)
                    )
                    i += 1
                else:
                    continue
        print("being idle for a while")
        for _ in range(100000):
            for _ in range(1000):
                pass

    print("finalizing...")
    processPoolWithCallbacks2.finalize(10)
    # unlock
    gLock.release()
