"""Tests for test_runner.parallel.executor (ParallelExecutor)."""

from __future__ import annotations

import threading

import pytest

from test_runner.common.models import TestFile
from test_runner.parallel.executor import ParallelExecutor

from .conftest import make_case_item as _case, make_test_file as _make_tf


# ---------------------------------------------------------------------------
# execute_read_only
# ---------------------------------------------------------------------------

class TestExecuteReadOnly:
    def test_returns_results_in_original_order(self):
        """Results must be returned in submission order, not completion order."""
        pe = ParallelExecutor(max_workers=4)
        tf = _make_tf("sp_a", cases=5)
        cases = [_case(tf, i) for i in range(5)]

        def fn(test_file: TestFile, case_idx: int, test_case: list) -> str:
            return f"{test_file.code_unit_name}:{case_idx}"

        results = pe.execute_read_only(cases, fn)
        assert results == [f"sp_a:{i}" for i in range(5)]

    def test_empty_cases(self):
        pe = ParallelExecutor(max_workers=2)
        results = pe.execute_read_only([], lambda *_: None)
        assert results == []

    def test_each_thread_gets_its_own_thread(self):
        """Verify that work is distributed across threads."""
        import time

        pe = ParallelExecutor(max_workers=4)
        tf = _make_tf("sp_a", cases=8)
        cases = [_case(tf, i) for i in range(8)]

        thread_ids: list[int] = []
        lock = threading.Lock()

        def fn(test_file: TestFile, case_idx: int, test_case: list) -> int:
            tid = threading.current_thread().ident
            with lock:
                thread_ids.append(tid)
            # Sleep briefly so threads overlap rather than running serially.
            time.sleep(0.01)
            return tid

        pe.execute_read_only(cases, fn)
        unique_threads = set(thread_ids)
        # With 8 cases, 4 workers, and 10ms sleep, we should see > 1 thread.
        assert len(unique_threads) > 1

    def test_exception_propagates(self):
        pe = ParallelExecutor(max_workers=2)
        tf = _make_tf("sp_a", cases=2)
        cases = [_case(tf, i) for i in range(2)]

        def fn(test_file: TestFile, case_idx: int, test_case: list):
            if case_idx == 1:
                raise ValueError("boom")
            return "ok"

        with pytest.raises(ValueError, match="boom"):
            pe.execute_read_only(cases, fn)


# ---------------------------------------------------------------------------
# Constructor validation
# ---------------------------------------------------------------------------

class TestConstructor:
    def test_max_workers_must_be_positive(self):
        with pytest.raises(ValueError, match="max_workers must be >= 1"):
            ParallelExecutor(max_workers=0)
