from dataclasses import replace

import numpy as np
import pytest

from m3m_disease.dataset import matrix_from_records
from m3m_disease.model import evaluate_binary, fit_logistic, load_model, model_sha256, save_model


def test_l2_logistic_model_fits_auditable_synthetic_features(trained_bundle):
    model = trained_bundle["model"]
    matrix, labels, _ = matrix_from_records(trained_bundle["assigned"], split="test")
    result = evaluate_binary(model, matrix, labels)
    assert result["metrics"]["accuracy"] >= 0.90
    assert result["engineeringSyntheticOnly"] is True
    assert "must not be reported" in result["warning"]
    assert model.l2 > 0


def test_json_model_round_trip_and_hash(tmp_path, trained_bundle):
    destination = save_model(tmp_path / "model.json", trained_bundle["model"])
    loaded = load_model(destination)
    assert model_sha256(loaded) == model_sha256(trained_bundle["model"])
    assert loaded.promotionStatus == "RESEARCH_ONLY"
    assert "Plant disease confirmation" in loaded.forbiddenUses


def test_tampered_json_model_is_rejected(tmp_path, trained_bundle):
    destination = save_model(tmp_path / "model.json", trained_bundle["model"])
    text = destination.read_text(encoding="utf-8").replace('"intercept": ', '"intercept": 1')
    destination.write_text(text, encoding="utf-8")
    with pytest.raises((ValueError, TypeError)):
        load_model(destination)


def test_fit_requires_both_classes():
    matrix = np.zeros((5, 18), dtype=np.float64)
    with pytest.raises(ValueError, match="both binary classes"):
        fit_logistic(matrix, np.zeros(5))


def test_ood_detector_rejects_extreme_feature_vector(trained_bundle):
    model = trained_bundle["model"]
    extreme = np.asarray(model.mean) + 100 * np.asarray(model.scale)
    receipt = model.ood_metrics(extreme)
    assert bool(receipt["rejected"][0]) is True


def test_abstention_thresholds_are_part_of_the_model(trained_bundle):
    model = replace(trained_bundle["model"], lowThreshold=0.2, highThreshold=0.8)
    model.validate()
    assert model.lowThreshold < model.highThreshold


def test_synthetic_training_cannot_be_promoted_by_a_claimed_receipt(trained_bundle):
    matrix, labels, _ = matrix_from_records(trained_bundle["assigned"], split="train")
    model = fit_logistic(
        matrix,
        labels,
        data_provenance="synthetic",
        promotion_receipt={"status": "FIELD_VALIDATION_CANDIDATE", "failures": []},
    )
    assert model.promotionStatus == "RESEARCH_ONLY"
