"""Tests for the stress harness helpers: LatencyReservoir and summarize(request_count).

These tests validate the bounded latency sampling introduced in the
m-performance-hotspot-optimization / python-sustained-memory-metric plan.
"""

import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

import bench.stress as stress_module
from bench.stress import LatencyReservoir, SUSTAINED_MAX_LATENCY_SAMPLES, Stability


class TestLatencyReservoir:
    """Verify bounded sample count and unbounded request count tracking."""

    def test_reservoir_tracks_all_count(self) -> None:
        """count must reflect the total number of add() calls, not just stored samples."""
        sampler = LatencyReservoir(max_samples=10)
        for i in range(500):
            sampler.add(float(i))
        assert sampler.count == 500

    def test_reservoir_bounds_samples_to_max(self) -> None:
        """samples list must never exceed max_samples length."""
        sampler = LatencyReservoir(max_samples=SUSTAINED_MAX_LATENCY_SAMPLES)
        for i in range(200_000):
            sampler.add(float(i))
        assert len(sampler.samples) == SUSTAINED_MAX_LATENCY_SAMPLES

    def test_reservoir_initial_state(self) -> None:
        assert LatencyReservoir(100).count == 0
        assert LatencyReservoir(100).samples == []

    def test_reservoir_small_values_fully_preserved(self) -> None:
        """When inputs are within the max, all values are stored."""
        sampler = LatencyReservoir(max_samples=5)
        for i in range(5):
            sampler.add(float(i * 10))
        assert len(sampler.samples) == 5
        assert sampler.count == 5

    def test_reservoir_deterministic_slot_mapping(self) -> None:
        """The slot formula must be deterministic for the same count."""
        sampler = LatencyReservoir(max_samples=3)
        sampler.add(1.0)
        sampler.add(2.0)
        sampler.add(3.0)
        sampler.add(4.0)
        sampler2 = LatencyReservoir(max_samples=3)
        for v in (1.0, 2.0, 3.0, 4.0):
            sampler2.add(v)
        assert sampler.samples == sampler2.samples


class TestSummarizeRequestCount:
    """Verify that summarize() actually passes request_count to emit_row().

    These tests use monkeypatch to intercept emit_row() kwargs so that the
    real summarize() function is exercised. A mock that reimplements the same
    logic would pass even if summarize() had a bug — this would not.
    """

    def test_summarize_uses_request_count_for_throughput(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """When request_count=200, emit_row must receive requests=200 and throughput=200.0."""
        captured: dict = {}

        def fake_emit_row(**kwargs: object) -> None:
            captured.update(kwargs)

        monkeypatch.setattr(stress_module, "emit_row", fake_emit_row)

        stability = Stability()
        stress_module.summarize(
            profile="sustained",
            axis="duration=1000ms",
            transport="tcp",
            latencies=[1.0, 2.0, 3.0],
            elapsed_ms=1000.0,
            client_count=1,
            stability=stability,
            mem=12.3,
            request_count=200,
        )

        assert captured.get("requests") == 200
        assert pytest.approx(float(captured["throughput"])) == 200.0
        # p50/p95/p99 are computed from the bounded sample [1.0, 2.0, 3.0], NOT from request_count
        assert pytest.approx(float(captured["p50"])) == 2.0
        assert pytest.approx(float(captured["p95"])) == 3.0
        assert pytest.approx(float(captured["p99"])) == 3.0

    def test_summarize_defaults_to_len_latencies(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """Without request_count, emit_row must receive requests=len(latencies)."""
        captured: dict = {}

        def fake_emit_row(**kwargs: object) -> None:
            captured.update(kwargs)

        original_emit = stress_module.emit_row
        monkeypatch.setattr(stress_module, "emit_row", fake_emit_row)

        stability = Stability()
        stress_module.summarize(
            profile="roundtrip",
            axis="concurrency=1",
            transport="tcp",
            latencies=[1.5, 2.5, 3.5],
            elapsed_ms=1000.0,
            client_count=1,
            stability=stability,
            mem=10.0,
        )

        assert captured.get("requests") == 3
        assert pytest.approx(float(captured["throughput"])) == 3.0

    def test_summarize_emits_correct_payload_and_status(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """Verify that non-latency fields (payload_bytes, status, mem) are also correct."""
        captured: dict = {}

        def fake_emit_row(**kwargs: object) -> None:
            captured.update(kwargs)

        monkeypatch.setattr(stress_module, "emit_row", fake_emit_row)

        stability = Stability()
        stress_module.summarize(
            profile="sustained",
            axis="duration=2000ms",
            transport="tcp",
            latencies=[5.0],
            elapsed_ms=2000.0,
            client_count=1,
            stability=stability,
            mem=45.6,
            request_count=1000,
        )

        assert captured.get("profile") == "sustained"
        assert captured.get("transport") == "tcp"
        assert captured.get("client_count") == 1
        assert captured.get("requests") == 1000
        assert pytest.approx(float(captured["throughput"])) == 1000.0 / 2000.0 * 1000.0
        assert captured.get("mem") == 45.6
        assert captured.get("payload_bytes") == stress_module.test_data_payload_bytes("req-0")
        assert captured.get("p50") == 5.0  # single sample => all percentiles = 5.0