# #region Test.Schemas.Auth [C:2] [TYPE Module] [SEMANTICS test,schema,auth,validation]
# @BRIEF Tests for schemas/auth.py — Token, TokenData, PermissionSchema, RoleSchema,
#        RoleCreate, RoleUpdate, ADGroupMappingSchema, ADGroupMappingCreate, UserBase,
#        UserCreate, UserUpdate, User.
# @RELATION BINDS_TO -> [AuthSchemas]
# @TEST_EDGE: missing_field -> validation catches missing required fields
# @TEST_EDGE: invalid_type -> field_validators reject bad inputs
# @TEST_EDGE: serialization -> round-trip via model_dump/model_validate
# @TEST_EDGE: password_strength -> validator checks uppercase, lowercase, digit, length

from datetime import datetime, timezone
import pytest
from pydantic import ValidationError

from src.schemas.auth import (
    ADGroupMappingCreate,
    ADGroupMappingSchema,
    PermissionSchema,
    RoleCreate,
    RoleSchema,
    RoleUpdate,
    Token,
    TokenData,
    User,
    UserBase,
    UserCreate,
    UserUpdate,
)


class TestToken:
    """Token — access_token and token_type."""

    def test_minimal(self):
        t = Token(access_token="jwt", token_type="bearer")
        assert t.access_token == "jwt"
        assert t.token_type == "bearer"

    def test_missing_raises(self):
        with pytest.raises(ValidationError):
            Token()

    def test_serialize_roundtrip(self):
        t = Token(access_token="tok", token_type="Bearer")
        data = t.model_dump()
        restored = Token.model_validate(data)
        assert restored.access_token == "tok"


class TestTokenData:
    """TokenData — username optional, scopes default empty."""

    def test_defaults(self):
        td = TokenData()
        assert td.username is None
        assert td.scopes == []

    def test_with_values(self):
        td = TokenData(username="alice", scopes=["admin"])
        assert td.username == "alice"
        assert td.scopes == ["admin"]

    def test_none_username(self):
        td = TokenData(username=None)
        assert td.username is None

    def test_serialize_roundtrip(self):
        td = TokenData(username="bob", scopes=["read", "write"])
        data = td.model_dump()
        restored = TokenData.model_validate(data)
        assert restored.username == "bob"
        assert "read" in restored.scopes


class TestPermissionSchema:
    """PermissionSchema — resource/action with optional id and from_attributes."""

    def test_minimal(self):
        p = PermissionSchema(resource="dashboard", action="READ")
        assert p.resource == "dashboard"
        assert p.action == "READ"
        assert p.id is None

    def test_with_id(self):
        p = PermissionSchema(id="perm-1", resource="dataset", action="WRITE")
        assert p.id == "perm-1"

    def test_serialize_roundtrip(self):
        p = PermissionSchema(resource="dashboard", action="READ")
        data = p.model_dump()
        restored = PermissionSchema.model_validate(data)
        assert restored.resource == "dashboard"

    def test_from_attributes(self):
        """ConfigDict(from_attributes=True) allows ORM mode."""
        class FakeORM:
            id = "perm-1"
            resource = "dashboard"
            action = "READ"
        p = PermissionSchema.model_validate(FakeORM())
        assert p.id == "perm-1"
        assert p.resource == "dashboard"


class TestRoleSchema:
    """RoleSchema — role with nested permissions."""

    def test_minimal(self):
        r = RoleSchema(id="role-1", name="Admin")
        assert r.description is None
        assert r.permissions == []

    def test_with_permissions(self):
        perms = [PermissionSchema(resource="dashboard", action="READ")]
        r = RoleSchema(id="role-1", name="Editor", description="Can edit", permissions=perms)
        assert len(r.permissions) == 1

    def test_serialize_roundtrip(self):
        r = RoleSchema(id="role-1", name="Viewer")
        data = r.model_dump()
        restored = RoleSchema.model_validate(data)
        assert restored.name == "Viewer"

    def test_from_attributes(self):
        class FakeORM:
            id = "role-1"
            name = "Admin"
            description = "Full access"
            permissions = []
        r = RoleSchema.model_validate(FakeORM())
        assert r.id == "role-1"
        assert r.description == "Full access"


class TestRoleCreate:
    """RoleCreate — name required, optional description and permissions."""

    def test_minimal(self):
        r = RoleCreate(name="Editor")
        assert r.description is None
        assert r.permissions == []

    def test_with_permissions(self):
        r = RoleCreate(name="Admin", description="Full", permissions=["dashboard:READ", "dataset:WRITE"])
        assert len(r.permissions) == 2

    def test_missing_name_raises(self):
        with pytest.raises(ValidationError):
            RoleCreate()

    def test_serialize_roundtrip(self):
        r = RoleCreate(name="Viewer", permissions=["dashboard:READ"])
        data = r.model_dump()
        restored = RoleCreate.model_validate(data)
        assert restored.name == "Viewer"


class TestRoleUpdate:
    """RoleUpdate — all fields optional for partial update."""

    def test_defaults(self):
        r = RoleUpdate()
        assert r.name is None
        assert r.description is None
        assert r.permissions is None

    def test_partial(self):
        r = RoleUpdate(name="Updated")
        assert r.name == "Updated"
        assert r.description is None

    def test_serialize_roundtrip(self):
        r = RoleUpdate(name="New", description="desc", permissions=["a:READ"])
        data = r.model_dump()
        restored = RoleUpdate.model_validate(data)
        assert restored.name == "New"
        assert restored.description == "desc"


class TestADGroupMappingSchema:
    """ADGroupMappingSchema — id, ad_group, role_id with from_attributes."""

    def test_minimal(self):
        m = ADGroupMappingSchema(id="m-1", ad_group="DOMAIN\\Users", role_id="role-1")
        assert m.ad_group == "DOMAIN\\Users"

    def test_serialize_roundtrip(self):
        m = ADGroupMappingSchema(id="m-1", ad_group="CN=Group,DC=corp", role_id="role-1")
        data = m.model_dump()
        restored = ADGroupMappingSchema.model_validate(data)
        assert restored.ad_group == "CN=Group,DC=corp"

    def test_from_attributes(self):
        class FakeORM:
            id = "m-1"
            ad_group = "DOMAIN\\Users"
            role_id = "role-1"
        m = ADGroupMappingSchema.model_validate(FakeORM())
        assert m.ad_group == "DOMAIN\\Users"


class TestADGroupMappingCreate:
    """ADGroupMappingCreate — ad_group with field_validator."""

    def test_valid_ad_group(self):
        m = ADGroupMappingCreate(ad_group="DOMAIN\\Users", role_id="role-1")
        assert m.ad_group == "DOMAIN\\Users"

    def test_valid_dn_format(self):
        m = ADGroupMappingCreate(ad_group="CN=MyGroup,DC=example,DC=com", role_id="role-1")
        assert "CN=MyGroup" in m.ad_group

    def test_empty_ad_group_raises(self):
        with pytest.raises(ValidationError, match="must not be empty"):
            ADGroupMappingCreate(ad_group="", role_id="role-1")

    def test_whitespace_only_raises(self):
        with pytest.raises(ValidationError):
            ADGroupMappingCreate(ad_group="   ", role_id="role-1")

    def test_invalid_chars_raises(self):
        with pytest.raises(ValidationError, match="invalid characters"):
            ADGroupMappingCreate(ad_group="bad group!", role_id="role-1")

    def test_strips_whitespace(self):
        m = ADGroupMappingCreate(ad_group="  DOMAIN\\Users  ", role_id="role-1")
        assert m.ad_group == "DOMAIN\\Users"

    def test_serialize_roundtrip(self):
        m = ADGroupMappingCreate(ad_group="DOMAIN\\Users", role_id="role-1")
        data = m.model_dump()
        restored = ADGroupMappingCreate.model_validate(data)
        assert restored.ad_group == "DOMAIN\\Users"


class TestUserBase:
    """UserBase — username, optional email, is_active default True."""

    def test_minimal(self):
        u = UserBase(username="alice")
        assert u.email is None
        assert u.is_active is True

    def test_with_email(self):
        u = UserBase(username="alice", email="alice@example.com")
        assert u.email == "alice@example.com"

    def test_inactive(self):
        u = UserBase(username="bob", is_active=False)
        assert u.is_active is False

    def test_serialize_roundtrip(self):
        u = UserBase(username="alice", email="a@b.com")
        data = u.model_dump()
        restored = UserBase.model_validate(data)
        assert restored.username == "alice"
        assert restored.email == "a@b.com"


class TestUserCreate:
    """UserCreate — extends UserBase with password validation and roles."""

    def test_valid_password(self):
        u = UserCreate(username="alice", password="ValidPass1")
        assert u.password == "ValidPass1"
        assert u.roles == []

    def test_too_short_raises(self):
        with pytest.raises(ValidationError, match="at least 8"):
            UserCreate(username="alice", password="Ab1")

    def test_no_uppercase_raises(self):
        with pytest.raises(ValidationError, match="uppercase"):
            UserCreate(username="alice", password="lowercase1")

    def test_no_lowercase_raises(self):
        with pytest.raises(ValidationError, match="lowercase"):
            UserCreate(username="alice", password="UPPERCASE1")

    def test_no_digit_raises(self):
        with pytest.raises(ValidationError, match="digit"):
            UserCreate(username="alice", password="Uppercase")

    def test_with_roles(self):
        u = UserCreate(username="alice", password="ValidPass1", roles=["admin"])
        assert u.roles == ["admin"]

    def test_serialize_roundtrip(self):
        u = UserCreate(username="alice", password="ValidPass1", email="a@b.com")
        data = u.model_dump()
        restored = UserCreate.model_validate(data)
        assert restored.username == "alice"
        assert restored.email == "a@b.com"


class TestUserUpdate:
    """UserUpdate — all fields optional for partial update."""

    def test_defaults(self):
        u = UserUpdate()
        assert u.email is None
        assert u.password is None
        assert u.is_active is None
        assert u.roles is None

    def test_partial_email(self):
        u = UserUpdate(email="new@example.com")
        assert u.email == "new@example.com"

    def test_serialize_roundtrip(self):
        u = UserUpdate(email="a@b.com", is_active=False)
        data = u.model_dump()
        restored = UserUpdate.model_validate(data)
        assert restored.is_active is False


class TestUser:
    """User — full user response with id, auth_source, timestamps, roles."""

    def make_user(self, **overrides):
        ts = datetime.now(timezone.utc)
        defaults = dict(
            id="u-1", username="alice", auth_source="database",
            created_at=ts, last_login=None, roles=[],
        )
        defaults.update(overrides)
        return User(**defaults)

    def test_required_fields(self):
        u = self.make_user()
        assert u.id == "u-1"
        assert u.username == "alice"
        assert u.auth_source == "database"

    def test_with_last_login(self):
        ts = datetime.now(timezone.utc)
        u = self.make_user(last_login=ts)
        assert u.last_login is not None

    def test_with_roles(self):
        role = RoleSchema(id="r-1", name="Admin")
        u = self.make_user(roles=[role])
        assert len(u.roles) == 1
        assert u.roles[0].name == "Admin"

    def test_serialize_roundtrip(self):
        u = self.make_user()
        data = u.model_dump()
        restored = User.model_validate(data)
        assert restored.id == "u-1"
        assert restored.username == "alice"

    def test_from_attributes(self):
        class FakeORM:
            id = "u-1"
            username = "alice"
            email = "a@b.com"
            is_active = True
            auth_source = "ldap"
            created_at = datetime.now(timezone.utc)
            last_login = None
            roles = []
        u = User.model_validate(FakeORM())
        assert u.auth_source == "ldap"
# #endregion Test.Schemas.Auth
