import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { MixpanelService, SpamBlockedEvent, MixpanelEventProperties } from '../src/services/mixpanel.service';
import { FieldEncryptionService } from '@shieldai/db';
import crypto from 'crypto';

const mockFetch = vi.fn();
(global as any).fetch = mockFetch;

describe('MixpanelService', () => {
  let service: MixpanelService;

  beforeEach(() => {
    service = new MixpanelService({
      token: 'test-token',
      apiHost: 'api.mixpanel.com',
      enableLogging: false,
    });
    mockFetch.mockReset();
    vi.spyOn(console, 'log').mockImplementation(() => {});
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  describe('spamBlocked', () => {
    it('hashes phoneNumber using SHA-256 before sending to Mixpanel', async () => {
      const phoneNumber = '+14155552671';
      const expectedHash = crypto.createHash('sha256').update(phoneNumber).digest('hex');

      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
      } as Response);

      const event: SpamBlockedEvent = {
        phoneNumber,
        decision: 'BLOCK',
        confidence: 0.92,
        ruleMatches: ['rule-1'],
        timestamp: new Date('2026-01-01T00:00:00Z'),
      };

      const result = await service.spamBlocked(event);

      expect(result.phoneNumberHash).toBe(expectedHash);
      expect(result.phoneNumberHash).not.toBe(phoneNumber);
      expect(result.phoneNumberHash.length).toBe(64);
    });

    it('uses FieldEncryptionService.hashPhoneNumber for hashing', async () => {
      const phoneNumber = '+14155552671';
      const hashSpy = vi.spyOn(FieldEncryptionService, 'hashPhoneNumber');

      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
      } as Response);

      const event: SpamBlockedEvent = {
        phoneNumber,
        decision: 'BLOCK',
        confidence: 0.85,
        timestamp: new Date(),
      };

      await service.spamBlocked(event);

      expect(hashSpy).toHaveBeenCalledWith(phoneNumber);
      hashSpy.mockRestore();
    });

    it('sends hashed phoneNumber in event properties', async () => {
      const phoneNumber = '+14155552671';
      const expectedHash = FieldEncryptionService.hashPhoneNumber(phoneNumber);

      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
      } as Response);

      const event: SpamBlockedEvent = {
        phoneNumber,
        decision: 'FLAG',
        confidence: 0.65,
        ruleMatches: ['rule-2', 'rule-3'],
        timestamp: new Date('2026-01-01T00:00:00Z'),
      };

      const result = await service.spamBlocked(event);

      expect(result.$event_name).toBe('spam_blocked');
      expect(result.phoneNumberHash).toBe(expectedHash);
      expect(result.decision).toBe('FLAG');
      expect(result.confidence).toBe(0.65);
      expect(result.ruleMatches).toEqual(['rule-2', 'rule-3']);
      expect(result.timestamp).toBe('2026-01-01T00:00:00.000Z');
    });

    it('includes raw phoneNumber only internally, sends hash to Mixpanel API', async () => {
      const phoneNumber = '+14155552671';
      const expectedHash = FieldEncryptionService.hashPhoneNumber(phoneNumber);

      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
      } as Response);

      const event: SpamBlockedEvent = {
        phoneNumber,
        decision: 'BLOCK',
        confidence: 0.95,
        timestamp: new Date(),
      };

      await service.spamBlocked(event);

      const callArgs = mockFetch.mock.calls[0] as any[];
      const body = JSON.parse(callArgs[1].body);
      const trackPayload = JSON.parse(body.data);

      expect(trackPayload.event).toBe('spam_blocked');
      expect(trackPayload.properties.phoneNumberHash).toBe(expectedHash);
      expect(trackPayload.properties.phoneNumberHash).not.toBe(phoneNumber);
    });

    it('handles different phone numbers with unique hashes', async () => {
      mockFetch.mockResolvedValue({
        ok: true,
        status: 200,
      } as Response);

      const phone1 = '+14155552671';
      const phone2 = '+442071234567';

      const event1: SpamBlockedEvent = {
        phoneNumber: phone1,
        decision: 'BLOCK',
        confidence: 0.9,
        timestamp: new Date(),
      };

      const event2: SpamBlockedEvent = {
        phoneNumber: phone2,
        decision: 'FLAG',
        confidence: 0.7,
        timestamp: new Date(),
      };

      const result1 = await service.spamBlocked(event1);
      const result2 = await service.spamBlocked(event2);

      expect(result1.phoneNumberHash).not.toBe(result2.phoneNumberHash);
      expect(result1.phoneNumberHash).toBe(FieldEncryptionService.hashPhoneNumber(phone1));
      expect(result2.phoneNumberHash).toBe(FieldEncryptionService.hashPhoneNumber(phone2));
    });

    it('produces deterministic hash for same phone number', async () => {
      mockFetch.mockResolvedValue({
        ok: true,
        status: 200,
      } as Response);

      const phoneNumber = '+14155552671';

      const event1: SpamBlockedEvent = {
        phoneNumber,
        decision: 'BLOCK',
        confidence: 0.9,
        timestamp: new Date(),
      };

      const event2: SpamBlockedEvent = {
        phoneNumber,
        decision: 'FLAG',
        confidence: 0.7,
        timestamp: new Date(),
      };

      const result1 = await service.spamBlocked(event1);
      const result2 = await service.spamBlocked(event2);

      expect(result1.phoneNumberHash).toBe(result2.phoneNumberHash);
    });

    it('stores event in internal events array', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
      } as Response);

      const event: SpamBlockedEvent = {
        phoneNumber: '+14155552671',
        decision: 'BLOCK',
        confidence: 0.9,
        timestamp: new Date(),
      };

      await service.spamBlocked(event);

      const events = service.getEvents();
      expect(events).toHaveLength(1);
      expect(events[0].phoneNumberHash).toBe(FieldEncryptionService.hashPhoneNumber('+14155552671'));
      expect(events[0].$event_name).toBe('spam_blocked');
    });

    it('handles BLOCK and FLAG decisions', async () => {
      mockFetch.mockResolvedValue({
        ok: true,
        status: 200,
      } as Response);

      const blockEvent: SpamBlockedEvent = {
        phoneNumber: '+14155552671',
        decision: 'BLOCK',
        confidence: 0.95,
        timestamp: new Date(),
      };

      const flagEvent: SpamBlockedEvent = {
        phoneNumber: '+14155552671',
        decision: 'FLAG',
        confidence: 0.6,
        timestamp: new Date(),
      };

      const blockResult = await service.spamBlocked(blockEvent);
      const flagResult = await service.spamBlocked(flagEvent);

      expect(blockResult.decision).toBe('BLOCK');
      expect(flagResult.decision).toBe('FLAG');
    });

    it('gracefully handles Mixpanel API failure', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
        statusText: 'Internal Server Error',
      } as Response);

      const event: SpamBlockedEvent = {
        phoneNumber: '+14155552671',
        decision: 'BLOCK',
        confidence: 0.9,
        timestamp: new Date(),
      };

      const result = await service.spamBlocked(event);

      expect(result.phoneNumberHash).toBe(FieldEncryptionService.hashPhoneNumber('+14155552671'));
      expect(result.status).toBe(500);
    });

    it('gracefully handles Mixpanel API network error', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 429,
        statusText: 'Too Many Requests',
      } as Response);

      const event: SpamBlockedEvent = {
        phoneNumber: '+14155552671',
        decision: 'BLOCK',
        confidence: 0.9,
        timestamp: new Date(),
      };

      const result = await service.spamBlocked(event);

      expect(result.phoneNumberHash).toBe(FieldEncryptionService.hashPhoneNumber('+14155552671'));
      expect(result.status).toBe(429);
    });
  });

  describe('hashPhoneNumber consistency', () => {
    it('hash matches SHA-256 hex digest for known input', () => {
      const phoneNumber = '+14155552671';
      const hash = FieldEncryptionService.hashPhoneNumber(phoneNumber);
      const expected = crypto.createHash('sha256').update(phoneNumber).digest('hex');

      expect(hash).toBe(expected);
      expect(hash).toBe('cb6880e416769253645cb9c6b8989154bf66a56a77fc14c81fb1019663cbb928');
    });

    it('hash is 64 characters (SHA-256 hex)', () => {
      const hash = FieldEncryptionService.hashPhoneNumber('+14155552671');
      expect(hash.length).toBe(64);
      expect(/^[0-9a-f]{64}$/.test(hash)).toBe(true);
    });

    it('different phone numbers produce different hashes', () => {
      const hash1 = FieldEncryptionService.hashPhoneNumber('+14155552671');
      const hash2 = FieldEncryptionService.hashPhoneNumber('+442071234567');
      expect(hash1).not.toBe(hash2);
    });
  });

  describe('configuration', () => {
    it('uses custom config when provided', () => {
      const customService = new MixpanelService({
        token: 'custom-token',
        apiHost: 'custom.api.com',
        enableLogging: true,
      });

      const config = customService.getConfig();
      expect(config.token).toBe('custom-token');
      expect(config.apiHost).toBe('custom.api.com');
      expect(config.enableLogging).toBe(true);
    });

    it('uses default config when no config provided', () => {
      const defaultService = new MixpanelService();
      const config = defaultService.getConfig();
      expect(config.apiHost).toBe('api.mixpanel.com');
    });
  });

  describe('event management', () => {
    it('clearEvents removes all stored events', async () => {
      mockFetch.mockResolvedValue({
        ok: true,
        status: 200,
      } as Response);

      const event: SpamBlockedEvent = {
        phoneNumber: '+14155552671',
        decision: 'BLOCK',
        confidence: 0.9,
        timestamp: new Date(),
      };

      await service.spamBlocked(event);
      expect(service.getEvents()).toHaveLength(1);

      service.clearEvents();
      expect(service.getEvents()).toHaveLength(0);
    });
  });
});
