import logging
import threading
import time
from uuid import uuid4

import pytest

from dj_raincheck.decorators import raincheck


def wait_for_request_finished_signal(wait_time: float = 0.0001):
    """Wait for the request_finished signal to fire."""

    time.sleep(wait_time)


def test_(client):
    actual = 0
    expected = 1

    @raincheck
    def func():
        nonlocal actual
        actual += 1

    # Schedule a function to run later
    func.schedule()
    assert actual == 0

    # Ensure that the scheduled function ran after the request completed
    client.get("/")
    wait_for_request_finished_signal()

    assert actual == expected


def test_arg(client):
    actual = ""
    expected = str(uuid4())

    @raincheck
    def func(s: str):
        nonlocal actual
        actual += s

    # Schedule a function to run later with an arg
    func.schedule(expected)
    assert actual == ""

    # Ensure that the scheduled function ran after the request completed
    client.get("/")
    wait_for_request_finished_signal()

    assert actual == expected


def test_kwarg(client):
    actual = ""
    expected = str(uuid4())

    @raincheck
    def func(*, s: str | None = None):
        nonlocal actual
        actual += s

    # Schedule a function to run later with an arg
    func.schedule(s=expected)
    assert actual == ""

    # Ensure that the scheduled function ran after the request completed
    client.get("/")
    wait_for_request_finished_signal()

    assert actual == expected


def test_missing_arg(client, caplog):
    expected = "test_missing_arg.<locals>.func() missing 1 required positional argument: 's'"

    @raincheck
    def func(s: str):
        pass

    with caplog.at_level(logging.ERROR):
        # Schedule a function without the required arg
        func.schedule()

        # Make a request and wait for it to complete
        client.get("/")
        wait_for_request_finished_signal()

        # Check that there is a log record
        assert len(caplog.records) == 1
        actual = caplog.records[0]

        assert actual.levelname == "ERROR"
        assert str(actual.msg) == expected


def test_missing_kwarg(client, caplog):
    expected = "test_missing_kwarg.<locals>.func() missing 1 required keyword-only argument: 's'"

    @raincheck
    def func(*, s: str):
        pass

    with caplog.at_level(logging.ERROR):
        # Schedule a function without the required kwarg
        func.schedule()

        # Make a request and wait for it to complete
        client.get("/")
        wait_for_request_finished_signal()

        # Check that an error was logged
        assert len(caplog.records) == 1
        actual = caplog.records[0]

        assert actual.levelname == "ERROR"
        assert str(actual.msg) == expected


def test_multiple(client):
    actual = 0
    expected = 2

    @raincheck
    def func():
        nonlocal actual
        actual += 1

    # Schedule 2 functions to run later
    func.schedule()
    func.schedule()
    assert actual == 0

    # Ensure that the scheduled functions ran after the request completed
    client.get("/")
    wait_for_request_finished_signal()

    assert actual == expected


def test_run_async(settings, client):
    settings.RAINCHECK_RUN_ASYNC = True
    execution_threads = []

    @raincheck
    def track_thread():
        execution_threads.append(threading.current_thread())

    # Schedule the function to run after response
    track_thread.schedule()

    # Make a request to trigger the signal
    client.get("/")
    wait_for_request_finished_signal()

    # Should have one thread (the one we created)
    assert len(execution_threads) == 1
    actual = execution_threads[0]

    # The scheduled thread should be different than the current one
    assert actual != threading.current_thread()

    # Update the run async setting
    settings.RAINCHECK_RUN_ASYNC = False

    execution_threads.clear()
    expected = threading.current_thread()

    # Schedule again
    track_thread.schedule()

    # Make another request
    client.get("/")
    wait_for_request_finished_signal()

    # Should have one more thread (same as main thread since we're running synchronously)
    assert len(execution_threads) == 1
    actual = execution_threads[0]

    assert actual == expected


def test_immediate(settings, client):
    settings.RAINCHECK_IMMEDIATE = False

    actual = []
    expected = ["before_schedule", "after_schedule", "scheduled_function"]

    @raincheck
    def immediate_func():
        actual.append("scheduled_function")

    # Schedule the function - should NOT run immediately
    actual.append("before_schedule")
    immediate_func.schedule()
    actual.append("after_schedule")

    # Function should NOT have run yet
    assert actual == ["before_schedule", "after_schedule"]

    # After request, function should run
    client.get("/")
    wait_for_request_finished_signal()

    # Now the function should have run
    assert actual == expected

    # Update the immediate setting
    settings.RAINCHECK_IMMEDIATE = True

    actual.clear()
    expected = ["before_schedule", "scheduled_function", "after_schedule"]

    # "Schedule" the function, but it should run immediately
    actual.append("before_schedule")
    immediate_func.schedule()
    actual.append("after_schedule")

    assert actual == expected


@pytest.mark.slow
def test_slow(client):
    actual = 0
    expected = 2

    @raincheck
    def slow_func():
        nonlocal actual
        time.sleep(0.5)
        actual += 1

    # Schedule a slow function to run later
    slow_func.schedule()

    # Ensure that the slow function has not run even after a request has happened
    assert actual == 0
    client.get("/")
    assert actual == 0

    # Schedule a second slow function to run later
    slow_func.schedule()

    # Make a second request
    start_time = time.time()
    client.get("/")
    assert actual == 0

    # Ensure that the second request took some amount of time,
    # but not as long as the sleep in the slow function
    duration = time.time() - start_time
    assert duration > 0
    assert duration < 0.5

    # Wait for the slow function to complete
    wait_for_request_finished_signal(1)

    # Ensure that both scheduled functions have completed
    assert actual == expected
