"""Tests for Pydantic data models."""

import pytest
from pydantic import ValidationError

from pyauthz import (
    AccessRequest,
    Condition,
    Decision,
    Effect,
    Permission,
    Policy,
    Resource,
    Role,
    Subject,
)


class TestPermission:
    def test_from_string(self):
        perm = Permission.from_string("documents:read")
        assert perm.resource_type == "documents"
        assert perm.action == "read"

    def test_from_string_wildcard(self):
        perm = Permission.from_string("*:*")
        assert perm.resource_type == "*"
        assert perm.action == "*"

    def test_from_string_no_colon_raises(self):
        with pytest.raises(ValueError, match="must be"):
            Permission.from_string("invalid")

    def test_matches_exact(self):
        perm = Permission.from_string("documents:read")
        assert perm.matches("documents", "read") is True
        assert perm.matches("documents", "write") is False
        assert perm.matches("users", "read") is False

    def test_matches_wildcard_action(self):
        perm = Permission.from_string("documents:*")
        assert perm.matches("documents", "read") is True
        assert perm.matches("documents", "write") is True
        assert perm.matches("users", "read") is False

    def test_matches_wildcard_resource(self):
        perm = Permission.from_string("*:read")
        assert perm.matches("documents", "read") is True
        assert perm.matches("users", "read") is True
        assert perm.matches("users", "write") is False

    def test_matches_wildcard_both(self):
        perm = Permission.from_string("*:*")
        assert perm.matches("anything", "anything") is True

    def test_str(self):
        perm = Permission.from_string("documents:read")
        assert str(perm) == "documents:read"

    def test_hash_and_eq(self):
        a = Permission.from_string("documents:read")
        b = Permission.from_string("documents:read")
        c = Permission.from_string("documents:write")
        assert a == b
        assert a != c
        assert hash(a) == hash(b)
        assert {a, b} == {a}

    def test_empty_resource_type_raises(self):
        with pytest.raises(ValidationError):
            Permission(resource_type="", action="read")

    def test_empty_action_raises(self):
        with pytest.raises(ValidationError):
            Permission(resource_type="documents", action="")


class TestEffect:
    def test_values(self):
        assert Effect.ALLOW == "allow"
        assert Effect.DENY == "deny"

    def test_from_string(self):
        assert Effect("allow") == Effect.ALLOW
        assert Effect("deny") == Effect.DENY


class TestSubject:
    def test_minimal(self):
        s = Subject(id="alice")
        assert s.id == "alice"
        assert s.roles == []
        assert s.attributes == {}

    def test_full(self):
        s = Subject(id="alice", roles=["admin"], attributes={"dept": "eng"})
        assert s.roles == ["admin"]
        assert s.attributes["dept"] == "eng"


class TestResource:
    def test_minimal(self):
        r = Resource(id="doc-1", type="documents")
        assert r.id == "doc-1"
        assert r.type == "documents"
        assert r.owner_id is None

    def test_with_owner(self):
        r = Resource(id="doc-1", type="documents", owner_id="alice")
        assert r.owner_id == "alice"


class TestRole:
    def test_minimal(self):
        role = Role(name="admin")
        assert role.name == "admin"
        assert role.permissions == []
        assert role.parents == []

    def test_with_permissions(self):
        role = Role(name="editor", permissions=["documents:write", "documents:read"])
        assert len(role.permissions) == 2


class TestCondition:
    def test_create(self):
        c = Condition(field="resource.owner_id", op="==", value="subject.id")
        assert c.field == "resource.owner_id"
        assert c.op == "=="
        assert c.value == "subject.id"

    def test_invalid_op_raises(self):
        with pytest.raises(ValidationError):
            Condition(field="x", op="invalid_op", value="y")


class TestPolicy:
    def test_minimal(self):
        p = Policy(name="test", effect=Effect.ALLOW)
        assert p.name == "test"
        assert p.effect == Effect.ALLOW
        assert p.roles == []
        assert p.permissions == []
        assert p.conditions == []
        assert p.priority == 0
        assert p.enabled is True

    def test_full(self):
        p = Policy(
            name="owner-edit",
            description="Owners can edit",
            effect=Effect.ALLOW,
            roles=["editor"],
            permissions=["documents:write"],
            conditions=[Condition(field="resource.owner_id", op="==", value="subject.id")],
            priority=10,
            enabled=True,
        )
        assert len(p.conditions) == 1
        assert p.priority == 10


class TestAccessRequest:
    def test_create(self):
        req = AccessRequest(
            subject=Subject(id="alice"),
            action="documents:read",
            resource=Resource(id="doc-1", type="documents"),
        )
        assert req.subject.id == "alice"
        assert req.action == "documents:read"
        assert req.environment == {}


class TestDecision:
    def test_explain_allowed(self):
        decision = Decision(
            allowed=True,
            effect=Effect.ALLOW,
            request=AccessRequest(
                subject=Subject(id="alice"),
                action="documents:read",
                resource=Resource(id="doc-1", type="documents"),
            ),
            matched_policies=["admin-all"],
            reasoning=["Policy 'admin-all' allows access"],
        )
        explanation = decision.explain()
        assert "ALLOWED" in explanation
        assert "alice" in explanation
        assert "documents:read" in explanation

    def test_explain_denied(self):
        decision = Decision(
            allowed=False,
            effect=Effect.DENY,
            request=AccessRequest(
                subject=Subject(id="bob"),
                action="pii:read",
                resource=Resource(id="ssn-1", type="pii"),
            ),
            reasoning=["no matching policy"],
        )
        assert "DENIED" in decision.explain()
        assert "no matching policy" in decision.explain()
