import { describe, expect, it } from 'vitest';
import {
  normalizeErrorMessage,
  parseStackTrace,
  normalizeStackFrames,
  generateFingerprint,
  levenshteinDistance,
  calculateFingerprintSimilarity,
  processErrorEvent,
  updateClusterWithError,
} from './error-normalization.js';
import type { ErrorEvent, ErrorFingerprint, ErrorClusterDoc } from './types.js';

// ============================================================================
// normalizeErrorMessage
// ============================================================================

describe('normalizeErrorMessage', () => {
  it('replaces UUIDs with <UUID>', () => {
    const msg = 'Failed for user 550e8400-e29b-41d4-a716-446655440000';
    expect(normalizeErrorMessage(msg)).toContain('<UUID>');
    expect(normalizeErrorMessage(msg)).not.toContain('550e8400');
  });

  it('replaces MongoDB ObjectIds with <ID>', () => {
    const msg = 'Document 507f1f77bcf86cd799439011 not found';
    expect(normalizeErrorMessage(msg)).toContain('<ID>');
  });

  it('replaces email addresses with <EMAIL>', () => {
    const msg = 'Login failed for alice@example.com';
    expect(normalizeErrorMessage(msg)).toContain('<EMAIL>');
    expect(normalizeErrorMessage(msg)).not.toContain('alice@example.com');
  });

  it('replaces IPv4 addresses with <IP>', () => {
    const msg = 'Connection refused from 192.168.1.100';
    expect(normalizeErrorMessage(msg)).toContain('<IP>');
  });

  it('replaces ISO timestamps with <DATE>', () => {
    const msg = 'Error at 2026-03-21T12:30:45.123Z';
    expect(normalizeErrorMessage(msg)).toContain('<DATE>');
  });

  it('replaces simple dates with <DATE>', () => {
    const msg = 'Last seen on 03/21/2026';
    expect(normalizeErrorMessage(msg)).toContain('<DATE>');
  });

  it('replaces user IDs with <USER_ID>', () => {
    const msg = 'Cannot find user_12345';
    expect(normalizeErrorMessage(msg)).toContain('<USER_ID>');
  });

  it('replaces long numbers with <NUM>', () => {
    const msg = 'Record 12345678901 overflow';
    expect(normalizeErrorMessage(msg)).toContain('<NUM>');
  });

  it('replaces URLs with <URL>', () => {
    const msg = 'Fetch failed for https://api.example.com/v1/data';
    expect(normalizeErrorMessage(msg)).toContain('<URL>');
  });

  it('normalizes file paths keeping filename', () => {
    const msg = 'Error in /src/lib/server.ts';
    const result = normalizeErrorMessage(msg);
    expect(result).toContain('server.ts');
  });

  it('handles messages with multiple replaceable parts', () => {
    const msg =
      'User user_999 at 192.168.0.1 failed at 2026-01-01T00:00:00Z with id 550e8400-e29b-41d4-a716-446655440000';
    const result = normalizeErrorMessage(msg);
    expect(result).toContain('<USER_ID>');
    expect(result).toContain('<IP>');
    expect(result).toContain('<DATE>');
    expect(result).toContain('<UUID>');
  });

  it('returns unchanged message if nothing to normalize', () => {
    expect(normalizeErrorMessage('TypeError: cannot read property')).toBe(
      'TypeError: cannot read property'
    );
  });
});

// ============================================================================
// parseStackTrace
// ============================================================================

describe('parseStackTrace', () => {
  it('parses JavaScript stack frames', () => {
    const stack = `Error: something went wrong
    at handleRequest (/app/src/server.ts:42:10)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)`;
    const frames = parseStackTrace(stack);
    expect(frames.length).toBeGreaterThanOrEqual(1);
    expect(frames[0].function).toBe('handleRequest');
    expect(frames[0].line).toBe(42);
  });

  it('parses async JavaScript stack frames', () => {
    const stack = '    at async fetchData (/app/lib/api.ts:10:3)';
    const frames = parseStackTrace(stack);
    expect(frames.length).toBe(1);
    expect(frames[0].function).toBe('fetchData');
  });

  it('parses Python stack frames', () => {
    const stack = '  File "/app/main.py", line 25, in handle_request';
    const frames = parseStackTrace(stack);
    expect(frames.length).toBe(1);
    expect(frames[0].function).toBe('handle_request');
    expect(frames[0].line).toBe(25);
  });

  it('parses Java/Kotlin stack frames', () => {
    const stack = '    at com.example.MyClass.doSomething(MyClass.java:55)';
    const frames = parseStackTrace(stack);
    expect(frames.length).toBe(1);
    expect(frames[0].line).toBe(55);
  });

  it('returns empty array for empty input', () => {
    expect(parseStackTrace('')).toEqual([]);
  });

  it('skips unparseable lines', () => {
    const stack = `Error: boom
This is not a stack frame
Another line
    at realFrame (/app/file.ts:1:1)`;
    const frames = parseStackTrace(stack);
    expect(frames.length).toBe(1);
    expect(frames[0].function).toBe('realFrame');
  });
});

// ============================================================================
// normalizeStackFrames
// ============================================================================

describe('normalizeStackFrames', () => {
  it('normalizes and joins frames', () => {
    const frames = [
      { function: 'handleRequest', file: '/app/server.ts', line: 42, column: 10 },
      { function: 'processData', file: '/app/lib/data.ts', line: 15 },
    ];
    const result = normalizeStackFrames(frames);
    expect(result).toContain('handleRequest@');
    expect(result).toContain('|');
    expect(result).toContain('processData@');
  });

  it('truncates to maxFrames', () => {
    const frames = Array.from({ length: 20 }, (_, i) => ({
      function: `fn${i}`,
      file: `/app/f${i}.ts`,
      line: i,
    }));
    const result = normalizeStackFrames(frames, 3);
    expect(result.split('|').length).toBe(3);
  });

  it('handles empty frames', () => {
    expect(normalizeStackFrames([])).toBe('');
  });
});

// ============================================================================
// generateFingerprint
// ============================================================================

describe('generateFingerprint', () => {
  it('generates a SHA-256 hash', () => {
    const result = generateFingerprint({
      errorType: 'TypeError',
      message: 'Cannot read property of null',
    });
    expect(result.hash).toMatch(/^[a-f0-9]{64}$/);
  });

  it('strips Error/Exception suffix from type', () => {
    const result = generateFingerprint({
      errorType: 'NullPointerException',
      message: 'Null pointer',
    });
    expect(result.normalizedType).toBe('NullPointer');
  });

  it('normalizes the message', () => {
    const result = generateFingerprint({
      errorType: 'Error',
      message: 'Failed for user user_123 at 192.168.1.1',
    });
    expect(result.normalizedMessage).toContain('<USER_ID>');
    expect(result.normalizedMessage).toContain('<IP>');
  });

  it('extracts source location from stack trace', () => {
    const result = generateFingerprint({
      errorType: 'Error',
      message: 'boom',
      stackTrace: '    at myFunc (/app/src/handler.ts:10:5)',
    });
    expect(result.sourceLocation).toBeDefined();
    expect(result.sourceLocation?.function).toBe('myFunc');
    expect(result.sourceLocation?.line).toBe(10);
  });

  it('produces same hash for same normalized input', () => {
    const a = generateFingerprint({
      errorType: 'TypeError',
      message: 'Cannot read property of null',
    });
    const b = generateFingerprint({
      errorType: 'TypeError',
      message: 'Cannot read property of null',
    });
    expect(a.hash).toBe(b.hash);
  });

  it('produces different hash for different inputs', () => {
    const a = generateFingerprint({ errorType: 'TypeError', message: 'a' });
    const b = generateFingerprint({ errorType: 'RangeError', message: 'b' });
    expect(a.hash).not.toBe(b.hash);
  });
});

// ============================================================================
// levenshteinDistance
// ============================================================================

describe('levenshteinDistance', () => {
  it('returns 0 for identical strings', () => {
    expect(levenshteinDistance('abc', 'abc')).toBe(0);
  });

  it('returns correct distance for single edit', () => {
    expect(levenshteinDistance('cat', 'bat')).toBe(1);
  });

  it('returns length of longer string when one is empty', () => {
    expect(levenshteinDistance('', 'abc')).toBe(3);
    expect(levenshteinDistance('abc', '')).toBe(3);
  });

  it('handles completely different strings', () => {
    expect(levenshteinDistance('abc', 'xyz')).toBe(3);
  });
});

// ============================================================================
// calculateFingerprintSimilarity
// ============================================================================

describe('calculateFingerprintSimilarity', () => {
  it('returns 1 for identical fingerprints', () => {
    const fp = {
      hash: 'abc',
      normalizedType: 'Type',
      normalizedMessage: 'msg',
      stackSignature: 'sig',
    };
    expect(calculateFingerprintSimilarity(fp, fp)).toBeCloseTo(1, 1);
  });

  it('returns high score for same type and similar message', () => {
    const a = {
      hash: 'a',
      normalizedType: 'TypeError',
      normalizedMessage: 'Cannot read property x',
      stackSignature: '',
    };
    const b = {
      hash: 'b',
      normalizedType: 'TypeError',
      normalizedMessage: 'Cannot read property y',
      stackSignature: '',
    };
    expect(calculateFingerprintSimilarity(a, b)).toBeGreaterThan(0.7);
  });

  it('returns low score for different types and messages', () => {
    const a = {
      hash: 'a',
      normalizedType: 'TypeError',
      normalizedMessage: 'null pointer',
      stackSignature: '',
    };
    const b = {
      hash: 'b',
      normalizedType: 'RangeError',
      normalizedMessage: 'stack overflow in recursive call',
      stackSignature: '',
    };
    expect(calculateFingerprintSimilarity(a, b)).toBeLessThan(0.5);
  });
});

// ============================================================================
// processErrorEvent
// ============================================================================

describe('processErrorEvent', () => {
  const baseEvent: ErrorEvent = {
    id: 'ev_1',
    productId: 'lysnrai',
    errorType: 'TypeError',
    message: 'Cannot read property of null',
    timestamp: new Date().toISOString(),
    platform: 'ios',
    osVersion: '18.0',
    appVersion: '1.2.0',
  };

  it('creates a new cluster when no existing fingerprints', () => {
    const result = processErrorEvent(baseEvent);
    expect(result.isNewCluster).toBe(true);
    expect(result.clusterId).toMatch(/^ec_/);
    expect(result.fingerprint.hash).toBeTruthy();
  });

  it('matches existing fingerprint when similarity >= 0.85', () => {
    const fp = generateFingerprint({
      errorType: baseEvent.errorType,
      message: baseEvent.message,
    });

    const existing: ErrorFingerprint = {
      id: 'ef_existing',
      productId: 'lysnrai',
      fingerprintHash: fp.hash,
      errorType: fp.normalizedType,
      messageTemplate: fp.normalizedMessage,
      stackSignature: fp.stackSignature,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
      occurrenceCount: 5,
      uniqueUsers: 3,
      ttl: 90 * 86400,
    };

    const existingMap = new Map<string, ErrorFingerprint>();
    existingMap.set(existing.id, existing);

    const result = processErrorEvent(baseEvent, existingMap);
    expect(result.isNewCluster).toBe(false);
    expect(result.clusterId).toBe('ef_existing');
  });

  it('extracts platform context from error event', () => {
    const result = processErrorEvent(baseEvent);
    expect(result.context.platform).toBe('ios');
    expect(result.context.osVersion).toBe('18.0');
    expect(result.context.appVersion).toBe('1.2.0');
  });
});

// ============================================================================
// updateClusterWithError
// ============================================================================

describe('updateClusterWithError', () => {
  const cluster: ErrorClusterDoc = {
    id: 'ec_1',
    productId: 'lysnrai',
    fingerprintHash: 'fp_1',
    firstSeenAt: '2026-03-20T00:00:00.000Z',
    lastSeenAt: '2026-03-20T12:00:00.000Z',
    occurrenceCount: 5,
    uniqueUsers: 2,
    errorType: 'TypeError',
    messageTemplate: 'Cannot read property',
    stackSignature: 'sig',
    relatedClusterIds: [],
    status: 'active',
    createdAt: '2026-03-20T00:00:00.000Z',
    updatedAt: '2026-03-20T12:00:00.000Z',
    ttl: 90 * 86400,
  };

  const errorEvent: ErrorEvent = {
    id: 'ev_2',
    productId: 'lysnrai',
    errorType: 'TypeError',
    message: 'Cannot read property of null',
    timestamp: new Date().toISOString(),
    platform: 'ios',
    osVersion: '18.1',
    appVersion: '1.3.0',
    userId: 'user_new',
  };

  const fingerprint = generateFingerprint({
    errorType: errorEvent.errorType,
    message: errorEvent.message,
  });

  it('increments occurrence count', () => {
    const updated = updateClusterWithError(cluster, errorEvent, fingerprint);
    expect(updated.occurrenceCount).toBe(6);
  });

  it('updates lastSeenAt timestamp', () => {
    const updated = updateClusterWithError(cluster, errorEvent, fingerprint);
    expect(new Date(updated.lastSeenAt).getTime()).toBeGreaterThan(
      new Date(cluster.lastSeenAt).getTime()
    );
  });

  it('re-activates resolved clusters', () => {
    const resolvedCluster = { ...cluster, status: 'resolved' as const };
    const updated = updateClusterWithError(resolvedCluster, errorEvent, fingerprint);
    expect(updated.status).toBe('active');
  });

  it('updates commonContext with new os/app version', () => {
    const updated = updateClusterWithError(cluster, errorEvent, fingerprint);
    expect(updated.commonContext).toBeDefined();
    expect(updated.commonContext!.osVersions).toEqual(
      expect.arrayContaining([expect.objectContaining({ version: '18.1' })])
    );
    expect(updated.commonContext!.appVersions).toEqual(
      expect.arrayContaining([expect.objectContaining({ version: '1.3.0' })])
    );
  });
});
