/**
 * TDD for the pure SLO math (no Prisma, no I/O).
 *
 * Covers: SLI from up/down samples (maintenance excluded), error-budget
 * (budget / consumed / remaining), attainment, outage-episode counting,
 * MTTR, and service-credit estimation.
 */
import {
    computeSli,
    computeErrorBudget,
    computeSlo,
    countOutageEpisodes,
    meanTimeToRecovery,
    estimateServiceCredit,
    THIRTY_DAYS_MINUTES,
    type StatusSample,
} from '../slo-math';

describe('computeSli', () => {
    it('returns 100 when there are no non-maintenance samples', () => {
        expect(computeSli({ up: 0, down: 0, maintenance: 5 })).toBe(100);
    });

    it('excludes maintenance from the denominator', () => {
        // 90 up, 10 down, 100 maintenance → 90/100 = 90% (maintenance ignored)
        expect(computeSli({ up: 90, down: 10, maintenance: 100 })).toBe(90);
    });

    it('computes a partial uptime to two decimals', () => {
        // 9990 up / 10000 = 99.90
        expect(computeSli({ up: 9990, down: 10, maintenance: 0 })).toBe(99.9);
    });
});

describe('computeErrorBudget', () => {
    it('total budget is (1 - target/100) * windowMinutes', () => {
        // target 99.9% over 30d (43200 min) → budget 43.2 min
        const b = computeErrorBudget({ targetPercent: 99.9, windowMinutes: THIRTY_DAYS_MINUTES, downtimeMinutes: 0 });
        expect(b.budgetMinutes).toBeCloseTo(43.2, 5);
        expect(b.consumedMinutes).toBe(0);
        expect(b.remainingMinutes).toBeCloseTo(43.2, 5);
        expect(b.remainingPercent).toBe(100);
        expect(b.breached).toBe(false);
    });

    it('consuming half the budget reports 50% remaining', () => {
        const b = computeErrorBudget({ targetPercent: 99.9, windowMinutes: THIRTY_DAYS_MINUTES, downtimeMinutes: 21.6 });
        expect(b.remainingPercent).toBeCloseTo(50, 5);
        expect(b.breached).toBe(false);
    });

    it('over-spending the budget reports negative remaining and breached', () => {
        const b = computeErrorBudget({ targetPercent: 99.9, windowMinutes: THIRTY_DAYS_MINUTES, downtimeMinutes: 86.4 });
        expect(b.remainingMinutes).toBeCloseTo(-43.2, 5);
        expect(b.remainingPercent).toBeLessThan(0);
        expect(b.breached).toBe(true);
    });

    it('target 100% leaves zero budget; any downtime breaches', () => {
        const none = computeErrorBudget({ targetPercent: 100, windowMinutes: THIRTY_DAYS_MINUTES, downtimeMinutes: 0 });
        expect(none.budgetMinutes).toBe(0);
        expect(none.remainingPercent).toBe(100);
        expect(none.breached).toBe(false);

        const some = computeErrorBudget({ targetPercent: 100, windowMinutes: THIRTY_DAYS_MINUTES, downtimeMinutes: 1 });
        expect(some.budgetMinutes).toBe(0);
        expect(some.remainingPercent).toBe(0);
        expect(some.breached).toBe(true);
    });
});

describe('computeSlo', () => {
    it('combines SLI, attainment, and error budget', () => {
        // 99.95% measured against 99.9% target over 30d.
        const slo = computeSlo({
            samples: { up: 9995, down: 5, maintenance: 0 },
            targetPercent: 99.9,
            windowMinutes: THIRTY_DAYS_MINUTES,
        });
        expect(slo.sli).toBe(99.95);
        expect(slo.targetPercent).toBe(99.9);
        expect(slo.attainment).toBeCloseTo(0.05, 5);
        expect(slo.meetingSla).toBe(true);
        // downtime = (5/10000) * 43200 = 21.6 min ; budget 43.2 → 50% remaining
        expect(slo.errorBudget.remainingPercent).toBeCloseTo(50, 5);
    });

    it('flags a missed SLA', () => {
        const slo = computeSlo({
            samples: { up: 980, down: 20, maintenance: 0 },
            targetPercent: 99.9,
            windowMinutes: THIRTY_DAYS_MINUTES,
        });
        expect(slo.sli).toBe(98);
        expect(slo.meetingSla).toBe(false);
        expect(slo.attainment).toBeLessThan(0);
        expect(slo.errorBudget.breached).toBe(true);
    });
});

describe('countOutageEpisodes', () => {
    const t = (min: number) => new Date(Date.UTC(2026, 5, 1, 0, min, 0));

    it('counts zero when never down', () => {
        const stream: StatusSample[] = [
            { at: t(0), status: 'up' },
            { at: t(1), status: 'up' },
        ];
        expect(countOutageEpisodes(stream)).toBe(0);
    });

    it('counts a single down→up episode', () => {
        const stream: StatusSample[] = [
            { at: t(0), status: 'up' },
            { at: t(1), status: 'down' },
            { at: t(2), status: 'down' },
            { at: t(3), status: 'up' },
        ];
        expect(countOutageEpisodes(stream)).toBe(1);
    });

    it('counts two separate episodes', () => {
        const stream: StatusSample[] = [
            { at: t(0), status: 'down' },
            { at: t(1), status: 'up' },
            { at: t(2), status: 'down' },
            { at: t(3), status: 'up' },
        ];
        expect(countOutageEpisodes(stream)).toBe(2);
    });

    it('treats a still-open outage at the end as one episode', () => {
        const stream: StatusSample[] = [
            { at: t(0), status: 'up' },
            { at: t(1), status: 'down' },
        ];
        expect(countOutageEpisodes(stream)).toBe(1);
    });

    it('ignores maintenance samples (no false episode boundary)', () => {
        const stream: StatusSample[] = [
            { at: t(0), status: 'up' },
            { at: t(1), status: 'maintenance' },
            { at: t(2), status: 'down' },
            { at: t(3), status: 'up' },
        ];
        expect(countOutageEpisodes(stream)).toBe(1);
    });
});

describe('meanTimeToRecovery', () => {
    const t = (min: number) => new Date(Date.UTC(2026, 5, 1, 0, min, 0));

    it('is 0 when there were no recovered outages', () => {
        expect(meanTimeToRecovery([{ at: t(0), status: 'up' }])).toBe(0);
    });

    it('averages recovery durations across episodes', () => {
        // outage 1: down@1 → up@3  (2 min). outage 2: down@10 → up@14 (4 min). mean = 3.
        const stream: StatusSample[] = [
            { at: t(0), status: 'up' },
            { at: t(1), status: 'down' },
            { at: t(3), status: 'up' },
            { at: t(10), status: 'down' },
            { at: t(14), status: 'up' },
        ];
        expect(meanTimeToRecovery(stream)).toBe(3);
    });

    it('excludes a still-open (unrecovered) outage from the MTTR mean', () => {
        const stream: StatusSample[] = [
            { at: t(0), status: 'down' },
            { at: t(2), status: 'up' }, // recovered in 2 min
            { at: t(5), status: 'down' }, // never recovers
        ];
        expect(meanTimeToRecovery(stream)).toBe(2);
    });
});

describe('estimateServiceCredit', () => {
    it('returns null when no creditTerms', () => {
        expect(estimateServiceCredit(null, 98, 99.9)).toBeNull();
        expect(estimateServiceCredit('', 98, 99.9)).toBeNull();
    });

    it('returns null for an unrecognised format', () => {
        expect(estimateServiceCredit('call your account manager', 98, 99.9)).toBeNull();
    });

    it('threshold pattern: credit applies only below the threshold', () => {
        // "10% credit if uptime < 99.9%"
        expect(estimateServiceCredit('10% credit if uptime < 99.9%', 99.95, 99.9)).toBe(0);
        expect(estimateServiceCredit('10% credit if uptime < 99.9%', 98, 99.9)).toBe(10);
    });

    it('tiered pattern: percent per 0.1% below target', () => {
        // "5% per 0.1% below target", target 99.9, measured 99.5 → 0.4 below → 4 tiers → 20%
        expect(estimateServiceCredit('5% per 0.1% below target', 99.5, 99.9)).toBe(20);
        // meeting target → 0
        expect(estimateServiceCredit('5% per 0.1% below target', 99.95, 99.9)).toBe(0);
    });
});
