import pytest
from dot_metrics import Constraint, EvalResult, Metric


def _make_result(constraint_value: float, threshold: float) -> EvalResult:
    c = Constraint(key="c1", value=constraint_value, threshold=threshold, passed=constraint_value <= threshold)
    m = Metric(key="m1", value=0.9)
    return EvalResult(metrics={"m1": m}, constraints={"c1": c})


def test_violations_empty_when_all_pass():
    result = _make_result(0.0, 0.0)
    assert result.violations == []


def test_violations_returns_failed_constraints():
    result = _make_result(0.1, 0.0)
    assert len(result.violations) == 1
    assert result.violations[0].key == "c1"


def test_constraints_ok_true_when_all_pass():
    result = _make_result(0.0, 0.0)
    assert result.constraints_ok is True


def test_constraints_ok_false_when_one_fails():
    result = _make_result(0.1, 0.0)
    assert result.constraints_ok is False


def test_assert_constraints_raises_on_failure():
    result = _make_result(0.2, 0.0)
    with pytest.raises(ValueError, match="c1"):
        result.assert_constraints()


def test_assert_constraints_ok_passes_silently():
    result = _make_result(0.0, 0.0)
    result.assert_constraints()  # should not raise


def test_score_returns_metric_value():
    result = _make_result(0.0, 0.0)
    assert result.score("m1") == pytest.approx(0.9)


def test_score_raises_key_error_for_missing():
    result = _make_result(0.0, 0.0)
    with pytest.raises(KeyError):
        result.score("nonexistent")


def test_score_returns_constraint_value():
    result = _make_result(0.05, 0.1)
    assert result.score("c1") == pytest.approx(0.05)


def test_score_prefers_metric_over_constraint():
    m = Metric(key="shared", value=0.9)
    c = Constraint(key="shared", value=0.1, threshold=0.5, passed=True)
    result = EvalResult(metrics={"shared": m}, constraints={"shared": c})
    assert result.score("shared") == pytest.approx(0.9)


def test_score_raises_key_error_for_unknown():
    result = _make_result(0.0, 0.0)
    with pytest.raises(KeyError):
        result.score("totally_unknown")


def test_len_returns_metrics_plus_constraints_count():
    result = _make_result(0.0, 0.0)
    assert len(result) == 2


def test_iter_yields_all_keys_metrics_first():
    result = _make_result(0.0, 0.0)
    assert list(result) == ["m1", "c1"]


def test_getitem_returns_metric():
    result = _make_result(0.0, 0.0)
    assert result["m1"].value == pytest.approx(0.9)
    assert isinstance(result["m1"], Metric)


def test_getitem_returns_constraint():
    result = _make_result(0.0, 0.0)
    assert result["c1"].key == "c1"
    assert isinstance(result["c1"], Constraint)


def test_getitem_raises_keyerror_for_unknown():
    result = _make_result(0.0, 0.0)
    with pytest.raises(KeyError):
        result["nonexistent"]


def test_empty_result_len_and_iter():
    result = EvalResult(metrics={}, constraints={})
    assert len(result) == 0
    assert list(result) == []


def test_value_leq_threshold_passes():
    c = Constraint(key="c", value=0.05, threshold=0.1, passed=0.05 <= 0.1)
    assert c.passed is True


def test_value_gt_threshold_fails():
    c = Constraint(key="c", value=0.15, threshold=0.1, passed=0.15 <= 0.1)
    assert c.passed is False


def test_constraint_higher_is_better_field():
    c = Constraint(key="c", value=0.9, threshold=0.8, passed=True, higher_is_better=True)
    assert c.higher_is_better is True
    assert c.passed is True


def test_constraint_higher_is_better_default_false():
    c = Constraint(key="c", value=0.1, threshold=0.5, passed=True)
    assert c.higher_is_better is False


def test_name_present_on_computed_results():
    m = Metric(key="m1", name="My Metric", value=0.9)
    c = Constraint(key="c1", name="My Constraint", value=0.0, threshold=1.0, passed=True)
    result = EvalResult(metrics={"m1": m}, constraints={"c1": c})
    assert result.metrics["m1"].name == "My Metric"
    assert result.constraints["c1"].name == "My Constraint"
