"""
Integration tests for Role-Based Access Control (RBAC) and permission resolution.

Verifies:
- Superadmin bypass: superadmin accesses any endpoint regardless of permissions
- Role grants: user with role permission passes authorization
- Insufficient permissions: returns HTTP 403 Forbidden
- User overrides:
    - Denial override blocks access even if role permits
    - Grant override allows access even if role denies
- Unauthenticated requests: returns HTTP 401 Unauthorized
"""

import pytest
import pytest_asyncio
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.permissions import PermissionCode, resolve_user_permissions
from app.core.security import create_access_token, hash_password
from app.models.lab import Lab
from app.models.user import (
    User,
    UserPermissionOverride,
    UserRole,
)
from app.repositories.permission_repository import PermissionRepository


@pytest_asyncio.fixture(autouse=True)
async def seed_perms(db_session: AsyncSession) -> None:
    """Ensure permission records and role defaults exist for test cases."""
    repo = PermissionRepository()
    await repo.seed_permissions(db_session)


@pytest_asyncio.fixture
async def rbac_lab(db_session: AsyncSession) -> Lab:
    """Create and commit a test lab for RBAC tests."""
    lab = Lab(name="RBAC Test Lab", address="Lalitpur, Nepal")
    db_session.add(lab)
    await db_session.commit()
    await db_session.refresh(lab)
    return lab


@pytest_asyncio.fixture
async def superadmin_user(db_session: AsyncSession, rbac_lab: Lab) -> User:
    """Create and commit a superadmin user."""
    user = User(
        lab_id=rbac_lab.id,
        name="Super Admin",
        phone="9800000001",
        email="superadmin@munal.test",
        role=UserRole.SUPERADMIN,
        hashed_password=hash_password("SuperSecret123!"),
        is_active=True,
    )
    db_session.add(user)
    await db_session.commit()
    await db_session.refresh(user)
    return user


@pytest_asyncio.fixture
async def receptionist_user(db_session: AsyncSession, rbac_lab: Lab) -> User:
    """Receptionist has patient:write, but NOT lab:write or user:write."""
    user = User(
        lab_id=rbac_lab.id,
        name="Receptionist Sita",
        phone="9800000002",
        email="sita@munal.test",
        role=UserRole.RECEPTIONIST,
        hashed_password=hash_password("Receptionist123!"),
        is_active=True,
    )
    db_session.add(user)
    await db_session.commit()
    await db_session.refresh(user)
    return user


@pytest.mark.asyncio
class TestRBACAuthorization:
    """Test suite for endpoint authorization via RBAC."""

    async def test_unauthenticated_request_fails_with_401(self, client: AsyncClient) -> None:
        """Calling protected endpoint without auth token must return 401."""
        response = await client.get("/api/v1/labs")
        assert response.status_code == 401
        body = response.json()
        assert body["success"] is False
        assert body["error"]["code"] == "UNAUTHORIZED"

    async def test_superadmin_bypasses_all_checks(self, client: AsyncClient, superadmin_user: User) -> None:
        """Superadmin can access any route, reaching the handler (200 OK) rather than 403."""
        token = create_access_token(
            subject=str(superadmin_user.id),
            extra_claims={"role": superadmin_user.role.value, "lab_id": str(superadmin_user.lab_id)},
        )
        response = await client.get(
            "/api/v1/labs",
            headers={"Authorization": f"Bearer {token}"},
        )
        # Reaches handler without 403 Forbidden
        assert response.status_code == 200

    async def test_global_permission_toggle_is_audited(
        self, client: AsyncClient, superadmin_user: User
    ) -> None:
        token = create_access_token(
            subject=str(superadmin_user.id),
            extra_claims={"role": superadmin_user.role.value, "lab_id": str(superadmin_user.lab_id)},
        )
        headers = {"Authorization": f"Bearer {token}"}
        catalog = await client.get("/api/v1/rbac/permissions", headers=headers)
        assert catalog.status_code == 200
        permission = catalog.json()["data"][0]
        updated = await client.patch(
            f"/api/v1/rbac/permissions/{permission['id']}/toggle",
            json={"is_active": not permission["is_active"]},
            headers=headers,
        )
        assert updated.status_code == 200
        audit = await client.get(
            f"/api/v1/audit-logs?lab_id={superadmin_user.lab_id}&search=global feature availability",
            headers=headers,
        )
        assert audit.status_code == 200
        assert any(entry["action"] == "PERMISSION_FEATURE_TOGGLED" for entry in audit.json()["data"])
        await client.patch(
            f"/api/v1/rbac/permissions/{permission['id']}/toggle",
            json={"is_active": permission["is_active"]},
            headers=headers,
        )

    async def test_role_granted_permission_allowed(
        self, client: AsyncClient, receptionist_user: User
    ) -> None:
        """Receptionist has PATIENT_READ by default; accessing /patients reaches handler."""
        token = create_access_token(
            subject=str(receptionist_user.id),
            extra_claims={"role": receptionist_user.role.value, "lab_id": str(receptionist_user.lab_id)},
        )
        response = await client.get(
            "/api/v1/patients",
            headers={"Authorization": f"Bearer {token}"},
        )
        assert response.status_code == 200
        assert response.json()["success"] is True

    async def test_role_denied_permission_returns_403(
        self, client: AsyncClient, receptionist_user: User
    ) -> None:
        """Receptionist does NOT have LAB_WRITE; attempting to POST /labs returns 403."""
        token = create_access_token(
            subject=str(receptionist_user.id),
            extra_claims={"role": receptionist_user.role.value, "lab_id": str(receptionist_user.lab_id)},
        )
        response = await client.post(
            "/api/v1/labs",
            json={"name": "Forbidden Lab"},
            headers={"Authorization": f"Bearer {token}"},
        )
        assert response.status_code == 403
        body = response.json()
        assert body["success"] is False
        assert body["error"]["code"] == "FORBIDDEN"

    async def test_user_override_grant_allows_access(
        self,
        client: AsyncClient,
        db_session: AsyncSession,
        receptionist_user: User,
    ) -> None:
        """Granting LAB_WRITE to receptionist via override allows access."""
        repo = PermissionRepository()
        perm = await repo.get_by_code(db_session, PermissionCode.LAB_WRITE.value)
        assert perm is not None

        override = UserPermissionOverride(
            user_id=receptionist_user.id,
            permission_id=perm.id,
            is_granted=True,
        )
        db_session.add(override)
        await db_session.commit()

        token = create_access_token(
            subject=str(receptionist_user.id),
            extra_claims={"role": receptionist_user.role.value, "lab_id": str(receptionist_user.lab_id)},
        )
        response = await client.post(
            "/api/v1/labs",
            json={"name": "Override Granted Lab"},
            headers={"Authorization": f"Bearer {token}"},
        )
        # With override granted, passes gate and successfully creates lab (201 Created)
        assert response.status_code == 201

    async def test_user_override_deny_blocks_access(
        self,
        client: AsyncClient,
        db_session: AsyncSession,
        receptionist_user: User,
    ) -> None:
        """Denying PATIENT_READ to receptionist via override blocks access despite role default."""
        repo = PermissionRepository()
        perm = await repo.get_by_code(db_session, PermissionCode.PATIENT_READ.value)
        assert perm is not None

        override = UserPermissionOverride(
            user_id=receptionist_user.id,
            permission_id=perm.id,
            is_granted=False,
        )
        db_session.add(override)
        await db_session.commit()

        token = create_access_token(
            subject=str(receptionist_user.id),
            extra_claims={"role": receptionist_user.role.value, "lab_id": str(receptionist_user.lab_id)},
        )
        response = await client.get(
            "/api/v1/patients",
            headers={"Authorization": f"Bearer {token}"},
        )
        assert response.status_code == 403
        body = response.json()
        assert body["success"] is False
        assert body["error"]["code"] == "FORBIDDEN"


@pytest.mark.asyncio
class TestPhlebotomistResultEntry:
    """Phlebotomy desk enters analyzer results and submits drafts to the pathologist."""

    async def test_phlebotomist_resolves_result_and_report_write(
        self, db_session: AsyncSession, rbac_lab: Lab
    ) -> None:
        """Phlebotomist role defaults must include result:write and report:write."""
        user = User(
            lab_id=rbac_lab.id,
            name="Phlebo Ram",
            phone="9800000003",
            email="ram@munal.test",
            role=UserRole.PHLEBOTOMIST,
            hashed_password=hash_password("Phlebo123!"),
            is_active=True,
        )
        db_session.add(user)
        await db_session.commit()
        await db_session.refresh(user)

        granted = await resolve_user_permissions(db_session, user)
        assert PermissionCode.RESULT_WRITE.value in granted
        assert PermissionCode.REPORT_WRITE.value in granted


@pytest.mark.asyncio
class TestPermissionSeedPersistence:
    """Regression: startup seed is insert-only and must preserve toggles."""

    async def test_reseed_preserves_revoked_grant(self, db_session: AsyncSession) -> None:
        """Revoking a role grant then re-seeding (BE restart) keeps it revoked."""
        from sqlalchemy import select

        from app.models.user import Permission, RolePermission

        repo = PermissionRepository()
        perm = (
            await db_session.execute(
                select(Permission).where(Permission.code == PermissionCode.BILL_WRITE.value)
            )
        ).scalar_one()
        row = (
            await db_session.execute(
                select(RolePermission).where(
                    RolePermission.role == UserRole.RECEPTIONIST,
                    RolePermission.permission_id == perm.id,
                )
            )
        ).scalar_one()
        row.default_granted = False
        await db_session.commit()

        await repo.seed_permissions(db_session)

        kept = (
            await db_session.execute(
                select(RolePermission).where(
                    RolePermission.role == UserRole.RECEPTIONIST,
                    RolePermission.permission_id == perm.id,
                )
            )
        ).scalar_one()
        assert kept.default_granted is False

    async def test_removed_roles_have_no_defaults(self, db_session: AsyncSession) -> None:
        """Retired roles cannot be assigned or regain seeded defaults."""
        assert not hasattr(UserRole, "ADMIN")
        assert not hasattr(UserRole, "MANAGER")
        from app.core.permissions import ROLE_DEFAULT_PERMISSIONS

        assert UserRole.SUPERADMIN in ROLE_DEFAULT_PERMISSIONS
        assert len(ROLE_DEFAULT_PERMISSIONS) == 7
        assert UserRole.GENERAL_USER.value not in UserRole.codes()
        assert UserRole.GENERAL_USER not in ROLE_DEFAULT_PERMISSIONS
        assert PermissionCode.REPORT_WRITE in ROLE_DEFAULT_PERMISSIONS[UserRole.PHLEBOTOMIST]
