import { EscalationService } from '../escalation.service';

// Mock the NotificationDispatcher so tick() doesn't try to re-enter
// the full notification pipeline.
jest.mock('@/lib/notification-system', () => ({
    NotificationDispatcher: {
        emit: jest.fn().mockResolvedValue(undefined),
    },
}));

function makeMockPrisma() {
    const create = jest.fn();
    const deleteMany = jest.fn();
    const findMany = jest.fn();
    const del = jest.fn();
    const heartbeatFindFirst = jest.fn();
    const incidentFindUnique = jest.fn();
    return {
        pendingEscalation: { create, deleteMany, findMany, delete: del },
        heartbeat: { findFirst: heartbeatFindFirst },
        statusIncident: { findUnique: incidentFindUnique },
        _spies: { create, deleteMany, findMany, del, heartbeatFindFirst, incidentFindUnique },
    };
}

describe('EscalationService', () => {
    const fixedNow = new Date('2026-05-22T18:00:00Z');

    describe('schedule', () => {
        it('writes a row with scheduledFor = now + delayMinutes', async () => {
            const prisma = makeMockPrisma();
            prisma._spies.create.mockResolvedValueOnce({ id: 7 });
            const svc = new EscalationService(prisma as never, () => fixedNow);
            const id = await svc.schedule({
                ruleId: 1,
                eventKey: 'monitor_down:42:0',
                event: 'monitor_down',
                payload: { monitorId: 42 },
                delayMinutes: 15,
                monitorId: 42,
            });
            expect(id).toBe(7);
            const call = prisma._spies.create.mock.calls[0][0];
            const sched = call.data.scheduledFor as Date;
            const diffMs = sched.getTime() - fixedNow.getTime();
            expect(diffMs).toBe(15 * 60_000);
            expect(call.data.payload).toContain('"monitorId":42');
        });
    });

    describe('cancelForMonitor / cancelForIncident', () => {
        it('deletes rows by monitorId', async () => {
            const prisma = makeMockPrisma();
            prisma._spies.deleteMany.mockResolvedValueOnce({ count: 3 });
            const svc = new EscalationService(prisma as never, () => fixedNow);
            expect(await svc.cancelForMonitor(42)).toBe(3);
            expect(prisma._spies.deleteMany).toHaveBeenCalledWith({ where: { monitorId: 42 } });
        });
        it('deletes rows by incidentId', async () => {
            const prisma = makeMockPrisma();
            prisma._spies.deleteMany.mockResolvedValueOnce({ count: 1 });
            const svc = new EscalationService(prisma as never, () => fixedNow);
            expect(await svc.cancelForIncident(99)).toBe(1);
            expect(prisma._spies.deleteMany).toHaveBeenCalledWith({ where: { incidentId: 99 } });
        });
    });

    describe('tick — dispatch vs cancel', () => {
        it('dispatches when the monitor is still down', async () => {
            const prisma = makeMockPrisma();
            prisma._spies.findMany.mockResolvedValueOnce([
                { id: 1, event: 'monitor_down', monitorId: 42, incidentId: null, payload: JSON.stringify({ monitorId: 42 }) },
            ]);
            prisma._spies.heartbeatFindFirst.mockResolvedValueOnce({ status: 'down' });
            const svc = new EscalationService(prisma as never, () => fixedNow);
            const result = await svc.tick(50);
            expect(result.dispatched).toBe(1);
            expect(result.cancelled).toBe(0);
            expect(prisma._spies.del).toHaveBeenCalledWith({ where: { id: 1 } });
        });

        it('cancels (silently drops) when the monitor recovered before the delay elapsed', async () => {
            const prisma = makeMockPrisma();
            prisma._spies.findMany.mockResolvedValueOnce([
                { id: 1, event: 'monitor_down', monitorId: 42, incidentId: null, payload: JSON.stringify({ monitorId: 42 }) },
            ]);
            prisma._spies.heartbeatFindFirst.mockResolvedValueOnce({ status: 'up' });
            const svc = new EscalationService(prisma as never, () => fixedNow);
            const result = await svc.tick(50);
            expect(result.dispatched).toBe(0);
            expect(result.cancelled).toBe(1);
            expect(prisma._spies.del).toHaveBeenCalledWith({ where: { id: 1 } });
        });

        it('cancels when an incident has been resolved', async () => {
            const prisma = makeMockPrisma();
            prisma._spies.findMany.mockResolvedValueOnce([
                { id: 2, event: 'incident_create', monitorId: null, incidentId: 9, payload: JSON.stringify({ incidentId: 9 }) },
            ]);
            prisma._spies.incidentFindUnique.mockResolvedValueOnce({ deletedAt: null, state: { name: 'resolved' } });
            const svc = new EscalationService(prisma as never, () => fixedNow);
            const result = await svc.tick(50);
            expect(result.dispatched).toBe(0);
            expect(result.cancelled).toBe(1);
        });

        it('continues after a per-row error (does not crash the batch)', async () => {
            const prisma = makeMockPrisma();
            prisma._spies.findMany.mockResolvedValueOnce([
                { id: 1, event: 'monitor_down', monitorId: 42, incidentId: null, payload: JSON.stringify({ monitorId: 42 }) },
                { id: 2, event: 'monitor_down', monitorId: 43, incidentId: null, payload: JSON.stringify({ monitorId: 43 }) },
            ]);
            // First row throws on heartbeat lookup; second succeeds.
            prisma._spies.heartbeatFindFirst
                .mockRejectedValueOnce(new Error('connection lost'))
                .mockResolvedValueOnce({ status: 'down' });
            const svc = new EscalationService(prisma as never, () => fixedNow);
            const result = await svc.tick(50);
            expect(result.errored).toBe(1);
            expect(result.dispatched).toBe(1);
        });
    });
});
