"""G9 — declarative ValidationRule.

Covers RANGE, REGEX, COMPARISON, and (sandboxed) CUSTOM_EXPRESSION; verifies
inactive rules are skipped and that ``record_data_point`` honours rules.
"""
from __future__ import annotations

from decimal import Decimal

import pytest

from apps.mel.indicators.models import (
    DataPoint,
    Indicator,
    IndicatorFrequency,
    LogFrame,
    LogFrameLevel,
    LogFrameRow,
    ValidationRule,
)
from apps.mel.indicators.services import (
    DataPointValidationError,
    record_data_point,
    validate_value,
)


pytestmark = pytest.mark.django_db


def _build_indicator(code: str = "g9-a"):
    lf = LogFrame.objects.create(name=f"LF {code}", slug=f"lf-{code}")
    impact = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.IMPACT, title="I")
    outcome = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.OUTCOME, title="O", parent=impact)
    output = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.OUTPUT, title="P", parent=outcome)
    return Indicator.objects.create(
        logframe_row=output,
        code=code,
        name=code.upper(),
        unit="%",
        calculation_method="mean",
        data_source="manual",
        frequency=IndicatorFrequency.QUARTERLY,
    )


def test_range_rule_rejects_out_of_bounds():
    ind = _build_indicator("g9-range")
    ValidationRule.objects.create(
        indicator=ind,
        rule_type=ValidationRule.RuleType.RANGE,
        params={"min": 0, "max": 100},
        message="Percentage must be 0–100",
    )
    with pytest.raises(DataPointValidationError) as exc:
        validate_value(ind, Decimal("150"))
    assert exc.value.code == "range"


def test_inactive_rule_does_not_reject():
    ind = _build_indicator("g9-inactive")
    ValidationRule.objects.create(
        indicator=ind,
        rule_type=ValidationRule.RuleType.RANGE,
        params={"min": 0, "max": 100},
        is_active=False,
    )
    validate_value(ind, Decimal("9999"))  # no exception


def test_regex_rule_rejects_non_matching_string():
    ind = _build_indicator("g9-regex")
    ValidationRule.objects.create(
        indicator=ind,
        rule_type=ValidationRule.RuleType.REGEX,
        params={"pattern": "^[0-9]+$"},
    )
    with pytest.raises(DataPointValidationError) as exc:
        validate_value(ind, "abc")
    assert exc.value.code == "regex"


def test_comparison_rule_rejects_value_below_previous():
    ind = _build_indicator("g9-compare")
    ValidationRule.objects.create(
        indicator=ind,
        rule_type=ValidationRule.RuleType.COMPARISON,
        params={"op": ">=", "other": 50},
    )
    with pytest.raises(DataPointValidationError) as exc:
        validate_value(ind, Decimal("10"))
    assert exc.value.code == "comparison"
    validate_value(ind, Decimal("75"))  # no exception


def test_custom_expression_rule_rejects():
    ind = _build_indicator("g9-expr")
    ValidationRule.objects.create(
        indicator=ind,
        rule_type=ValidationRule.RuleType.CUSTOM_EXPRESSION,
        params={"expression": "value > 0 and value < 100"},
    )
    with pytest.raises(DataPointValidationError) as exc:
        validate_value(ind, Decimal("150"))
    assert exc.value.code == "custom_expression"
    validate_value(ind, Decimal("50"))  # no exception


def test_custom_expression_cannot_access_unsafe_names():
    ind = _build_indicator("g9-expr-safe")
    ValidationRule.objects.create(
        indicator=ind,
        rule_type=ValidationRule.RuleType.CUSTOM_EXPRESSION,
        params={"expression": "__import__('os')"},
    )
    # Attempted name access raises a simpleeval NameNotDefined → wrapped as
    # a DataPointValidationError by validate_value.
    with pytest.raises(DataPointValidationError):
        validate_value(ind, Decimal("1"))


def test_record_data_point_surfaces_validation_error():
    ind = _build_indicator("g9-record")
    ValidationRule.objects.create(
        indicator=ind,
        rule_type=ValidationRule.RuleType.RANGE,
        params={"min": 0, "max": 100},
        message="Percentage must be 0–100",
    )
    with pytest.raises(DataPointValidationError):
        record_data_point(indicator=ind, value=Decimal("150"), period_label="2026Q2")
    # No DataPoint should have been written.
    assert DataPoint.objects.filter(indicator=ind).count() == 0


def test_record_data_point_with_raise_on_invalid_false_skips_write():
    ind = _build_indicator("g9-record-skip")
    ValidationRule.objects.create(
        indicator=ind,
        rule_type=ValidationRule.RuleType.RANGE,
        params={"min": 0, "max": 100},
    )
    result = record_data_point(
        indicator=ind, value=Decimal("150"), period_label="2026Q2",
        raise_on_invalid=False,
    )
    assert result.created is False
    assert result.data_point is None
    assert DataPoint.objects.filter(indicator=ind).count() == 0
