/**
 * P2-6 — merge latest raw heartbeat + recent hourly aggregates into
 * the heartbeat-shaped array the dashboard sparkline expects.
 *
 * Goal: dashboard renders unchanged after the switch from
 * `take: 60 raw` to `1 latest raw + 24 hourly`. We hide the aggregate/raw
 * distinction at this seam so the client doesn't need new types.
 */
import { mergeDashboardHeartbeats } from '../dashboard-heartbeat-merge';

describe('mergeDashboardHeartbeats (P2-6)', () => {
    const baseRaw = {
        id: 999,
        status: 'up' as const,
        responseTimeMs: 123,
        createdAt: new Date('2026-06-01T12:00:00Z'),
        tlsValid: true,
        tlsExpiresAt: new Date('2027-01-01T00:00:00Z'),
        tlsIssuer: 'Test CA',
    };

    const hourly = (h: number, uptimePercent: number, avgLatency: number) => ({
        timestamp: new Date(`2026-06-01T${String(h).padStart(2, '0')}:00:00Z`),
        avgLatency,
        uptimePercent,
        totalChecks: 60,
    });

    it('emits the raw heartbeat FIRST so dashboard "[0]" reads current status', () => {
        const merged = mergeDashboardHeartbeats(baseRaw, [hourly(11, 100, 80), hourly(10, 100, 75)]);
        expect(merged.length).toBe(3);
        expect(merged[0].id).toBe(999);
        expect(merged[0].status).toBe('up');
        expect(merged[0].responseTimeMs).toBe(123);
    });

    it('maps hourly aggregates to synthetic heartbeats with status derived from uptime%', () => {
        const merged = mergeDashboardHeartbeats(baseRaw, [
            hourly(11, 100, 80),  // -> up
            hourly(10, 50, 200),  // -> down
            hourly(9, 99.5, 90),  // -> up (>= 99)
            hourly(8, 98.9, 100), // -> down (< 99)
        ]);
        expect(merged[1].status).toBe('up');
        expect(merged[2].status).toBe('down');
        expect(merged[3].status).toBe('up');
        expect(merged[4].status).toBe('down');
    });

    it('uses avgLatency for the synthetic responseTimeMs', () => {
        const merged = mergeDashboardHeartbeats(baseRaw, [hourly(11, 100, 80)]);
        expect(merged[1].responseTimeMs).toBe(80);
    });

    it('uses hourly timestamp for createdAt', () => {
        const merged = mergeDashboardHeartbeats(baseRaw, [hourly(11, 100, 80)]);
        expect(merged[1].createdAt.toISOString()).toBe('2026-06-01T11:00:00.000Z');
    });

    it('synthetic rows have null TLS info (aggregates do not carry it)', () => {
        const merged = mergeDashboardHeartbeats(baseRaw, [hourly(11, 100, 80)]);
        expect(merged[1].tlsValid).toBeNull();
        expect(merged[1].tlsExpiresAt).toBeNull();
        expect(merged[1].tlsIssuer).toBeNull();
    });

    it('returns ONLY the raw heartbeat when no hourly rows exist', () => {
        const merged = mergeDashboardHeartbeats(baseRaw, []);
        expect(merged).toHaveLength(1);
        expect(merged[0].id).toBe(999);
    });

    it('returns an empty array when there is no raw heartbeat AND no hourlies', () => {
        const merged = mergeDashboardHeartbeats(null, []);
        expect(merged).toEqual([]);
    });

    it('returns only synthetic rows when there is no raw heartbeat but hourlies exist', () => {
        const merged = mergeDashboardHeartbeats(null, [hourly(11, 100, 80), hourly(10, 50, 200)]);
        expect(merged).toHaveLength(2);
        expect(merged[0].status).toBe('up');
        expect(merged[1].status).toBe('down');
    });

    it('preserves the hourly array order (caller controls newest-first)', () => {
        const newer = hourly(12, 100, 80);
        const older = hourly(11, 100, 90);
        const merged = mergeDashboardHeartbeats(baseRaw, [newer, older]);
        expect(merged[1].createdAt.getTime()).toBeGreaterThan(merged[2].createdAt.getTime());
    });
});
