import threading

import pytest

from soniclue_basekit.exec_time_profiler import ExecTimeProfiler
from soniclue_basekit.lock_protector import LockProtector


# --- Basic lock/unlock ---


def test_lock_yields_object():
    p = LockProtector({"value": 42})
    with p.lock() as obj:
        assert obj["value"] == 42


def test_lock_released_after_context():
    p = LockProtector([])
    with p.lock():
        pass
    assert not p.get_lock().locked()


def test_lock_released_on_exception():
    p = LockProtector([])
    with pytest.raises(RuntimeError):
        with p.lock():
            raise RuntimeError("boom")
    assert not p.get_lock().locked()


def test_get_lock_returns_threading_lock():
    p = LockProtector("obj")
    assert isinstance(p.get_lock(), type(threading.Lock()))


# --- Profiler integration ---


def test_default_profiler_is_nop():
    from soniclue_basekit.exec_time_profiler import NopProfiler

    p = LockProtector("x")
    assert isinstance(p.profiler, NopProfiler)


def test_custom_profiler_records_stage():
    profiler = ExecTimeProfiler()
    p = LockProtector("x", profiler=profiler)
    with p.lock(caller="my_caller"):
        pass
    names = [s[0] for s in profiler.stages]
    assert "my_caller" in names


# --- Inner protector ordering ---


def test_no_inner_protectors_locks_cleanly():
    outer = LockProtector("outer")
    inner = LockProtector("inner")
    outer.add_inner_protector(inner)
    # Acquire inner first then outer — allowed since inner is not locked here.
    with outer.lock():
        pass


def test_inner_locked_by_same_thread_raises():
    outer = LockProtector("outer")
    inner = LockProtector("inner")
    outer.add_inner_protector(inner)
    with inner.lock():
        with pytest.raises(RuntimeError, match="[Ii]nner protector already locked"):
            with outer.lock():
                pass


def test_circular_relationship_raises():
    a = LockProtector("a")
    b = LockProtector("b")
    a.add_inner_protector(b)
    b.add_inner_protector(a)
    with pytest.raises(RuntimeError, match="[Cc]ircular"):
        a._assert_acquisition_order()


def test_deep_chain_inner_locked_raises():
    a = LockProtector("a")
    b = LockProtector("b")
    c = LockProtector("c")
    a.add_inner_protector(b)
    b.add_inner_protector(c)
    # Lock c, then try to acquire a — should raise because c is inner of b which is inner of a.
    with c.lock():
        with pytest.raises(RuntimeError):
            with a.lock():
                pass


def test_inner_locked_by_different_thread_does_not_raise():
    outer = LockProtector("outer")
    inner = LockProtector("inner")
    outer.add_inner_protector(inner)

    barrier = threading.Barrier(2)
    errors = []

    def hold_inner():
        try:
            with inner.lock():
                barrier.wait()  # Signal: inner is now locked.
                barrier.wait()  # Wait: outer check done.
        except Exception as e:
            errors.append(e)

    t = threading.Thread(target=hold_inner)
    t.start()
    barrier.wait()  # Wait for inner to be locked by other thread.
    try:
        # Inner locked by a different thread — should not raise.
        with outer.lock():
            pass
    except RuntimeError as e:
        errors.append(e)
    finally:
        barrier.wait()
    t.join()
    assert errors == []


# --- check_inner_only_in_pytest flag ---


def test_check_inner_only_in_pytest_still_checks_in_pytest():
    # We are in pytest, so the check should still be enforced.
    outer = LockProtector("outer", check_inner_only_in_pytest=True)
    inner = LockProtector("inner")
    outer.add_inner_protector(inner)
    with inner.lock():
        with pytest.raises(RuntimeError):
            with outer.lock():
                pass
