import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
  collectExposureSummary,
  collectSpamStats,
  collectVoiceStats,
  collectHomeTitleStats,
  generateRecommendations,
  calculateProtectionScore,
  collectAllReportData,
} from './data-collector';

const mocks = vi.hoisted(() => {
  const mockExposureFindMany = vi.fn();
  const mockAlertCount = vi.fn();
  const mockSpamFeedbackFindMany = vi.fn();
  const mockVoiceAnalysisFindMany = vi.fn();
  const mockVoiceEnrollmentCount = vi.fn();
  const mockWatchlistItemFindMany = vi.fn();
  const mockPrisma = {
    exposure: { findMany: mockExposureFindMany },
    alert: { count: mockAlertCount },
    spamFeedback: { findMany: mockSpamFeedbackFindMany },
    voiceAnalysis: { findMany: mockVoiceAnalysisFindMany },
    voiceEnrollment: { count: mockVoiceEnrollmentCount },
    watchlistItem: { findMany: mockWatchlistItemFindMany },
  };
  return {
    mockExposureFindMany,
    mockAlertCount,
    mockSpamFeedbackFindMany,
    mockVoiceAnalysisFindMany,
    mockVoiceEnrollmentCount,
    mockWatchlistItemFindMany,
    mockPrisma,
  };
});

const {
  mockExposureFindMany,
  mockAlertCount,
  mockSpamFeedbackFindMany,
  mockVoiceAnalysisFindMany,
  mockVoiceEnrollmentCount,
  mockWatchlistItemFindMany,
  mockPrisma,
} = mocks;

vi.mock('@shieldai/db', () => ({
  prisma: mocks.mockPrisma,
}));

describe('data-collector', () => {
  const periodStart = new Date('2025-01-01');
  const periodEnd = new Date('2025-01-31');

  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('collectExposureSummary', () => {
    it('returns correct counts for mixed severity exposures', async () => {
      mockExposureFindMany.mockResolvedValue([
        { severity: 'critical', source: 'breach1', isFirstTime: true },
        { severity: 'warning', source: 'breach2', isFirstTime: true },
        { severity: 'info', source: 'breach1', isFirstTime: false },
      ]);
      mockAlertCount.mockResolvedValue(1);

      const result = await collectExposureSummary('sub-1', periodStart, periodEnd);

      expect(result.totalExposures).toBe(3);
      expect(result.newExposures).toBe(2);
      expect(result.criticalExposures).toBe(1);
      expect(result.warningExposures).toBe(1);
      expect(result.infoExposures).toBe(1);
      expect(result.resolvedExposures).toBe(1);
      expect(result.exposuresBySource).toEqual({ breach1: 2, breach2: 1 });
    });

    it('returns zeros when no exposures', async () => {
      mockPrisma.exposure.findMany.mockResolvedValue([]);
      mockAlertCount.mockResolvedValue(0);

      const result = await collectExposureSummary('sub-1', periodStart, periodEnd);

      expect(result.totalExposures).toBe(0);
      expect(result.newExposures).toBe(0);
      expect(result.criticalExposures).toBe(0);
    });
  });

  describe('collectSpamStats', () => {
    it('calculates spam stats correctly', async () => {
      mockPrisma.spamFeedback.findMany.mockResolvedValue([
        { isSpam: true, feedbackType: 'initial_detection', metadata: { channel: 'call' } },
        { isSpam: true, feedbackType: 'initial_detection', metadata: { channel: 'sms' } },
        { isSpam: true, feedbackType: 'user_rejection', metadata: { channel: 'call' } },
        { isSpam: false, feedbackType: 'initial_detection', metadata: { channel: 'call' } },
      ]);

      const result = await collectSpamStats('user-1', periodStart, periodEnd);

      expect(result.callsBlocked).toBe(2);
      expect(result.textsBlocked).toBe(1);
      expect(result.falsePositives).toBe(1);
      expect(result.totalSpamEvents).toBe(3);
    });

    it('returns zeros when no spam events', async () => {
      mockPrisma.spamFeedback.findMany.mockResolvedValue([]);

      const result = await collectSpamStats('user-1', periodStart, periodEnd);

      expect(result.totalSpamEvents).toBe(0);
      expect(result.callsBlocked).toBe(0);
      expect(result.textsBlocked).toBe(0);
    });
  });

  describe('collectVoiceStats', () => {
    it('calculates voice stats correctly', async () => {
      mockPrisma.voiceAnalysis.findMany.mockResolvedValue([
        { isSynthetic: true, confidence: 0.9, enrollmentId: 'enr-1' },
        { isSynthetic: true, confidence: 0.5, enrollmentId: 'enr-1' },
        { isSynthetic: false, confidence: 0.8, enrollmentId: 'enr-2' },
      ]);
      mockPrisma.voiceEnrollment.count.mockResolvedValue(2);

      const result = await collectVoiceStats('user-1', periodStart, periodEnd);

      expect(result.analysesRun).toBe(3);
      expect(result.threatsDetected).toBe(1);
      expect(result.syntheticDetections).toBe(2);
      expect(result.enrollmentsActive).toBe(2);
    });

    it('returns zeros when no analyses', async () => {
      mockPrisma.voiceAnalysis.findMany.mockResolvedValue([]);
      mockPrisma.voiceEnrollment.count.mockResolvedValue(0);

      const result = await collectVoiceStats('user-1', periodStart, periodEnd);

      expect(result.analysesRun).toBe(0);
      expect(result.threatsDetected).toBe(0);
      expect(result.enrollmentsActive).toBe(0);
    });
  });

  describe('collectHomeTitleStats', () => {
    it('calculates home title stats', async () => {
      mockPrisma.watchlistItem.findMany.mockResolvedValue([
        { subscriptionId: 'sub-1', type: 'address', isActive: true },
        { subscriptionId: 'sub-1', type: 'address', isActive: true },
      ]);
      mockAlertCount.mockResolvedValue(10);

      const result = await collectHomeTitleStats('sub-1', periodStart, periodEnd);

      expect(result.propertiesMonitored).toBe(2);
      expect(result.alertsTriggered).toBe(10);
      expect(result.changesDetected).toBe(Math.round(10 * 0.3));
    });

    it('returns zeros when no watchlist items', async () => {
      mockPrisma.watchlistItem.findMany.mockResolvedValue([]);
      mockAlertCount.mockResolvedValue(0);

      const result = await collectHomeTitleStats('sub-1', periodStart, periodEnd);

      expect(result.propertiesMonitored).toBe(0);
      expect(result.changesDetected).toBe(0);
    });
  });

  describe('calculateProtectionScore', () => {
    it('starts at 100 and deducts for issues', () => {
      const exposure = {
        criticalExposures: 1,
        warningExposures: 2,
        infoExposures: 3,
        newExposures: 0,
        resolvedExposures: 0,
        exposuresBySource: {},
      };
      const spam = {
        callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
        falsePositives: 0, totalSpamEvents: 5,
      };
      const voice = {
        analysesRun: 0, threatsDetected: 0, enrollmentsActive: 1,
        syntheticDetections: 0, voiceMismatchEvents: 0,
      };

      const score = calculateProtectionScore(exposure, spam, voice);

      // 100 - 10 (1 critical) - 10 (2 warnings) - 6 (3 info) - 5 (spam) = 69
      expect(score).toBe(69);
    });

    it('caps score between 0 and 100', () => {
      const exposure = {
        criticalExposures: 15,
        warningExposures: 10,
        infoExposures: 10,
        newExposures: 0,
        resolvedExposures: 0,
        exposuresBySource: {},
      };
      const spam = {
        callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
        falsePositives: 0, totalSpamEvents: 30,
      };
      const voice = {
        analysesRun: 0, threatsDetected: 0, enrollmentsActive: 0,
        syntheticDetections: 5, voiceMismatchEvents: 0,
      };

      const score = calculateProtectionScore(exposure, spam, voice);

      // 100 - 150 - 50 - 20 - 20 - 40 - 5 = -85, capped to 0
      expect(score).toBe(0);
    });

    it('deducts 5 for no active enrollments', () => {
      const exposure = {
        criticalExposures: 0, warningExposures: 0, infoExposures: 0,
        newExposures: 0, resolvedExposures: 0, exposuresBySource: {},
      };
      const spam = {
        callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
        falsePositives: 0, totalSpamEvents: 0,
      };
      const voice = {
        analysesRun: 0, threatsDetected: 0, enrollmentsActive: 0,
        syntheticDetections: 0, voiceMismatchEvents: 0,
      };

      const score = calculateProtectionScore(exposure, spam, voice);

      expect(score).toBe(95);
    });
  });

  describe('generateRecommendations', () => {
    it('suggests addressing critical exposures', () => {
      const exposure = {
        criticalExposures: 3, warningExposures: 0, infoExposures: 0,
        newExposures: 0, resolvedExposures: 0, exposuresBySource: {},
      };
      const spam = {
        callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
        falsePositives: 0, totalSpamEvents: 0,
      };
      const voice = {
        analysesRun: 0, threatsDetected: 0, enrollmentsActive: 1,
        syntheticDetections: 0, voiceMismatchEvents: 0,
      };

      const recs = generateRecommendations(exposure, spam, voice, 70);

      expect(recs).toHaveLength(1);
      expect(recs[0].priority).toBe('high');
      expect(recs[0].category).toBe('dark_web');
    });

    it('suggests reviewing new exposures when > 5', () => {
      const exposure = {
        criticalExposures: 0, warningExposures: 0, infoExposures: 0,
        newExposures: 10, resolvedExposures: 0, exposuresBySource: {},
      };
      const spam = {
        callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
        falsePositives: 0, totalSpamEvents: 0,
      };
      const voice = {
        analysesRun: 0, threatsDetected: 0, enrollmentsActive: 1,
        syntheticDetections: 0, voiceMismatchEvents: 0,
      };

      const recs = generateRecommendations(exposure, spam, voice, 70);

      expect(recs).toHaveLength(1);
      expect(recs[0].title).toBe('Review New Exposures');
    });

    it('suggests voice cloning threat when synthetic detected', () => {
      const exposure = {
        criticalExposures: 0, warningExposures: 0, infoExposures: 0,
        newExposures: 0, resolvedExposures: 0, exposuresBySource: {},
      };
      const spam = {
        callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
        falsePositives: 0, totalSpamEvents: 0,
      };
      const voice = {
        analysesRun: 10, threatsDetected: 2, enrollmentsActive: 1,
        syntheticDetections: 2, voiceMismatchEvents: 2,
      };

      const recs = generateRecommendations(exposure, spam, voice, 70);

      expect(recs).toHaveLength(1);
      expect(recs[0].category).toBe('voice');
      expect(recs[0].priority).toBe('high');
    });

    it('suggests enrolling voices when no active enrollments', () => {
      const exposure = {
        criticalExposures: 0, warningExposures: 0, infoExposures: 0,
        newExposures: 0, resolvedExposures: 0, exposuresBySource: {},
      };
      const spam = {
        callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
        falsePositives: 0, totalSpamEvents: 0,
      };
      const voice = {
        analysesRun: 0, threatsDetected: 0, enrollmentsActive: 0,
        syntheticDetections: 0, voiceMismatchEvents: 0,
      };

      const recs = generateRecommendations(exposure, spam, voice, 70);

      expect(recs).toHaveLength(1);
      expect(recs[0].priority).toBe('low');
    });

    it('suggests improving protection score when < 50', () => {
      const exposure = {
        criticalExposures: 0, warningExposures: 0, infoExposures: 0,
        newExposures: 0, resolvedExposures: 0, exposuresBySource: {},
      };
      const spam = {
        callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
        falsePositives: 0, totalSpamEvents: 0,
      };
      const voice = {
        analysesRun: 0, threatsDetected: 0, enrollmentsActive: 1,
        syntheticDetections: 0, voiceMismatchEvents: 0,
      };

      const recs = generateRecommendations(exposure, spam, voice, 45);

      expect(recs).toHaveLength(1);
      expect(recs[0].category).toBe('general');
      expect(recs[0].priority).toBe('high');
    });

    it('returns multiple recommendations for complex scenario', () => {
      const exposure = {
        criticalExposures: 2, warningExposures: 0, infoExposures: 0,
        newExposures: 8, resolvedExposures: 0, exposuresBySource: {},
      };
      const spam = {
        callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
        falsePositives: 0, totalSpamEvents: 25,
      };
      const voice = {
        analysesRun: 10, threatsDetected: 1, enrollmentsActive: 0,
        syntheticDetections: 1, voiceMismatchEvents: 1,
      };

      const recs = generateRecommendations(exposure, spam, voice, 40);

      expect(recs.length).toBeGreaterThan(2);
      const categories = recs.map((r) => r.category);
      expect(categories).toContain('dark_web');
      expect(categories).toContain('spam');
      expect(categories).toContain('voice');
      expect(categories).toContain('general');
    });
  });

  describe('collectAllReportData', () => {
    it('includes homeTitleStats for ANNUAL_PREMIUM', async () => {
      mockPrisma.exposure.findMany.mockResolvedValue([]);
      mockAlertCount.mockResolvedValue(0);
      mockPrisma.spamFeedback.findMany.mockResolvedValue([]);
      mockPrisma.voiceAnalysis.findMany.mockResolvedValue([]);
      mockPrisma.voiceEnrollment.count.mockResolvedValue(0);
      mockPrisma.watchlistItem.findMany.mockResolvedValue([]);

      const result = await collectAllReportData(
        'user-1', 'sub-1', 'ANNUAL_PREMIUM', periodStart, periodEnd
      );

      expect(result.homeTitleStats).toBeDefined();
      expect(result.exposureSummary).toBeDefined();
      expect(result.spamStats).toBeDefined();
      expect(result.voiceStats).toBeDefined();
      expect(result.recommendations).toBeDefined();
      expect(result.protectionScore).toBeDefined();
    });

    it('excludes homeTitleStats for MONTHLY_PLUS', async () => {
      mockPrisma.exposure.findMany.mockResolvedValue([]);
      mockAlertCount.mockResolvedValue(0);
      mockPrisma.spamFeedback.findMany.mockResolvedValue([]);
      mockPrisma.voiceAnalysis.findMany.mockResolvedValue([]);
      mockPrisma.voiceEnrollment.count.mockResolvedValue(0);

      const result = await collectAllReportData(
        'user-1', 'sub-1', 'MONTHLY_PLUS', periodStart, periodEnd
      );

      expect(result.homeTitleStats).toBeUndefined();
    });
  });
});
