"""Step definitions for large-project scaling performance validation."""

from __future__ import annotations

import json
import math
from pathlib import Path
from typing import Any

from behave import given, then, when
from behave.runner import Context

_FIXTURES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "scale"


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------


@given("a scaling performance test environment is ready")
def step_scaling_perf_env_ready(context: Context) -> None:
    """Verify scale test fixtures directory exists."""
    assert _FIXTURES_DIR.is_dir(), f"Fixtures dir missing: {_FIXTURES_DIR}"
    context.sp_metadata = None
    context.sp_thresholds = None


# ---------------------------------------------------------------------------
# Fixture loading
# ---------------------------------------------------------------------------


@when("I scaling perf load the scale metadata fixture")
def step_scaling_perf_load_metadata(context: Context) -> None:
    path = _FIXTURES_DIR / "scale_metadata.json"
    assert path.exists(), f"Missing fixture: {path}"
    with open(path) as fh:
        context.sp_metadata = json.load(fh)


@when("I scaling perf load the baseline thresholds fixture")
def step_scaling_perf_load_thresholds(context: Context) -> None:
    path = _FIXTURES_DIR / "baseline_thresholds.json"
    assert path.exists(), f"Missing fixture: {path}"
    with open(path) as fh:
        context.sp_thresholds = json.load(fh)


# ---------------------------------------------------------------------------
# Profile validation
# ---------------------------------------------------------------------------


def _get_profile(context: Context, name: str) -> dict[str, Any]:
    """Return the profile dict with the given name, or fail."""
    assert context.sp_metadata is not None, "Metadata not loaded"
    for p in context.sp_metadata["scale_profiles"]:
        if p["name"] == name:
            return p
    raise AssertionError(f"Profile {name!r} not found in metadata")


@then('the scaling perf metadata should contain profile "{name}"')
def step_scaling_perf_has_profile(context: Context, name: str) -> None:
    _get_profile(context, name)


@then('the scaling perf profile "{name}" should have file count {count:d}')
def step_scaling_perf_profile_file_count(
    context: Context, name: str, count: int
) -> None:
    profile = _get_profile(context, name)
    actual = profile["file_count"]
    assert actual == count, f"Profile {name}: expected {count}, got {actual}"


@then('the scaling perf language mix should sum to 1.0 for profile "{name}"')
def step_scaling_perf_lang_mix_sums(context: Context, name: str) -> None:
    profile = _get_profile(context, name)
    total = sum(profile["language_mix"].values())
    assert math.isclose(total, 1.0, rel_tol=1e-6), (
        f"Language mix for {name} sums to {total}, expected 1.0"
    )


# ---------------------------------------------------------------------------
# Threshold coverage
# ---------------------------------------------------------------------------


@then('the scaling perf indexing thresholds should cover "{file_count}"')
def step_scaling_perf_indexing_covers(context: Context, file_count: str) -> None:
    assert context.sp_thresholds is not None
    assert file_count in context.sp_thresholds["indexing"], (
        f"Missing indexing threshold for {file_count}"
    )


@then('the scaling perf decomposition thresholds should cover "{file_count}"')
def step_scaling_perf_decomp_covers(context: Context, file_count: str) -> None:
    assert context.sp_thresholds is not None
    assert file_count in context.sp_thresholds["decomposition"], (
        f"Missing decomposition threshold for {file_count}"
    )


# ---------------------------------------------------------------------------
# Monotonicity
# ---------------------------------------------------------------------------


@then('scaling perf p50 should be less than p95 for "{key}" indexing')
def step_scaling_perf_p50_lt_p95(context: Context, key: str) -> None:
    assert context.sp_thresholds is not None
    t = context.sp_thresholds["indexing"][key]
    assert t["p50_ms"] < t["p95_ms"], (
        f"p50 ({t['p50_ms']}) >= p95 ({t['p95_ms']}) for {key}"
    )


@then('scaling perf p95 should be less than p99 for "{key}" indexing')
def step_scaling_perf_p95_lt_p99(context: Context, key: str) -> None:
    assert context.sp_thresholds is not None
    t = context.sp_thresholds["indexing"][key]
    assert t["p95_ms"] < t["p99_ms"], (
        f"p95 ({t['p95_ms']}) >= p99 ({t['p99_ms']}) for {key}"
    )


# ---------------------------------------------------------------------------
# Sub-linear scaling checks
# ---------------------------------------------------------------------------


@then("scaling perf 50K indexing p50 should be less than 5x the 10K p50")
def step_scaling_perf_50k_sublinear(context: Context) -> None:
    assert context.sp_thresholds is not None
    idx = context.sp_thresholds["indexing"]
    p50_10k = idx["10000_files"]["p50_ms"]
    p50_50k = idx["50000_files"]["p50_ms"]
    assert p50_50k < 5 * p50_10k, (
        f"50K p50 ({p50_50k}) should be < 5x 10K p50 ({5 * p50_10k})"
    )


@then("scaling perf 100K indexing p50 should be less than 2.5x the 50K p50")
def step_scaling_perf_100k_sublinear(context: Context) -> None:
    assert context.sp_thresholds is not None
    idx = context.sp_thresholds["indexing"]
    p50_50k = idx["50000_files"]["p50_ms"]
    p50_100k = idx["100000_files"]["p50_ms"]
    limit = int(2.5 * p50_50k)
    assert p50_100k < limit, f"100K p50 ({p50_100k}) should be < 2.5x 50K p50 ({limit})"


# ---------------------------------------------------------------------------
# Context assembly
# ---------------------------------------------------------------------------


@then("the scaling perf thresholds should have context assembly section")
def step_scaling_perf_has_ctx_assembly(context: Context) -> None:
    assert context.sp_thresholds is not None
    assert "context_assembly" in context.sp_thresholds


@then('the scaling perf context assembly thresholds should cover "{fragment_count}"')
def step_scaling_perf_ctx_assembly_covers(
    context: Context, fragment_count: str
) -> None:
    assert context.sp_thresholds is not None
    assert fragment_count in context.sp_thresholds["context_assembly"], (
        f"Missing context_assembly threshold for {fragment_count}"
    )


# ---------------------------------------------------------------------------
# Execution throughput
# ---------------------------------------------------------------------------


@then("the scaling perf thresholds should have execution throughput section")
def step_scaling_perf_has_exec_throughput(context: Context) -> None:
    assert context.sp_thresholds is not None
    assert "execution_throughput" in context.sp_thresholds


@then('the scaling perf execution thresholds should cover "{plan_count}"')
def step_scaling_perf_exec_covers(context: Context, plan_count: str) -> None:
    assert context.sp_thresholds is not None
    assert plan_count in context.sp_thresholds["execution_throughput"], (
        f"Missing execution_throughput threshold for {plan_count}"
    )


# ---------------------------------------------------------------------------
# Memory thresholds
# ---------------------------------------------------------------------------


@then('the scaling perf memory thresholds should cover "{file_count}"')
def step_scaling_perf_memory_covers(context: Context, file_count: str) -> None:
    assert context.sp_thresholds is not None
    assert file_count in context.sp_thresholds["memory_usage_mb"], (
        f"Missing memory_usage_mb threshold for {file_count}"
    )


@then('scaling perf memory peak should exceed steady state for "{file_count}"')
def step_scaling_perf_memory_peak_gt_steady(context: Context, file_count: str) -> None:
    assert context.sp_thresholds is not None
    mem = context.sp_thresholds["memory_usage_mb"][file_count]
    assert mem["peak_mb"] > mem["steady_state_mb"], (
        f"Peak ({mem['peak_mb']}) <= steady ({mem['steady_state_mb']}) for {file_count}"
    )
