import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { handleActuateAPI } from '../../api/index.js';
import { initDB } from '../../db.js';
import { generateApiKey } from '../../security/api-key-enhanced.js';
/**
 * Contract tests for the Settings → Updates endpoints:
 *   - GET  /updates/check   → any authed session; admins additionally see the
 *                              GitHub-integration config hints (`canManage`).
 *   - GET  /updates/config  → admin only.
 *   - PUT  /updates/config  → admin only; encrypts credentials at rest.
 *   - POST /updates/apply   → admin only.
 *
 * Regression guard for the role-check bug where these routes compared
 * `session.role !== 'admin'` (lowercase) while roles are stored uppercase
 * (`'ADMIN'` / `'OWNER'`) — which 403'd every real admin. Admin auth here is
 * exercised through an admin-scoped API key (the same path real admin sessions
 * resolve through `requireAdminScope`).
 */
const VALID_SECRET = 'a'.repeat(48);
const VALID_ENC_KEY = 'a'.repeat(64);
function createMockDB(state, apiKey) {
    return {
        apiKey: {
            findMany: async () => (apiKey ? [apiKey] : []),
            findUnique: async ({ where }) => apiKey && apiKey.keyHash === where.keyHash ? apiKey : null,
            update: async () => apiKey,
        },
        document: {
            findFirst: async () => state.configDoc,
            upsert: async (args) => {
                state.upsertArgs = args;
                return { id: 'cfg-doc' };
            },
        },
        user: { count: async () => 1 },
        auditLog: { create: async () => ({ id: 'log1' }) },
        version: { create: async () => ({ id: 'v1' }) },
    };
}
async function makeKey(scopes) {
    const { key, keyHash, keyPrefix } = await generateApiKey({ prefix: 'act_sk', scopes });
    return {
        key,
        record: {
            id: 'apikey-1',
            keyHash,
            keyPrefix,
            userId: 'user-1',
            scopes,
            ipRestrictions: null,
            expiresAt: null,
            lastUsedAt: null,
            revokedAt: null,
        },
    };
}
function freshState() {
    return { configDoc: null };
}
beforeEach(() => {
    delete globalThis.__actuateConfig;
    process.env.CMS_SECRET = VALID_SECRET;
    process.env.CMS_ENCRYPTION_KEY = VALID_ENC_KEY;
    delete process.env.ACTUATE_GITHUB_TOKEN;
    delete process.env.ACTUATE_GITHUB_REPO;
});
afterEach(() => {
    vi.restoreAllMocks();
});
/**
 * Stub the network so `checkForUpdates` (and the future update-server hybrid)
 * resolve deterministically without hitting npm. The update-server URL is
 * forced to fail so the npm-registry fallback path is what we assert.
 */
function stubUpdateNetwork(latest = '0.46.0') {
    vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
        const url = String(input);
        if (url.includes('registry.npmjs.org')) {
            return new Response(JSON.stringify({
                'dist-tags': { latest },
                time: { [latest]: '2026-01-01T00:00:00.000Z' },
                versions: { [latest]: {} },
            }), { status: 200, headers: { 'Content-Type': 'application/json' } });
        }
        // Any update-server call fails → exercises the npm fallback.
        return new Response('unavailable', { status: 503 });
    });
}
describe('GET /updates/check', () => {
    it('requires authentication', async () => {
        const db = createMockDB(freshState());
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        const res = await handler(new Request('https://example.com/api/cms/updates/check'));
        expect(res.status).toBe(401);
    });
    it('returns version info to a non-admin session without GitHub config hints', async () => {
        stubUpdateNetwork();
        const { key, record } = await makeKey({ media: true });
        const db = createMockDB(freshState(), record);
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        const res = await handler(new Request('https://example.com/api/cms/updates/check', {
            headers: { Authorization: `Bearer ${key}` },
        }));
        expect(res.status).toBe(200);
        const body = (await res.json());
        expect(body.data.canManage).toBe(false);
        expect(body.data.githubRepo).toBeUndefined();
        expect(body.data).not.toHaveProperty('hasGithubToken');
        expect(typeof body.data.current).toBe('string');
    });
    it('exposes GitHub config hints to admins', async () => {
        stubUpdateNetwork();
        const { key, record } = await makeKey({ admin: true });
        const db = createMockDB(freshState(), record);
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        const res = await handler(new Request('https://example.com/api/cms/updates/check', {
            headers: { Authorization: `Bearer ${key}` },
        }));
        expect(res.status).toBe(200);
        const body = (await res.json());
        expect(body.data.canManage).toBe(true);
        expect(body.data.hasGithubToken).toBe(false);
        expect(body.data).toHaveProperty('githubRepo');
    });
});
describe('GET /updates/config', () => {
    it('rejects unauthenticated requests', async () => {
        const db = createMockDB(freshState());
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        const res = await handler(new Request('https://example.com/api/cms/updates/config'));
        expect(res.status).toBe(401);
    });
    it('rejects non-admin API keys', async () => {
        const { key, record } = await makeKey({ media: true });
        const db = createMockDB(freshState(), record);
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        const res = await handler(new Request('https://example.com/api/cms/updates/config', {
            headers: { Authorization: `Bearer ${key}` },
        }));
        expect(res.status).toBe(403);
    });
    it('allows admins (regression: uppercase ADMIN role must pass)', async () => {
        const { key, record } = await makeKey({ admin: true });
        const db = createMockDB(freshState(), record);
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        const res = await handler(new Request('https://example.com/api/cms/updates/config', {
            headers: { Authorization: `Bearer ${key}` },
        }));
        expect(res.status).toBe(200);
        const body = (await res.json());
        expect(body.data.hasGithubToken).toBe(false);
        expect(body.data.githubRepo).toBe('');
    });
});
describe('PUT /updates/config', () => {
    it('rejects non-admin API keys', async () => {
        const { key, record } = await makeKey({ media: true });
        const db = createMockDB(freshState(), record);
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        const res = await handler(new Request('https://example.com/api/cms/updates/config', {
            method: 'PUT',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ githubRepo: 'owner/repo' }),
        }));
        expect(res.status).toBe(403);
    });
    it('validates the repository format', async () => {
        const { key, record } = await makeKey({ admin: true });
        const db = createMockDB(freshState(), record);
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        const res = await handler(new Request('https://example.com/api/cms/updates/config', {
            method: 'PUT',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ githubRepo: 'not a repo' }),
        }));
        expect(res.status).toBe(400);
    });
    it('encrypts credentials at rest (never persists plaintext)', async () => {
        const { key, record } = await makeKey({ admin: true });
        const state = freshState();
        const db = createMockDB(state, record);
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        const res = await handler(new Request('https://example.com/api/cms/updates/config', {
            method: 'PUT',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ githubToken: 'ghp_secret_token_value', githubRepo: 'owner/repo' }),
        }));
        expect(res.status).toBe(200);
        const persisted = state.upsertArgs?.create?.data ?? {};
        expect(persisted.githubToken).toBeTruthy();
        expect(persisted.githubRepo).toBeTruthy();
        // The raw token/repo must never be stored in plaintext.
        expect(JSON.stringify(state.upsertArgs)).not.toContain('ghp_secret_token_value');
        expect(persisted.githubRepo).not.toBe('owner/repo');
    });
});
describe('POST /updates/apply', () => {
    it('rejects non-admin API keys', async () => {
        const { key, record } = await makeKey({ media: true });
        const db = createMockDB(freshState(), record);
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        const res = await handler(new Request('https://example.com/api/cms/updates/apply', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ targetVersion: '1.0.0' }),
        }));
        expect(res.status).toBe(403);
    });
    it('lets admins through the role gate (then fails on missing GitHub config)', async () => {
        const { key, record } = await makeKey({ admin: true });
        const db = createMockDB(freshState(), record);
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        const res = await handler(new Request('https://example.com/api/cms/updates/apply', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ targetVersion: '1.0.0' }),
        }));
        // Not 403 — the admin passed the role check; it fails later (400) because
        // no GitHub token/repo is configured in this test environment.
        expect(res.status).toBe(400);
        const body = (await res.json());
        expect(String(body.error)).toMatch(/github/i);
    });
    it('rejects an invalid target version', async () => {
        const { key, record } = await makeKey({ admin: true });
        const db = createMockDB(freshState(), record);
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        const res = await handler(new Request('https://example.com/api/cms/updates/apply', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ targetVersion: 'not-a-version' }),
        }));
        expect(res.status).toBe(400);
    });
});
//# sourceMappingURL=updates-routes.test.js.map