# #region Test.AsyncJobRunner [C:3] [TYPE Module] [SEMANTICS test,async,runner,scheduler]
# @BRIEF Unit tests for AsyncJobRunner — centralized async/sync bridge.
# @RELATION BINDS_TO -> [AsyncJobRunner]
# @TEST_CONTRACT: AsyncJobRunner.run -> result | executes coroutine and returns result
# @TEST_CONTRACT: AsyncJobRunner.run_later -> None | schedules delayed coroutine execution
# @TEST_EDGE: run_propagates_exception -> exception from coroutine is re-raised
# @TEST_EDGE: run_later_executes_after_delay -> coroutine runs after specified delay
# @TEST_EDGE: run_from_background_thread -> works correctly from non-main thread
# @TEST_INVARIANT: coroutine_must_be_awaited -> VERIFIED_BY: [test_run_executes_coroutine, test_run_propagates_exception]

import asyncio
import sys
import threading
import time
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))

from src.core.async_job_runner import AsyncJobRunner


# -- Helpers -----------------------------------------------------------------

async def async_return_value(value):
    """Simple async function that returns a value."""
    return value


async def async_raise(exc_class, message=""):
    """Simple async function that raises an exception."""
    raise exc_class(message)


async def async_delay(value, delay_seconds):
    """Async function that sleeps then returns a value."""
    await asyncio.sleep(delay_seconds)
    return value


# -- Fixtures ----------------------------------------------------------------

@pytest.fixture
def event_loop():
    """Create a new event loop for each test."""
    loop = asyncio.new_event_loop()
    yield loop
    loop.close()


@pytest.fixture
def runner(event_loop):
    """Create an AsyncJobRunner with a new event loop."""
    return AsyncJobRunner(event_loop)


# -- Tests: run() ------------------------------------------------------------

# #region test_run_executes_coroutine [C:2] [TYPE Function]
# @BRIEF run() executes a coroutine and returns its result.
def test_run_executes_coroutine(runner, event_loop):
    """run() dispatches coroutine to event loop and waits for result."""
    # Start the event loop in a background thread
    thread = threading.Thread(target=event_loop.run_forever, daemon=True)
    thread.start()
    try:
        result = runner.run(async_return_value(42))
        assert result == 42
    finally:
        event_loop.call_soon_threadsafe(event_loop.stop)
        thread.join(timeout=2)
# #endregion test_run_executes_coroutine


# #region test_run_returns_string [C:2] [TYPE Function]
# @BRIEF run() works with string return values.
def test_run_returns_string(runner, event_loop):
    """run() correctly returns string results."""
    thread = threading.Thread(target=event_loop.run_forever, daemon=True)
    thread.start()
    try:
        result = runner.run(async_return_value("hello"))
        assert result == "hello"
    finally:
        event_loop.call_soon_threadsafe(event_loop.stop)
        thread.join(timeout=2)
# #endregion test_run_returns_string


# #region test_run_returns_none [C:2] [TYPE Function]
# @BRIEF run() works when coroutine returns None.
def test_run_returns_none(runner, event_loop):
    """run() correctly handles None return value."""
    async def async_none():
        return None

    thread = threading.Thread(target=event_loop.run_forever, daemon=True)
    thread.start()
    try:
        result = runner.run(async_none())
        assert result is None
    finally:
        event_loop.call_soon_threadsafe(event_loop.stop)
        thread.join(timeout=2)
# #endregion test_run_returns_none


# #region test_run_propagates_exception [C:2] [TYPE Function]
# @BRIEF run() re-raises exceptions from the coroutine.
def test_run_propagates_exception(runner, event_loop):
    """run() re-raises exceptions from the coroutine in the calling thread."""
    thread = threading.Thread(target=event_loop.run_forever, daemon=True)
    thread.start()
    try:
        with pytest.raises(ValueError, match="boom"):
            runner.run(async_raise(ValueError, "boom"))
    finally:
        event_loop.call_soon_threadsafe(event_loop.stop)
        thread.join(timeout=2)
# #endregion test_run_propagates_exception


# #region test_run_propagates_runtime_error [C:2] [TYPE Function]
# @BRIEF run() re-raises RuntimeError from the coroutine.
def test_run_propagates_runtime_error(runner, event_loop):
    """run() re-raises RuntimeError from the coroutine."""
    thread = threading.Thread(target=event_loop.run_forever, daemon=True)
    thread.start()
    try:
        with pytest.raises(RuntimeError, match="connection lost"):
            runner.run(async_raise(RuntimeError, "connection lost"))
    finally:
        event_loop.call_soon_threadsafe(event_loop.stop)
        thread.join(timeout=2)
# #endregion test_run_propagates_runtime_error


# -- Tests: run_later() ------------------------------------------------------

# #region test_run_later_executes_after_delay [C:2] [TYPE Function]
# @BRIEF run_later() schedules a coroutine to execute after a delay.
def test_run_later_executes_after_delay(runner, event_loop):
    """run_later() dispatches coroutine after specified delay."""
    called = []

    async def track():
        called.append(True)

    thread = threading.Thread(target=event_loop.run_forever, daemon=True)
    thread.start()
    try:
        runner.run_later(track(), delay_seconds=0.1)
        time.sleep(0.3)
        assert len(called) == 1, f"Expected coroutine to execute once, got {len(called)} times"
    finally:
        event_loop.call_soon_threadsafe(event_loop.stop)
        thread.join(timeout=2)
# #endregion test_run_later_executes_after_delay


# #region test_run_later_does_not_execute_immediately [C:2] [TYPE Function]
# @BRIEF run_later() does not execute coroutine before the delay.
def test_run_later_does_not_execute_immediately(runner, event_loop):
    """run_later() does not execute coroutine before the delay expires."""
    called = []

    async def track():
        called.append(True)

    thread = threading.Thread(target=event_loop.run_forever, daemon=True)
    thread.start()
    try:
        runner.run_later(track(), delay_seconds=0.5)
        time.sleep(0.1)  # Check before delay expires
        assert len(called) == 0, "Coroutine should not have executed yet"
    finally:
        event_loop.call_soon_threadsafe(event_loop.stop)
        thread.join(timeout=2)
# #endregion test_run_later_does_not_execute_immediately


# -- Tests: from background thread ------------------------------------------

# #region test_run_from_background_thread [C:2] [TYPE Function]
# @BRIEF run() works correctly when called from a non-main thread.
def test_run_from_background_thread(event_loop):
    """run() can be called from a background thread (simulating APScheduler)."""
    runner = AsyncJobRunner(event_loop)
    results = []
    errors = []

    def background_work():
        try:
            result = runner.run(async_return_value(99))
            results.append(result)
        except Exception as e:
            errors.append(e)

    # Start the event loop in a background thread
    loop_thread = threading.Thread(target=event_loop.run_forever, daemon=True)
    loop_thread.start()

    try:
        # Call run() from another background thread (simulating APScheduler)
        work_thread = threading.Thread(target=background_work)
        work_thread.start()
        work_thread.join(timeout=5)

        assert len(errors) == 0, f"Unexpected errors: {errors}"
        assert results == [99], f"Expected [99], got {results}"
    finally:
        event_loop.call_soon_threadsafe(event_loop.stop)
        loop_thread.join(timeout=2)
# #endregion test_run_from_background_thread


# -- Tests: edge cases -------------------------------------------------------

# #region test_run_with_complex_return_value [C:2] [TYPE Function]
# @BRIEF run() works with complex return values (dict, list).
def test_run_with_complex_return_value(runner, event_loop):
    """run() correctly returns complex objects."""
    async def async_dict():
        return {"key": "value", "nested": [1, 2, 3]}

    thread = threading.Thread(target=event_loop.run_forever, daemon=True)
    thread.start()
    try:
        result = runner.run(async_dict())
        assert result == {"key": "value", "nested": [1, 2, 3]}
    finally:
        event_loop.call_soon_threadsafe(event_loop.stop)
        thread.join(timeout=2)
# #endregion test_run_with_complex_return_value


# #region test_run_later_with_zero_delay [C:2] [TYPE Function]
# @BRIEF run_later() with zero delay executes immediately.
def test_run_later_with_zero_delay(runner, event_loop):
    """run_later() with delay_seconds=0 executes immediately."""
    called = []

    async def track():
        called.append(True)

    thread = threading.Thread(target=event_loop.run_forever, daemon=True)
    thread.start()
    try:
        runner.run_later(track(), delay_seconds=0)
        time.sleep(0.2)
        assert len(called) == 1, "Coroutine should have executed immediately"
    finally:
        event_loop.call_soon_threadsafe(event_loop.stop)
        thread.join(timeout=2)
# #endregion test_run_later_with_zero_delay
