import { describe, it, expect } from 'vitest';
import { LATENCY_BUCKETS, HISTOGRAM_LENGTH, bucketStartFor, estimatePercentile, histogramBucketIndex, makeHistogramFor, mergeHistograms, normalizeRoute, shouldRecord, statusBucketOf, } from '../../diagnostics/api-metrics.js';
describe('statusBucketOf', () => {
    it('maps standard HTTP families to xx buckets', () => {
        expect(statusBucketOf(200)).toBe('2xx');
        expect(statusBucketOf(204)).toBe('2xx');
        expect(statusBucketOf(301)).toBe('3xx');
        expect(statusBucketOf(304)).toBe('3xx');
        expect(statusBucketOf(400)).toBe('4xx');
        expect(statusBucketOf(404)).toBe('4xx');
        expect(statusBucketOf(429)).toBe('4xx');
        expect(statusBucketOf(500)).toBe('5xx');
        expect(statusBucketOf(503)).toBe('5xx');
    });
    it('puts unknown statuses in the "other" bucket', () => {
        expect(statusBucketOf(99)).toBe('other');
        expect(statusBucketOf(700)).toBe('other');
        expect(statusBucketOf(-1)).toBe('other');
    });
});
describe('histogramBucketIndex', () => {
    it('assigns sub-1ms to bucket 0', () => {
        expect(histogramBucketIndex(0)).toBe(0);
        expect(histogramBucketIndex(0.4)).toBe(0);
        expect(histogramBucketIndex(0.99)).toBe(0);
    });
    it('uses log-2 boundaries', () => {
        expect(histogramBucketIndex(1)).toBe(1); // 1 < 2
        expect(histogramBucketIndex(1.5)).toBe(1);
        expect(histogramBucketIndex(2)).toBe(2); // 2 < 4
        expect(histogramBucketIndex(4)).toBe(3);
        expect(histogramBucketIndex(127)).toBe(7); // 64-128
        expect(histogramBucketIndex(128)).toBe(8); // 128-256
    });
    it('puts ≥1024ms in the overflow bucket', () => {
        expect(histogramBucketIndex(1024)).toBe(LATENCY_BUCKETS.length);
        expect(histogramBucketIndex(60_000)).toBe(LATENCY_BUCKETS.length);
    });
    it('handles non-finite input as bucket 0', () => {
        expect(histogramBucketIndex(NaN)).toBe(0);
        expect(histogramBucketIndex(-1)).toBe(0);
        expect(histogramBucketIndex(Infinity)).toBe(0);
    });
});
describe('makeHistogramFor + mergeHistograms', () => {
    it('initial histogram has exactly one count in the right bucket', () => {
        const hist = makeHistogramFor(50); // 32-64ms → bucket 6
        expect(hist).toHaveLength(HISTOGRAM_LENGTH);
        expect(hist[6]).toBe(1);
        expect(hist.reduce((s, v) => s + v, 0)).toBe(1);
    });
    it('merges element-wise without mutating inputs', () => {
        const a = makeHistogramFor(50);
        const b = makeHistogramFor(100); // 64-128ms → bucket 7
        const merged = mergeHistograms(a, b);
        expect(merged[6]).toBe(1);
        expect(merged[7]).toBe(1);
        expect(a[7]).toBe(0); // unchanged
    });
    it('tolerates undefined / short prior arrays', () => {
        const b = makeHistogramFor(10);
        expect(() => mergeHistograms(undefined, b)).not.toThrow();
        expect(() => mergeHistograms([1, 2, 3], b)).not.toThrow();
        const merged = mergeHistograms([1, 2, 3], b);
        expect(merged[0]).toBe(1);
        expect(merged[1]).toBe(2);
        expect(merged[2]).toBe(3);
    });
});
describe('estimatePercentile', () => {
    it('returns 0 for an empty histogram', () => {
        expect(estimatePercentile(new Array(HISTOGRAM_LENGTH).fill(0), 0.95)).toBe(0);
    });
    it('returns the bucket boundary that crosses the p-watermark', () => {
        // 90% of traffic in bucket 4 (8-16ms), 10% in bucket 7 (64-128ms).
        // p50 should land in bucket 4 (upper bound = 16).
        // p95 must cross into bucket 7 (upper bound = 128).
        const hist = new Array(HISTOGRAM_LENGTH).fill(0);
        hist[4] = 90;
        hist[7] = 10;
        expect(estimatePercentile(hist, 0.5)).toBe(16);
        expect(estimatePercentile(hist, 0.95)).toBe(128);
    });
    it('estimates beyond the last bucket using the overflow heuristic', () => {
        const hist = new Array(HISTOGRAM_LENGTH).fill(0);
        hist[HISTOGRAM_LENGTH - 1] = 10; // overflow bucket
        const result = estimatePercentile(hist, 0.95);
        expect(result).toBeGreaterThan(LATENCY_BUCKETS[LATENCY_BUCKETS.length - 1]);
    });
});
describe('bucketStartFor', () => {
    it('snaps to the start of the UTC minute', () => {
        const t = new Date('2026-05-28T14:37:42.123Z').getTime();
        const start = bucketStartFor(t);
        expect(start.toISOString()).toBe('2026-05-28T14:37:00.000Z');
    });
    it('is idempotent — calling twice returns the same boundary', () => {
        const t = new Date('2026-05-28T14:37:42.123Z').getTime();
        const a = bucketStartFor(t);
        const b = bucketStartFor(a);
        expect(a.getTime()).toBe(b.getTime());
    });
});
describe('normalizeRoute', () => {
    it('passes static routes through verbatim', () => {
        expect(normalizeRoute('/stats')).toBe('/stats');
        expect(normalizeRoute('/health')).toBe('/health');
        expect(normalizeRoute('/auth/login')).toBe('/auth/login');
        expect(normalizeRoute('/notifications')).toBe('/notifications');
    });
    it('strips the /api/cms prefix if present', () => {
        expect(normalizeRoute('/api/cms/stats')).toBe('/stats');
        expect(normalizeRoute('/api/cms/collections/posts/abc')).toBe('/collections/:slug/:id');
    });
    it('parametrises collection routes by document id', () => {
        expect(normalizeRoute('/collections/posts')).toBe('/collections/:slug');
        expect(normalizeRoute('/collections/posts/abc123')).toBe('/collections/:slug/:id');
        expect(normalizeRoute('/collections/posts/abc123/restore')).toBe('/collections/:slug/:id/restore');
        expect(normalizeRoute('/collections/posts/abc123/versions')).toBe('/collections/:slug/:id/versions');
    });
    it('parametrises globals, media, forms, users, api-keys', () => {
        expect(normalizeRoute('/globals/site-settings')).toBe('/globals/:slug');
        expect(normalizeRoute('/media/abc')).toBe('/media/:id');
        expect(normalizeRoute('/forms/xyz/submit')).toBe('/forms/:id/submit');
        expect(normalizeRoute('/forms/xyz')).toBe('/forms/:id');
        expect(normalizeRoute('/users/u1')).toBe('/users/:id');
        expect(normalizeRoute('/api-keys/k1')).toBe('/api-keys/:id');
    });
    it('collapses unknown long paths into the /other catch-all', () => {
        expect(normalizeRoute('/some/random/path/we/never/added')).toBe('/other');
        expect(normalizeRoute('/x/y/z')).toBe('/other');
    });
    it('strips query strings and trailing slashes', () => {
        expect(normalizeRoute('/stats?from=2026')).toBe('/stats');
        expect(normalizeRoute('/collections/posts/')).toBe('/collections/:slug');
    });
});
describe('shouldRecord', () => {
    it('skips the dashboard polling routes', () => {
        expect(shouldRecord('/stats')).toBe(false);
        expect(shouldRecord('/stats/api-delivery')).toBe(false);
        expect(shouldRecord('/stats/content-health')).toBe(false);
        expect(shouldRecord('/health')).toBe(false);
        expect(shouldRecord('/notifications/stream')).toBe(false);
    });
    it('records normal API traffic', () => {
        expect(shouldRecord('/collections/:slug')).toBe(true);
        expect(shouldRecord('/collections/:slug/:id')).toBe(true);
        expect(shouldRecord('/auth/login')).toBe(true);
        expect(shouldRecord('/other')).toBe(true);
    });
});
//# sourceMappingURL=api-metrics.test.js.map