import { DedupService } from '../dedup.service';
import type { DedupRepository } from '../dedup.repository';

class FakeRepo implements DedupRepository {
    private rows = new Map<string, Date>();

    async claim(eventKey: string, windowSeconds: number, now: Date): Promise<boolean> {
        const existing = this.rows.get(eventKey);
        if (existing && existing > now) return false;
        const expiresAt = new Date(now.getTime() + windowSeconds * 1000);
        this.rows.set(eventKey, expiresAt);
        return true;
    }

    async purgeExpired(now: Date): Promise<number> {
        let deleted = 0;
        for (const [key, expiresAt] of this.rows) {
            if (expiresAt <= now) {
                this.rows.delete(key);
                deleted++;
            }
        }
        return deleted;
    }
}

describe('DedupService', () => {
    it('returns true on first occurrence', async () => {
        const repo = new FakeRepo();
        const svc = new DedupService({ repository: repo, now: () => new Date('2026-05-12T10:00:00Z') });

        expect(await svc.shouldDeliver('monitor_down:1:5')).toBe(true);
    });

    it('returns false for the same key inside the window', async () => {
        const t0 = new Date('2026-05-12T10:00:00Z');
        let clock = t0;
        const repo = new FakeRepo();
        const svc = new DedupService({ repository: repo, now: () => clock });

        expect(await svc.shouldDeliver('k', 300)).toBe(true);

        clock = new Date(t0.getTime() + 30_000); // +30s, still within 5min window
        expect(await svc.shouldDeliver('k', 300)).toBe(false);

        clock = new Date(t0.getTime() + 299_000); // just before window expiry
        expect(await svc.shouldDeliver('k', 300)).toBe(false);
    });

    it('returns true again after the window expires', async () => {
        const t0 = new Date('2026-05-12T10:00:00Z');
        let clock = t0;
        const repo = new FakeRepo();
        const svc = new DedupService({ repository: repo, now: () => clock });

        expect(await svc.shouldDeliver('k', 60)).toBe(true);

        clock = new Date(t0.getTime() + 61_000); // 1s after window
        expect(await svc.shouldDeliver('k', 60)).toBe(true);
    });

    it('treats different keys independently', async () => {
        const repo = new FakeRepo();
        const svc = new DedupService({ repository: repo, now: () => new Date('2026-05-12T10:00:00Z') });

        expect(await svc.shouldDeliver('monitor_down:1:5')).toBe(true);
        expect(await svc.shouldDeliver('monitor_down:2:5')).toBe(true);
        expect(await svc.shouldDeliver('monitor_down:1:6')).toBe(true);
    });

    it('purgeExpired removes only expired rows', async () => {
        const t0 = new Date('2026-05-12T10:00:00Z');
        let clock = t0;
        const repo = new FakeRepo();
        const svc = new DedupService({ repository: repo, now: () => clock });

        await svc.shouldDeliver('short', 10);
        await svc.shouldDeliver('long', 600);

        clock = new Date(t0.getTime() + 11_000); // short window expired
        const deleted = await svc.purgeExpired();
        expect(deleted).toBe(1);

        // long key still locks
        expect(await svc.shouldDeliver('long', 600)).toBe(false);
        // short key is fresh again
        expect(await svc.shouldDeliver('short', 10)).toBe(true);
    });

    it('uses default window when caller omits it', async () => {
        const repo = new FakeRepo();
        const claimSpy = jest.spyOn(repo, 'claim');
        const svc = new DedupService({ repository: repo, now: () => new Date('2026-05-12T10:00:00Z') });

        await svc.shouldDeliver('k');

        expect(claimSpy).toHaveBeenCalledWith('k', 300, expect.any(Date));
    });
});
