from __future__ import annotations

import pytest
from pydantic import ValidationError

from complyform.core.assessor.check_evaluator import (
    _MISSING,
    CheckEvaluator,
    CheckResult,
    get_nested_attribute,
)
from complyform.core.profiles.models import Check


@pytest.fixture()
def evaluator() -> CheckEvaluator:
    return CheckEvaluator()


def _check(
    operator: str,
    attribute: str = "key",
    expected: object = None,
    failure_message: str = "",
) -> Check:
    return Check(
        operator=operator,
        attribute=attribute,
        expected=expected,
        failure_message=failure_message,
    )


# ── equals ──────────────────────────────────────────────────────────


class TestEquals:
    @pytest.mark.parametrize(
        "value, expected",
        [
            ("TLS", "TLS"),
            (42, 42),
            (True, True),
        ],
    )
    def test_match_passes(
        self, evaluator: CheckEvaluator, value: object, expected: object
    ) -> None:
        result = evaluator.evaluate(_check("equals", expected=expected), {"key": value})
        assert result.passed is True
        assert result.message == ""

    def test_mismatch_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(_check("equals", expected="TLS"), {"key": "SSL"})
        assert result.passed is False
        assert result.actual_value == "SSL"

    def test_missing_attribute_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(_check("equals", expected="TLS"), {})
        assert result.passed is False
        assert result.actual_value is None


# ── not_equals ──────────────────────────────────────────────────────


class TestNotEquals:
    def test_different_value_passes(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(
            _check("not_equals", expected="SSL"), {"key": "TLS"}
        )
        assert result.passed is True

    def test_same_value_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(
            _check("not_equals", expected="SSL"), {"key": "SSL"}
        )
        assert result.passed is False

    def test_missing_attribute_passes(self, evaluator: CheckEvaluator) -> None:
        """Missing attribute means value is definitely not equal → PASS."""
        result = evaluator.evaluate(_check("not_equals", expected="SSL"), {})
        assert result.passed is True
        assert result.actual_value is None


# ── not_empty ───────────────────────────────────────────────────────


class TestNotEmpty:
    @pytest.mark.parametrize(
        "value",
        ["hello", [1, 2], {"a": 1}, 42, True],
        ids=["string", "list", "dict", "int", "bool"],
    )
    def test_non_empty_passes(self, evaluator: CheckEvaluator, value: object) -> None:
        result = evaluator.evaluate(_check("not_empty"), {"key": value})
        assert result.passed is True

    @pytest.mark.parametrize(
        "value, label",
        [("", "empty_string"), (None, "none"), ([], "empty_list")],
    )
    def test_empty_values_fail(
        self, evaluator: CheckEvaluator, value: object, label: str
    ) -> None:
        result = evaluator.evaluate(_check("not_empty"), {"key": value})
        assert result.passed is False

    def test_missing_attribute_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(_check("not_empty"), {})
        assert result.passed is False
        assert result.actual_value is None


# ── exists ──────────────────────────────────────────────────────────


class TestExists:
    def test_present_passes(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(_check("exists"), {"key": "val"})
        assert result.passed is True

    def test_present_none_passes(self, evaluator: CheckEvaluator) -> None:
        """Attribute present but set to None still counts as existing."""
        result = evaluator.evaluate(_check("exists"), {"key": None})
        assert result.passed is True

    def test_missing_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(_check("exists"), {})
        assert result.passed is False


# ── in_list ─────────────────────────────────────────────────────────


class TestInList:
    def test_value_in_list_passes(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(
            _check("in_list", expected=["TLS", "SSL"]),
            {"key": "TLS"},
        )
        assert result.passed is True

    def test_value_not_in_list_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(
            _check("in_list", expected=["TLS", "SSL"]),
            {"key": "PLAIN"},
        )
        assert result.passed is False

    def test_missing_attribute_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(
            _check("in_list", expected=["TLS"]),
            {},
        )
        assert result.passed is False


# ── gte ─────────────────────────────────────────────────────────────


class TestGte:
    @pytest.mark.parametrize(
        "value, expected",
        [(10, 5), (5, 5), (3.14, 3.0)],
        ids=["above", "equal", "float_above"],
    )
    def test_passes(
        self, evaluator: CheckEvaluator, value: object, expected: object
    ) -> None:
        result = evaluator.evaluate(_check("gte", expected=expected), {"key": value})
        assert result.passed is True

    def test_below_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(_check("gte", expected=10), {"key": 5})
        assert result.passed is False

    def test_missing_attribute_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(_check("gte", expected=10), {})
        assert result.passed is False

    def test_non_numeric_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(_check("gte", expected=10), {"key": "abc"})
        assert result.passed is False
        assert "Non-numeric" in result.message


# ── lte ─────────────────────────────────────────────────────────────


class TestLte:
    @pytest.mark.parametrize(
        "value, expected",
        [(3, 5), (5, 5), (2.5, 3.0)],
        ids=["below", "equal", "float_below"],
    )
    def test_passes(
        self, evaluator: CheckEvaluator, value: object, expected: object
    ) -> None:
        result = evaluator.evaluate(_check("lte", expected=expected), {"key": value})
        assert result.passed is True

    def test_above_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(_check("lte", expected=5), {"key": 10})
        assert result.passed is False

    def test_missing_attribute_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(_check("lte", expected=5), {})
        assert result.passed is False

    def test_non_numeric_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(_check("lte", expected=5), {"key": "abc"})
        assert result.passed is False
        assert "Non-numeric" in result.message


# ── regex_match ─────────────────────────────────────────────────────


class TestRegexMatch:
    def test_match_passes(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(
            _check("regex_match", expected=r"^https://"),
            {"key": "https://example.com"},
        )
        assert result.passed is True

    def test_no_match_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(
            _check("regex_match", expected=r"^https://"),
            {"key": "http://example.com"},
        )
        assert result.passed is False

    def test_missing_attribute_fails(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(
            _check("regex_match", expected=r"^https://"),
            {},
        )
        assert result.passed is False

    def test_invalid_regex_fails_with_message(self, evaluator: CheckEvaluator) -> None:
        result = evaluator.evaluate(
            _check("regex_match", expected=r"[invalid"),
            {"key": "anything"},
        )
        assert result.passed is False
        assert "Invalid regex" in result.message


# ── get_nested_attribute ────────────────────────────────────────────


class TestGetNestedAttribute:
    def test_depth_3_traversal(self) -> None:
        attrs = {
            "settings": [
                {
                    "ip_configuration": [
                        {"ssl_mode": "ENCRYPTED_ONLY"},
                    ],
                },
            ],
        }
        assert (
            get_nested_attribute(attrs, "settings.0.ip_configuration.0.ssl_mode")
            == "ENCRYPTED_ONLY"
        )

    def test_list_index_0(self) -> None:
        attrs = {"rules": [{"action": "allow"}, {"action": "deny"}]}
        assert get_nested_attribute(attrs, "rules.0.action") == "allow"

    def test_list_index_1(self) -> None:
        attrs = {"rules": [{"action": "allow"}, {"action": "deny"}]}
        assert get_nested_attribute(attrs, "rules.1.action") == "deny"

    def test_missing_key_returns_sentinel(self) -> None:
        assert get_nested_attribute({}, "no.such.key") is _MISSING

    def test_out_of_range_index_returns_sentinel(self) -> None:
        assert get_nested_attribute({"items": [1]}, "items.5") is _MISSING

    def test_negative_index_returns_sentinel(self) -> None:
        assert get_nested_attribute({"items": [1, 2]}, "items.-1") is _MISSING

    def test_none_intermediate_returns_sentinel(self) -> None:
        assert get_nested_attribute({"a": None}, "a.b") is _MISSING

    def test_non_dict_non_list_intermediate_returns_sentinel(self) -> None:
        assert get_nested_attribute({"a": 42}, "a.b") is _MISSING


# ── unknown operator ────────────────────────────────────────────────


def test_unknown_operator_fails(evaluator: CheckEvaluator) -> None:
    result = evaluator.evaluate(_check("bogus_op"), {"key": "val"})
    assert result.passed is False
    assert "Unknown operator" in result.message


# ── custom failure_message propagation ──────────────────────────────


@pytest.mark.parametrize(
    "operator, attrs",
    [
        ("equals", {"key": "wrong"}),
        ("not_equals", {"key": "bad"}),
        ("not_empty", {"key": ""}),
        ("exists", {}),
        ("in_list", {"key": "x"}),
        ("gte", {"key": 1}),
        ("lte", {"key": 100}),
        ("regex_match", {"key": "no"}),
    ],
)
def test_custom_failure_message_propagates(
    evaluator: CheckEvaluator,
    operator: str,
    attrs: dict[str, object],
) -> None:
    """When a check fails and failure_message is set, the result uses it."""
    expected_map: dict[str, object] = {
        "equals": "match",
        "not_equals": "bad",
        "not_empty": None,
        "exists": None,
        "in_list": ["a", "b"],
        "gte": 999,
        "lte": 0,
        "regex_match": r"^zzz$",
    }
    custom = "Custom compliance failure"
    check = _check(
        operator,
        expected=expected_map[operator],
        failure_message=custom,
    )
    result = evaluator.evaluate(check, attrs)
    assert result.passed is False
    assert result.message == custom


# ── CheckResult is frozen ───────────────────────────────────────────


def test_check_result_is_frozen() -> None:
    result = CheckResult(passed=True, actual_value="x", message="")
    with pytest.raises(ValidationError):
        result.passed = False  # type: ignore[misc]
