# #region Test.BlockingExecutors [C:3] [TYPE Module] [SEMANTICS test,async,executors,blocking,threadpool]
# @BRIEF Tests for core/utils/executors.py — init, shutdown, run_blocking, run_cpu_blocking.
# @RELATION BINDS_TO -> [BlockingExecutorsModule]

from pathlib import Path
import sys

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

import unittest
from unittest.mock import MagicMock, patch, AsyncMock
import asyncio
import pytest


class TestInitShutdown:
    """init_executors and shutdown_executors."""

    def test_init_creates_executors(self):
        from src.core.utils.executors import init_executors
        with patch("src.core.utils.executors.ThreadPoolExecutor") as mock_tpe:
            init_executors(db_workers=5, file_workers=3, git_workers=2)
            assert mock_tpe.call_count == 3

    def test_shutdown_without_init(self):
        # shutdown should not raise when executors are None
        from src.core.utils.executors import shutdown_executors
        shutdown_executors()

    def test_shutdown_calls_shutdown(self):
        from src.core.utils.executors import init_executors, shutdown_executors
        import src.core.utils.executors as exec_mod
        with patch.object(exec_mod, "_db_executor", None), \
             patch.object(exec_mod, "_file_executor", None), \
             patch.object(exec_mod, "_git_executor", None):
            with patch("src.core.utils.executors.ThreadPoolExecutor") as mock_tpe:
                mock_executor = MagicMock()
                mock_tpe.return_value = mock_executor
                init_executors()
                shutdown_executors()
                assert mock_executor.shutdown.call_count >= 1
                for call in mock_executor.shutdown.call_args_list:
                    assert call == unittest.mock.call(wait=True, cancel_futures=True)

    def test_shutdown_no_wait(self):
        from src.core.utils.executors import init_executors, shutdown_executors
        import src.core.utils.executors as exec_mod
        with patch.object(exec_mod, "_db_executor", None), \
             patch.object(exec_mod, "_file_executor", None), \
             patch.object(exec_mod, "_git_executor", None):
            with patch("src.core.utils.executors.ThreadPoolExecutor") as mock_tpe:
                mock_executor = MagicMock()
                mock_tpe.return_value = mock_executor
                init_executors()
                shutdown_executors(wait=False, cancel_futures=False)
                assert mock_executor.shutdown.call_count >= 1
                for call in mock_executor.shutdown.call_args_list:
                    assert call == unittest.mock.call(wait=False, cancel_futures=False)


class TestGetExecutor:
    """_get_executor — returns executor and semaphore by kind."""

    def test_db_kind(self):
        from src.core.utils.executors import _get_executor, init_executors, shutdown_executors
        with patch("src.core.utils.executors.ThreadPoolExecutor"):
            init_executors()
            executor, sem = _get_executor("db")
            assert executor is not None
            assert sem is not None
            shutdown_executors()

    def test_file_kind(self):
        from src.core.utils.executors import _get_executor, init_executors, shutdown_executors
        with patch("src.core.utils.executors.ThreadPoolExecutor"):
            init_executors()
            executor, sem = _get_executor("file")
            assert executor is not None
            assert sem is not None
            shutdown_executors()

    def test_git_kind(self):
        from src.core.utils.executors import _get_executor, init_executors, shutdown_executors
        with patch("src.core.utils.executors.ThreadPoolExecutor"):
            init_executors()
            executor, sem = _get_executor("git")
            assert executor is not None
            assert sem is not None
            shutdown_executors()

    def test_unknown_kind_raises(self):
        from src.core.utils.executors import _get_executor, init_executors, shutdown_executors
        with patch("src.core.utils.executors.ThreadPoolExecutor"):
            init_executors()
            with pytest.raises(ValueError, match="Unknown executor kind"):
                _get_executor("unknown")
            shutdown_executors()


class TestRunBlocking:
    """run_blocking — execute blocking function in named executor."""

    @pytest.mark.asyncio
    async def test_runs_function(self):
        from src.core.utils.executors import run_blocking, init_executors, shutdown_executors

        with patch("src.core.utils.executors.ThreadPoolExecutor") as mock_tpe:
            mock_executor = MagicMock()
            mock_tpe.return_value = mock_executor
            init_executors()

            # Make run_in_executor work
            mock_loop = AsyncMock()
            mock_loop.run_in_executor.return_value = "result"
            get_running_loop = asyncio.get_running_loop

            with patch.object(asyncio, "get_running_loop", return_value=mock_loop):
                result = await run_blocking("db", lambda: "hello")
                assert result == "result"

            shutdown_executors()

    @pytest.mark.asyncio
    async def test_semaphore_acquisition_failure(self):
        from src.core.utils.executors import run_blocking, init_executors, shutdown_executors

        with patch("src.core.utils.executors.ThreadPoolExecutor"):
            init_executors(queue_timeout=0.1)

            with (
                patch("src.core.utils.executors._db_semaphore") as mock_sem,
                pytest.raises(TimeoutError, match="queue timeout"),
            ):
                mock_sem.acquire.side_effect = TimeoutError()
                await run_blocking("db", lambda: "hello", timeout=0.1)

            shutdown_executors()

    @pytest.mark.asyncio
    async def test_semaphore_release_on_success(self):
        from src.core.utils.executors import run_blocking, init_executors, shutdown_executors

        with patch("src.core.utils.executors.ThreadPoolExecutor"):
            init_executors()
            mock_loop = AsyncMock()
            mock_loop.run_in_executor.return_value = "done"

            # Track semaphore acquire/release
            with patch.object(asyncio, "get_running_loop", return_value=mock_loop):
                result = await run_blocking("file", lambda: "work")
                assert result == "done"

            shutdown_executors()

    @pytest.mark.asyncio
    async def test_cancelled_error_propagates(self):
        from src.core.utils.executors import run_blocking, init_executors, shutdown_executors

        with patch("src.core.utils.executors.ThreadPoolExecutor"):
            init_executors()
            mock_loop = AsyncMock()
            mock_loop.run_in_executor.side_effect = asyncio.CancelledError()

            with (
                patch.object(asyncio, "get_running_loop", return_value=mock_loop),
                pytest.raises(asyncio.CancelledError),
            ):
                await run_blocking("git", lambda: "fail")

            shutdown_executors()


class TestRunCpuBlocking:
    """run_cpu_blocking — CPU-bound work in default executor."""

    @pytest.mark.asyncio
    async def test_runs_in_default_executor(self):
        from src.core.utils.executors import run_cpu_blocking

        mock_loop = AsyncMock()
        mock_loop.run_in_executor.return_value = 42

        with patch.object(asyncio, "get_running_loop", return_value=mock_loop):
            result = await run_cpu_blocking(lambda: 42)
            assert result == 42
            mock_loop.run_in_executor.assert_called_once_with(None, mock_loop.run_in_executor.call_args[0][1])
# #endregion Test.BlockingExecutors
