"""Tests for the condition evaluation engine (ABAC)."""

import pytest

from pyauthz.core.conditions import _resolve_field, evaluate_conditions
from pyauthz.models.condition import Condition
from pyauthz.models.context import AccessRequest
from pyauthz.models.resource import Resource
from pyauthz.models.subject import Subject


@pytest.fixture
def alice():
    return Subject(
        id="alice",
        roles=["editor"],
        attributes={"department": "engineering", "clearance": "TS"},
    )


@pytest.fixture
def doc():
    return Resource(
        id="doc-1",
        type="documents",
        owner_id="alice",
        attributes={"classification": "TOP_SECRET", "tags": ["internal", "draft"]},
    )


@pytest.fixture
def request_ctx(alice, doc):
    return AccessRequest(
        subject=alice,
        action="documents:write",
        resource=doc,
        environment={"hour": 14, "ip": "10.0.0.1", "day_of_week": "mon"},
    )


# --- Field resolution ---


class TestFieldResolution:
    def test_subject_id(self, request_ctx):
        val, err = _resolve_field("subject.id", request_ctx)
        assert val == "alice"
        assert err is None

    def test_subject_roles(self, request_ctx):
        val, err = _resolve_field("subject.roles", request_ctx)
        assert val == ["editor"]

    def test_subject_attribute(self, request_ctx):
        val, err = _resolve_field("subject.attributes.department", request_ctx)
        assert val == "engineering"

    def test_resource_id(self, request_ctx):
        val, err = _resolve_field("resource.id", request_ctx)
        assert val == "doc-1"

    def test_resource_type(self, request_ctx):
        val, err = _resolve_field("resource.type", request_ctx)
        assert val == "documents"

    def test_resource_owner_id(self, request_ctx):
        val, err = _resolve_field("resource.owner_id", request_ctx)
        assert val == "alice"

    def test_resource_deep_attribute(self, request_ctx):
        val, err = _resolve_field("resource.attributes.classification", request_ctx)
        assert val == "TOP_SECRET"

    def test_environment_value(self, request_ctx):
        val, err = _resolve_field("environment.hour", request_ctx)
        assert val == 14

    def test_missing_attribute_returns_error(self, request_ctx):
        val, err = _resolve_field("subject.attributes.nonexistent", request_ctx)
        assert val is None
        assert "not found" in err

    def test_unknown_root_returns_error(self, request_ctx):
        val, err = _resolve_field("unknown.field", request_ctx)
        assert val is None
        assert "unknown root" in err

    def test_invalid_path_too_short(self, request_ctx):
        val, err = _resolve_field("subject", request_ctx)
        assert val is None
        assert "invalid field path" in err


# --- Equality operators ---


class TestEqualityOps:
    def test_equals_literal(self, request_ctx):
        cond = Condition(field="resource.owner_id", op="==", value="alice")
        passed, reasons = evaluate_conditions([cond], request_ctx)
        assert passed is True

    def test_equals_fails(self, request_ctx):
        cond = Condition(field="resource.owner_id", op="==", value="bob")
        passed, reasons = evaluate_conditions([cond], request_ctx)
        assert passed is False
        assert any("failed" in r.lower() for r in reasons)

    def test_not_equals(self, request_ctx):
        cond = Condition(field="resource.owner_id", op="!=", value="bob")
        passed, reasons = evaluate_conditions([cond], request_ctx)
        assert passed is True

    def test_not_equals_fails(self, request_ctx):
        cond = Condition(field="resource.owner_id", op="!=", value="alice")
        passed, reasons = evaluate_conditions([cond], request_ctx)
        assert passed is False


# --- Cross-reference conditions ---


class TestCrossReference:
    def test_owner_equals_subject_id(self, request_ctx):
        """resource.owner_id == subject.id (the canonical cross-reference)."""
        cond = Condition(field="resource.owner_id", op="==", value="subject.id")
        passed, reasons = evaluate_conditions([cond], request_ctx)
        assert passed is True

    def test_owner_not_equals_subject_id(self):
        """Cross-reference fails when IDs don't match."""
        subject = Subject(id="bob", roles=["editor"])
        resource = Resource(id="doc-1", type="documents", owner_id="alice")
        request = AccessRequest(subject=subject, action="write", resource=resource)
        cond = Condition(field="resource.owner_id", op="==", value="subject.id")
        passed, reasons = evaluate_conditions([cond], request)
        assert passed is False

    def test_cross_ref_missing_value_field(self, request_ctx):
        """Cross-reference where the value field doesn't exist → fail-safe."""
        cond = Condition(
            field="resource.owner_id", op="==", value="subject.attributes.nonexistent"
        )
        passed, reasons = evaluate_conditions([cond], request_ctx)
        assert passed is False
        assert any("not found" in r for r in reasons)


# --- Comparison operators ---


class TestComparisonOps:
    def test_greater_than(self, request_ctx):
        cond = Condition(field="environment.hour", op=">", value=10)
        passed, _ = evaluate_conditions([cond], request_ctx)
        assert passed is True

    def test_greater_than_fails(self, request_ctx):
        cond = Condition(field="environment.hour", op=">", value=20)
        passed, _ = evaluate_conditions([cond], request_ctx)
        assert passed is False

    def test_less_than(self, request_ctx):
        cond = Condition(field="environment.hour", op="<", value=20)
        passed, _ = evaluate_conditions([cond], request_ctx)
        assert passed is True

    def test_greater_equal(self, request_ctx):
        cond = Condition(field="environment.hour", op=">=", value=14)
        passed, _ = evaluate_conditions([cond], request_ctx)
        assert passed is True

    def test_less_equal(self, request_ctx):
        cond = Condition(field="environment.hour", op="<=", value=14)
        passed, _ = evaluate_conditions([cond], request_ctx)
        assert passed is True


# --- Set operators ---


class TestSetOps:
    def test_in_list(self, request_ctx):
        cond = Condition(field="subject.attributes.clearance", op="in", value=["TS", "TS/SCI"])
        passed, _ = evaluate_conditions([cond], request_ctx)
        assert passed is True

    def test_in_list_fails(self, request_ctx):
        cond = Condition(
            field="subject.attributes.clearance", op="in", value=["SECRET", "UNCLASSIFIED"]
        )
        passed, _ = evaluate_conditions([cond], request_ctx)
        assert passed is False

    def test_not_in(self, request_ctx):
        cond = Condition(field="subject.attributes.clearance", op="not_in", value=["SECRET"])
        passed, _ = evaluate_conditions([cond], request_ctx)
        assert passed is True

    def test_not_in_fails(self, request_ctx):
        cond = Condition(field="subject.attributes.clearance", op="not_in", value=["TS", "SECRET"])
        passed, _ = evaluate_conditions([cond], request_ctx)
        assert passed is False

    def test_contains(self, request_ctx):
        """Field is a list, check if it contains a value."""
        cond = Condition(field="resource.attributes.tags", op="contains", value="internal")
        passed, _ = evaluate_conditions([cond], request_ctx)
        assert passed is True

    def test_contains_fails(self, request_ctx):
        cond = Condition(field="resource.attributes.tags", op="contains", value="public")
        passed, _ = evaluate_conditions([cond], request_ctx)
        assert passed is False


# --- Regex operator ---


class TestRegexOp:
    def test_matches(self, request_ctx):
        cond = Condition(field="environment.ip", op="matches", value=r"^10\.\d+\.\d+\.\d+$")
        passed, _ = evaluate_conditions([cond], request_ctx)
        assert passed is True

    def test_matches_fails(self, request_ctx):
        cond = Condition(field="environment.ip", op="matches", value=r"^192\.168\.")
        passed, _ = evaluate_conditions([cond], request_ctx)
        assert passed is False


# --- Multiple conditions (AND logic) ---


class TestMultipleConditions:
    def test_all_pass(self, request_ctx):
        conditions = [
            Condition(field="resource.owner_id", op="==", value="subject.id"),
            Condition(field="environment.hour", op=">=", value=9),
            Condition(field="environment.hour", op="<=", value=17),
        ]
        passed, reasons = evaluate_conditions(conditions, request_ctx)
        assert passed is True

    def test_one_fails_stops_early(self, request_ctx):
        conditions = [
            Condition(field="resource.owner_id", op="==", value="bob"),  # fails
            Condition(field="environment.hour", op=">=", value=9),
        ]
        passed, reasons = evaluate_conditions(conditions, request_ctx)
        assert passed is False
        # Should have exactly one failure reason (short-circuit)
        assert len(reasons) == 1

    def test_empty_conditions_pass(self, request_ctx):
        passed, reasons = evaluate_conditions([], request_ctx)
        assert passed is True


# --- Fail-safe: missing attributes ---


class TestFailSafe:
    def test_missing_subject_attribute(self, request_ctx):
        cond = Condition(field="subject.attributes.nonexistent", op="==", value="anything")
        passed, reasons = evaluate_conditions([cond], request_ctx)
        assert passed is False
        assert any("not found" in r for r in reasons)

    def test_missing_resource_attribute(self, request_ctx):
        cond = Condition(field="resource.attributes.nonexistent", op="==", value="anything")
        passed, reasons = evaluate_conditions([cond], request_ctx)
        assert passed is False

    def test_missing_environment_key(self, request_ctx):
        cond = Condition(field="environment.nonexistent", op="==", value="anything")
        passed, reasons = evaluate_conditions([cond], request_ctx)
        assert passed is False

    def test_type_mismatch_is_safe(self, request_ctx):
        """Comparing incompatible types should fail safely, not crash."""
        cond = Condition(field="environment.hour", op=">", value="not_a_number")
        passed, reasons = evaluate_conditions([cond], request_ctx)
        assert passed is False
        assert any("cannot apply" in r.lower() for r in reasons)
