"""Mutation-friendly contracts for model, architecture, and device decisions."""

from __future__ import annotations

import pytest
from hypothesis import given, strategies as st

from obliteratus.runtime_contracts import (
    ModelLoadPolicy,
    attention_projection_names,
    classify_weight_storage,
    classify_architecture_size,
    effective_model_memory_gb,
    is_quantized_parameter,
    norm_restoration_ratio,
    quantized_model_fits_gpu,
    resolve_model_load_policy,
    select_single_cuda_device,
    should_snapshot_model,
    supports_bfloat16_target,
    validate_model_load_request,
)


@pytest.mark.parametrize(
    ("original", "current", "expected"),
    [
        (10.0, 10.0, None),
        (0.0, 4.0, None),
        (10.0, 0.0, None),
        (float("nan"), 4.0, None),
        (10.0, float("inf"), None),
        (1.0, 0.5, 1.1),
        (8.0, 10.0, 0.8),
        (12.0, 10.0, 1.1),
    ],
)
def test_norm_restoration_ratio_is_bounded_and_fail_closed(original, current, expected):
    assert norm_restoration_ratio(original, current, max_ratio=1.1) == expected


def test_norm_restoration_ratio_rejects_invalid_policy_bounds():
    with pytest.raises(ValueError) as error:
        norm_restoration_ratio(2.0, 1.0, max_ratio=0.0)
    assert str(error.value) == "max norm ratio must be a positive finite number"


def test_norm_restoration_ratio_allows_subunit_policy_cap():
    assert norm_restoration_ratio(2.0, 1.0, max_ratio=0.5) == 0.5


def test_norm_restoration_ratio_tolerance_boundary_is_a_noop():
    assert norm_restoration_ratio(
        1.0,
        1.25,
        max_ratio=1.1,
        tolerance=0.25,
    ) is None


@pytest.mark.parametrize("model_name", [None, 7, "", " \t"])
def test_model_name_must_be_a_nonempty_string(model_name):
    with pytest.raises(ValueError) as error:
        validate_model_load_request(model_name, "causal_lm", None, "float32")
    assert str(error.value) == "model_name must be a non-empty HuggingFace identifier or local path"


@pytest.mark.parametrize(
    ("field", "value", "message"),
    [
        ("task", "embedding", "Unknown task 'embedding'. Choose from ['causal_lm', 'classification']"),
        ("quantization", "3bit", "Unknown quantization '3bit'. Choose None, '4bit', or '8bit'"),
        ("dtype", "float128", "Unknown dtype 'float128'. Choose from ['float32', 'float16', 'bfloat16']"),
    ],
)
def test_invalid_loader_enums_are_rejected(field, value, message):
    request = {
        "model_name": "local/model",
        "task": "causal_lm",
        "quantization": None,
        "dtype": "float32",
    }
    request[field] = value
    with pytest.raises(ValueError) as error:
        validate_model_load_request(**request)
    assert str(error.value) == message


@pytest.mark.parametrize("task", ["causal_lm", "classification"])
@pytest.mark.parametrize("quantization", [None, "4bit", "8bit"])
@pytest.mark.parametrize("dtype", ["float32", "float16", "bfloat16"])
def test_every_documented_loader_enum_combination_is_valid(task, quantization, dtype):
    validate_model_load_request("local/model", task, quantization, dtype)


def test_loader_task_validation_can_follow_the_provider_registry():
    validate_model_load_request(
        "local/model",
        "custom_task",
        None,
        "float32",
        valid_tasks={"custom_task": object()},
    )


@pytest.mark.parametrize(
    ("overrides", "expected"),
    [
        (
            {},
            ModelLoadPolicy("none", True, False, True),
        ),
        (
            {"device": "auto", "device_map_auto_supported": True},
            ModelLoadPolicy("none", True, True, False),
        ),
        (
            {"device": "auto", "resolved_device": "mps"},
            ModelLoadPolicy("none", True, False, True),
        ),
        (
            {"quantization": "4bit", "bitsandbytes_supported": True},
            ModelLoadPolicy("bitsandbytes", True, True, False),
        ),
        (
            {"quantization": "8bit", "bitsandbytes_supported": True},
            ModelLoadPolicy("bitsandbytes", True, True, False),
        ),
        (
            {"quantization": "4bit", "has_native_quantization": True},
            ModelLoadPolicy("native", False, True, False),
        ),
    ],
)
def test_model_load_policy_precedence_is_explicit(overrides, expected):
    inputs = {
        "device": "cpu",
        "resolved_device": "cpu",
        "dtype": "float16",
        "quantization": None,
        "has_native_quantization": False,
        "device_map_auto_supported": False,
        "bitsandbytes_supported": False,
    }
    inputs.update(overrides)
    assert resolve_model_load_policy(**inputs) == expected


def test_model_load_policy_rejects_unsupported_quantization_before_branching():
    with pytest.raises(ValueError) as error:
        resolve_model_load_policy(
            device="auto",
            resolved_device="cuda",
            dtype="float16",
            quantization="3bit",
            has_native_quantization=False,
            device_map_auto_supported=True,
            bitsandbytes_supported=True,
        )
    assert str(error.value) == "Unknown quantization '3bit'. Choose None, '4bit', or '8bit'"


@pytest.mark.parametrize("quantization", ["4bit", "8bit"])
def test_requested_quantization_requires_a_compatible_resolved_device(quantization):
    with pytest.raises(RuntimeError) as error:
        resolve_model_load_policy(
            device="auto",
            resolved_device="mps",
            dtype="bfloat16",
            quantization=quantization,
            has_native_quantization=False,
            device_map_auto_supported=False,
            bitsandbytes_supported=False,
        )
    assert str(error.value) == (
        f"Quantization '{quantization}' requires an available NVIDIA CUDA device; "
        "resolved device was 'mps'. Remove --quantization to load in bfloat16."
    )


_OUTPUT_NAMES = ("o_proj",)
_INPUT_NAMES = ("q_proj", "k_proj", "v_proj", "k_norm")


@pytest.mark.parametrize("layer_index", range(6))
def test_nonshared_attention_projects_every_configured_weight(layer_index):
    assert attention_projection_names(
        projection_target="all",
        layer_index=layer_index,
        num_layers=6,
        num_kv_shared_layers=0,
        output_names=_OUTPUT_NAMES,
        input_names=_INPUT_NAMES,
    ) == _OUTPUT_NAMES + _INPUT_NAMES


def test_output_only_projection_is_independent_of_shared_kv_layout():
    assert attention_projection_names(
        projection_target="output",
        layer_index=5,
        num_layers=6,
        num_kv_shared_layers=3,
        output_names=_OUTPUT_NAMES,
        input_names=_INPUT_NAMES,
    ) == _OUTPUT_NAMES


def test_nonshared_projection_does_not_require_shared_storage_ownership_metadata():
    assert attention_projection_names(
        projection_target="all",
        layer_index=-1,
        num_layers=0,
        num_kv_shared_layers=0,
        output_names=_OUTPUT_NAMES,
        input_names=_INPUT_NAMES,
    ) == _OUTPUT_NAMES + _INPUT_NAMES


@pytest.mark.parametrize(
    ("layer_index", "expected"),
    [
        (2, _OUTPUT_NAMES + _INPUT_NAMES),
        (3, _OUTPUT_NAMES + _INPUT_NAMES),
        (4, ("o_proj", "q_proj")),
        (5, ("o_proj", "q_proj")),
    ],
)
def test_shared_kv_owner_projects_storage_once_and_borrowers_skip_it(layer_index, expected):
    assert attention_projection_names(
        projection_target="all",
        layer_index=layer_index,
        num_layers=6,
        num_kv_shared_layers=3,
        output_names=_OUTPUT_NAMES,
        input_names=_INPUT_NAMES,
    ) == expected


def test_single_layer_can_own_kv_storage_shared_by_the_whole_model():
    assert attention_projection_names(
        projection_target="all",
        layer_index=0,
        num_layers=1,
        num_kv_shared_layers=1,
        output_names=_OUTPUT_NAMES,
        input_names=_INPUT_NAMES,
    ) == _OUTPUT_NAMES + _INPUT_NAMES


@pytest.mark.parametrize(
    ("layer_index", "num_layers", "shared_layers", "message"),
    [
        (-1, 6, 3, "valid layer index"),
        (6, 6, 3, "valid layer index"),
        (0, 0, 1, "valid layer index"),
        (0, 6, 7, "cannot exceed"),
    ],
)
def test_shared_kv_layout_rejects_impossible_ownership(
    layer_index,
    num_layers,
    shared_layers,
    message,
):
    with pytest.raises(ValueError) as error:
        attention_projection_names(
            projection_target="all",
            layer_index=layer_index,
            num_layers=num_layers,
            num_kv_shared_layers=shared_layers,
            output_names=_OUTPUT_NAMES,
            input_names=_INPUT_NAMES,
        )
    expected = (
        "num_kv_shared_layers cannot exceed the model layer count"
        if message == "cannot exceed"
        else "shared-KV projection requires a valid layer index and layer count"
    )
    assert str(error.value) == expected


@pytest.mark.parametrize(
    ("class_name", "has_quant_state", "expected"),
    [
        ("Parameter", False, False),
        ("Parameter", True, True),
        ("Params4bit", False, True),
        ("Int8Params", False, True),
        ("QuantLinear", False, True),
        ("WQLinear_GEMM", False, True),
    ],
)
def test_quantized_parameter_markers_are_closed_and_explicit(
    class_name,
    has_quant_state,
    expected,
):
    assert is_quantized_parameter(
        class_name=class_name,
        has_quant_state=has_quant_state,
    ) is expected


@pytest.mark.parametrize(
    ("overrides", "expected"),
    [
        ({}, "float"),
        ({"data_is_floating_point": False}, "integer"),
        ({"has_quant_state": True}, "quantized_parameter"),
        ({"parameter_class_name": "Int8Params"}, "quantized_parameter"),
        ({"module_class_name": "QuantLinear"}, "packed_module"),
        (
            {
                "module_class_name": "WQLinear_GEMV",
                "has_quant_state": True,
                "data_is_floating_point": False,
            },
            "packed_module",
        ),
    ],
)
def test_weight_storage_classification_uses_safe_precedence(overrides, expected):
    inputs = {
        "module_class_name": "Linear",
        "parameter_class_name": "Parameter",
        "has_quant_state": False,
        "data_is_floating_point": True,
    }
    inputs.update(overrides)
    assert classify_weight_storage(**inputs) == expected


@given(st.floats(min_value=0, max_value=10_000, allow_nan=False, allow_infinity=False))
def test_effective_memory_respects_quantization_ratios(estimate_gb):
    assert effective_model_memory_gb(estimate_gb, None) == estimate_gb
    assert effective_model_memory_gb(estimate_gb, "4bit") == estimate_gb / 4
    assert effective_model_memory_gb(estimate_gb, "8bit") == estimate_gb / 2


@pytest.mark.parametrize(
    ("estimate_gb", "quantization", "available_gb", "expected"),
    [
        (44.0, "4bit", 16.0, True),
        (44.8, "4bit", 16.0, False),
        (22.0, "8bit", 16.0, True),
        (22.4, "8bit", 16.0, False),
        (2.0, "4bit", 1.0, True),
        (1.0, None, 16.0, False),
        (0.0, "4bit", 16.0, False),
        (1.0, "4bit", 0.0, False),
    ],
)
def test_quantized_fit_requires_positive_memory_and_thirty_percent_headroom(
    estimate_gb,
    quantization,
    available_gb,
    expected,
):
    assert quantized_model_fits_gpu(estimate_gb, quantization, available_gb) is expected


def _snapshot(**overrides):
    values = {
        "skip_snapshot": None,
        "initial_gpu_free_gb": 0.0,
        "remaining_gpu_free_gb": 0.0,
        "has_native_quantization": False,
        "estimate_gb": 0.0,
        "quantization": None,
    }
    values.update(overrides)
    return should_snapshot_model(**values)


def test_explicit_snapshot_choice_overrides_every_memory_signal():
    constrained = {
        "initial_gpu_free_gb": 1.0,
        "remaining_gpu_free_gb": 0.0,
        "has_native_quantization": True,
        "estimate_gb": 1000.0,
    }
    assert _snapshot(skip_snapshot=True, **constrained) is False
    assert _snapshot(skip_snapshot=False, **constrained) is True


@pytest.mark.parametrize(
    ("remaining_gb", "expected"),
    [(3.99, False), (4.0, True), (4.01, True)],
)
def test_native_quantization_snapshot_boundary_is_forty_percent(remaining_gb, expected):
    assert _snapshot(
        initial_gpu_free_gb=10.0,
        remaining_gpu_free_gb=remaining_gb,
        has_native_quantization=True,
    ) is expected


def test_native_snapshot_policy_uses_any_positive_gpu_signal():
    assert _snapshot(
        initial_gpu_free_gb=0.5,
        remaining_gpu_free_gb=0.1,
        has_native_quantization=True,
    ) is False
    assert _snapshot(
        initial_gpu_free_gb=0.0,
        remaining_gpu_free_gb=-1.0,
        has_native_quantization=True,
    ) is True


@pytest.mark.parametrize(
    ("estimate_gb", "quantization", "expected"),
    [(5.0, None, True), (5.01, None, False), (20.0, "4bit", True), (20.04, "4bit", False)],
)
def test_estimated_snapshot_boundary_is_half_of_free_memory(
    estimate_gb,
    quantization,
    expected,
):
    assert _snapshot(
        initial_gpu_free_gb=10.0,
        estimate_gb=estimate_gb,
        quantization=quantization,
    ) is expected


def test_estimated_snapshot_policy_uses_any_positive_gpu_signal():
    assert _snapshot(initial_gpu_free_gb=0.5, estimate_gb=0.3) is False
    assert _snapshot(initial_gpu_free_gb=0.0, estimate_gb=1.0) is True


def test_snapshot_defaults_to_enabled_without_a_positive_gpu_signal():
    assert _snapshot() is True
    assert _snapshot(initial_gpu_free_gb=-1.0, estimate_gb=100.0) is True


@pytest.mark.parametrize(
    ("kwargs", "expected"),
    [
        ({"is_moe": False, "total_params_b": 999, "num_experts": 999}, "dense"),
        ({"is_moe": True, "total_params_b": 99.99, "num_experts": 999}, "small_moe"),
        ({"is_moe": True, "total_params_b": 0.5, "num_experts": 999}, "small_moe"),
        ({"is_moe": True, "total_params_b": 100.0, "num_experts": 1}, "large_moe"),
        ({"is_moe": True, "total_params_b": 0.0, "num_experts": 1}, "small_moe"),
        ({"is_moe": True, "total_params_b": 0.0, "num_experts": 16}, "small_moe"),
        ({"is_moe": True, "total_params_b": 0.0, "num_experts": 17}, "large_moe"),
    ],
)
def test_architecture_classification_precedence_and_boundaries(kwargs, expected):
    assert classify_architecture_size(
        model_name="custom/model",
        large_moe_name_patterns=("large-model",),
        **kwargs,
    ) == expected


def test_architecture_name_fallback_is_case_insensitive_and_conservative():
    common = {
        "is_moe": True,
        "total_params_b": 0.0,
        "num_experts": 0,
        "large_moe_name_patterns": ("giant-moe",),
    }
    assert classify_architecture_size(model_name="ORG/GIANT-MOE-V1", **common) == "large_moe"
    assert classify_architecture_size(model_name="org/unknown-moe", **common) == "small_moe"


def test_single_expert_precedes_the_large_name_fallback():
    assert classify_architecture_size(
        is_moe=True,
        total_params_b=0.0,
        num_experts=1,
        model_name="org/giant-moe-v1",
        large_moe_name_patterns=("giant-moe",),
    ) == "small_moe"


@pytest.mark.parametrize(
    ("target", "cuda_available", "cuda_major", "version", "expected"),
    [
        ("cuda", True, 8, "2.0.0", True),
        ("cuda:1", True, 7, "2.0.0", False),
        ("cuda", False, None, "2.0.0", False),
        ("mps", False, None, "2.2.9", False),
        ("mps", False, None, "2.3.0", True),
        ("mps", False, None, "2.10.0", True),
        ("cpu", True, 1, "1.0.0", True),
    ],
)
def test_bfloat16_capability_boundaries(
    target,
    cuda_available,
    cuda_major,
    version,
    expected,
):
    assert supports_bfloat16_target(
        target,
        cuda_available=cuda_available,
        cuda_major=cuda_major,
        torch_version=version,
    ) is expected


def test_single_cuda_device_prefers_most_free_memory_then_lowest_index():
    assert select_single_cuda_device([64.0, 80.0, 80.0], 54.0) == 1


def test_single_cuda_device_requires_one_device_to_fit_with_headroom():
    with pytest.raises(RuntimeError, match=r"complete text model on one CUDA device"):
        select_single_cuda_device([60.0, 63.0], 54.0)


@pytest.mark.parametrize(
    ("required", "utilization", "message"),
    [
        (0.0, 0.85, "required_memory_gb"),
        (float("nan"), 0.85, "required_memory_gb"),
        (1.0, 0.0, "max_utilization"),
        (1.0, 1.1, "max_utilization"),
    ],
)
def test_single_cuda_device_rejects_invalid_policy(required, utilization, message):
    with pytest.raises(ValueError, match=message):
        select_single_cuda_device([80.0], required, max_utilization=utilization)
