"""Tests for the query-time filtering system (Phase 4)."""

import pytest

from pyauthz import (
    AllowAll,
    And,
    Compare,
    Condition,
    DenyAll,
    Effect,
    FilterResult,
    Not,
    Or,
    Policy,
    PyAuthz,
    Resource,
    Role,
    Subject,
)

# --- FilterExpression AST tests ---


class TestFilterExpressionAST:
    def test_allow_all(self):
        expr = AllowAll()
        assert isinstance(expr, AllowAll)

    def test_deny_all(self):
        expr = DenyAll()
        assert isinstance(expr, DenyAll)

    def test_compare(self):
        expr = Compare(field="owner_id", op="==", value="alice")
        assert expr.field == "owner_id"
        assert expr.op == "=="
        assert expr.value == "alice"

    def test_and(self):
        expr = And(
            children=[
                Compare(field="owner_id", op="==", value="alice"),
                Compare(field="type", op="==", value="doc"),
            ]
        )
        assert len(expr.children) == 2

    def test_or(self):
        expr = Or(
            children=[
                Compare(field="owner_id", op="==", value="alice"),
                Compare(field="owner_id", op="==", value="bob"),
            ]
        )
        assert len(expr.children) == 2

    def test_not(self):
        expr = Not(child=Compare(field="classification", op="==", value="SECRET"))
        assert isinstance(expr.child, Compare)


# --- authorized_filter() with auto-default ---


class TestAuthorizedFilterAutoDefault:
    def test_admin_gets_allow_all(self):
        rp = PyAuthz()
        rp.add_role(Role(name="admin", permissions=["*:*"]))
        user = Subject(id="alice", roles=["admin"])

        result = rp.authorized_filter(user, "documents:read")
        assert isinstance(result, FilterResult)
        assert isinstance(result.expression, AllowAll)

    def test_no_matching_permission_gets_deny_all(self):
        rp = PyAuthz()
        rp.add_role(Role(name="viewer", permissions=["documents:read"]))
        user = Subject(id="bob", roles=["viewer"])

        result = rp.authorized_filter(user, "documents:write")
        assert isinstance(result.expression, DenyAll)

    def test_matching_permission_gets_allow_all(self):
        rp = PyAuthz()
        rp.add_role(Role(name="viewer", permissions=["documents:read"]))
        user = Subject(id="bob", roles=["viewer"])

        result = rp.authorized_filter(user, "documents:read")
        assert isinstance(result.expression, AllowAll)

    def test_no_roles_gets_deny_all(self):
        rp = PyAuthz()
        rp.add_role(Role(name="admin", permissions=["*:*"]))
        user = Subject(id="nobody", roles=[])

        result = rp.authorized_filter(user, "documents:read")
        assert isinstance(result.expression, DenyAll)

    def test_inherited_role_gets_allow_all(self):
        rp = PyAuthz()
        rp.add_role(Role(name="viewer", permissions=["documents:read"]))
        rp.add_role(Role(name="editor", permissions=["documents:write"], parents=["viewer"]))
        user = Subject(id="alice", roles=["editor"])

        result = rp.authorized_filter(user, "documents:read")
        assert isinstance(result.expression, AllowAll)


# --- authorized_filter() with explicit policies ---


class TestAuthorizedFilterPolicies:
    def test_simple_allow_policy_no_conditions(self):
        rp = PyAuthz()
        rp.add_role(Role(name="viewer", permissions=["documents:read"]))
        rp.add_policy(
            Policy(
                name="viewer-read",
                effect=Effect.ALLOW,
                roles=["viewer"],
                permissions=["documents:read"],
            )
        )
        user = Subject(id="alice", roles=["viewer"])

        result = rp.authorized_filter(user, "documents:read")
        # No conditions → AllowAll for this policy
        assert isinstance(result.expression, AllowAll)

    def test_allow_policy_with_owner_condition(self):
        rp = PyAuthz()
        rp.add_role(Role(name="editor", permissions=["documents:write"]))
        rp.add_policy(
            Policy(
                name="owner-edit",
                effect=Effect.ALLOW,
                roles=["editor"],
                permissions=["documents:write"],
                conditions=[
                    Condition(field="resource.owner_id", op="==", value="subject.id"),
                ],
            )
        )
        alice = Subject(id="alice", roles=["editor"])

        result = rp.authorized_filter(alice, "documents:write")
        # Should produce Compare(field="owner_id", op="==", value="alice")
        assert isinstance(result.expression, Compare)
        assert result.expression.field == "owner_id"
        assert result.expression.op == "=="
        assert result.expression.value == "alice"

    def test_allow_policy_with_multiple_conditions(self):
        rp = PyAuthz()
        rp.add_role(Role(name="analyst", permissions=["documents:read"]))
        rp.add_policy(
            Policy(
                name="classified-access",
                effect=Effect.ALLOW,
                roles=["analyst"],
                permissions=["documents:read"],
                conditions=[
                    Condition(
                        field="resource.attributes.classification", op="==", value="TOP_SECRET"
                    ),
                    Condition(field="resource.owner_id", op="==", value="subject.id"),
                ],
            )
        )
        alice = Subject(id="alice", roles=["analyst"])

        result = rp.authorized_filter(alice, "documents:read")
        assert isinstance(result.expression, And)
        assert len(result.expression.children) == 2

    def test_no_matching_policy_returns_deny_all(self):
        rp = PyAuthz()
        rp.add_role(Role(name="viewer", permissions=["documents:read"]))
        rp.add_policy(
            Policy(
                name="viewer-read",
                effect=Effect.ALLOW,
                roles=["viewer"],
                permissions=["documents:read"],
            )
        )
        # User has no roles that match
        user = Subject(id="bob", roles=["unknown"])

        result = rp.authorized_filter(user, "documents:read")
        assert isinstance(result.expression, DenyAll)

    def test_deny_policy_produces_not(self):
        rp = PyAuthz()
        rp.add_role(Role(name="viewer", permissions=["documents:read"]))
        rp.add_policy(
            Policy(
                name="allow-read",
                effect=Effect.ALLOW,
                roles=["viewer"],
                permissions=["documents:read"],
            )
        )
        rp.add_policy(
            Policy(
                name="deny-secret",
                effect=Effect.DENY,
                roles=["viewer"],
                permissions=["documents:read"],
                conditions=[
                    Condition(field="resource.attributes.classification", op="==", value="SECRET"),
                ],
            )
        )
        user = Subject(id="alice", roles=["viewer"])

        result = rp.authorized_filter(user, "documents:read")
        # deny-override: AllowAll AND NOT Compare(classification==SECRET)
        assert isinstance(result.expression, And)
        children = result.expression.children
        assert len(children) == 2
        assert isinstance(children[0], AllowAll)
        assert isinstance(children[1], Not)

    def test_multiple_allow_policies_produce_or(self):
        rp = PyAuthz()
        rp.add_role(Role(name="user", permissions=["documents:read"]))
        rp.add_policy(
            Policy(
                name="own-docs",
                effect=Effect.ALLOW,
                roles=["user"],
                permissions=["documents:read"],
                conditions=[
                    Condition(field="resource.owner_id", op="==", value="subject.id"),
                ],
            )
        )
        rp.add_policy(
            Policy(
                name="public-docs",
                effect=Effect.ALLOW,
                roles=["user"],
                permissions=["documents:read"],
                conditions=[
                    Condition(field="resource.attributes.public", op="==", value=True),
                ],
            )
        )
        alice = Subject(id="alice", roles=["user"])

        result = rp.authorized_filter(alice, "documents:read")
        assert isinstance(result.expression, Or)
        assert len(result.expression.children) == 2

    def test_environment_conditions_flagged_as_unpushable(self):
        rp = PyAuthz()
        rp.add_role(Role(name="user", permissions=["documents:read"]))
        rp.add_policy(
            Policy(
                name="business-hours",
                effect=Effect.ALLOW,
                roles=["user"],
                permissions=["documents:read"],
                conditions=[
                    Condition(field="resource.owner_id", op="==", value="subject.id"),
                    Condition(field="environment.hour", op=">=", value=9),
                ],
            )
        )
        alice = Subject(id="alice", roles=["user"])

        result = rp.authorized_filter(alice, "documents:read")
        assert len(result.unpushable_conditions) >= 1
        assert any("environment" in u.lower() for u in result.unpushable_conditions)


# --- allow-override combiner ---


class TestAllowOverrideCombiner:
    def test_allow_override_ignores_deny(self):
        rp = PyAuthz(combiner="allow-override")
        rp.add_role(Role(name="user", permissions=["documents:read"]))
        rp.add_policy(
            Policy(
                name="allow-read",
                effect=Effect.ALLOW,
                roles=["user"],
                permissions=["documents:read"],
            )
        )
        rp.add_policy(
            Policy(
                name="deny-secret",
                effect=Effect.DENY,
                roles=["user"],
                permissions=["documents:read"],
                conditions=[
                    Condition(field="resource.attributes.classification", op="==", value="SECRET"),
                ],
            )
        )
        user = Subject(id="alice", roles=["user"])

        result = rp.authorized_filter(user, "documents:read")
        # In allow-override, deny policies don't produce NOT clauses
        assert isinstance(result.expression, AllowAll)


# --- Raw SQL compiler tests ---


class TestRawSQLCompiler:
    def test_allow_all(self):
        from pyauthz.filtering.raw_sql import to_raw_sql

        where, params = to_raw_sql(AllowAll())
        assert where == "1=1"
        assert params == {}

    def test_deny_all(self):
        from pyauthz.filtering.raw_sql import to_raw_sql

        where, params = to_raw_sql(DenyAll())
        assert where == "1=0"
        assert params == {}

    def test_simple_compare(self):
        from pyauthz.filtering.raw_sql import to_raw_sql

        expr = Compare(field="owner_id", op="==", value="alice")
        where, params = to_raw_sql(expr)
        assert where == "owner_id = :p0"
        assert params == {"p0": "alice"}

    def test_compare_with_table_prefix(self):
        from pyauthz.filtering.raw_sql import to_raw_sql

        expr = Compare(field="owner_id", op="==", value="alice")
        where, params = to_raw_sql(expr, table="documents")
        assert where == "documents.owner_id = :p0"
        assert params == {"p0": "alice"}

    def test_and_expression(self):
        from pyauthz.filtering.raw_sql import to_raw_sql

        expr = And(
            children=[
                Compare(field="owner_id", op="==", value="alice"),
                Compare(field="type", op="==", value="doc"),
            ]
        )
        where, params = to_raw_sql(expr)
        assert where == "(owner_id = :p0 AND type = :p1)"
        assert params == {"p0": "alice", "p1": "doc"}

    def test_or_expression(self):
        from pyauthz.filtering.raw_sql import to_raw_sql

        expr = Or(
            children=[
                Compare(field="owner_id", op="==", value="alice"),
                Compare(field="owner_id", op="==", value="bob"),
            ]
        )
        where, params = to_raw_sql(expr)
        assert where == "(owner_id = :p0 OR owner_id = :p1)"
        assert params == {"p0": "alice", "p1": "bob"}

    def test_not_expression(self):
        from pyauthz.filtering.raw_sql import to_raw_sql

        expr = Not(child=Compare(field="classification", op="==", value="SECRET"))
        where, params = to_raw_sql(expr)
        assert where == "NOT (classification = :p0)"
        assert params == {"p0": "SECRET"}

    def test_in_operator(self):
        from pyauthz.filtering.raw_sql import to_raw_sql

        expr = Compare(field="status", op="in", value=["active", "pending"])
        where, params = to_raw_sql(expr)
        assert where == "status IN (:p0, :p1)"
        assert params == {"p0": "active", "p1": "pending"}

    def test_not_in_operator(self):
        from pyauthz.filtering.raw_sql import to_raw_sql

        expr = Compare(field="status", op="not_in", value=["deleted"])
        where, params = to_raw_sql(expr)
        assert where == "status NOT IN (:p0)"
        assert params == {"p0": "deleted"}

    def test_contains_operator(self):
        from pyauthz.filtering.raw_sql import to_raw_sql

        expr = Compare(field="name", op="contains", value="test")
        where, params = to_raw_sql(expr)
        assert where == "name LIKE :p0"
        assert params == {"p0": "%test%"}

    def test_matches_operator(self):
        from pyauthz.filtering.raw_sql import to_raw_sql

        expr = Compare(field="email", op="matches", value=".*@example.com")
        where, params = to_raw_sql(expr)
        assert where == "email REGEXP :p0"
        assert params == {"p0": ".*@example.com"}

    def test_comparison_operators(self):
        from pyauthz.filtering.raw_sql import to_raw_sql

        for op, sql_op in [("!=", "!="), (">", ">"), ("<", "<"), (">=", ">="), ("<=", "<=")]:
            expr = Compare(field="priority", op=op, value=5)
            where, params = to_raw_sql(expr)
            assert sql_op in where
            assert params["p0"] == 5

    def test_complex_nested_expression(self):
        from pyauthz.filtering.raw_sql import to_raw_sql

        expr = And(
            children=[
                Or(
                    children=[
                        Compare(field="owner_id", op="==", value="alice"),
                        Compare(field="public", op="==", value=True),
                    ]
                ),
                Not(child=Compare(field="classification", op="==", value="SECRET")),
            ]
        )
        where, params = to_raw_sql(expr)
        assert "AND" in where
        assert "OR" in where
        assert "NOT" in where
        assert len(params) == 3

    def test_json_field_access(self):
        from pyauthz.filtering.raw_sql import to_raw_sql

        expr = Compare(field="attributes.classification", op="==", value="SECRET")
        where, params = to_raw_sql(expr)
        assert "->>" in where
        assert params["p0"] == "SECRET"


# --- SQLAlchemy compiler tests ---


class TestSQLAlchemyCompiler:
    @pytest.fixture(autouse=True)
    def _check_sqlalchemy(self):
        pytest.importorskip("sqlalchemy")

    def _make_model(self):
        from sqlalchemy import Column, Integer, String
        from sqlalchemy.orm import DeclarativeBase

        class Base(DeclarativeBase):
            pass

        class Document(Base):
            __tablename__ = "documents"
            id = Column(Integer, primary_key=True)
            owner_id = Column(String)
            classification = Column(String)
            status = Column(String)

        return Document

    def test_allow_all(self):
        from pyauthz.filtering.sql import to_sqlalchemy_filter

        model = self._make_model()
        clause = to_sqlalchemy_filter(AllowAll(), model)
        assert str(clause) == "true"

    def test_deny_all(self):
        from pyauthz.filtering.sql import to_sqlalchemy_filter

        model = self._make_model()
        clause = to_sqlalchemy_filter(DenyAll(), model)
        assert str(clause) == "false"

    def test_simple_compare(self):
        from pyauthz.filtering.sql import to_sqlalchemy_filter

        model = self._make_model()
        expr = Compare(field="owner_id", op="==", value="alice")
        clause = to_sqlalchemy_filter(expr, model)
        compiled = str(clause.compile(compile_kwargs={"literal_binds": True}))
        assert "owner_id" in compiled
        assert "'alice'" in compiled

    def test_and_expression(self):
        from pyauthz.filtering.sql import to_sqlalchemy_filter

        model = self._make_model()
        expr = And(
            children=[
                Compare(field="owner_id", op="==", value="alice"),
                Compare(field="status", op="==", value="active"),
            ]
        )
        clause = to_sqlalchemy_filter(expr, model)
        compiled = str(clause.compile(compile_kwargs={"literal_binds": True}))
        assert "AND" in compiled

    def test_or_expression(self):
        from pyauthz.filtering.sql import to_sqlalchemy_filter

        model = self._make_model()
        expr = Or(
            children=[
                Compare(field="owner_id", op="==", value="alice"),
                Compare(field="owner_id", op="==", value="bob"),
            ]
        )
        clause = to_sqlalchemy_filter(expr, model)
        compiled = str(clause.compile(compile_kwargs={"literal_binds": True}))
        assert "OR" in compiled

    def test_not_expression(self):
        from pyauthz.filtering.sql import to_sqlalchemy_filter

        model = self._make_model()
        expr = Not(child=Compare(field="classification", op="==", value="SECRET"))
        clause = to_sqlalchemy_filter(expr, model)
        compiled = str(clause.compile(compile_kwargs={"literal_binds": True}))
        assert "NOT" in compiled or "classification" in compiled

    def test_in_operator(self):
        from pyauthz.filtering.sql import to_sqlalchemy_filter

        model = self._make_model()
        expr = Compare(field="status", op="in", value=["active", "pending"])
        clause = to_sqlalchemy_filter(expr, model)
        compiled = str(clause.compile(compile_kwargs={"literal_binds": True}))
        assert "IN" in compiled

    def test_invalid_field_raises(self):
        from pyauthz.filtering.sql import to_sqlalchemy_filter

        model = self._make_model()
        expr = Compare(field="nonexistent", op="==", value="test")
        with pytest.raises(ValueError, match="has no column"):
            to_sqlalchemy_filter(expr, model)


# --- Performance benchmarks ---


class TestPerformanceBenchmarks:
    def test_simple_rbac_under_100us(self):
        """Simple RBAC check should complete in under 100 microseconds."""
        import time

        rp = PyAuthz()
        rp.add_role(Role(name="admin", permissions=["*:*"]))
        user = Subject(id="alice", roles=["admin"])
        resource = Resource(id="doc-1", type="documents")

        # Warm up
        for _ in range(100):
            rp.is_allowed(user, "read", resource)

        # Benchmark
        iterations = 1000
        start = time.perf_counter_ns()
        for _ in range(iterations):
            rp.is_allowed(user, "read", resource)
        elapsed_ns = time.perf_counter_ns() - start

        avg_us = elapsed_ns / iterations / 1000
        # Target: <100μs per check. Use generous margin for CI.
        assert avg_us < 500, (
            f"Simple RBAC took {avg_us:.1f}μs (target <100μs, CI threshold <500μs)"
        )

    def test_abac_under_500us(self):
        """ABAC check with conditions should complete in under 500 microseconds."""
        import time

        rp = PyAuthz()
        rp.add_role(Role(name="editor", permissions=["documents:write"]))
        rp.add_policy(
            Policy(
                name="owner-edit",
                effect=Effect.ALLOW,
                roles=["editor"],
                permissions=["documents:write"],
                conditions=[
                    Condition(field="resource.owner_id", op="==", value="subject.id"),
                ],
            )
        )
        user = Subject(id="alice", roles=["editor"])
        resource = Resource(id="doc-1", type="documents", owner_id="alice")

        # Warm up
        for _ in range(100):
            rp.is_allowed(user, "write", resource)

        # Benchmark
        iterations = 1000
        start = time.perf_counter_ns()
        for _ in range(iterations):
            rp.is_allowed(user, "write", resource)
        elapsed_ns = time.perf_counter_ns() - start

        avg_us = elapsed_ns / iterations / 1000
        # Target: <500μs per check. Use generous margin for CI.
        assert avg_us < 2000, f"ABAC took {avg_us:.1f}μs (target <500μs, CI threshold <2000μs)"

    def test_filter_compilation_performance(self):
        """Filter compilation should be fast."""
        import time

        rp = PyAuthz()
        rp.add_role(Role(name="editor", permissions=["documents:write"]))
        rp.add_policy(
            Policy(
                name="owner-edit",
                effect=Effect.ALLOW,
                roles=["editor"],
                permissions=["documents:write"],
                conditions=[
                    Condition(field="resource.owner_id", op="==", value="subject.id"),
                ],
            )
        )
        user = Subject(id="alice", roles=["editor"])

        # Warm up
        for _ in range(100):
            rp.authorized_filter(user, "documents:write")

        # Benchmark
        iterations = 1000
        start = time.perf_counter_ns()
        for _ in range(iterations):
            rp.authorized_filter(user, "documents:write")
        elapsed_ns = time.perf_counter_ns() - start

        avg_us = elapsed_ns / iterations / 1000
        assert avg_us < 2000, f"Filter compilation took {avg_us:.1f}μs"
