import contextlib
import time

from promrs import CollectorRegistry, Counter, Gauge, Histogram, generate_latest


def test_histogram_time_context_manager() -> None:
    registry = CollectorRegistry()
    h = Histogram("cm_hist", "test", registry=registry, buckets=[0.01, 0.1, 1.0])
    with h.time():
        time.sleep(0.02)
    output = generate_latest(registry).decode()
    assert "cm_hist_count{} 1" in output
    assert 'cm_hist_bucket{le="0.01"} 0' in output
    assert 'cm_hist_bucket{le="0.1"} 1' in output


def test_histogram_time_decorator() -> None:
    registry = CollectorRegistry()
    h = Histogram("dec_hist", "test", registry=registry, buckets=[0.01, 0.1, 1.0])

    @h.time()
    def do_work() -> str:
        time.sleep(0.02)
        return "done"

    result = do_work()
    assert result == "done"
    output = generate_latest(registry).decode()
    assert "dec_hist_count{} 1" in output


def test_histogram_child_time() -> None:
    registry = CollectorRegistry()
    h = Histogram("child_cm_hist", "test", ["op"], registry=registry, buckets=[0.01, 0.1, 1.0])
    with h.labels(op="read").time():
        time.sleep(0.02)
    output = generate_latest(registry).decode()
    assert 'child_cm_hist_count{op="read"} 1' in output


def test_gauge_time_context_manager() -> None:
    registry = CollectorRegistry()
    g = Gauge("cm_gauge", "test", registry=registry)
    with g.time():
        time.sleep(0.02)
    output = generate_latest(registry).decode()
    for line in output.split("\n"):
        if line.startswith("cm_gauge{"):
            value = float(line.split("} ")[1])
            assert value >= 0.02
            break
    else:
        raise AssertionError("cm_gauge not found in output")


def test_gauge_time_decorator() -> None:
    registry = CollectorRegistry()
    g = Gauge("dec_gauge", "test", registry=registry)

    @g.time()
    def do_work() -> str:
        time.sleep(0.02)
        return "done"

    result = do_work()
    assert result == "done"
    output = generate_latest(registry).decode()
    for line in output.split("\n"):
        if line.startswith("dec_gauge{"):
            value = float(line.split("} ")[1])
            assert value >= 0.02
            break
    else:
        raise AssertionError("dec_gauge not found in output")


def test_gauge_child_time() -> None:
    registry = CollectorRegistry()
    g = Gauge("child_cm_gauge", "test", ["k"], registry=registry)
    with g.labels(k="v").time():
        time.sleep(0.02)
    output = generate_latest(registry).decode()
    for line in output.split("\n"):
        if 'child_cm_gauge{k="v"}' in line:
            value = float(line.split("} ")[1])
            assert value >= 0.02
            break
    else:
        raise AssertionError("child_cm_gauge not found in output")


def test_gauge_track_inprogress_context_manager() -> None:
    registry = CollectorRegistry()
    g = Gauge("inprog_gauge", "test", registry=registry)
    # Before: metric not yet emitted (no value set)
    with g.track_inprogress():
        output_during = generate_latest(registry).decode()
        assert "inprog_gauge{} 1.0" in output_during
    output_after = generate_latest(registry).decode()
    assert "inprog_gauge{} 0.0" in output_after


def test_gauge_track_inprogress_decorator() -> None:
    registry = CollectorRegistry()
    g = Gauge("dec_inprog_gauge", "test", registry=registry)

    @g.track_inprogress()
    def do_work() -> str:
        output_during = generate_latest(registry).decode()
        assert "dec_inprog_gauge{} 1.0" in output_during
        return "done"

    result = do_work()
    assert result == "done"
    output_after = generate_latest(registry).decode()
    assert "dec_inprog_gauge{} 0.0" in output_after


def test_gauge_child_track_inprogress() -> None:
    registry = CollectorRegistry()
    g = Gauge("child_inprog_gauge", "test", ["k"], registry=registry)
    with g.labels(k="v").track_inprogress():
        output = generate_latest(registry).decode()
        assert 'child_inprog_gauge{k="v"} 1.0' in output
    output = generate_latest(registry).decode()
    assert 'child_inprog_gauge{k="v"} 0.0' in output


def test_counter_count_exceptions_context_manager() -> None:
    registry = CollectorRegistry()
    c = Counter("exc_counter", "test", registry=registry)
    try:
        with c.count_exceptions():
            raise ValueError("test error")
    except ValueError:
        pass
    output = generate_latest(registry).decode()
    assert "exc_counter_total{} 1.0" in output


def test_counter_count_exceptions_no_exception() -> None:
    registry = CollectorRegistry()
    c = Counter("no_exc_counter", "test", registry=registry)
    with c.count_exceptions():
        pass  # no exception
    # Counter was never incremented, so it shouldn't appear or should be 0
    output = generate_latest(registry).decode()
    assert "no_exc_counter_total{} 1.0" not in output


def test_counter_count_exceptions_filtered() -> None:
    registry = CollectorRegistry()
    c = Counter("filtered_exc_counter", "test", registry=registry)
    # ValueError is a subclass of Exception, so should be counted
    try:
        with c.count_exceptions(ValueError):
            raise ValueError("caught")
    except ValueError:
        pass
    # TypeError should NOT be counted when filtering for ValueError
    try:
        with c.count_exceptions(ValueError):
            raise TypeError("not caught")
    except TypeError:
        pass
    output = generate_latest(registry).decode()
    assert "filtered_exc_counter_total{} 1.0" in output


def test_counter_count_exceptions_decorator() -> None:
    registry = CollectorRegistry()
    c = Counter("dec_exc_counter", "test", registry=registry)

    @c.count_exceptions()
    def failing_func() -> None:
        raise RuntimeError("fail")

    with contextlib.suppress(RuntimeError):
        failing_func()
    output = generate_latest(registry).decode()
    assert "dec_exc_counter_total{} 1.0" in output


def test_counter_child_count_exceptions() -> None:
    registry = CollectorRegistry()
    c = Counter("child_exc_counter", "test", ["op"], registry=registry)
    try:
        with c.labels(op="write").count_exceptions():
            raise ValueError("test")
    except ValueError:
        pass
    output = generate_latest(registry).decode()
    assert 'child_exc_counter_total{op="write"} 1.0' in output
