"""Simulated MLX contracts that run without Apple hardware or MLX packages."""

from __future__ import annotations

import sys
from types import ModuleType, SimpleNamespace
from unittest.mock import Mock

import numpy as np
import pytest
import torch

from obliteratus import mlx_backend


@pytest.fixture
def fake_mlx(monkeypatch):
    core = ModuleType("mlx.core")
    core.array = np.array
    core.eval = Mock()
    core.matmul = np.matmul
    core.save_safetensors = Mock()
    nn = ModuleType("mlx.nn")
    package = ModuleType("mlx")
    package.core = core
    package.nn = nn
    lm = ModuleType("mlx_lm")
    lm.load = Mock(return_value=(SimpleNamespace(config={"kind": "fixture"}), SimpleNamespace()))
    lm.generate = Mock(return_value="generated")
    lm.upload_to_hub = Mock()
    monkeypatch.setitem(sys.modules, "mlx", package)
    monkeypatch.setitem(sys.modules, "mlx.core", core)
    monkeypatch.setitem(sys.modules, "mlx.nn", nn)
    monkeypatch.setitem(sys.modules, "mlx_lm", lm)
    monkeypatch.setattr(mlx_backend, "MLX_AVAILABLE", True)
    monkeypatch.setattr(mlx_backend, "_mx", core)
    monkeypatch.setattr(mlx_backend, "_mlx_nn", nn)
    monkeypatch.setattr(mlx_backend, "_mlx_lm", lm)
    return SimpleNamespace(core=core, lm=lm)


@pytest.mark.parametrize(
    "call",
    [
        lambda: mlx_backend.load_model("model"),
        lambda: mlx_backend.generate(SimpleNamespace(), "prompt"),
        lambda: mlx_backend.get_activations(SimpleNamespace(), [], []),
        lambda: mlx_backend.get_weight(SimpleNamespace(), 0, "weight"),
        lambda: mlx_backend.modify_weights(SimpleNamespace(), 0, "weight", lambda value: value),
        lambda: mlx_backend.project_out_direction(None, None),
        lambda: mlx_backend.save_model(SimpleNamespace(), "out"),
        lambda: mlx_backend.torch_tensor_to_mlx(torch.ones(1)),
    ],
)
def test_missing_mlx_fails_with_install_guidance(monkeypatch, call):
    monkeypatch.setattr(mlx_backend, "MLX_AVAILABLE", False)
    with pytest.raises(RuntimeError, match="pip install mlx>=0.22 mlx-lm>=0.20"):
        call()


def test_load_generate_and_handle_config(fake_mlx):
    handle = mlx_backend.load_model("local/model", dtype="bfloat16")
    assert handle.model_name == "local/model"
    assert handle.config == {"kind": "fixture"}
    fake_mlx.lm.load.assert_called_once_with("local/model")
    assert mlx_backend.generate(
        handle,
        "prompt",
        max_tokens=4,
        temperature=0.2,
        top_p=0.8,
        repetition_penalty=1.1,
    ) == "generated"
    assert fake_mlx.lm.generate.call_args.kwargs == {
        "prompt": "prompt",
        "max_tokens": 4,
        "temp": 0.2,
        "top_p": 0.8,
        "repetition_penalty": 1.1,
    }
    mlx_backend.generate(handle, "prompt", repetition_penalty=None)
    assert "repetition_penalty" not in fake_mlx.lm.generate.call_args.kwargs


class _Layer:
    def __call__(self, value):
        return (value + 1, "attention")


def test_activation_capture_supports_llama_and_truncation(fake_mlx):
    inner = SimpleNamespace(
        layers=[_Layer(), _Layer()],
        embed_tokens=lambda ids: np.repeat(ids[:, :, None], 3, axis=2),
    )
    handle = mlx_backend.MLXModelHandle(
        model=SimpleNamespace(model=inner),
        tokenizer=SimpleNamespace(encode=lambda _prompt: [1, 2, 3, 4]),
        model_name="fixture",
    )
    activations = mlx_backend.get_activations(handle, ["a", "b"], [0, 1], max_length=2)
    assert np.array_equal(activations[0][0], np.array([3, 3, 3]))
    assert np.array_equal(activations[1][1], np.array([4, 4, 4]))
    assert fake_mlx.core.eval.call_count == 4


def test_activation_capture_rejects_unknown_layers_and_embeddings(fake_mlx):
    handle = mlx_backend.MLXModelHandle(SimpleNamespace(), SimpleNamespace(), "unknown")
    with pytest.raises(RuntimeError, match="Cannot locate transformer layers"):
        mlx_backend.get_activations(handle, ["x"], [0])

    model = SimpleNamespace(model=SimpleNamespace(layers=[_Layer()]))
    handle = mlx_backend.MLXModelHandle(model, SimpleNamespace(encode=lambda _prompt: [1]), "unknown")
    with pytest.raises(RuntimeError, match="Cannot find embedding layer"):
        mlx_backend.get_activations(handle, ["x"], [0])


def test_get_and_modify_weights_support_update_and_assignment(fake_mlx):
    update_parent = SimpleNamespace(weight=np.array([1.0]), update=Mock())
    assign_parent = SimpleNamespace(weight=np.array([2.0]))
    layers = [SimpleNamespace(attention=update_parent), SimpleNamespace(attention=assign_parent)]
    handle = mlx_backend.MLXModelHandle(
        SimpleNamespace(transformer=SimpleNamespace(h=layers)),
        SimpleNamespace(),
        "fixture",
    )
    assert np.array_equal(mlx_backend.get_weight(handle, 0, "attention.weight"), np.array([1.0]))
    mlx_backend.modify_weights(handle, 0, "attention.weight", lambda value: value + 1)
    update_parent.update.assert_called_once()
    mlx_backend.modify_weights(handle, 1, "attention.weight", lambda value: value + 1)
    assert np.array_equal(assign_parent.weight, np.array([3.0]))
    assert fake_mlx.core.eval.call_count == 2


def test_projection_removes_requested_direction(fake_mlx):
    weight = np.array([[2.0, 3.0], [4.0, 5.0]])
    direction = np.array([1.0, 0.0])
    projected = mlx_backend.project_out_direction(weight, direction)
    assert np.array_equal(projected, np.array([[0.0, 3.0], [0.0, 5.0]]))


def test_save_model_native_fallback_and_upload(fake_mlx, monkeypatch, tmp_path):
    tokenizer = SimpleNamespace(save_pretrained=Mock())
    model = SimpleNamespace(parameters=lambda: {"block": {"weight": np.array([1.0])}}.items())
    handle = mlx_backend.MLXModelHandle(model, tokenizer, "fixture")
    fake_mlx.lm.save_model = Mock()
    out = mlx_backend.save_model(handle, tmp_path / "native", upload_repo="org/model")
    assert out.is_dir()
    fake_mlx.lm.save_model.assert_called_once()
    fake_mlx.lm.upload_to_hub.assert_called_once_with(str(out), "org/model")

    del fake_mlx.lm.save_model
    out = mlx_backend.save_model(handle, tmp_path / "fallback")
    fake_mlx.core.save_safetensors.assert_called_once()
    tokenizer.save_pretrained.assert_called_once_with(str(out))


def test_tensor_conversions_and_internal_helpers(fake_mlx):
    source = torch.tensor([1.0, 2.0], requires_grad=True)
    converted = mlx_backend.torch_tensor_to_mlx(source)
    assert np.array_equal(converted, np.array([1.0, 2.0], dtype=np.float32))
    restored = mlx_backend.mlx_to_torch_tensor(np.array([3.0]), device="cpu")
    assert torch.equal(restored, torch.tensor([3.0], dtype=torch.float64))

    layers = [object()]
    assert mlx_backend._get_layers(SimpleNamespace(gpt_neox=SimpleNamespace(layers=layers))) is layers
    with pytest.raises(RuntimeError, match="Cannot locate transformer layers"):
        mlx_backend._get_layers(SimpleNamespace())
    flattened = {}
    mlx_backend._flatten_dict({"a": {"b": 1}, "c": 2}, "", flattened)
    assert flattened == {"a.b": 1, "c": 2}
