import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { handleActuateAPI } from '../../api/index.js';
import { createSession } from '../../auth/session.js';
import { generateToken as generateCsrfToken } from '../../security/csrf.js';
import { initDB } from '../../db.js';
import { setEmailSender } from '../../email/sender.js';
import { setActuateConfig } from '../../config/runtime.js';
/**
 * Integration tests for the User detail drawer + security action routes:
 * GET /users/:id, sessions list/revoke/revoke-all, permissions, suspend,
 * reactivate, lock/unlock, force/send password reset, require-mfa, mfa-reset.
 *
 * Focus: response contracts + the access-control guards (admin-only, Owner-only
 * on Owners, last-Owner protection, self-action blocks, reauth on MFA reset).
 */
const SECRET = 'test-secret-for-users-detail-routes-1234567890abc';
function createMockDB(userRows, sessionRows = []) {
    const users = userRows.map((r) => ({ isActive: true, passwordHash: 'h', ...r }));
    const sessions = [...sessionRows];
    const audit = [];
    const resetTokens = [];
    return {
        _users: users,
        _sessions: sessions,
        _audit: audit,
        _resetTokens: resetTokens,
        user: {
            findMany: async () => users,
            findUnique: async ({ where }) => users.find((u) => u.id === where.id) ?? null,
            findFirst: async ({ where }) => {
                if (where?.email?.equals) {
                    const t = String(where.email.equals).toLowerCase();
                    return users.find((u) => u.email.toLowerCase() === t) ?? null;
                }
                return null;
            },
            count: async ({ where } = {}) => users.filter((u) => (where?.role ? u.role === where.role : true) &&
                (where?.isActive !== undefined ? (u.isActive ?? true) === where.isActive : true) &&
                (where?.id?.not ? u.id !== where.id.not : true)).length,
            update: async ({ where, data }) => {
                const u = users.find((x) => x.id === where.id);
                Object.assign(u, data);
                return u;
            },
        },
        session: {
            findMany: async ({ where } = {}) => sessions.filter((s) => (where?.userId ? s.userId === where.userId : true)),
            findUnique: async ({ where }) => {
                const found = sessions.find((s) => s.id === where.id);
                if (found)
                    return found;
                // Auth session lookup performed by extractSession — the JWT carries a
                // `session-<userId>` id that has no corresponding row in these tests.
                // Treat it as a valid, non-revoked session so auth succeeds.
                if (String(where.id).startsWith('session-')) {
                    return { id: where.id, revokedAt: null, expiresAt: new Date(Date.now() + 1e8) };
                }
                return null;
            },
            update: async ({ where, data }) => {
                const s = sessions.find((x) => x.id === where.id);
                Object.assign(s, data);
                return s;
            },
            updateMany: async ({ where, data }) => {
                let n = 0;
                for (const s of sessions) {
                    const matchUser = where?.userId ? s.userId === where.userId : true;
                    const matchRevoked = where?.revokedAt === null ? s.revokedAt === null : true;
                    const matchNot = where?.id?.not ? s.id !== where.id.not : true;
                    if (matchUser && matchRevoked && matchNot) {
                        Object.assign(s, data);
                        n++;
                    }
                }
                return { count: n };
            },
        },
        auditLog: {
            findMany: async () => audit,
            create: async ({ data }) => {
                audit.push(data);
                return data;
            },
        },
        oAuthAccount: { findMany: async () => [] },
        document: {
            count: async () => 0,
            updateMany: async ({ where }) => {
                // Reassign any docs "owned" by the from-user in this lightweight model.
                const n = where?.createdById === 'u-owns' ? 2 : 0;
                return { count: n };
            },
        },
        media: { count: async () => 0, updateMany: async () => ({ count: 0 }) },
        contentTemplate: { count: async () => 0, updateMany: async () => ({ count: 0 }) },
        passwordResetToken: {
            updateMany: async () => ({ count: 0 }),
            create: async ({ data }) => {
                resetTokens.push(data);
                return data;
            },
        },
    };
}
async function headersFor(userId, role) {
    const token = await createSession({ userId, role, sessionId: `session-${userId}` }, { secret: SECRET });
    const csrf = await generateCsrfToken();
    return {
        cookie: `actuate_session=${token}; actuate_csrf=${csrf}`,
        'x-csrf-token': csrf,
        'content-type': 'application/json',
    };
}
const BASE = 'https://cms.example.com/api/cms';
describe('User detail + security action routes', () => {
    const original = {
        CMS_SECRET: process.env.CMS_SECRET,
        CMS_SESSION_SECRET: process.env.CMS_SESSION_SECRET,
        NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
    };
    beforeEach(() => {
        process.env.CMS_SECRET = SECRET;
        delete process.env.CMS_SESSION_SECRET;
        process.env.NEXT_PUBLIC_SITE_URL = 'https://cms.example.com';
        setEmailSender(null);
        // Pin a clean runtime config so a `platform.email` adapter leaked onto the
        // global config by a previously-run test file can't make the admin
        // password-reset flow await a foreign (possibly hanging) sender instead of
        // the one registered via `setEmailSender`.
        setActuateConfig({ admin: { path: '/admin' } });
    });
    afterEach(() => {
        setEmailSender(null);
        for (const [k, v] of Object.entries(original)) {
            if (v === undefined)
                delete process.env[k];
            else
                process.env[k] = v;
        }
    });
    function makeHandler(users, sessions = []) {
        const db = createMockDB(users, sessions);
        initDB(db);
        return { handler: handleActuateAPI({ prismaClient: db }), db };
    }
    const admin = () => ({
        id: 'admin-1',
        email: 'admin@a.com',
        name: 'Admin',
        role: 'ADMIN',
    });
    const owner = () => ({
        id: 'owner-1',
        email: 'owner@a.com',
        name: 'Owner',
        role: 'OWNER',
    });
    function req(path, headers, method = 'GET', body) {
        return new Request(`${BASE}${path}`, {
            method,
            headers,
            ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
        });
    }
    it('GET /users/:id returns the detail shape (admin-only)', async () => {
        const { handler } = makeHandler([
            admin(),
            { id: 'u2', email: 'p@a.com', name: 'Priya', role: 'EDITOR' },
        ]);
        const res = await handler(req('/users/u2', await headersFor('admin-1', 'ADMIN')));
        expect(res.status).toBe(200);
        const body = (await res.json());
        expect(body.data.id).toBe('u2');
        expect(body.data.roleName).toBe('Editor');
        expect(body.data.status).toBe('active');
    });
    it('rejects a non-admin (editor) with 403', async () => {
        const { handler } = makeHandler([{ id: 'ed-1', email: 'e@a.com', name: 'Ed', role: 'EDITOR' }]);
        const res = await handler(req('/users/ed-1', await headersFor('ed-1', 'EDITOR')));
        expect(res.status).toBe(403);
    });
    it('GET /users/:id/sessions lists sessions with current marker', async () => {
        const { handler } = makeHandler([admin()], [
            {
                id: 'session-admin-1',
                userId: 'admin-1',
                userAgent: 'Chrome',
                expiresAt: new Date(Date.now() + 86400000),
                createdAt: new Date(),
                revokedAt: null,
            },
        ]);
        const res = await handler(req('/users/admin-1/sessions', await headersFor('admin-1', 'ADMIN')));
        expect(res.status).toBe(200);
        const body = (await res.json());
        expect(body.data[0].current).toBe(true);
        expect(body.data[0].active).toBe(true);
    });
    it('DELETE /users/:id/sessions/:sessionId revokes one session', async () => {
        const { handler, db } = makeHandler([admin(), { id: 'u2', email: 'p@a.com', name: 'Priya', role: 'EDITOR' }], [
            {
                id: 'sx',
                userId: 'u2',
                expiresAt: new Date(Date.now() + 86400000),
                createdAt: new Date(),
                revokedAt: null,
            },
        ]);
        const res = await handler(req('/users/u2/sessions/sx', await headersFor('admin-1', 'ADMIN'), 'DELETE'));
        expect(res.status).toBe(200);
        expect(db._sessions[0].revokedAt).toBeInstanceOf(Date);
    });
    it('POST /users/:id/sessions/revoke-all revokes every active session for another user', async () => {
        const { handler, db } = makeHandler([admin(), { id: 'u2', email: 'p@a.com', name: 'Priya', role: 'EDITOR' }], [
            {
                id: 's1',
                userId: 'u2',
                expiresAt: new Date(Date.now() + 1e8),
                createdAt: new Date(),
                revokedAt: null,
            },
            {
                id: 's2',
                userId: 'u2',
                expiresAt: new Date(Date.now() + 1e8),
                createdAt: new Date(),
                revokedAt: null,
            },
        ]);
        const res = await handler(req('/users/u2/sessions/revoke-all', await headersFor('admin-1', 'ADMIN'), 'POST'));
        expect(res.status).toBe(200);
        expect(db._sessions.every((s) => s.revokedAt instanceof Date)).toBe(true);
    });
    it('GET /users/:id/permissions returns derived permissions', async () => {
        const { handler } = makeHandler([
            admin(),
            { id: 'u2', email: 'v@a.com', name: 'V', role: 'VIEWER' },
        ]);
        const res = await handler(req('/users/u2/permissions', await headersFor('admin-1', 'ADMIN')));
        const body = (await res.json());
        expect(body.data.roleName).toBe('Viewer');
        expect(body.data.permissions).toEqual(['dashboard.view', 'content.view']);
    });
    it('POST /users/:id/suspend suspends a user and revokes sessions', async () => {
        const { handler, db } = makeHandler([admin(), { id: 'u2', email: 'p@a.com', name: 'Priya', role: 'EDITOR' }], [
            {
                id: 's1',
                userId: 'u2',
                expiresAt: new Date(Date.now() + 1e8),
                createdAt: new Date(),
                revokedAt: null,
            },
        ]);
        const res = await handler(req('/users/u2/suspend', await headersFor('admin-1', 'ADMIN'), 'POST'));
        expect(res.status).toBe(200);
        expect(db._users.find((u) => u.id === 'u2').suspendedAt).toBeInstanceOf(Date);
        expect(db._sessions[0].revokedAt).toBeInstanceOf(Date);
    });
    it('blocks suspending your own account', async () => {
        const { handler } = makeHandler([admin()]);
        const res = await handler(req('/users/admin-1/suspend', await headersFor('admin-1', 'ADMIN'), 'POST'));
        expect(res.status).toBe(400);
    });
    it('protects the last Owner from suspension', async () => {
        const { handler } = makeHandler([owner(), admin()]);
        const res = await handler(req('/users/owner-1/suspend', await headersFor('owner-1', 'OWNER'), 'POST'));
        // owner-1 is acting on themselves here → blocked as self-action first
        expect(res.status).toBe(400);
    });
    it('forbids a non-Owner admin from acting on an Owner', async () => {
        const { handler } = makeHandler([admin(), owner()]);
        const res = await handler(req('/users/owner-1/suspend', await headersFor('admin-1', 'ADMIN'), 'POST'));
        expect(res.status).toBe(403);
    });
    it('blocks suspending the last Owner (acted on by another Owner)', async () => {
        const { handler } = makeHandler([
            owner(),
            { id: 'owner-2', email: 'o2@a.com', name: 'O2', role: 'OWNER', isActive: false },
        ]);
        const res = await handler(req('/users/owner-1/suspend', await headersFor('owner-2', 'OWNER'), 'POST'));
        // owner-2 is inactive but still the actor; owner-1 is the only active Owner.
        expect(res.status).toBe(409);
    });
    it('reactivate clears suspension', async () => {
        const { handler, db } = makeHandler([
            admin(),
            { id: 'u2', email: 'p@a.com', name: 'Priya', role: 'EDITOR', suspendedAt: new Date() },
        ]);
        const res = await handler(req('/users/u2/reactivate', await headersFor('admin-1', 'ADMIN'), 'POST'));
        expect(res.status).toBe(200);
        expect(db._users.find((u) => u.id === 'u2').suspendedAt).toBeNull();
    });
    it('force-password-reset sets the flag', async () => {
        const { handler, db } = makeHandler([
            admin(),
            { id: 'u2', email: 'p@a.com', name: 'P', role: 'EDITOR' },
        ]);
        const res = await handler(req('/users/u2/force-password-reset', await headersFor('admin-1', 'ADMIN'), 'POST', {
            enabled: true,
        }));
        expect(res.status).toBe(200);
        expect(db._users.find((u) => u.id === 'u2').forcePasswordReset).toBe(true);
    });
    it('require-mfa sets the flag', async () => {
        const { handler, db } = makeHandler([
            admin(),
            { id: 'u2', email: 'p@a.com', name: 'P', role: 'EDITOR' },
        ]);
        const res = await handler(req('/users/u2/require-mfa', await headersFor('admin-1', 'ADMIN'), 'POST', {
            required: true,
        }));
        expect(res.status).toBe(200);
        expect(db._users.find((u) => u.id === 'u2').mfaRequired).toBe(true);
    });
    it('mfa-reset requires reauthentication (401) when no password header is sent', async () => {
        const { handler } = makeHandler([
            admin(),
            { id: 'u2', email: 'p@a.com', name: 'P', role: 'EDITOR', totpEnabled: true },
        ]);
        const res = await handler(req('/users/u2/mfa-reset', await headersFor('admin-1', 'ADMIN'), 'POST'));
        expect(res.status).toBe(401);
        const body = (await res.json());
        expect(body.code ?? body.error).toBeTruthy();
    });
    it('password-reset is rejected for SSO-only accounts (409)', async () => {
        const { handler } = makeHandler([
            admin(),
            { id: 'u2', email: 'sso@a.com', name: 'SSO', role: 'EDITOR', passwordHash: null },
        ]);
        const res = await handler(req('/users/u2/password-reset', await headersFor('admin-1', 'ADMIN'), 'POST'));
        expect(res.status).toBe(409);
    });
    it('password-reset sends a reset email for a normal user when email is configured', async () => {
        const sent = [];
        setEmailSender({ send: async (o) => void sent.push({ to: o.to }) });
        const { handler } = makeHandler([
            admin(),
            { id: 'u2', email: 'p@a.com', name: 'P', role: 'EDITOR', passwordHash: 'h' },
        ]);
        const res = await handler(req('/users/u2/password-reset', await headersFor('admin-1', 'ADMIN'), 'POST'));
        expect(res.status).toBe(200);
        expect(sent).toEqual([{ to: 'p@a.com' }]);
    });
    it('transfer-content reassigns ownership to an active recipient', async () => {
        const { handler } = makeHandler([
            admin(),
            { id: 'u-owns', email: 'owns@a.com', name: 'Owns', role: 'EDITOR' },
            { id: 'u-recv', email: 'recv@a.com', name: 'Recv', role: 'EDITOR' },
        ]);
        const res = await handler(req('/users/u-owns/transfer-content', await headersFor('admin-1', 'ADMIN'), 'POST', {
            toUserId: 'u-recv',
        }));
        expect(res.status).toBe(200);
        const body = (await res.json());
        expect(body.data.documents).toBe(2);
        expect(body.data.total).toBe(2);
    });
    it('transfer-content rejects a missing recipient (400)', async () => {
        const { handler } = makeHandler([
            admin(),
            { id: 'u-owns', email: 'o@a.com', name: 'O', role: 'EDITOR' },
        ]);
        const res = await handler(req('/users/u-owns/transfer-content', await headersFor('admin-1', 'ADMIN'), 'POST', {}));
        expect(res.status).toBe(400);
    });
    it('transfer-content rejects transferring to a suspended user (409)', async () => {
        const { handler } = makeHandler([
            admin(),
            { id: 'u-owns', email: 'o@a.com', name: 'O', role: 'EDITOR' },
            { id: 'u-susp', email: 's@a.com', name: 'S', role: 'EDITOR', suspendedAt: new Date() },
        ]);
        const res = await handler(req('/users/u-owns/transfer-content', await headersFor('admin-1', 'ADMIN'), 'POST', {
            toUserId: 'u-susp',
        }));
        expect(res.status).toBe(409);
    });
    it('transfer-content rejects transferring to the same user (400)', async () => {
        const { handler } = makeHandler([
            admin(),
            { id: 'u-owns', email: 'o@a.com', name: 'O', role: 'EDITOR' },
        ]);
        const res = await handler(req('/users/u-owns/transfer-content', await headersFor('admin-1', 'ADMIN'), 'POST', {
            toUserId: 'u-owns',
        }));
        expect(res.status).toBe(400);
    });
    it('lock then unlock toggles the locked state', async () => {
        const { handler, db } = makeHandler([
            admin(),
            { id: 'u2', email: 'p@a.com', name: 'P', role: 'EDITOR' },
        ]);
        const lock = await handler(req('/users/u2/lock', await headersFor('admin-1', 'ADMIN'), 'POST'));
        expect(lock.status).toBe(200);
        expect(db._users.find((u) => u.id === 'u2').lockedAt).toBeInstanceOf(Date);
        const unlock = await handler(req('/users/u2/unlock', await headersFor('admin-1', 'ADMIN'), 'POST'));
        expect(unlock.status).toBe(200);
        expect(db._users.find((u) => u.id === 'u2').lockedAt).toBeNull();
    });
});
//# sourceMappingURL=users-detail.test.js.map