import * as crypto from 'crypto';
import { ZoomAnalyticsWebhookSignatureUtil } from './zoom-analytics-webhook-signature.util';

describe('ZoomAnalyticsWebhookSignatureUtil', () => {
  const secret = 'test-webhook-secret';

  function sign(body: unknown, timestamp: string): string {
    const message = `v0:${timestamp}:${JSON.stringify(body)}`;
    return `v0=${crypto.createHmac('sha256', secret).update(message).digest('hex')}`;
  }

  describe('verifySignature', () => {
    it('accepts a signature computed the same way Zoom computes it', () => {
      const body = { event: 'webinar.participant_joined', payload: { object: { id: '123' } } };
      const timestamp = String(Math.floor(Date.now() / 1000));
      const signature = sign(body, timestamp);

      expect(ZoomAnalyticsWebhookSignatureUtil.verifySignature(body, signature, timestamp, secret)).toBe(true);
    });

    it('rejects a signature computed with the wrong secret', () => {
      const body = { event: 'webinar.participant_joined' };
      const timestamp = String(Math.floor(Date.now() / 1000));
      const badSignature = `v0=${crypto.createHmac('sha256', 'wrong-secret').update(`v0:${timestamp}:${JSON.stringify(body)}`).digest('hex')}`;

      expect(ZoomAnalyticsWebhookSignatureUtil.verifySignature(body, badSignature, timestamp, secret)).toBe(false);
    });

    it('rejects when the body has been tampered with after signing', () => {
      const original = { event: 'webinar.participant_joined', payload: { object: { id: '123' } } };
      const timestamp = String(Math.floor(Date.now() / 1000));
      const signature = sign(original, timestamp);
      const tampered = { ...original, payload: { object: { id: '999' } } };

      expect(ZoomAnalyticsWebhookSignatureUtil.verifySignature(tampered, signature, timestamp, secret)).toBe(false);
    });

    it('rejects when the signature or timestamp header is missing', () => {
      const body = { event: 'webinar.participant_joined' };
      expect(ZoomAnalyticsWebhookSignatureUtil.verifySignature(body, undefined, '123', secret)).toBe(false);
      expect(ZoomAnalyticsWebhookSignatureUtil.verifySignature(body, 'v0=abc', undefined, secret)).toBe(false);
    });

    it('rejects a malformed signature instead of throwing (length mismatch in timingSafeEqual)', () => {
      const body = { event: 'webinar.participant_joined' };
      const timestamp = String(Math.floor(Date.now() / 1000));
      expect(() =>
        ZoomAnalyticsWebhookSignatureUtil.verifySignature(body, 'too-short', timestamp, secret),
      ).not.toThrow();
      expect(ZoomAnalyticsWebhookSignatureUtil.verifySignature(body, 'too-short', timestamp, secret)).toBe(false);
    });
  });

  describe('isTimestampFresh', () => {
    it('accepts a current timestamp', () => {
      expect(ZoomAnalyticsWebhookSignatureUtil.isTimestampFresh(String(Math.floor(Date.now() / 1000)))).toBe(true);
    });

    it('rejects a timestamp older than the max age window (replay protection)', () => {
      const staleTimestamp = String(Math.floor(Date.now() / 1000) - 3600);
      expect(ZoomAnalyticsWebhookSignatureUtil.isTimestampFresh(staleTimestamp)).toBe(false);
    });

    it('rejects a missing or non-numeric timestamp', () => {
      expect(ZoomAnalyticsWebhookSignatureUtil.isTimestampFresh(undefined)).toBe(false);
      expect(ZoomAnalyticsWebhookSignatureUtil.isTimestampFresh('not-a-number')).toBe(false);
    });
  });

  describe('buildUrlValidationResponse', () => {
    it('returns the plain token plus its HMAC-encrypted form', () => {
      const plainToken = 'abc123';
      const result = ZoomAnalyticsWebhookSignatureUtil.buildUrlValidationResponse(plainToken, secret);

      expect(result.plainToken).toBe(plainToken);
      expect(result.encryptedToken).toBe(
        crypto.createHmac('sha256', secret).update(plainToken).digest('hex'),
      );
    });
  });
});
