import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
  propertyWatchlistService,
  normalizeAddressValue,
  hashAddressValue,
} from '../src/watchlist.service';
import { HomeTitleAlertPipeline } from '../src/alert.pipeline';
import { detectChanges } from '../src/change-detector';
import { PropertySnapshot } from '../src/types';

const mockedDb = vi.hoisted(() => {
  const mocks = {
    subscription: {
      findUnique: vi.fn(),
      count: vi.fn(),
    },
    propertyWatchlistItem: {
      count: vi.fn(),
      findFirst: vi.fn(),
      create: vi.fn(),
      findMany: vi.fn(),
      update: vi.fn(),
    },
    alert: {
      create: vi.fn(),
      findFirst: vi.fn(),
    },
    normalizedAlert: {
      create: vi.fn(),
      updateMany: vi.fn(),
    },
    correlationGroup: {
      create: vi.fn(),
    },
    user: {
      findUnique: vi.fn(),
    },
  };
  return mocks;
});

vi.mock('@shieldai/db', () => ({
  prisma: {
    subscription: mockedDb.subscription,
    propertyWatchlistItem: mockedDb.propertyWatchlistItem,
    alert: mockedDb.alert,
    normalizedAlert: mockedDb.normalizedAlert,
    correlationGroup: mockedDb.correlationGroup,
    user: mockedDb.user,
  },
  AlertSeverity: { INFO: 'INFO', WARNING: 'WARNING', CRITICAL: 'CRITICAL' },
  AlertChannel: { EMAIL: 'email', PUSH: 'push', SMS: 'sms' },
}));

vi.mock('@shieldai/shared-notifications', () => {
  const mockSend = vi.fn().mockResolvedValue({ notificationId: 'mock-notif', status: 'sent' });
  class MockNS {
    send = mockSend;
    static getInstance() { return new MockNS(); }
  }
  return {
    NotificationService: MockNS,
    loadNotificationConfig: () => ({ apiKey: 'test', baseUrl: 'http://localhost' }),
  };
});

const PREMIUM_SUB = { id: 'sub-premium', tier: 'premium' as const };
const PLUS_SUB = { id: 'sub-plus', tier: 'plus' as const };
const BASIC_SUB = { id: 'sub-basic', tier: 'basic' as const };

function makeSnapshot(overrides: Partial<PropertySnapshot> = {}): PropertySnapshot {
  return {
    id: 'snap-1',
    propertyId: 'prop-001',
    capturedAt: '2026-01-01T00:00:00Z',
    ownerName: 'John Doe',
    address: {
      streetNumber: '123',
      streetName: 'main',
      streetType: 'st',
      city: 'springfield',
      state: 'IL',
      zip: '62701',
    },
    propertyType: 'residential',
    ...overrides,
  };
}

describe('PropertyWatchlistService', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.clearAllMocks();
  });

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

  describe('addItem', () => {
    it('creates a new watchlist item', async () => {
      mockedDb.subscription.findUnique.mockResolvedValue(PREMIUM_SUB);
      mockedDb.propertyWatchlistItem.count.mockResolvedValue(0);
      mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue(null);
      mockedDb.propertyWatchlistItem.create.mockResolvedValue({
        id: 'pw-1',
        subscriptionId: 'sub-premium',
        address: '123 main st',
        parcelId: null,
        ownerName: null,
        streetAddress: '123 main st',
        city: '',
        state: '',
        zipCode: '',
        latitude: null,
        longitude: null,
        isActive: true,
        createdAt: new Date(),
        updatedAt: new Date(),
      });

      const item = await propertyWatchlistService.addItem(
        'sub-premium',
        '123 Main St',
        'parcel-001',
        'John Doe',
      );

      expect(item.address).toBe('123 main st');
      expect(mockedDb.propertyWatchlistItem.create).toHaveBeenCalled();
    });

    it('enforces BASIC tier limit of 3', async () => {
      mockedDb.subscription.findUnique.mockResolvedValue(BASIC_SUB);
      mockedDb.propertyWatchlistItem.count.mockResolvedValue(3);

      await expect(
        propertyWatchlistService.addItem('sub-basic', '456 Oak Ave', 'parcel-002')
      ).rejects.toThrow(/limit reached/);
    });

    it('enforces PLUS tier limit of 5', async () => {
      mockedDb.subscription.findUnique.mockResolvedValue(PLUS_SUB);
      mockedDb.propertyWatchlistItem.count.mockResolvedValue(5);

      await expect(
        propertyWatchlistService.addItem('sub-plus', '789 Elm Blvd', 'parcel-003')
      ).rejects.toThrow(/limit reached/);
    });

    it('allows up to 50 for PREMIUM tier', async () => {
      mockedDb.subscription.findUnique.mockResolvedValue(PREMIUM_SUB);
      mockedDb.propertyWatchlistItem.count.mockResolvedValue(49);

      mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue(null);
      mockedDb.propertyWatchlistItem.create.mockResolvedValue({
        id: 'pw-50',
        subscriptionId: 'sub-premium',
        address: '50th property',
        parcelId: 'parcel-050',
        ownerName: null,
        streetAddress: '50th property',
        city: '',
        state: '',
        zipCode: '',
        latitude: null,
        longitude: null,
        isActive: true,
        createdAt: new Date(),
        updatedAt: new Date(),
      });

      const item = await propertyWatchlistService.addItem(
        'sub-premium',
        '50th Property',
        'parcel-050',
      );

      expect(item).toBeDefined();
      expect(item.address).toBe('50th property');
    });

    it('deduplicates by normalized address', async () => {
      const existingItem = {
        id: 'pw-existing',
        subscriptionId: 'sub-premium',
        address: '123 main st',
        isActive: true,
      };
      mockedDb.subscription.findUnique.mockResolvedValue(PREMIUM_SUB);
      mockedDb.propertyWatchlistItem.count.mockResolvedValue(1);
      mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue(existingItem);

      const result = await propertyWatchlistService.addItem(
        'sub-premium',
        '123 Main St',
        'parcel-001',
      );

      expect(result.id).toBe('pw-existing');
      expect(mockedDb.propertyWatchlistItem.create).not.toHaveBeenCalled();
    });

    it('reactivates a deactivated item', async () => {
      const deactivatedItem = {
        id: 'pw-deactivated',
        subscriptionId: 'sub-premium',
        address: '123 main st',
        isActive: false,
      };
      mockedDb.subscription.findUnique.mockResolvedValue(PREMIUM_SUB);
      mockedDb.propertyWatchlistItem.count.mockResolvedValue(1);
      mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue(deactivatedItem);
      mockedDb.propertyWatchlistItem.update.mockResolvedValue({
        ...deactivatedItem,
        isActive: true,
      });

      const result = await propertyWatchlistService.addItem(
        'sub-premium',
        '123 Main St',
      );

      expect(result.isActive).toBe(true);
      expect(mockedDb.propertyWatchlistItem.update).toHaveBeenCalled();
    });

    it('throws on invalid subscription', async () => {
      mockedDb.subscription.findUnique.mockResolvedValue(null);

      await expect(
        propertyWatchlistService.addItem('sub-invalid', '123 Main St')
      ).rejects.toThrow(/not found/);
    });
  });

  describe('getItems', () => {
    it('returns active items for subscription', async () => {
      const items = [
        {
          id: 'pw-1',
          subscriptionId: 'sub-premium',
          address: '123 main st',
          isActive: true,
          createdAt: new Date('2026-01-01'),
          updatedAt: new Date('2026-01-01'),
        },
        {
          id: 'pw-2',
          subscriptionId: 'sub-premium',
          address: '456 oak ave',
          isActive: true,
          createdAt: new Date('2026-02-01'),
          updatedAt: new Date('2026-02-01'),
        },
      ];
      mockedDb.propertyWatchlistItem.findMany.mockResolvedValue(items);

      const result = await propertyWatchlistService.getItems('sub-premium');

      expect(result).toHaveLength(2);
      expect(mockedDb.propertyWatchlistItem.findMany).toHaveBeenCalledWith({
        where: { subscriptionId: 'sub-premium', isActive: true },
        orderBy: { createdAt: 'desc' },
      });
    });
  });

  describe('removeItem', () => {
    it('deactivates an item', async () => {
      mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue({
        id: 'pw-1',
        subscriptionId: 'sub-premium',
        address: '123 main st',
        isActive: true,
      });
      mockedDb.propertyWatchlistItem.update.mockResolvedValue({
        id: 'pw-1',
        subscriptionId: 'sub-premium',
        address: '123 main st',
        isActive: false,
      });

      const result = await propertyWatchlistService.removeItem('pw-1', 'sub-premium');

      expect(result.isActive).toBe(false);
    });

    it('throws on missing item', async () => {
      mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue(null);

      await expect(
        propertyWatchlistService.removeItem('pw-missing', 'sub-premium')
      ).rejects.toThrow(/not found/);
    });
  });

  describe('getActiveItemsForScan', () => {
    it('returns items with latest snapshot', async () => {
      mockedDb.propertyWatchlistItem.findMany.mockResolvedValue([
        {
          id: 'pw-1',
          subscriptionId: 'sub-premium',
          address: '123 main st',
          isActive: true,
          snapshots: [{ id: 'snap-1', capturedAt: new Date('2026-01-01') }],
        },
      ]);

      const result = await propertyWatchlistService.getActiveItemsForScan('sub-premium');

      expect(result).toHaveLength(1);
      expect(result[0].snapshots).toHaveLength(1);
    });
  });

  describe('max items for tier', () => {
    it('returns correct limits per tier', async () => {
      mockedDb.subscription.findUnique.mockResolvedValue(BASIC_SUB);
      expect(await propertyWatchlistService.getMaxItemsForTier('sub-basic')).toBe(3);

      mockedDb.subscription.findUnique.mockResolvedValue(PLUS_SUB);
      expect(await propertyWatchlistService.getMaxItemsForTier('sub-plus')).toBe(5);

      mockedDb.subscription.findUnique.mockResolvedValue(PREMIUM_SUB);
      expect(await propertyWatchlistService.getMaxItemsForTier('sub-premium')).toBe(50);
    });

    it('returns 3 for unknown subscription', async () => {
      mockedDb.subscription.findUnique.mockResolvedValue(null);
      expect(await propertyWatchlistService.getMaxItemsForTier('sub-unknown')).toBe(3);
    });
  });

  describe('normalizeAddressValue', () => {
    it('lowercases and trims', () => {
      expect(normalizeAddressValue('  123 Main St  ')).toBe('123 main st');
    });

    it('collapses multiple spaces', () => {
      expect(normalizeAddressValue('123   Main   St')).toBe('123 main st');
    });
  });

  describe('hashAddressValue', () => {
    it('produces consistent sha256 hash', () => {
      const hash1 = hashAddressValue('123 main st');
      const hash2 = hashAddressValue('123 main st');
      expect(hash1).toBe(hash2);
      expect(hash1).toHaveLength(64);
    });

    it('different addresses produce different hashes', () => {
      const hash1 = hashAddressValue('123 main st');
      const hash2 = hashAddressValue('456 oak ave');
      expect(hash1).not.toBe(hash2);
    });
  });
});

describe('Integration: Full Pipeline', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.clearAllMocks();
  });

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

  it('happy path: add property -> detect change -> create alert', async () => {
    // Setup: add property to watchlist
    mockedDb.subscription.findUnique.mockResolvedValue(PREMIUM_SUB);
    mockedDb.propertyWatchlistItem.count.mockResolvedValue(0);
    mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue(null);
    mockedDb.propertyWatchlistItem.create.mockResolvedValue({
      id: 'pw-1',
      subscriptionId: 'sub-premium',
      address: '123 main st',
      parcelId: 'parcel-001',
      ownerName: 'John Doe',
      streetAddress: '123 main st',
      city: '',
      state: '',
      zipCode: '',
      isActive: true,
      createdAt: new Date(),
      updatedAt: new Date(),
    });

    const item = await propertyWatchlistService.addItem(
      'sub-premium',
      '123 Main St',
      'parcel-001',
      'John Doe',
    );
    expect(item.address).toBe('123 main st');

    // Detect change: ownership transfer
    const previous: PropertySnapshot = makeSnapshot({
      ownerName: 'John Doe',
      capturedAt: '2026-01-01T00:00:00Z',
    });
    const current: PropertySnapshot = makeSnapshot({
      id: 'snap-2',
      capturedAt: '2026-02-01T00:00:00Z',
      ownerName: 'Jane Smith',
    });

    const changeResult = detectChanges(previous, current);
    expect(changeResult.changeType).toBe('ownership_transfer');
    expect(changeResult.severity).toBe('critical');
    expect(changeResult.confidence).toBeGreaterThan(0.9);

    // Pipeline processes the change
    mockedDb.alert.findFirst.mockResolvedValue(null);
    mockedDb.alert.create.mockResolvedValue({
      id: 'alert-001',
      subscriptionId: 'sub-premium',
      userId: 'user-001',
      type: 'system_warning',
      title: '[CRITICAL] Ownership Transfer detected',
      message: 'Change detected',
      severity: 'CRITICAL' as any,
      channel: ['email', 'push', 'sms'],
      createdAt: new Date('2026-05-14T12:00:00Z'),
    });

    const pipeline = new HomeTitleAlertPipeline();
    const alert = await pipeline.processChangeDetection(
      changeResult,
      'sub-premium',
      'user-001',
    );

    expect(alert).toBeDefined();
    expect(alert?.changeType).toBe('ownership_transfer');
    expect(alert?.severity).toBe('critical');
    expect(mockedDb.alert.create).toHaveBeenCalled();
    expect(mockedDb.normalizedAlert.create).toHaveBeenCalled();
  });

  it('tier gating: premium gets more watchlist items than plus', async () => {
    // Plus tier: 5 items max
    mockedDb.subscription.findUnique.mockResolvedValue(PLUS_SUB);
    mockedDb.propertyWatchlistItem.count.mockResolvedValue(5);

    await expect(
      propertyWatchlistService.addItem('sub-plus', '50th property')
    ).rejects.toThrow(/limit reached/);

    // Premium tier: 50 items max
    mockedDb.subscription.findUnique.mockResolvedValue(PREMIUM_SUB);
    mockedDb.propertyWatchlistItem.count.mockResolvedValue(49);
    mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue(null);
    mockedDb.propertyWatchlistItem.create.mockResolvedValue({
      id: 'pw-50',
      subscriptionId: 'sub-premium',
      address: '50th property',
      parcelId: 'parcel-050',
      ownerName: null,
      streetAddress: '50th property',
      city: '',
      state: '',
      zipCode: '',
      isActive: true,
      createdAt: new Date(),
      updatedAt: new Date(),
    });

    const item = await propertyWatchlistService.addItem(
      'sub-premium',
      '50th Property',
      'parcel-050',
    );
    expect(item).toBeDefined();
  });

  it('fuzzy matching: similar names are detected as matches', async () => {
    const { matchRecords } = await import('../src/matcher.service');

    const addr = {
      streetNumber: '123',
      streetName: 'main',
      streetType: 'st',
      city: 'springfield',
      state: 'IL',
      zip: '62701',
    };

    // Slight typo in name
    const result = matchRecords(
      'John Doe',
      addr,
      'Jhon Doe',
      addr,
    );

    expect(result.isMatch).toBe(true);
    expect(result.nameScore).toBeGreaterThan(0.7);
  });

  it('fuzzy matching: completely different names don\'t match', async () => {
    const { matchRecords } = await import('../src/matcher.service');

    const addr = {
      streetNumber: '123',
      streetName: 'main',
      streetType: 'st',
      city: 'springfield',
      state: 'IL',
      zip: '62701',
    };

    const result = matchRecords(
      'John Doe',
      addr,
      'Robert Williams',
      addr,
    );

    expect(result.isMatch).toBe(false);
    expect(result.nameScore).toBeLessThan(0.5);
  });

  it('change detection: tax change is minor severity', async () => {
    const previous: PropertySnapshot = makeSnapshot({
      taxAmount: 2500,
    });
    const current: PropertySnapshot = makeSnapshot({
      id: 'snap-2',
      capturedAt: '2026-02-01T00:00:00Z',
      taxAmount: 3500,
    });

    const result = detectChanges(previous, current);
    expect(result.changes.some(c => c.changeType === 'tax_change')).toBe(true);
    expect(result.severity).toBe('info');
  });

  it('change detection: lien filing is moderate severity', async () => {
    const previous: PropertySnapshot = makeSnapshot({
      lienCount: 0,
    });
    const current: PropertySnapshot = makeSnapshot({
      id: 'snap-2',
      capturedAt: '2026-02-01T00:00:00Z',
      lienCount: 2,
    });

    const result = detectChanges(previous, current);
    expect(result.changes.some(c => c.changeType === 'lien_filing')).toBe(true);
    expect(result.severity).toBe('warning');
  });
});
