# #region Test.AsyncJobRunnerIntegration [C:3] [TYPE Module] [SEMANTICS test,async,runner,integration]
# @BRIEF Integration tests for AsyncJobRunner with real event loop and threading.
# @RELATION BINDS_TO -> [AsyncJobRunner]
# @TEST_CONTRACT: AsyncJobRunner.run_with_real_loop -> result | works with real asyncio event loop
# @TEST_CONTRACT: AsyncJobRunner.run_later_with_real_loop -> result | delayed execution works
# @TEST_EDGE: concurrent_runs -> multiple run() calls from different threads
# @TEST_EDGE: run_with_await -> coroutine with await works correctly
# @TEST_INVARIANT: thread_safety -> VERIFIED_BY: [test_concurrent_runs_from_multiple_threads]

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_db_operation(value, delay=0.01):
    """Simulate an async DB operation."""
    await asyncio.sleep(delay)
    return {"result": value, "status": "ok"}


async def async_chain_operation(value):
    """Simulate chained async operations."""
    result1 = await async_db_operation(value)
    result2 = await async_db_operation(result1["result"] * 2)
    return result2


# -- Tests: Integration with real event loop ---------------------------------

# #region test_run_with_real_event_loop [C:2] [TYPE Function]
# @BRIEF run() works with a real asyncio event loop running in a background thread.
def test_run_with_real_event_loop():
    """run() dispatches coroutine to a real event loop and waits for result."""
    loop = asyncio.new_event_loop()
    runner = AsyncJobRunner(loop)

    def run_loop():
        loop.run_forever()

    loop_thread = threading.Thread(target=run_loop, daemon=True)
    loop_thread.start()

    try:
        result = runner.run(async_db_operation("test"))
        assert result == {"result": "test", "status": "ok"}
    finally:
        loop.call_soon_threadsafe(loop.stop)
        loop_thread.join(timeout=2)
        loop.close()
# #endregion test_run_with_real_event_loop


# #region test_run_later_with_real_event_loop [C:2] [TYPE Function]
# @BRIEF run_later() works with a real event loop — coroutine executes after delay.
def test_run_later_with_real_event_loop():
    """run_later() schedules coroutine on real event loop with correct timing."""
    loop = asyncio.new_event_loop()
    runner = AsyncJobRunner(loop)
    execution_times = []

    async def track_time():
        execution_times.append(time.monotonic())

    def run_loop():
        loop.run_forever()

    loop_thread = threading.Thread(target=run_loop, daemon=True)
    loop_thread.start()

    try:
        start = time.monotonic()
        runner.run_later(track_time(), delay_seconds=0.2)
        time.sleep(0.5)
        assert len(execution_times) == 1
        elapsed = execution_times[0] - start
        assert 0.15 < elapsed < 0.4, f"Expected ~0.2s delay, got {elapsed:.3f}s"
    finally:
        loop.call_soon_threadsafe(loop.stop)
        loop_thread.join(timeout=2)
        loop.close()
# #endregion test_run_later_with_real_event_loop


# #region test_concurrent_runs_from_multiple_threads [C:2] [TYPE Function]
# @BRIEF Multiple threads can call run() concurrently without deadlocks.
def test_concurrent_runs_from_multiple_threads():
    """run() is thread-safe — multiple threads can dispatch coroutines concurrently."""
    loop = asyncio.new_event_loop()
    runner = AsyncJobRunner(loop)
    results = {}
    errors = []

    def worker(thread_id):
        try:
            result = runner.run(async_db_operation(f"thread-{thread_id}"))
            results[thread_id] = result
        except Exception as e:
            errors.append((thread_id, e))

    def run_loop():
        loop.run_forever()

    loop_thread = threading.Thread(target=run_loop, daemon=True)
    loop_thread.start()

    try:
        threads = []
        for i in range(5):
            t = threading.Thread(target=worker, args=(i,))
            threads.append(t)
            t.start()

        for t in threads:
            t.join(timeout=10)

        assert len(errors) == 0, f"Errors in threads: {errors}"
        assert len(results) == 5
        for i in range(5):
            assert results[i]["result"] == f"thread-{i}"
    finally:
        loop.call_soon_threadsafe(loop.stop)
        loop_thread.join(timeout=2)
        loop.close()
# #endregion test_concurrent_runs_from_multiple_threads


# #region test_run_with_chained_await [C:2] [TYPE Function]
# @BRIEF run() works with coroutines that await other coroutines.
def test_run_with_chained_await():
    """run() correctly handles coroutines that internally await other coroutines."""
    loop = asyncio.new_event_loop()
    runner = AsyncJobRunner(loop)

    def run_loop():
        loop.run_forever()

    loop_thread = threading.Thread(target=run_loop, daemon=True)
    loop_thread.start()

    try:
        result = runner.run(async_chain_operation(5))
        assert result == {"result": 10, "status": "ok"}
    finally:
        loop.call_soon_threadsafe(loop.stop)
        loop_thread.join(timeout=2)
        loop.close()
# #endregion test_run_with_chained_await


# #region test_run_later_multiple_tasks [C:2] [TYPE Function]
# @BRIEF Multiple run_later() calls schedule multiple coroutines correctly.
def test_run_later_multiple_tasks():
    """Multiple run_later() calls all execute their coroutines."""
    loop = asyncio.new_event_loop()
    runner = AsyncJobRunner(loop)
    execution_order = []

    async def track(name):
        execution_order.append(name)

    def run_loop():
        loop.run_forever()

    loop_thread = threading.Thread(target=run_loop, daemon=True)
    loop_thread.start()

    try:
        runner.run_later(track("first"), delay_seconds=0.1)
        runner.run_later(track("second"), delay_seconds=0.2)
        runner.run_later(track("third"), delay_seconds=0.3)
        time.sleep(0.5)
        assert len(execution_order) == 3
        assert execution_order == ["first", "second", "third"]
    finally:
        loop.call_soon_threadsafe(loop.stop)
        loop_thread.join(timeout=2)
        loop.close()
# #endregion test_run_later_multiple_tasks
