"""Tests for app.core.permissions — RBAC roles, get_current_user, RequireRole"""

import pytest
from unittest.mock import AsyncMock, MagicMock, patch


class TestRoleHierarchy:
    def test_enum_values(self):
        from app.core.permissions import TenantRole
        assert TenantRole.OWNER.value == "owner"
        assert TenantRole.ADMIN.value == "admin"
        assert TenantRole.EDITOR.value == "editor"
        assert TenantRole.VIEWER.value == "viewer"

    def test_hierarchy_levels(self):
        from app.core.permissions import ROLE_HIERARCHY, TenantRole
        assert ROLE_HIERARCHY[TenantRole.VIEWER] == 1
        assert ROLE_HIERARCHY[TenantRole.EDITOR] == 2
        assert ROLE_HIERARCHY[TenantRole.ADMIN] == 3
        assert ROLE_HIERARCHY[TenantRole.OWNER] == 4
        assert ROLE_HIERARCHY[TenantRole.ADMIN] > ROLE_HIERARCHY[TenantRole.EDITOR]


@pytest.mark.asyncio
async def test_get_current_user_no_credentials():
    from app.core.permissions import get_current_user
    with pytest.raises(Exception) as exc:
        await get_current_user(None)
    assert exc.value.status_code == 401


@pytest.mark.asyncio
async def test_get_current_user_invalid_token():
    from app.core.permissions import get_current_user
    from fastapi.security import HTTPAuthorizationCredentials

    mock_creds = MagicMock(spec=HTTPAuthorizationCredentials)
    mock_creds.credentials = "invalid-token"

    with patch("app.core.permissions.decode_token", side_effect=Exception("bad token")):
        with pytest.raises(Exception) as exc:
            await get_current_user(mock_creds)
    assert exc.value.status_code == 401


@pytest.mark.asyncio
async def test_get_current_user_wrong_token_type():
    from app.core.permissions import get_current_user
    from fastapi.security import HTTPAuthorizationCredentials

    mock_creds = MagicMock(spec=HTTPAuthorizationCredentials)
    mock_creds.credentials = "refresh-token"

    with patch("app.core.permissions.decode_token", return_value={"type": "refresh", "jti": "abc"}):
        with pytest.raises(Exception) as exc:
            await get_current_user(mock_creds)
    assert exc.value.status_code == 401


@pytest.mark.asyncio
async def test_get_current_user_blacklisted():
    from app.core.permissions import get_current_user
    from fastapi.security import HTTPAuthorizationCredentials

    mock_creds = MagicMock(spec=HTTPAuthorizationCredentials)
    mock_creds.credentials = "valid-token"

    with patch("app.core.permissions.decode_token", return_value={"type": "access", "jti": "blacklisted-jti", "tid": "tenant-1"}):
        with patch("app.core.token_store.token_store") as mock_store:
            mock_store.is_blacklisted = AsyncMock(return_value=True)
            with pytest.raises(Exception) as exc:
                await get_current_user(mock_creds)
    assert exc.value.status_code == 401


@pytest.mark.asyncio
async def test_get_current_user_success():
    from app.core.permissions import get_current_user
    from fastapi.security import HTTPAuthorizationCredentials

    mock_creds = MagicMock(spec=HTTPAuthorizationCredentials)
    mock_creds.credentials = "valid-token"

    with patch("app.core.permissions.decode_token", return_value={"type": "access", "sub": "user-1", "jti": "abc", "tid": "tenant-1"}):
        with patch("app.core.token_store.token_store") as mock_store:
            mock_store.is_blacklisted = AsyncMock(return_value=False)
            result = await get_current_user(mock_creds)
    assert result["sub"] == "user-1"
    assert result["tid"] == "tenant-1"


@pytest.mark.asyncio
async def test_get_current_user_http_exception_passthrough():
    """HTTPException raised by decode_token should propagate directly"""
    from app.core.permissions import get_current_user
    from fastapi import HTTPException
    from fastapi.security import HTTPAuthorizationCredentials

    mock_creds = MagicMock(spec=HTTPAuthorizationCredentials)
    mock_creds.credentials = "token"

    with patch("app.core.permissions.decode_token", side_effect=HTTPException(status_code=401, detail="Custom")):
        with pytest.raises(HTTPException) as exc:
            await get_current_user(mock_creds)
    assert exc.value.status_code == 401
    assert exc.value.detail == "Custom"


@pytest.mark.asyncio
async def test_require_role_no_tenant():
    from app.core.permissions import RequireRole, TenantRole

    checker = RequireRole(TenantRole.VIEWER)
    mock_user = {"sub": "user-1"}

    from app.core.tenant_context import tenant_ctx
    token = tenant_ctx.set(None)
    try:
        with pytest.raises(Exception) as exc:
            await checker(_user=mock_user, db=AsyncMock())
        assert exc.value.status_code == 400
    finally:
        tenant_ctx.reset(token)


@pytest.mark.asyncio
async def test_require_role_not_member():
    from app.core.permissions import RequireRole, TenantRole
    from app.core.tenant_context import tenant_ctx

    checker = RequireRole(TenantRole.VIEWER)
    mock_user = {"sub": "550e8400-e29b-41d4-a716-446655440000"}

    mock_db = AsyncMock()
    mock_scalar = MagicMock()
    mock_scalar.scalar.return_value = None  # not a member
    mock_db.execute.return_value = mock_scalar

    token = tenant_ctx.set("550e8400-e29b-41d4-a716-446655440001")
    try:
        with pytest.raises(Exception) as exc:
            await checker(_user=mock_user, db=mock_db)
        assert exc.value.status_code == 403
    finally:
        tenant_ctx.reset(token)


@pytest.mark.asyncio
async def test_require_role_insufficient():
    from app.core.permissions import RequireRole, TenantRole
    from app.core.tenant_context import tenant_ctx

    checker = RequireRole(TenantRole.ADMIN)
    mock_user = {"sub": "550e8400-e29b-41d4-a716-446655440000"}

    mock_db = AsyncMock()
    mock_scalar = MagicMock()
    mock_scalar.scalar.return_value = "viewer"
    mock_db.execute.return_value = mock_scalar

    token = tenant_ctx.set("550e8400-e29b-41d4-a716-446655440001")
    try:
        with pytest.raises(Exception) as exc:
            await checker(_user=mock_user, db=mock_db)
        assert exc.value.status_code == 403
    finally:
        tenant_ctx.reset(token)


@pytest.mark.asyncio
async def test_require_role_sufficient():
    from app.core.permissions import RequireRole, TenantRole
    from app.core.tenant_context import tenant_ctx

    checker = RequireRole(TenantRole.VIEWER)
    mock_user = {"sub": "550e8400-e29b-41d4-a716-446655440000"}

    mock_db = AsyncMock()
    mock_scalar = MagicMock()
    mock_scalar.scalar.return_value = "admin"
    mock_db.execute.return_value = mock_scalar

    token = tenant_ctx.set("550e8400-e29b-41d4-a716-446655440001")
    try:
        result = await checker(_user=mock_user, db=mock_db)
        assert result == "admin"
    finally:
        tenant_ctx.reset(token)


@pytest.mark.asyncio
async def test_require_role_invalid_role_in_db():
    from app.core.permissions import RequireRole, TenantRole
    from app.core.tenant_context import tenant_ctx

    checker = RequireRole(TenantRole.VIEWER)
    mock_user = {"sub": "550e8400-e29b-41d4-a716-446655440000"}

    mock_db = AsyncMock()
    mock_scalar = MagicMock()
    mock_scalar.scalar.return_value = "superadmin"  # not in TenantRole enum
    mock_db.execute.return_value = mock_scalar

    token = tenant_ctx.set("550e8400-e29b-41d4-a716-446655440001")
    try:
        with pytest.raises(Exception) as exc:
            await checker(_user=mock_user, db=mock_db)
        assert exc.value.status_code == 403
    finally:
        tenant_ctx.reset(token)
