/**
 * Unit tests for the email delivery status state machine.
 *
 * These lock in the two guarantees that fix the "delivered email shows Failed"
 * class of bug:
 *   1. Forward-only: positive progress never regresses; duplicates are no-ops.
 *   2. A webhook can never produce 'failed' — post-send failures are 'bounced',
 *      and proven delivery/open is never overwritten.
 */

import { computeDeliveryUpdate } from '../emailDeliveryStatus';
import type { EmailDispatchStatus } from '../../../models/EmailDispatch';

const AT = new Date('2026-07-31T12:00:00Z');

describe('computeDeliveryUpdate', () => {
  describe('forward progression', () => {
    it('advances sent → delivered', () => {
      expect(computeDeliveryUpdate('sent', 'delivered', AT)).toEqual({ status: 'delivered', deliveredAt: AT });
    });

    it('advances delivered → opened', () => {
      expect(computeDeliveryUpdate('delivered', 'opened', AT)).toEqual({ status: 'opened', openedAt: AT });
    });

    it('advances sent → opened (open implies delivery)', () => {
      const u = computeDeliveryUpdate('sent', 'opened', AT);
      expect(u?.status).toBe('opened');
      expect(u?.openedAt).toBe(AT);
    });

    it('treats clicked as opened', () => {
      expect(computeDeliveryUpdate('sent', 'clicked', AT)?.status).toBe('opened');
    });
  });

  describe('no regression / idempotency', () => {
    it('does not regress opened → delivered (only backfills timestamp)', () => {
      const u = computeDeliveryUpdate('opened', 'delivered', AT);
      expect(u).not.toBeNull();
      expect(u?.status).toBeUndefined();       // status unchanged
      expect(u?.deliveredAt).toBe(AT);          // timestamp still recorded
    });

    it('is a no-op-ish on duplicate delivered (keeps status, backfills ts)', () => {
      const u = computeDeliveryUpdate('delivered', 'delivered', AT);
      expect(u?.status).toBeUndefined();
    });
  });

  describe('never produces failed; delivery is never overwritten', () => {
    const events = ['delivered', 'opened', 'clicked', 'hard_bounce', 'soft_bounce', 'blocked', 'spam'] as const;
    const statuses: EmailDispatchStatus[] = ['queued', 'sending', 'sent', 'delivered', 'opened', 'bounced', 'failed', 'cancelled'];

    it('no event, from any status, ever yields status "failed"', () => {
      for (const s of statuses) {
        for (const e of events) {
          expect(computeDeliveryUpdate(s, e, AT)?.status).not.toBe('failed');
        }
      }
    });

    it('a bounce after proven delivery/open is ignored (stays delivered/opened)', () => {
      expect(computeDeliveryUpdate('delivered', 'hard_bounce', AT)).toBeNull();
      expect(computeDeliveryUpdate('opened', 'soft_bounce', AT)).toBeNull();
      expect(computeDeliveryUpdate('opened', 'blocked', AT)).toBeNull();
    });
  });

  describe('a delivery event CORRECTS a wrongly-failed record (the reported bug)', () => {
    it('failed → delivered when a delivered webhook arrives', () => {
      expect(computeDeliveryUpdate('failed', 'delivered', AT)).toEqual({ status: 'delivered', deliveredAt: AT });
    });
    it('failed → opened when an open/click webhook arrives', () => {
      expect(computeDeliveryUpdate('failed', 'opened', AT)?.status).toBe('opened');
      expect(computeDeliveryUpdate('failed', 'clicked', AT)?.status).toBe('opened');
    });
    it('failed → bounced when a bounce webhook arrives (provider accepted, then bounced)', () => {
      expect(computeDeliveryUpdate('failed', 'hard_bounce', AT, 'no such mailbox')).toEqual({
        status: 'bounced', bouncedAt: AT, bounceType: 'hard', bounceReason: 'no such mailbox',
      });
    });
    it('cancelled is never resurrected by any event', () => {
      const allEvents = ['delivered', 'opened', 'clicked', 'hard_bounce', 'soft_bounce', 'blocked', 'spam'] as const;
      for (const e of allEvents) {
        expect(computeDeliveryUpdate('cancelled', e, AT)).toBeNull();
      }
    });
  });

  describe('bounces', () => {
    it('sent → bounced on hard bounce with type + reason', () => {
      expect(computeDeliveryUpdate('sent', 'hard_bounce', AT, 'mailbox full')).toEqual({
        status: 'bounced', bouncedAt: AT, bounceType: 'hard', bounceReason: 'mailbox full',
      });
    });

    it('maps blocked to a blocked bounce type', () => {
      expect(computeDeliveryUpdate('sent', 'blocked', AT)?.bounceType).toBe('blocked');
    });

    it('an open after a soft bounce still wins (delivery proven)', () => {
      expect(computeDeliveryUpdate('bounced', 'opened', AT)?.status).toBe('opened');
      expect(computeDeliveryUpdate('bounced', 'delivered', AT)).toBeNull(); // bounce means not delivered
    });
  });

  describe('spam', () => {
    it('does not change status (delivered email should not be downgraded)', () => {
      expect(computeDeliveryUpdate('delivered', 'spam', AT)).toBeNull();
    });
  });
});
