import { describe, expect, it, vi, beforeEach } from 'vitest';
const dbHolder = { current: {} };
vi.mock('../../db.js', () => ({ getDB: () => dbHolder.current }));
vi.mock('../../security/audit.js', () => ({ logEvent: vi.fn(async () => { }) }));
// Identity secret storage so the "encrypted" value equals the plaintext in tests.
vi.mock('../../security/secret-storage.js', () => ({
    encryptSecret: async (s) => s,
    decryptSecret: async (s) => s,
}));
const { SsrfBlockedError, safeFetchMock } = vi.hoisted(() => {
    class SsrfBlockedError extends Error {
    }
    return { SsrfBlockedError, safeFetchMock: vi.fn() };
});
vi.mock('../../security/safe-fetch.js', () => ({
    safeFetch: (...args) => safeFetchMock(...args),
    SsrfBlockedError,
}));
import { createFormWebhook, listFormWebhooks, updateFormWebhook, deleteFormWebhook, dispatchFormWebhooks, testFormWebhook, FormWebhookError, } from '../../forms/webhooks.js';
import { signPayload } from '../../webhooks/index.js';
const ctx = { userId: 'u1', role: 'admin', locale: 'en', db: {} };
function fakeDb() {
    const rows = [];
    return {
        rows,
        formWebhook: {
            create: async ({ data }) => {
                const row = {
                    id: `wh_${rows.length + 1}`,
                    createdAt: new Date(),
                    updatedAt: new Date(),
                    lastDeliveryAt: null,
                    lastDeliveryStatus: null,
                    ...data,
                };
                rows.push(row);
                return row;
            },
            findMany: async ({ where }) => rows.filter((r) => r.formId === where.formId &&
                (where.enabled === undefined || r.enabled === where.enabled)),
            findUnique: async ({ where }) => rows.find((r) => r.id === where.id) ?? null,
            update: async ({ where, data }) => {
                const r = rows.find((x) => x.id === where.id);
                Object.assign(r, data);
                return r;
            },
            delete: async ({ where }) => {
                const i = rows.findIndex((x) => x.id === where.id);
                rows.splice(i, 1);
                return {};
            },
        },
    };
}
function httpResponse(status) {
    return { ok: status >= 200 && status < 300, status, text: async () => '' };
}
beforeEach(() => {
    dbHolder.current = fakeDb();
    safeFetchMock.mockReset();
});
describe('form webhook CRUD', () => {
    it('creates a webhook, auto-generates a signing secret, and returns it once', async () => {
        const wh = await createFormWebhook(ctx, 'form_1', { url: 'https://hooks.example.com/x' });
        expect(wh.secret).toMatch(/^whsec_/);
        expect(wh.events).toEqual(['form.submitted']);
        expect(wh.enabled).toBe(true);
    });
    it('masks the secret in list responses', async () => {
        await createFormWebhook(ctx, 'form_1', { url: 'https://hooks.example.com/x', secret: 's3cret' });
        const list = await listFormWebhooks('form_1');
        expect(list).toHaveLength(1);
        expect(list[0].secret).toBeUndefined();
    });
    it('rejects SSRF / private webhook URLs', async () => {
        await expect(createFormWebhook(ctx, 'form_1', { url: 'http://localhost:3000/hook' })).rejects.toBeInstanceOf(FormWebhookError);
        await expect(createFormWebhook(ctx, 'form_1', { url: 'http://169.254.169.254/latest' })).rejects.toBeInstanceOf(FormWebhookError);
    });
    it('clamps retryCount and normalizes events on update', async () => {
        const wh = await createFormWebhook(ctx, 'form_1', { url: 'https://hooks.example.com/x' });
        const updated = await updateFormWebhook(ctx, wh.id, {
            retryCount: 99,
            events: ['form.submitted', 'bogus'],
        });
        expect(updated.retryCount).toBe(10);
        expect(updated.events).toEqual(['form.submitted']);
    });
    it('deletes a webhook', async () => {
        const wh = await createFormWebhook(ctx, 'form_1', { url: 'https://hooks.example.com/x' });
        await deleteFormWebhook(ctx, wh.id);
        expect(await listFormWebhooks('form_1')).toHaveLength(0);
    });
});
describe('dispatchFormWebhooks', () => {
    it('delivers a signed payload to matching enabled webhooks', async () => {
        safeFetchMock.mockResolvedValue(httpResponse(200));
        await createFormWebhook(ctx, 'form_1', {
            url: 'https://hooks.example.com/x',
            secret: 'shh',
            events: ['form.submitted'],
        });
        const res = await dispatchFormWebhooks('form_1', {
            event: 'form.submitted',
            formId: 'form_1',
            submissionId: 'sub_1',
            data: { name: 'Ada' },
        });
        expect(res).toEqual({ matched: 1, delivered: 1 });
        expect(safeFetchMock).toHaveBeenCalledTimes(1);
        const [url, opts] = safeFetchMock.mock.calls[0];
        expect(url).toBe('https://hooks.example.com/x');
        const expectedSig = await signPayload(opts.body, 'shh');
        expect(opts.headers['X-Actuate-Signature']).toBe(expectedSig);
        expect(opts.headers['X-Actuate-Event']).toBe('form.submitted');
        expect(dbHolder.current.rows[0].lastDeliveryStatus).toBe('success');
    });
    it('ignores webhooks not subscribed to the event', async () => {
        safeFetchMock.mockResolvedValue(httpResponse(200));
        await createFormWebhook(ctx, 'form_1', {
            url: 'https://hooks.example.com/x',
            events: ['form.entry.starred'],
        });
        const res = await dispatchFormWebhooks('form_1', {
            event: 'form.submitted',
            formId: 'form_1',
        });
        expect(res.matched).toBe(0);
        expect(safeFetchMock).not.toHaveBeenCalled();
    });
    it('does not deliver to disabled webhooks', async () => {
        safeFetchMock.mockResolvedValue(httpResponse(200));
        await createFormWebhook(ctx, 'form_1', {
            url: 'https://hooks.example.com/x',
            enabled: false,
        });
        const res = await dispatchFormWebhooks('form_1', { event: 'form.submitted', formId: 'form_1' });
        expect(res.matched).toBe(0);
    });
    it('stops retrying on a 4xx response', async () => {
        safeFetchMock.mockResolvedValue(httpResponse(400));
        await createFormWebhook(ctx, 'form_1', {
            url: 'https://hooks.example.com/x',
            retryCount: 3,
        });
        const res = await dispatchFormWebhooks('form_1', { event: 'form.submitted', formId: 'form_1' });
        expect(res.delivered).toBe(0);
        expect(safeFetchMock).toHaveBeenCalledTimes(1);
        expect(dbHolder.current.rows[0].lastDeliveryStatus).toContain('failed');
    });
    it('retries on a 5xx response and succeeds with backoff', async () => {
        // Fire backoff timers immediately so the test doesn't wait real seconds.
        const timeoutSpy = vi.spyOn(global, 'setTimeout').mockImplementation(((fn) => {
            fn();
            return 0;
        }));
        try {
            safeFetchMock
                .mockResolvedValueOnce(httpResponse(503))
                .mockResolvedValueOnce(httpResponse(200));
            await createFormWebhook(ctx, 'form_1', {
                url: 'https://hooks.example.com/x',
                retryCount: 2,
            });
            const res = await dispatchFormWebhooks('form_1', {
                event: 'form.submitted',
                formId: 'form_1',
            });
            expect(res.delivered).toBe(1);
            expect(safeFetchMock).toHaveBeenCalledTimes(2);
        }
        finally {
            timeoutSpy.mockRestore();
        }
    });
    it('treats an SSRF block as a permanent failure (no retry)', async () => {
        safeFetchMock.mockRejectedValue(new SsrfBlockedError('blocked'));
        await createFormWebhook(ctx, 'form_1', {
            url: 'https://hooks.example.com/x',
            retryCount: 3,
        });
        const res = await dispatchFormWebhooks('form_1', { event: 'form.submitted', formId: 'form_1' });
        expect(res.delivered).toBe(0);
        expect(safeFetchMock).toHaveBeenCalledTimes(1);
    });
});
describe('testFormWebhook', () => {
    it('sends a single sample delivery and records the result', async () => {
        safeFetchMock.mockResolvedValue(httpResponse(200));
        const wh = await createFormWebhook(ctx, 'form_1', {
            url: 'https://hooks.example.com/x',
            secret: 'shh',
        });
        const result = await testFormWebhook(ctx, wh.id);
        expect(result.ok).toBe(true);
        expect(result.attempts).toBe(1);
        const [, opts] = safeFetchMock.mock.calls[0];
        expect(JSON.parse(opts.body).data._test).toBe(true);
    });
    it('throws a 404 FormWebhookError for an unknown webhook', async () => {
        await expect(testFormWebhook(ctx, 'missing')).rejects.toMatchObject({ status: 404 });
    });
});
//# sourceMappingURL=webhooks.test.js.map