"""Module containing tests for the abllib.wrapper module"""

# pylint: disable=function-redefined, consider-using-with, unused-argument

import re
import os
from datetime import datetime
from time import sleep

import pytest

from abllib import error, wrapper, log
from abllib.pproc import WorkerThread

def test_lock():
    """Ensure that Lock works as expected"""

    assert hasattr(wrapper, "Lock")
    assert callable(wrapper.Lock)

    lock = wrapper.Lock()

    assert not lock.locked()
    assert lock.acquire(blocking=True, timeout=1)
    assert lock.locked()

    assert not lock.acquire(blocking=True, timeout=1)

    lock.release()
    assert not lock.locked()

def test_semaphore():
    """Ensure that Semaphore works as expected"""

    assert hasattr(wrapper, "Semaphore")
    assert callable(wrapper.Semaphore)

    sem = wrapper.Semaphore(3)

    assert not sem.locked()
    assert sem.acquire(blocking=True, timeout=1)
    assert sem.locked()
    assert sem.acquire(blocking=True, timeout=1)
    assert sem.locked()
    assert sem.acquire(blocking=True, timeout=1)
    assert sem.locked()

    # the semaphore is full
    assert not sem.acquire(blocking=True, timeout=1)

    sem.release()
    assert sem.locked()
    sem.release()
    assert sem.locked()
    sem.release()
    assert not sem.locked()

def test_namedlock():
    """Ensure that NamedLock works as expected"""

    assert hasattr(wrapper, "NamedLock")
    assert callable(wrapper.NamedLock)

    @wrapper.NamedLock("test1", timeout=0.1)
    def func1():
        return True

    assert not wrapper.NamedLock("test1").locked()
    assert func1()
    assert not wrapper.NamedLock("test1").locked()

    wrapper.NamedLock("test2").acquire()
    assert wrapper.NamedLock("test2").locked()
    assert wrapper.NamedLock("test2", timeout=1).locked()

    wrapper.NamedLock("test3").acquire()
    def func2():
        assert wrapper.NamedLock("test3").locked()
        wrapper.NamedLock("test3", timeout=4).acquire()

    start_time = datetime.now()
    thread = WorkerThread(target=func2)
    thread.start()
    with pytest.raises(error.LockAcquisitionTimeoutError):
        thread.join(reraise=True)

    duration = datetime.now() - start_time
    assert duration.total_seconds() > 3.5
    assert duration.total_seconds() < 4.5

def test_namedsemaphore():
    """Ensure that NamedSemaphore works as expected"""

    assert hasattr(wrapper, "NamedSemaphore")
    assert callable(wrapper.NamedSemaphore)

    @wrapper.NamedSemaphore("test1", timeout=0.1)
    def func1():
        return True

    assert not wrapper.NamedSemaphore("test1").locked()
    assert func1()
    assert not wrapper.NamedSemaphore("test1").locked()

    wrapper.NamedSemaphore("test2").acquire()
    assert wrapper.NamedSemaphore("test2").locked()
    assert wrapper.NamedSemaphore("test2", timeout=1).locked()

    wrapper.NamedLock("test3").acquire()
    def func2():
        assert not wrapper.NamedSemaphore("test3").locked()
        wrapper.NamedSemaphore("test3", timeout=4).acquire()

    start_time = datetime.now()
    thread = WorkerThread(target=func2)
    thread.start()
    with pytest.raises(error.LockAcquisitionTimeoutError):
        thread.join(reraise=True)

    duration = datetime.now() - start_time
    assert duration.total_seconds() > 3.5
    assert duration.total_seconds() < 4.5

def test_namedlocks_combined():
    """Ensure that NamedLock and NamedSemaphore work together correctly"""

    @wrapper.NamedLock("test1", timeout=0.1)
    def func():
        return True

    wrapper.NamedSemaphore("test1").acquire()
    with pytest.raises(error.LockAcquisitionTimeoutError):
        func()
    wrapper.NamedSemaphore("test1").release()


    @wrapper.NamedLock("test2", timeout=0.1)
    def func():
        return True

    wrapper.NamedSemaphore("test2").acquire()
    wrapper.NamedSemaphore("test2").acquire()
    wrapper.NamedSemaphore("test2").acquire()
    with pytest.raises(error.LockAcquisitionTimeoutError):
        func()
    wrapper.NamedSemaphore("test2").release()
    with pytest.raises(error.LockAcquisitionTimeoutError):
        func()
    wrapper.NamedSemaphore("test2").release()
    with pytest.raises(error.LockAcquisitionTimeoutError):
        func()
    wrapper.NamedSemaphore("test2").release()
    func()


    @wrapper.NamedSemaphore("test3", timeout=0.1)
    def func():
        return True

    wrapper.NamedLock("test3").acquire()
    with pytest.raises(error.LockAcquisitionTimeoutError):
        func()
    wrapper.NamedLock("test3").release()

def test_locks_underscore_names():
    """Ensure that named lock names can start with an underscore"""

    lock = wrapper.NamedSemaphore("_test1")
    assert not lock.locked()
    lock.acquire()
    assert lock.locked()
    assert wrapper.NamedSemaphore("_test1").locked()
    lock.release()
    assert not lock.locked()
    assert not wrapper.NamedSemaphore("_test1").locked()

    lock = wrapper.NamedLock("_test2")
    assert not lock.locked()
    lock.acquire()
    assert lock.locked()
    assert wrapper.NamedLock("_test2").locked()
    lock.release()
    assert not lock.locked()
    assert not wrapper.NamedLock("_test2").locked()

def test_singleuse():
    """Ensure that singleuse works as expected"""

    @wrapper.singleuse
    def func1():
        pass

    func1()

    with pytest.raises(error.CalledMultipleTimesError):
        func1()

    with pytest.raises(error.CalledMultipleTimesError):
        func1()

def test_singleuse_exception():
    """Ensure that raised exceptions reset singleuse flag"""

    data = [1, 2, 3]

    @wrapper.singleuse
    def func1():
        if len(data) > 0:
            data.pop(0)
            raise error.InternalCalculationError()

    with pytest.raises(error.InternalCalculationError):
        func1()
    with pytest.raises(error.InternalCalculationError):
        func1()
    with pytest.raises(error.InternalCalculationError):
        func1()

    func1()

    with pytest.raises(error.CalledMultipleTimesError):
        func1()

    with pytest.raises(error.CalledMultipleTimesError):
        func1()

def test_log_error_default(capture_logs):
    """Ensure that log_error uses the root logger by default"""

    @wrapper.log_error
    def func1():
        raise RuntimeError("my message")

    with pytest.raises(RuntimeError):
        func1()

    assert os.path.isfile("test.log")
    with open("test.log", "r", encoding="utf8") as f:
        content = f.readlines()

        # remove "pointer" lines only present in python 3.12
        content = list(filter(lambda x: x.strip().strip("^") != "", content))

        assert len(content) == 7
        assert re.match(r"\[.*\] \[ERROR   \] root: my message", content[0])
        assert re.match(r"Traceback \(most recent call last\):", content[1])
        assert re.match(r"  File \".*_log_error.py\", line \d+, in wrapper", content[2])
        assert re.match(r"    return func\(\*args, \*\*kwargs\)", content[3])
        assert re.match(r"  File \".*wrapper_test.py\", line \d+, in func1", content[4])
        assert re.match(r"    raise RuntimeError\(\"my message\"\)", content[5])
        assert re.match(r"RuntimeError: my message", content[6])

def test_log_error_loggername(capture_logs):
    """Ensure that log_error uses the provided logger name"""

    @wrapper.log_error("mycustomlogger")
    def func1():
        raise RuntimeError("my message")

    with pytest.raises(RuntimeError):
        func1()

    assert os.path.isfile("test.log")
    with open("test.log", "r", encoding="utf8") as f:
        content = f.readlines()

        # remove "pointer" lines only present in python 3.12
        content = list(filter(lambda x: x.strip().strip("^") != "", content))

        assert len(content) == 7
        assert re.match(r"\[.*\] \[ERROR   \] mycustomlogger: my message", content[0])
        assert re.match(r"Traceback \(most recent call last\):", content[1])
        assert re.match(r"  File \".*_log_error.py\", line \d+, in wrapper", content[2])
        assert re.match(r"    return func\(\*args, \*\*kwargs\)", content[3])
        assert re.match(r"  File \".*wrapper_test.py\", line \d+, in func1", content[4])
        assert re.match(r"    raise RuntimeError\(\"my message\"\)", content[5])
        assert re.match(r"RuntimeError: my message", content[6])

def test_log_error_customlogger(capture_logs):
    """Ensure that log_error uses the provided custom logger"""

    @wrapper.log_error(log.get_logger("mycustomlogger"))
    def func1():
        raise RuntimeError("my message")

    with pytest.raises(RuntimeError):
        func1()

    assert os.path.isfile("test.log")
    with open("test.log", "r", encoding="utf8") as f:
        content = f.readlines()

        # remove "pointer" lines only present in python 3.12
        content = list(filter(lambda x: x.strip().strip("^") != "", content))

        assert len(content) == 7
        assert re.match(r"\[.*\] \[ERROR   \] mycustomlogger: my message", content[0])
        assert re.match(r"Traceback \(most recent call last\):", content[1])
        assert re.match(r"  File \".*_log_error.py\", line \d+, in wrapper", content[2])
        assert re.match(r"    return func\(\*args, \*\*kwargs\)", content[3])
        assert re.match(r"  File \".*wrapper_test.py\", line \d+, in func1", content[4])
        assert re.match(r"    raise RuntimeError\(\"my message\"\)", content[5])
        assert re.match(r"RuntimeError: my message", content[6])

def test_log_error_handler():
    """Ensure that log_error uses the provided handler"""

    results = []
    def myhandler(exc_text):
        results.append(exc_text)

    @wrapper.log_error(handler=myhandler)
    def func1():
        raise RuntimeError("my message")

    with pytest.raises(RuntimeError):
        func1()

    assert len(results) == 1
    assert results[0] == "RuntimeError: my message"

def test_log_io_default(capture_logs):
    """Ensure that log_io uses the root logger by default"""

    @wrapper.log_io
    def func1(name, age, message = ""):
        return f"{name}: {message}"

    assert func1("Tom", 46) == "Tom: "
    assert func1("Anna", 77, message="no") == "Anna: no"

    assert os.path.isfile("test.log")
    with open("test.log", "r", encoding="utf8") as f:
        content = f.readlines()

        assert len(content) == 6
        assert re.match(r'\[.*\] \[DEBUG   \] root: func: func1', content[0])
        assert re.match(r'\[.*\] \[DEBUG   \] root: in  : "Tom", 46', content[1])
        assert re.match(r'\[.*\] \[DEBUG   \] root: out : "Tom: "', content[2])
        assert re.match(r'\[.*\] \[DEBUG   \] root: func: func1', content[3])
        assert re.match(r'\[.*\] \[DEBUG   \] root: in  : "Anna", 77, message="no"', content[4])
        assert re.match(r'\[.*\] \[DEBUG   \] root: out : "Anna: no"', content[5])

def test_log_io_loggername(capture_logs):
    """Ensure that log_io uses the provided logger name"""

    @wrapper.log_io("SpecialLogger")
    def func1(name, age, message = ""):
        return f"{name}: {message}"

    assert func1("Tom", 46) == "Tom: "
    assert func1("Anna", 77, "no") == "Anna: no"

    assert os.path.isfile("test.log")
    with open("test.log", "r", encoding="utf8") as f:
        content = f.readlines()

        assert len(content) == 6
        assert re.match(r'\[.*\] \[DEBUG   \] SpecialLogger: func: func1', content[0])
        assert re.match(r'\[.*\] \[DEBUG   \] SpecialLogger: in  : "Tom", 46', content[1])
        assert re.match(r'\[.*\] \[DEBUG   \] SpecialLogger: out : "Tom: "', content[2])
        assert re.match(r'\[.*\] \[DEBUG   \] SpecialLogger: func: func1', content[3])
        assert re.match(r'\[.*\] \[DEBUG   \] SpecialLogger: in  : "Anna", 77, "no"', content[4])
        assert re.match(r'\[.*\] \[DEBUG   \] SpecialLogger: out : "Anna: no"', content[5])

def test_log_io_custom_logger(capture_logs):
    """Ensure that log_io uses a custom provided logger"""

    @wrapper.log_io(log.get_logger("ExtraLogger"))
    def func1(name, age, message = ""):
        return f"{name}: {message}"

    assert func1("Tom", 46) == "Tom: "
    assert func1("Anna", 77, "no") == "Anna: no"

    assert os.path.isfile("test.log")
    with open("test.log", "r", encoding="utf8") as f:
        content = f.readlines()

        assert len(content) == 6
        assert re.match(r'\[.*\] \[DEBUG   \] ExtraLogger: func: func1', content[0])
        assert re.match(r'\[.*\] \[DEBUG   \] ExtraLogger: in  : "Tom", 46', content[1])
        assert re.match(r'\[.*\] \[DEBUG   \] ExtraLogger: out : "Tom: "', content[2])
        assert re.match(r'\[.*\] \[DEBUG   \] ExtraLogger: func: func1', content[3])
        assert re.match(r'\[.*\] \[DEBUG   \] ExtraLogger: in  : "Anna", 77, "no"', content[4])
        assert re.match(r'\[.*\] \[DEBUG   \] ExtraLogger: out : "Anna: no"', content[5])

def test_timeit_default(capture_logs):
    """Ensure that timeit uses the root logger by default"""

    @wrapper.timeit
    def func1():
        sleep(0.001)
    @wrapper.timeit
    def func2():
        sleep(0.01)
    @wrapper.timeit
    def func3():
        sleep(0.1)
    @wrapper.timeit
    def func4():
        sleep(1)
    @wrapper.timeit
    def func5():
        sleep(10)


    func1()
    func2()
    func3()
    func4()
    func5()

    assert os.path.isfile("test.log")
    with open("test.log", "r", encoding="utf8") as f:
        content = f.readlines()

        assert len(content) == 5
        assert re.match(r'\[.*\] \[DEBUG   \] root: func1: \d{1}\.\d{2} ms elapsed', content[0])
        assert re.match(r'\[.*\] \[DEBUG   \] root: func2: \d{2}\.\d{2} ms elapsed', content[1])
        assert re.match(r'\[.*\] \[DEBUG   \] root: func3: \d{3}\.\d{2} ms elapsed', content[2])
        assert re.match(r'\[.*\] \[DEBUG   \] root: func4: \d{1}\.\d{2} s elapsed', content[3])
        assert re.match(r'\[.*\] \[DEBUG   \] root: func5: \d{2}\.\d{2} s elapsed', content[4])

def test_timeit_loggername(capture_logs):
    """Ensure that timeit uses the provided logger name"""

    @wrapper.timeit("SpecialLogger")
    def func1():
        sleep(0.001)

    func1()

    assert os.path.isfile("test.log")
    with open("test.log", "r", encoding="utf8") as f:
        content = f.readlines()

        assert len(content) == 1
        assert re.match(r'\[.*\] \[DEBUG   \] SpecialLogger: func1: \d{1}\.\d{2} ms elapsed', content[0])

def test_timeit_custom_logger(capture_logs):
    """Ensure that timeit uses a custom provided logger"""

    @wrapper.timeit(log.get_logger("ExtraLogger"))
    def func1():
        sleep(0.001)

    func1()

    assert os.path.isfile("test.log")
    with open("test.log", "r", encoding="utf8") as f:
        content = f.readlines()

        assert len(content) == 1
        assert re.match(r'\[.*\] \[DEBUG   \] ExtraLogger: func1: \d{1}\.\d{2} ms elapsed', content[0])
