import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { handleActuateAPI } from '../../api/index.js';
import { createSession } from '../../auth/session.js';
import { initDB } from '../../db.js';
import { generateToken as generateCsrfToken } from '../../security/csrf.js';
/**
 * Integration tests for the dashboard stats endpoints introduced for
 * the "Delivery API" and "Content Health" cards on the dashboard.
 *
 * These tests lock down the JSON response shape the dashboard depends
 * on so a future refactor can't silently regress the client. They use
 * the same mock-DB pattern as `notifications-routes.test.ts`.
 */
const SECRET = 'test-secret-for-stats-routes-aaaaaaaaaaaaaaaaaa';
function createMockDB(args = {}) {
    const metrics = args.metrics ?? [];
    const issues = args.issues ?? [];
    return {
        session: { findUnique: async () => ({ revokedAt: null }) },
        user: {},
        apiRequestMetric: {
            async findMany({ where, select: _select, }) {
                return metrics.filter((m) => {
                    if (m.bucketStart < where.bucketStart.gte)
                        return false;
                    if (where.bucketStart.lt && m.bucketStart >= where.bucketStart.lt)
                        return false;
                    return true;
                });
            },
        },
        webhookEndpoint: {
            async count({ where } = {}) {
                if (where?.active === true)
                    return 2;
                return 3;
            },
        },
        contentIssue: {
            async groupBy({ by, _sum }) {
                void by;
                void _sum;
                const grouped = new Map();
                for (const i of issues)
                    grouped.set(i.type, (grouped.get(i.type) ?? 0) + i.count);
                return Array.from(grouped, ([type, count]) => ({ type, _sum: { count } }));
            },
            async findFirst({ orderBy: _orderBy, select: _select }) {
                if (issues.length === 0)
                    return null;
                const latest = issues.reduce((a, b) => (a.lastSeenAt > b.lastSeenAt ? a : b));
                return { lastSeenAt: latest.lastSeenAt };
            },
        },
    };
}
async function sessionHeaders(userId = 'user-1') {
    const token = await createSession({ userId, role: 'ADMIN', 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',
    };
}
describe('GET /api/cms/stats/api-delivery', () => {
    const originalSecret = process.env.CMS_SECRET;
    beforeEach(() => {
        process.env.CMS_SECRET = SECRET;
    });
    afterEach(() => {
        if (originalSecret === undefined)
            delete process.env.CMS_SECRET;
        else
            process.env.CMS_SECRET = originalSecret;
    });
    it('rejects unauthenticated requests with 401', async () => {
        const db = createMockDB();
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db });
        const res = await handler(new Request('https://example.com/api/cms/stats/api-delivery'));
        expect(res.status).toBe(401);
    });
    it('returns the empty payload (hasData=false) when no metrics exist', async () => {
        const db = createMockDB();
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db });
        const headers = await sessionHeaders();
        const res = await handler(new Request('https://example.com/api/cms/stats/api-delivery', {
            headers: { cookie: headers.cookie },
        }));
        expect(res.status).toBe(200);
        const body = (await res.json());
        expect(body.data).toMatchObject({
            hasData: false,
            requests24h: 0,
            requestsPrev24h: 0,
            deltaPercent: null,
            errorRate: 0,
            avgLatencyMs: 0,
            p95LatencyMs: 0,
        });
        // Webhook counters still come from the mocked WebhookEndpoint table.
        expect(body.data.webhookCount).toBe(3);
        expect(body.data.webhookActiveCount).toBe(2);
    });
    it('aggregates 24h request volume, error rate, and avg latency', async () => {
        // Two buckets:
        //   - 100 2xx requests, sumLatency 5000 (avg 50ms)
        //   - 10  5xx requests, sumLatency 1000 (avg 100ms)
        // Total: 110 requests, 10 errors → 9.09% error rate. Avg latency
        // = 6000 / 110 ≈ 54.5ms.
        const now = new Date();
        const bucketTime = new Date(now.getTime() - 60_000);
        const hist2xx = new Array(12).fill(0);
        hist2xx[6] = 100; // 32-64ms bucket
        const hist5xx = new Array(12).fill(0);
        hist5xx[7] = 10; // 64-128ms bucket
        const db = createMockDB({
            metrics: [
                {
                    statusBucket: '2xx',
                    count: 100,
                    sumLatencyMs: 5000,
                    maxLatencyMs: 60,
                    latencyHistogram: hist2xx,
                    bucketStart: bucketTime,
                },
                {
                    statusBucket: '5xx',
                    count: 10,
                    sumLatencyMs: 1000,
                    maxLatencyMs: 110,
                    latencyHistogram: hist5xx,
                    bucketStart: bucketTime,
                },
            ],
        });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db });
        const headers = await sessionHeaders();
        const res = await handler(new Request('https://example.com/api/cms/stats/api-delivery', {
            headers: { cookie: headers.cookie },
        }));
        expect(res.status).toBe(200);
        const body = (await res.json());
        expect(body.data.hasData).toBe(true);
        expect(body.data.requests24h).toBe(110);
        expect(body.data.errorRate).toBeCloseTo(0.0909, 3);
        expect(body.data.avgLatencyMs).toBeCloseTo(54.5, 1);
        // p95 of 100×bucket6 + 10×bucket7 = watermark at 104.5; cumulative
        // hits 110 in bucket 7 → upper bound 128.
        expect(body.data.p95LatencyMs).toBe(128);
    });
});
describe('GET /api/cms/stats/content-health', () => {
    const originalSecret = process.env.CMS_SECRET;
    beforeEach(() => {
        process.env.CMS_SECRET = SECRET;
    });
    afterEach(() => {
        if (originalSecret === undefined)
            delete process.env.CMS_SECRET;
        else
            process.env.CMS_SECRET = originalSecret;
    });
    it('rejects unauthenticated requests with 401', async () => {
        const db = createMockDB();
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db });
        const res = await handler(new Request('https://example.com/api/cms/stats/content-health'));
        expect(res.status).toBe(401);
    });
    it('returns a perfect score (100, "Good") and zero counts when no issues exist', async () => {
        const db = createMockDB();
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db });
        const headers = await sessionHeaders();
        const res = await handler(new Request('https://example.com/api/cms/stats/content-health', {
            headers: { cookie: headers.cookie },
        }));
        expect(res.status).toBe(200);
        const body = (await res.json());
        expect(body.data.score).toBe(100);
        expect(body.data.label).toBe('Good');
        expect(body.data.totalIssues).toBe(0);
        expect(body.data.lastScanAt).toBeNull();
        expect(body.data.counts).toEqual({
            MISSING_META_DESCRIPTION: 0,
            MISSING_ALT_TEXT: 0,
            BROKEN_INTERNAL_LINK: 0,
            OUTDATED_CONTENT: 0,
        });
    });
    it('aggregates counts and computes a penalty-weighted score', async () => {
        // 5 missing meta (5×5=25 penalty)
        // 12 missing alt (12×2=24 penalty)
        // 2 broken links (2×10=20 penalty)
        // 8 outdated (8×3=24 penalty)
        // → score = 100 - 93 = 7
        const db = createMockDB({
            issues: [
                {
                    type: 'MISSING_META_DESCRIPTION',
                    count: 5,
                    lastSeenAt: new Date('2026-05-28T00:00:00Z'),
                },
                { type: 'MISSING_ALT_TEXT', count: 12, lastSeenAt: new Date('2026-05-28T01:00:00Z') },
                { type: 'BROKEN_INTERNAL_LINK', count: 2, lastSeenAt: new Date('2026-05-28T02:00:00Z') },
                { type: 'OUTDATED_CONTENT', count: 8, lastSeenAt: new Date('2026-05-28T03:00:00Z') },
            ],
        });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db });
        const headers = await sessionHeaders();
        const res = await handler(new Request('https://example.com/api/cms/stats/content-health', {
            headers: { cookie: headers.cookie },
        }));
        expect(res.status).toBe(200);
        const body = (await res.json());
        expect(body.data.totalIssues).toBe(27);
        expect(body.data.score).toBe(7);
        expect(body.data.label).toBe('Poor');
        expect(body.data.counts.MISSING_META_DESCRIPTION).toBe(5);
        expect(body.data.counts.MISSING_ALT_TEXT).toBe(12);
        expect(body.data.counts.BROKEN_INTERNAL_LINK).toBe(2);
        expect(body.data.counts.OUTDATED_CONTENT).toBe(8);
        expect(body.data.lastScanAt).toBe('2026-05-28T03:00:00.000Z');
    });
    it('floors the score at 0 when penalties exceed 100', async () => {
        const db = createMockDB({
            issues: [
                {
                    type: 'BROKEN_INTERNAL_LINK',
                    count: 50, // 500 penalty
                    lastSeenAt: new Date(),
                },
            ],
        });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db });
        const headers = await sessionHeaders();
        const res = await handler(new Request('https://example.com/api/cms/stats/content-health', {
            headers: { cookie: headers.cookie },
        }));
        const body = (await res.json());
        expect(body.data.score).toBe(0);
        expect(body.data.label).toBe('Poor');
    });
});
//# sourceMappingURL=stats-routes.test.js.map