import time

import pytest

from soniclue_basekit.exec_time_profiler import ExecTimeProfiler, NopProfiler, nop_profiler


# --- ExecTimeProfiler ---


def test_single_stage_recorded():
    p = ExecTimeProfiler()
    with p.timeit("a"):
        pass
    stages = p.stages
    assert len(stages) == 1
    name, size, total, stdev = stages[0]
    assert name == "a"
    assert size == 1
    assert total >= 0
    assert stdev == 0.0


def test_multiple_samples_accumulate():
    p = ExecTimeProfiler()
    for _ in range(3):
        with p.timeit("a"):
            pass
    name, size, total, stdev = p.stages[0]
    assert size == 3
    assert total >= 0


def test_stdev_nonzero_for_varying_samples():
    p = ExecTimeProfiler()
    with p.timeit("a"):
        pass
    with p.timeit("a"):
        time.sleep(0.02)
    _, _, _, stdev = p.stages[0]
    assert stdev > 0


def test_multiple_distinct_stages_ordered():
    p = ExecTimeProfiler()
    with p.timeit("first"):
        pass
    with p.timeit("second"):
        pass
    names = [s[0] for s in p.stages]
    assert names == ["first", "second"]


def test_get_stage_times_sec():
    p = ExecTimeProfiler()
    with p.timeit("s"):
        time.sleep(0.01)
    times = p.get_stage_times("s")
    assert len(times) == 1
    assert times[0] >= 0.01


def test_get_stage_times_ms():
    p = ExecTimeProfiler()
    with p.timeit("s"):
        time.sleep(0.01)
    times = p.get_stage_times("s", unit="ms")
    assert times[0] >= 10


def test_get_stage_times_missing_stage_returns_empty():
    p = ExecTimeProfiler()
    assert p.get_stage_times("nonexistent") == []


def test_clear_removes_all_stages():
    p = ExecTimeProfiler()
    with p.timeit("a"):
        pass
    p.clear()
    assert p.stages == []
    assert p.get_stage_times("a") == []


def test_to_string_sec_format():
    p = ExecTimeProfiler()
    with p.timeit("step"):
        pass
    s = p.to_string()
    assert s.startswith("[step:")
    assert s.endswith("]")


def test_to_string_ms_format():
    p = ExecTimeProfiler()
    with p.timeit("step"):
        time.sleep(0.01)
    s = p.to_string(unit="ms")
    assert "step:" in s


def test_to_string_invalid_unit_raises():
    p = ExecTimeProfiler()
    with p.timeit("s"):
        pass
    with pytest.raises(AssertionError):
        p.to_string(unit="us")


def test_nested_stages_independent():
    p = ExecTimeProfiler()
    with p.timeit("outer"):
        with p.timeit("inner"):
            pass
    names = [s[0] for s in p.stages]
    assert "outer" in names
    assert "inner" in names


def test_exclude_inner_subtracts_child_time():
    p = ExecTimeProfiler()
    with p.timeit("outer", exclude_inner=True):
        with p.timeit("inner"):
            time.sleep(0.02)
    outer_total = p.get_stage_times("outer")[0]
    inner_total = p.get_stage_times("inner")[0]
    # Outer should be much less than inner since child time is excluded.
    assert outer_total < inner_total


def test_stack_empty_after_timeit():
    p = ExecTimeProfiler()
    with p.timeit("a"):
        pass
    assert p._top is None


def test_exception_inside_timeit_still_records():
    p = ExecTimeProfiler()
    with pytest.raises(ValueError):
        with p.timeit("a"):
            raise ValueError("boom")
    assert len(p.get_stage_times("a")) == 1


# --- NopProfiler ---


def test_nop_profiler_yields_without_recording():
    nop = NopProfiler()
    with nop.timeit("x"):
        pass
    assert nop.stages == []


def test_nop_profiler_singleton_unchanged():
    with nop_profiler.timeit("x"):
        pass
    assert nop_profiler.stages == []
