# #region Test.Api.Admin [C:3] [TYPE Module] [SEMANTICS test,admin,api]
# @BRIEF Unit tests for admin API routes — users, roles, permissions, AD mappings.
# @RELATION BINDS_TO -> [AdminApi]
# @TEST_EDGE: user_not_found -> 404
# @TEST_EDGE: username_exists -> 400
# @TEST_EDGE: role_not_found -> 404
# @TEST_EDGE: role_already_exists -> 400

import os

# Set env BEFORE any source imports
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests")

import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient

_src = str(Path(__file__).resolve().parent.parent.parent / "src")
if _src not in sys.path:
    sys.path.insert(0, _src)


def _make_client(overrides: dict | None = None) -> TestClient:
    """Build TestClient with admin router and all deps overridden."""
    from src.api.routes.admin import router
    from src.core.database import get_auth_db
    from src.dependencies import get_current_user, has_permission
    from src.schemas.auth import User, RoleSchema

    app = FastAPI()
    app.include_router(router)

    mock_db = MagicMock()
    mock_user = User(
        id="admin-1",
        username="admin",
        email="admin@example.com",
        auth_source="LOCAL",
        is_active=True,
        created_at=__import__("datetime").datetime.now(),
        roles=[RoleSchema(id="role-1", name="Admin", description="Admin", permissions=[])],
    )

    defaults = {
        get_auth_db: lambda: mock_db,
        get_current_user: lambda: mock_user,
        has_permission: lambda *args, **kwargs: lambda: None,
    }
    if overrides:
        defaults.update(overrides)
    for dep, mock_fn in defaults.items():
        app.dependency_overrides[dep] = mock_fn
    return TestClient(app)


# ── Users ──

class TestListUsers:
    """GET /api/admin/users"""

    def test_list_users_success(self):
        """Happy path: list all users returns 200."""
        from src.core.database import get_auth_db

        mock_user = MagicMock()
        mock_user.id = "user-1"
        mock_user.username = "alice"
        mock_user.email = "alice@example.com"
        mock_user.is_active = True
        mock_user.auth_source = "LOCAL"
        mock_user.created_at = __import__("datetime").datetime.now()
        mock_user.last_login = None
        mock_user.roles = []

        mock_session = MagicMock()
        mock_session.query.return_value.all.return_value = [mock_user]

        client = _make_client({get_auth_db: lambda: mock_session})
        resp = client.get("/api/admin/users")
        assert resp.status_code == 200
        data = resp.json()
        assert isinstance(data, list)
        assert data[0]["username"] == "alice"


class TestCreateUser:
    """POST /api/admin/users"""

    def test_create_user_success(self):
        """Happy path: user created with 201."""
        from datetime import datetime
        from src.core.database import get_auth_db

        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_repo.get_user_by_username.return_value = None

        # Build a proper role mock with string attributes, not MagicMock(name=...)
        def _get_role_by_name(name):
            if name != "Admin":
                return None
            role = MagicMock()
            role.id = f"role-{name}"
            role.name = name
            role.description = "Administrator role"
            role.permissions = []
            return role

        mock_repo.get_role_by_name.side_effect = _get_role_by_name

        # Make mock_session.refresh set fields the response schema needs
        def _refresh_side_effect(obj):
            obj.id = "new-user-id"
            obj.created_at = datetime.now()

        mock_session.refresh.side_effect = _refresh_side_effect

        with patch("src.api.routes.admin.AuthRepository", return_value=mock_repo):
            client = _make_client({get_auth_db: lambda: mock_session})
            resp = client.post("/api/admin/users", json={
                "username": "newuser",
                "email": "new@example.com",
                "password": "StrongPass1",
                "is_active": True,
                "roles": ["Admin"],
            })
        assert resp.status_code == 201
        mock_session.add.assert_called_once()
        mock_session.commit.assert_called_once()

    def test_create_user_duplicate_username(self):
        """Username already exists returns 400."""
        from src.core.database import get_auth_db
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_repo.get_user_by_username.return_value = MagicMock()

        with patch("src.api.routes.admin.AuthRepository", return_value=mock_repo):
            client = _make_client({get_auth_db: lambda: mock_session})
            resp = client.post("/api/admin/users", json={
                "username": "exists",
                "email": "dup@example.com",
                "password": "StrongPass1",
            })
        assert resp.status_code == 400
        assert "Username already exists" in resp.text

    def test_create_user_weak_password(self):
        """Weak password returns 422."""
        client = _make_client()
        resp = client.post("/api/admin/users", json={
            "username": "weakuser",
            "email": "weak@example.com",
            "password": "short",
        })
        assert resp.status_code == 422

    def test_create_user_no_permission(self):
        """User without write permission gets 403."""
        from src.core.database import get_auth_db
        from src.dependencies import get_current_user, has_permission
        from src.schemas.auth import User, RoleSchema

        app = FastAPI()
        from src.api.routes.admin import router
        app.include_router(router)
        app.dependency_overrides[get_auth_db] = lambda: MagicMock()
        app.dependency_overrides[get_current_user] = lambda: User(
            id="user-1", username="regular", email="u@x.com", auth_source="LOCAL",
            created_at=__import__("datetime").datetime.now(), roles=[]
        )
        # Keep has_permission as real — it will use the mock get_current_user
        client = TestClient(app)
        resp = client.post("/api/admin/users", json={
            "username": "test",
            "email": "test@example.com",
            "password": "StrongPass1",
        })
        assert resp.status_code == 403


class TestUpdateUser:
    """PUT /api/admin/users/{user_id}"""

    def test_update_user_success(self):
        """Happy path: user updated and returned."""
        from src.core.database import get_auth_db
        mock_session = MagicMock()
        mock_repo = MagicMock()
        existing_user = MagicMock()
        existing_user.id = "user-1"
        existing_user.username = "oldname"
        existing_user.email = "old@example.com"
        existing_user.is_active = True
        existing_user.auth_source = "LOCAL"
        existing_user.created_at = __import__("datetime").datetime.now()
        existing_user.roles = []
        mock_repo.get_user_by_id.return_value = existing_user

        with patch("src.api.routes.admin.AuthRepository", return_value=mock_repo):
            client = _make_client({get_auth_db: lambda: mock_session})
            resp = client.put("/api/admin/users/user-1", json={"email": "new@example.com"})
        assert resp.status_code == 200
        assert existing_user.email == "new@example.com"
        mock_session.commit.assert_called_once()

    def test_update_user_not_found(self):
        """Non-existent user returns 404."""
        from src.core.database import get_auth_db
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_repo.get_user_by_id.return_value = None

        with patch("src.api.routes.admin.AuthRepository", return_value=mock_repo):
            client = _make_client({get_auth_db: lambda: mock_session})
            resp = client.put("/api/admin/users/user-999", json={"email": "x@y.com"})
        assert resp.status_code == 404


class TestDeleteUser:
    """DELETE /api/admin/users/{user_id}"""

    def test_delete_user_success(self):
        """Happy path: user deleted returns 204."""
        from src.core.database import get_auth_db
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_repo.get_user_by_id.return_value = MagicMock(username="testuser")

        with patch("src.api.routes.admin.AuthRepository", return_value=mock_repo):
            client = _make_client({get_auth_db: lambda: mock_session})
            resp = client.delete("/api/admin/users/user-1")
        assert resp.status_code == 204
        mock_session.delete.assert_called_once()
        mock_session.commit.assert_called_once()

    def test_delete_user_not_found(self):
        """Non-existent user returns 404."""
        from src.core.database import get_auth_db
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_repo.get_user_by_id.return_value = None

        with patch("src.api.routes.admin.AuthRepository", return_value=mock_repo):
            client = _make_client({get_auth_db: lambda: mock_session})
            resp = client.delete("/api/admin/users/user-999")
        assert resp.status_code == 404


# ── Roles ──

class TestListRoles:
    """GET /api/admin/roles"""

    def test_list_roles_success(self):
        """Happy path: list roles returns 200."""
        mock_role = MagicMock()
        mock_role.id = "role-1"
        mock_role.name = "Admin"
        mock_role.description = "Administrator"
        mock_role.permissions = []

        mock_session = MagicMock()
        mock_session.query.return_value.all.return_value = [mock_role]

        from src.core.database import get_auth_db
        client = _make_client({get_auth_db: lambda: mock_session})
        resp = client.get("/api/admin/roles")
        assert resp.status_code == 200
        assert isinstance(resp.json(), list)


class TestCreateRole:
    """POST /api/admin/roles"""

    def test_create_role_success(self):
        """Happy path: role created with 201."""
        mock_session = MagicMock()
        mock_session.query.return_value.filter.return_value.first.return_value = None
        mock_repo = MagicMock()
        mock_perm = MagicMock()
        mock_perm.id = "perm-1"
        mock_perm.resource = "users"
        mock_perm.action = "read"
        mock_repo.get_permission_by_id.return_value = mock_perm

        def _refresh_side_effect(obj):
            obj.id = "new-role-id"

        mock_session.refresh.side_effect = _refresh_side_effect

        with patch("src.api.routes.admin.AuthRepository", return_value=mock_repo):
            from src.core.database import get_auth_db
            client = _make_client({get_auth_db: lambda: mock_session})
            resp = client.post("/api/admin/roles", json={
                "name": "Editor",
                "description": "Can edit",
                "permissions": ["perm-1"],
            })
        assert resp.status_code == 201
        mock_session.add.assert_called_once()
        mock_session.commit.assert_called_once()

    def test_create_role_duplicate(self):
        """Duplicate role name returns 400."""
        mock_session = MagicMock()
        mock_session.query.return_value.filter.return_value.first.return_value = MagicMock()

        from src.core.database import get_auth_db
        client = _make_client({get_auth_db: lambda: mock_session})
        resp = client.post("/api/admin/roles", json={
            "name": "Admin",
            "description": "Already exists",
            "permissions": [],
        })
        assert resp.status_code == 400
        assert "Role already exists" in resp.text


class TestUpdateRole:
    """PUT /api/admin/roles/{role_id}"""

    def test_update_role_success(self):
        """Happy path: role updated."""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        existing_role = MagicMock()
        existing_role.id = "role-1"
        existing_role.name = "OldName"
        existing_role.description = "Old desc"
        existing_role.permissions = []
        mock_repo.get_role_by_id.return_value = existing_role

        with patch("src.api.routes.admin.AuthRepository", return_value=mock_repo):
            from src.core.database import get_auth_db
            client = _make_client({get_auth_db: lambda: mock_session})
            resp = client.put("/api/admin/roles/role-1", json={"name": "NewName", "description": "New desc"})
        assert resp.status_code == 200
        assert existing_role.name == "NewName"
        mock_session.commit.assert_called_once()

    def test_update_role_not_found(self):
        """Non-existent role returns 404."""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_repo.get_role_by_id.return_value = None

        with patch("src.api.routes.admin.AuthRepository", return_value=mock_repo):
            from src.core.database import get_auth_db
            client = _make_client({get_auth_db: lambda: mock_session})
            resp = client.put("/api/admin/roles/role-999", json={"name": "Ghost"})
        assert resp.status_code == 404


class TestDeleteRole:
    """DELETE /api/admin/roles/{role_id}"""

    def test_delete_role_success(self):
        """Happy path: role deleted returns 204."""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_repo.get_role_by_id.return_value = MagicMock()

        with patch("src.api.routes.admin.AuthRepository", return_value=mock_repo):
            from src.core.database import get_auth_db
            client = _make_client({get_auth_db: lambda: mock_session})
            resp = client.delete("/api/admin/roles/role-1")
        assert resp.status_code == 204
        mock_session.delete.assert_called_once()
        mock_session.commit.assert_called_once()

    def test_delete_role_not_found(self):
        """Non-existent role returns 404."""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_repo.get_role_by_id.return_value = None

        with patch("src.api.routes.admin.AuthRepository", return_value=mock_repo):
            from src.core.database import get_auth_db
            client = _make_client({get_auth_db: lambda: mock_session})
            resp = client.delete("/api/admin/roles/role-999")
        assert resp.status_code == 404


# ── Permissions ──

class TestListPermissions:
    """GET /api/admin/permissions"""

    def test_list_permissions_success(self):
        """Happy path: returns permissions list."""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_perm = MagicMock()
        mock_perm.id = "perm-1"
        mock_perm.resource = "users"
        mock_perm.action = "read"
        mock_repo.list_permissions.return_value = [mock_perm]

        with patch("src.api.routes.admin.AuthRepository", return_value=mock_repo), \
             patch("src.api.routes.admin.discover_declared_permissions", return_value=[]), \
             patch("src.api.routes.admin.sync_permission_catalog", return_value=0):
            from src.core.database import get_auth_db
            from src.dependencies import get_plugin_loader
            client = _make_client({
                get_auth_db: lambda: mock_session,
                get_plugin_loader: lambda: MagicMock(),
            })
            resp = client.get("/api/admin/permissions")
        assert resp.status_code == 200
        assert isinstance(resp.json(), list)

    def test_list_permissions_with_sync(self):
        """When new permissions discovered, sync is called."""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_repo.list_permissions.return_value = []

        with patch("src.api.routes.admin.AuthRepository", return_value=mock_repo), \
             patch("src.api.routes.admin.discover_declared_permissions", return_value=["new:perm"]), \
             patch("src.api.routes.admin.sync_permission_catalog", return_value=2):
            from src.core.database import get_auth_db
            from src.dependencies import get_plugin_loader
            client = _make_client({
                get_auth_db: lambda: mock_session,
                get_plugin_loader: lambda: MagicMock(),
            })
            resp = client.get("/api/admin/permissions")
        assert resp.status_code == 200


# ── AD Mappings ──

class TestListAdMappings:
    """GET /api/admin/ad-mappings"""

    def test_list_mappings_success(self):
        """Happy path: returns AD mappings."""
        mock_mapping = MagicMock()
        mock_mapping.id = "map-1"
        mock_mapping.ad_group = "DOMAIN\\group1"
        mock_mapping.role_id = "role-1"

        mock_session = MagicMock()
        mock_session.query.return_value.all.return_value = [mock_mapping]

        from src.core.database import get_auth_db
        client = _make_client({get_auth_db: lambda: mock_session})
        resp = client.get("/api/admin/ad-mappings")
        assert resp.status_code == 200
        assert isinstance(resp.json(), list)


class TestCreateAdMapping:
    """POST /api/admin/ad-mappings"""

    def test_create_mapping_success(self):
        """Happy path: AD mapping created."""
        mock_session = MagicMock()
        mock_session.add = MagicMock()

        def _refresh_side_effect(obj):
            obj.id = "new-mapping-id"

        mock_session.refresh.side_effect = _refresh_side_effect

        from src.core.database import get_auth_db
        client = _make_client({get_auth_db: lambda: mock_session})
        resp = client.post("/api/admin/ad-mappings", json={
            "ad_group": "DOMAIN\\newgroup",
            "role_id": "role-1",
        })
        assert resp.status_code == 200
        mock_session.add.assert_called_once()
        mock_session.commit.assert_called_once()

    def test_create_mapping_invalid_ad_group(self):
        """Invalid AD group name returns 422."""
        client = _make_client()
        resp = client.post("/api/admin/ad-mappings", json={
            "ad_group": "invalid group with spaces!!!",
            "role_id": "role-1",
        })
        assert resp.status_code == 422
# #endregion Test.Api.Admin
