"""Tests for the performance interpolator module.

Covers issues #6-#11:
  #6  load from CSV/JSON
  #7  predict_ttft
  #8  predict_itl
  #9  load throughput data
  #10 predict_throughput
  #11 reverse_ttft

Uses only CPU-only, in-memory data — no hardware required.
"""

from __future__ import annotations

import json
import textwrap
from pathlib import Path

import pytest

from sagellm.profiling import (
    InterpolationMode,
    PerformanceInterpolator,
    ProfileDataPoint,
    ProfileDataset,
)

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

SAMPLE_POINTS: list[tuple[int, float, float, float]] = [
    (64, 0.05, 0.010, 450.0),
    (128, 0.08, 0.011, 430.0),
    (256, 0.14, 0.013, 390.0),
    (512, 0.24, 0.017, 330.0),
    (1024, 0.42, 0.025, 250.0),
]


@pytest.fixture
def sample_interp() -> PerformanceInterpolator:
    return PerformanceInterpolator.from_points(
        [(isl, ttft, itl, tput) for isl, ttft, itl, tput in SAMPLE_POINTS]
    )


@pytest.fixture
def csv_path(tmp_path: Path) -> Path:
    content = textwrap.dedent(
        """\
        isl,ttft,itl,throughput
        64,0.05,0.010,450.0
        128,0.08,0.011,430.0
        256,0.14,0.013,390.0
        512,0.24,0.017,330.0
        1024,0.42,0.025,250.0
    """
    )
    p = tmp_path / "profile.csv"
    p.write_text(content)
    return p


@pytest.fixture
def json_path(tmp_path: Path) -> Path:
    records = [
        {"isl": isl, "ttft": ttft, "itl": itl, "throughput": tput}
        for isl, ttft, itl, tput in SAMPLE_POINTS
    ]
    p = tmp_path / "profile.json"
    p.write_text(json.dumps(records))
    return p


# ---------------------------------------------------------------------------
# Issue #6 — load from CSV / JSON
# ---------------------------------------------------------------------------


class TestLoadFromProfiling:
    """Issue #6 & #9: loading profile data from CSV and JSON."""

    def test_load_csv_returns_correct_point_count(self, csv_path: Path) -> None:
        ds = ProfileDataset.from_csv(csv_path)
        assert len(ds.points) == len(SAMPLE_POINTS)

    def test_load_csv_parses_first_point(self, csv_path: Path) -> None:
        ds = ProfileDataset.from_csv(csv_path)
        pts = ds.sorted_points()
        first = pts[0]
        assert first.isl == 64
        assert first.ttft == pytest.approx(0.05, rel=1e-6)
        assert first.throughput == pytest.approx(450.0, rel=1e-6)

    def test_load_json_returns_correct_point_count(self, json_path: Path) -> None:
        ds = ProfileDataset.from_json(json_path)
        assert len(ds.points) == len(SAMPLE_POINTS)

    def test_load_json_parses_last_point(self, json_path: Path) -> None:
        ds = ProfileDataset.from_json(json_path)
        pts = ds.sorted_points()
        last = pts[-1]
        assert last.isl == 1024
        assert last.ttft == pytest.approx(0.42, rel=1e-6)
        assert last.throughput == pytest.approx(250.0, rel=1e-6)

    def test_missing_csv_raises_file_not_found(self, tmp_path: Path) -> None:
        with pytest.raises(FileNotFoundError):
            ProfileDataset.from_csv(tmp_path / "nonexistent.csv")

    def test_missing_json_raises_file_not_found(self, tmp_path: Path) -> None:
        with pytest.raises(FileNotFoundError):
            ProfileDataset.from_json(tmp_path / "nonexistent.json")

    def test_csv_missing_isl_column_raises_value_error(self, tmp_path: Path) -> None:
        bad_csv = tmp_path / "bad.csv"
        bad_csv.write_text("ttft,itl\n0.05,0.01\n")
        with pytest.raises(ValueError, match="isl"):
            ProfileDataset.from_csv(bad_csv)

    def test_from_csv_interpolator_factory(self, csv_path: Path) -> None:
        interp = PerformanceInterpolator.from_csv(csv_path)
        assert isinstance(interp, PerformanceInterpolator)

    def test_from_json_interpolator_factory(self, json_path: Path) -> None:
        interp = PerformanceInterpolator.from_json(json_path)
        assert isinstance(interp, PerformanceInterpolator)


# ---------------------------------------------------------------------------
# Issue #7 — predict_ttft
# ---------------------------------------------------------------------------


class TestPredictTTFT:
    """Issue #7: TTFT prediction via interpolation."""

    def test_exact_point_returns_correct_ttft(self, sample_interp: PerformanceInterpolator) -> None:
        # 512 is an exact data point
        assert sample_interp.predict_ttft(512) == pytest.approx(0.24, rel=1e-6)

    def test_midpoint_interpolation(self, sample_interp: PerformanceInterpolator) -> None:
        # Midpoint between ISL=128 (ttft=0.08) and ISL=256 (ttft=0.14)
        # Expected: 0.08 + 0.5 * (0.14 - 0.08) = 0.11
        result = sample_interp.predict_ttft(192)
        assert result == pytest.approx(0.11, rel=1e-3)

    def test_below_range_clamps_to_first_point(
        self, sample_interp: PerformanceInterpolator
    ) -> None:
        # ISL below first data point should clamp to first TTFT
        assert sample_interp.predict_ttft(10) == pytest.approx(0.05, rel=1e-6)

    def test_above_range_extrapolates(self, sample_interp: PerformanceInterpolator) -> None:
        # ISL above last data point: should produce a TTFT > 0.42
        result = sample_interp.predict_ttft(2048)
        assert result > 0.42

    def test_negative_isl_raises_value_error(self, sample_interp: PerformanceInterpolator) -> None:
        with pytest.raises(ValueError):
            sample_interp.predict_ttft(-1)

    def test_nearest_mode(self, sample_interp: PerformanceInterpolator) -> None:
        interp = PerformanceInterpolator(sample_interp._dataset, mode=InterpolationMode.NEAREST)
        # ISL=100 → nearest is 128 with ttft=0.08
        assert interp.predict_ttft(100) == pytest.approx(0.08, rel=1e-6)


# ---------------------------------------------------------------------------
# Issue #8 — predict_itl
# ---------------------------------------------------------------------------


class TestPredictITL:
    """Issue #8: ITL prediction via interpolation."""

    def test_exact_point(self, sample_interp: PerformanceInterpolator) -> None:
        assert sample_interp.predict_itl(256) == pytest.approx(0.013, rel=1e-6)

    def test_midpoint_interpolation(self, sample_interp: PerformanceInterpolator) -> None:
        # Between ISL=512 (itl=0.017) and ISL=1024 (itl=0.025)
        # At ISL=768: 0.017 + 0.5 * (0.025 - 0.017) = 0.021
        result = sample_interp.predict_itl(768)
        assert result == pytest.approx(0.021, rel=1e-3)

    def test_negative_isl_raises_value_error(self, sample_interp: PerformanceInterpolator) -> None:
        with pytest.raises(ValueError):
            sample_interp.predict_itl(-1)

    def test_no_itl_data_raises_value_error(self) -> None:
        pts = [ProfileDataPoint(isl=64, ttft=0.05, itl=None, throughput=None)]
        ds = ProfileDataset(points=pts)
        interp = PerformanceInterpolator(ds)
        with pytest.raises(ValueError, match="itl"):
            interp.predict_itl(64)


# ---------------------------------------------------------------------------
# Issue #9/#10 — throughput
# ---------------------------------------------------------------------------


class TestPredictThroughput:
    """Issues #9 & #10: loading throughput data and predicting."""

    def test_exact_point(self, sample_interp: PerformanceInterpolator) -> None:
        assert sample_interp.predict_throughput(64) == pytest.approx(450.0, rel=1e-6)

    def test_midpoint_interpolation(self, sample_interp: PerformanceInterpolator) -> None:
        # Between ISL=256 (tput=390) and ISL=512 (tput=330)
        # At ISL=384: 390 + 0.5 * (330 - 390) = 360
        result = sample_interp.predict_throughput(384)
        assert result == pytest.approx(360.0, rel=1e-3)

    def test_throughput_decreases_with_isl(self, sample_interp: PerformanceInterpolator) -> None:
        t1 = sample_interp.predict_throughput(64)
        t2 = sample_interp.predict_throughput(1024)
        assert t1 > t2

    def test_no_throughput_data_raises_value_error(self) -> None:
        pts = [ProfileDataPoint(isl=64, ttft=0.05, itl=0.01, throughput=None)]
        ds = ProfileDataset(points=pts)
        interp = PerformanceInterpolator(ds)
        with pytest.raises(ValueError, match="throughput"):
            interp.predict_throughput(64)

    def test_load_from_csv_and_predict(self, csv_path: Path) -> None:
        interp = PerformanceInterpolator.from_csv(csv_path)
        assert interp.predict_throughput(128) == pytest.approx(430.0, rel=1e-6)


# ---------------------------------------------------------------------------
# Issue #11 — reverse_ttft
# ---------------------------------------------------------------------------


class TestReverseTTFT:
    """Issue #11: reverse interpolation to find max ISL for target TTFT."""

    def test_exact_boundary(self, sample_interp: PerformanceInterpolator) -> None:
        # At TTFT budget = 0.24 we should get ISL = 512
        assert sample_interp.reverse_ttft(0.24) == 512

    def test_midbudget(self, sample_interp: PerformanceInterpolator) -> None:
        # Budget = 0.11 → midpoint between ISL=128 and ISL=256
        max_isl = sample_interp.reverse_ttft(0.11)
        assert 128 <= max_isl <= 256

    def test_budget_below_minimum_returns_zero(
        self, sample_interp: PerformanceInterpolator
    ) -> None:
        # Budget smaller than our smallest measured TTFT
        result = sample_interp.reverse_ttft(0.01)
        assert result == 0

    def test_budget_above_maximum_returns_max_isl(
        self, sample_interp: PerformanceInterpolator
    ) -> None:
        # Budget larger than our largest measured TTFT
        result = sample_interp.reverse_ttft(1.0)
        assert result == 1024

    def test_negative_budget_raises_value_error(
        self, sample_interp: PerformanceInterpolator
    ) -> None:
        with pytest.raises(ValueError):
            sample_interp.reverse_ttft(-0.1)

    def test_no_ttft_data_raises_value_error(self) -> None:
        pts = [ProfileDataPoint(isl=64, ttft=None, itl=0.01, throughput=450.0)]
        ds = ProfileDataset(points=pts)
        interp = PerformanceInterpolator(ds)
        with pytest.raises(ValueError, match="ttft"):
            interp.reverse_ttft(0.1)


# ---------------------------------------------------------------------------
# ProfileDataPoint validation
# ---------------------------------------------------------------------------


class TestProfileDataPointValidation:
    def test_negative_isl_raises(self) -> None:
        with pytest.raises(ValueError):
            ProfileDataPoint(isl=-1, ttft=0.05)

    def test_negative_ttft_raises(self) -> None:
        with pytest.raises(ValueError):
            ProfileDataPoint(isl=64, ttft=-0.01)

    def test_negative_throughput_raises(self) -> None:
        with pytest.raises(ValueError):
            ProfileDataPoint(isl=64, ttft=0.05, throughput=-1.0)

    def test_valid_point(self) -> None:
        pt = ProfileDataPoint(isl=128, ttft=0.08, itl=0.011, throughput=430.0)
        assert pt.isl == 128


# ---------------------------------------------------------------------------
# Repr
# ---------------------------------------------------------------------------


def test_repr(sample_interp: PerformanceInterpolator) -> None:
    r = repr(sample_interp)
    assert "PerformanceInterpolator" in r
    assert "n=5" in r
