import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { HomeTitleSchedulerService } from '../src/scheduler.service';
import { PropertySnapshot } from '../src/types';

// Mock @shieldai/db
const mockPrisma = {
  subscription: {
    findMany: vi.fn(),
  },
  $queryRaw: vi.fn(),
};

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

// Mock alert pipeline
const mockProcessChangeDetection = vi.fn();
const mockHomeTitleAlertPipeline = {
  processChangeDetection: mockProcessChangeDetection,
};

vi.mock('../src/alert.pipeline', () => ({
  homeTitleAlertPipeline: mockHomeTitleAlertPipeline,
  HomeTitleAlertPipeline: class {},
}));

// Mock change-detector
const mockDetectChanges = vi.fn();
const mockShouldTriggerAlert = vi.fn();

vi.mock('../src/change-detector', () => ({
  detectChanges: mockDetectChanges,
  shouldTriggerAlert: mockShouldTriggerAlert,
}));

// Mock uuid
vi.mock('uuid', () => ({
  v4: () => 'scan-uuid-' + Date.now(),
}));

const mockSubscription = {
  id: 'sub-001',
  userId: 'user-001',
  tier: 'premium' as const,
};

function mockLatestSnapshots(snapshots: PropertySnapshot[]) {
  mockPrisma.$queryRaw.mockResolvedValue(
    snapshots.map(s => ({
      id: s.id,
      propertyId: s.propertyId,
      capturedAt: s.capturedAt,
      ownerName: s.ownerName,
      address: JSON.stringify(s.address),
      deedDate: s.deedDate ?? null,
      taxId: s.taxId ?? null,
      propertyType: s.propertyType,
      taxAmount: s.taxAmount ?? null,
      lienCount: s.lienCount ?? null,
    }))
  );
}

function mockPreviousSnapshot(snapshot: PropertySnapshot | null) {
  if (!snapshot) {
    mockPrisma.$queryRaw.mockResolvedValue([]);
  } else {
    mockPrisma.$queryRaw.mockResolvedValue([
      {
        id: snapshot.id,
        propertyId: snapshot.propertyId,
        capturedAt: snapshot.capturedAt,
        ownerName: snapshot.ownerName,
        address: JSON.stringify(snapshot.address),
        deedDate: snapshot.deedDate ?? null,
        taxId: snapshot.taxId ?? null,
        propertyType: snapshot.propertyType,
        taxAmount: snapshot.taxAmount ?? null,
        lienCount: snapshot.lienCount ?? null,
      },
    ]);
  }
}

describe('HomeTitleSchedulerService', () => {
  let scheduler: HomeTitleSchedulerService;

  beforeEach(() => {
    vi.useFakeTimers();
    scheduler = new HomeTitleSchedulerService({
      scanIntervalMinutes: 60,
      maxPropertiesPerScan: 100,
      enabled: true,
    });
    vi.clearAllMocks();
  });

  afterEach(() => {
    scheduler.stop();
    vi.useRealTimers();
  });

  describe('constructor and config', () => {
    it('uses default config when none provided', () => {
      const defaultScheduler = new HomeTitleSchedulerService();
      const config = defaultScheduler.getConfig();
      expect(config.scanIntervalMinutes).toBe(60);
      expect(config.maxPropertiesPerScan).toBe(100);
      expect(config.enabled).toBe(true);
      defaultScheduler.stop();
    });

    it('accepts custom config', () => {
      scheduler = new HomeTitleSchedulerService({ scanIntervalMinutes: 30 });
      const config = scheduler.getConfig();
      expect(config.scanIntervalMinutes).toBe(30);
    });

    it('updates config dynamically', () => {
      scheduler.updateConfig({ scanIntervalMinutes: 15 });
      const config = scheduler.getConfig();
      expect(config.scanIntervalMinutes).toBe(15);
    });
  });

  describe('start/stop', () => {
    it('starts the scheduler', () => {
      scheduler.start();
      expect(scheduler.isRunning()).toBe(true);
    });

    it('stops the scheduler', () => {
      scheduler.start();
      scheduler.stop();
      expect(scheduler.isRunning()).toBe(false);
    });

    it('does not start when disabled', () => {
      scheduler = new HomeTitleSchedulerService({ enabled: false });
      scheduler.start();
      expect(scheduler.isRunning()).toBe(false);
    });
  });

  describe('runScan', () => {
    it('returns empty results when no subscriptions', async () => {
      mockPrisma.subscription.findMany.mockResolvedValue([]);

      const result = await scheduler.runScan();

      expect(result.propertiesScanned).toBe(0);
      expect(result.changesDetected).toBe(0);
      expect(result.alertsCreated).toBe(0);
      expect(result.errors).toEqual([]);
    });

    it('detects changes and creates alerts', async () => {
      const previousSnapshot: PropertySnapshot = {
        id: 'snap-1',
        propertyId: 'prop-001',
        capturedAt: '2026-01-01T00:00:00Z',
        ownerName: 'John Doe',
        address: { streetNumber: '123', streetName: 'main', city: 'springfield', state: 'IL', zip: '62701' },
        propertyType: 'residential',
      };
      const currentSnapshot: PropertySnapshot = {
        ...previousSnapshot,
        id: 'snap-2',
        capturedAt: '2026-02-01T00:00:00Z',
        ownerName: 'Jane Smith',
      };

      mockPrisma.subscription.findMany.mockResolvedValue([mockSubscription]);
      mockLatestSnapshots([currentSnapshot]);
      mockPreviousSnapshot(previousSnapshot);
      mockDetectChanges.mockReturnValue({
        propertyId: 'prop-001',
        changeType: 'ownership_transfer',
        severity: 'major',
        confidence: 0.95,
        changes: [],
        previousSnapshot,
        currentSnapshot,
        detectedAt: new Date().toISOString(),
      });
      mockShouldTriggerAlert.mockReturnValue(true);
      mockProcessChangeDetection.mockResolvedValue({
        id: 'alert-001',
        propertyId: 'prop-001',
        subscriptionId: 'sub-001',
        userId: 'user-001',
        changeType: 'ownership_transfer',
        severity: 'critical',
        title: '[MAJOR] Ownership Transfer detected',
        message: 'Change detected',
        changeDetectionResult: {} as any,
        channel: ['email', 'push', 'sms'],
        dedupKey: 'hometitle:user-001:prop-001:ownership_transfer',
        createdAt: new Date().toISOString(),
      });

      const result = await scheduler.runScan();

      expect(result.propertiesScanned).toBeGreaterThanOrEqual(0);
      expect(result.changesDetected).toBeGreaterThanOrEqual(1);
      expect(result.alertsCreated).toBeGreaterThanOrEqual(1);
      expect(result.notificationsSent).toBeGreaterThanOrEqual(1);
    });

    it('skips snapshots without previous', async () => {
      mockPrisma.subscription.findMany.mockResolvedValue([mockSubscription]);
      mockLatestSnapshots([{
        id: 'snap-1',
        propertyId: 'prop-001',
        capturedAt: '2026-01-01T00:00:00Z',
        ownerName: 'John Doe',
        address: { streetNumber: '123', streetName: 'main', city: 'springfield', state: 'IL', zip: '62701' },
        propertyType: 'residential',
      }]);
      mockPreviousSnapshot(null);

      const result = await scheduler.runScan();

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

    it('handles subscription scan errors gracefully', async () => {
      mockPrisma.subscription.findMany.mockResolvedValue([mockSubscription]);
      mockLatestSnapshots([]);
      mockPreviousSnapshot(null);
      mockDetectChanges.mockReturnValue({
        propertyId: 'prop-001',
        changeType: 'metadata_change',
        severity: 'minor',
        confidence: 0.5,
        changes: [],
        previousSnapshot: {} as any,
        currentSnapshot: {} as any,
        detectedAt: new Date().toISOString(),
      });
      mockShouldTriggerAlert.mockReturnValue(false);

      const result = await scheduler.runScan();

      expect(result.errors).toEqual([]);
      expect(result.propertiesScanned).toBe(0);
    });

    it('tracks scan metadata', async () => {
      mockPrisma.subscription.findMany.mockResolvedValue([]);

      const result = await scheduler.runScan();

      expect(result.scanId).toBeDefined();
      expect(result.startedAt).toBeDefined();
      expect(result.completedAt).toBeDefined();
      // completedAt should be after startedAt
      expect(new Date(result.completedAt).getTime()).toBeGreaterThanOrEqual(
        new Date(result.startedAt).getTime()
      );
    });

    it('does not send notifications for non-premium tier', async () => {
      const previousSnapshot: PropertySnapshot = {
        id: 'snap-1',
        propertyId: 'prop-001',
        capturedAt: '2026-01-01T00:00:00Z',
        ownerName: 'John Doe',
        address: { streetNumber: '123', streetName: 'main', city: 'springfield', state: 'IL', zip: '62701' },
        propertyType: 'residential',
      };
      const currentSnapshot: PropertySnapshot = {
        ...previousSnapshot,
        id: 'snap-2',
        capturedAt: '2026-02-01T00:00:00Z',
        ownerName: 'Jane Smith',
      };

      const nonPremiumSub = { ...mockSubscription, tier: 'plus' as const };
      mockPrisma.subscription.findMany.mockResolvedValue([nonPremiumSub]);
      mockLatestSnapshots([currentSnapshot]);
      mockPreviousSnapshot(previousSnapshot);
      mockDetectChanges.mockReturnValue({
        propertyId: 'prop-001',
        changeType: 'ownership_transfer',
        severity: 'major',
        confidence: 0.95,
        changes: [],
        previousSnapshot,
        currentSnapshot,
        detectedAt: new Date().toISOString(),
      });
      mockShouldTriggerAlert.mockReturnValue(true);
      mockProcessChangeDetection.mockResolvedValue({
        id: 'alert-002',
        propertyId: 'prop-001',
        subscriptionId: 'sub-001',
        userId: 'user-001',
        changeType: 'ownership_transfer',
        severity: 'critical',
        title: '[MAJOR] Ownership Transfer detected',
        message: 'Change detected',
        changeDetectionResult: {} as any,
        channel: ['email', 'push'],
        dedupKey: 'hometitle:user-001:prop-001:ownership_transfer',
        createdAt: new Date().toISOString(),
      });

      const result = await scheduler.runScan();

      expect(result.changesDetected).toBeGreaterThanOrEqual(1);
      expect(result.alertsCreated).toBeGreaterThanOrEqual(1);
      expect(result.notificationsSent).toBe(0);
    });
  });

  describe('getLastScanResult', () => {
    it('returns null before first scan', () => {
      expect(scheduler.getLastScanResult()).toBeNull();
    });

    it('returns last scan result after scan', async () => {
      mockPrisma.subscription.findMany.mockResolvedValue([]);
      await scheduler.runScan();
      expect(scheduler.getLastScanResult()).not.toBeNull();
    });
  });
});
