/**
 * Telemetry module tests — types, policy evaluation, fingerprinting.
 */

import { describe, it, expect } from 'vitest';
import {
  TelemetryEventSchema,
  TelemetryIngestRequestSchema,
  CreatePolicySchema,
  UpdatePolicySchema,
  TelemetryQuerySchema,
  type TelemetryEventDoc,
  type TelemetryCollectionPolicyDoc,
} from './types.js';
import {
  containsPII,
  computePk,
  normalizeMessage,
  generateFingerprint,
  policyMatchesContext,
  mergePolicies,
  checkRateLimit,
  extractClientIp,
} from './routes.js';
import { UpdateClusterSchema, ClusterStatusEnum } from './types.js';

// ─── Minimal valid event for reuse ──────────────────────────────────

function validEvent(overrides: Record<string, unknown> = {}) {
  return {
    id: '550e8400-e29b-41d4-a716-446655440000',
    productId: 'lysnrai',
    anonymousInstallId: '660e8400-e29b-41d4-a716-446655440001',
    sessionId: 'sess_abc123',
    platform: 'ios',
    channel: 'keyboard_extension',
    osFamily: 'ios',
    appVersion: '1.0.0',
    buildNumber: '26',
    releaseChannel: 'beta',
    eventType: 'error',
    module: 'keyboard_dictation',
    eventName: 'recognition_failed',
    occurredAt: '2026-02-17T08:00:00.000Z',
    ...overrides,
  };
}

// ─── TelemetryEventSchema ───────────────────────────────────────────

describe('TelemetryEventSchema', () => {
  it('accepts a valid event with anonymousInstallId', () => {
    const result = TelemetryEventSchema.safeParse(validEvent());
    expect(result.success).toBe(true);
  });

  it('accepts a valid event with userId', () => {
    const result = TelemetryEventSchema.safeParse(
      validEvent({ userId: 'usr_abc', anonymousInstallId: undefined })
    );
    expect(result.success).toBe(true);
  });

  it('accepts event with both userId and anonymousInstallId', () => {
    const result = TelemetryEventSchema.safeParse(validEvent({ userId: 'usr_abc' }));
    expect(result.success).toBe(true);
  });

  it('rejects event with neither userId nor anonymousInstallId', () => {
    const result = TelemetryEventSchema.safeParse(validEvent({ anonymousInstallId: undefined }));
    expect(result.success).toBe(false);
  });

  it('rejects empty id', () => {
    const result = TelemetryEventSchema.safeParse(validEvent({ id: 'not-a-uuid' }));
    expect(result.success).toBe(false);
  });

  it('rejects invalid platform', () => {
    const result = TelemetryEventSchema.safeParse(validEvent({ platform: 'symbian' }));
    expect(result.success).toBe(false);
  });

  it('rejects invalid channel', () => {
    const result = TelemetryEventSchema.safeParse(validEvent({ channel: 'fax_machine' }));
    expect(result.success).toBe(false);
  });

  it('rejects invalid osFamily', () => {
    const result = TelemetryEventSchema.safeParse(validEvent({ osFamily: 'beos' }));
    expect(result.success).toBe(false);
  });

  it('rejects invalid eventType', () => {
    const result = TelemetryEventSchema.safeParse(validEvent({ eventType: 'trace' }));
    expect(result.success).toBe(false);
  });

  it('rejects message > 512 chars', () => {
    const result = TelemetryEventSchema.safeParse(validEvent({ message: 'x'.repeat(513) }));
    expect(result.success).toBe(false);
  });

  it('accepts message at exactly 512 chars', () => {
    const result = TelemetryEventSchema.safeParse(validEvent({ message: 'x'.repeat(512) }));
    expect(result.success).toBe(true);
  });

  it('rejects stackTrace > 8192 chars', () => {
    const result = TelemetryEventSchema.safeParse(
      validEvent({ stackTrace: 'x'.repeat(8193), eventType: 'fatal' })
    );
    expect(result.success).toBe(false);
  });

  it('accepts optional fields', () => {
    const result = TelemetryEventSchema.safeParse(
      validEvent({
        feature: 'voice_typing',
        errorDomain: 'kAFAssistantErrorDomain',
        errorCode: '209',
        message: 'Recognition timed out',
        fingerprint: 'abc123def456',
        tags: { backend: 'azure' },
        metrics: { duration_ms: 5000 },
        context: { dictation: { backend: 'azure' } },
        osVersion: 'iOS 18.2',
        deviceModel: 'iPhone17,3',
        locale: 'en-US',
        timezone: 'America/Los_Angeles',
      })
    );
    expect(result.success).toBe(true);
  });

  it('validates all platform enum values', () => {
    for (const p of ['ios', 'android', 'web', 'desktop']) {
      const result = TelemetryEventSchema.safeParse(validEvent({ platform: p }));
      expect(result.success).toBe(true);
    }
  });

  it('validates all channel enum values', () => {
    for (const c of [
      'mobile_app',
      'keyboard_extension',
      'web_app',
      'desktop_app',
      'backend_service',
    ]) {
      const result = TelemetryEventSchema.safeParse(validEvent({ channel: c }));
      expect(result.success).toBe(true);
    }
  });

  it('validates all osFamily enum values', () => {
    for (const os of ['ios', 'android', 'macos', 'windows', 'linux', 'chromeos', 'other']) {
      const result = TelemetryEventSchema.safeParse(validEvent({ osFamily: os }));
      expect(result.success).toBe(true);
    }
  });

  it('validates all eventType enum values', () => {
    for (const et of ['debug', 'info', 'warn', 'error', 'fatal']) {
      const result = TelemetryEventSchema.safeParse(validEvent({ eventType: et }));
      expect(result.success).toBe(true);
    }
  });

  it('validates all releaseChannel enum values', () => {
    for (const rc of ['dev', 'beta', 'prod']) {
      const result = TelemetryEventSchema.safeParse(validEvent({ releaseChannel: rc }));
      expect(result.success).toBe(true);
    }
  });
});

// ─── TelemetryIngestRequestSchema ───────────────────────────────────

describe('TelemetryIngestRequestSchema', () => {
  it('accepts valid batch', () => {
    const result = TelemetryIngestRequestSchema.safeParse({
      productId: 'lysnrai',
      events: [validEvent()],
    });
    expect(result.success).toBe(true);
  });

  it('rejects empty events array', () => {
    const result = TelemetryIngestRequestSchema.safeParse({
      productId: 'lysnrai',
      events: [],
    });
    expect(result.success).toBe(false);
  });

  it('rejects > 50 events', () => {
    const events = Array.from({ length: 51 }, (_, i) =>
      validEvent({ id: `550e8400-e29b-41d4-a716-44665544${String(i).padStart(4, '0')}` })
    );
    const result = TelemetryIngestRequestSchema.safeParse({
      productId: 'lysnrai',
      events,
    });
    expect(result.success).toBe(false);
  });

  it('accepts clientClockSkewMs', () => {
    const result = TelemetryIngestRequestSchema.safeParse({
      productId: 'lysnrai',
      events: [validEvent()],
      clientClockSkewMs: -500,
    });
    expect(result.success).toBe(true);
  });
});

// ─── CreatePolicySchema ─────────────────────────────────────────────

describe('CreatePolicySchema', () => {
  it('accepts minimal policy', () => {
    const result = CreatePolicySchema.safeParse({ name: 'Test policy' });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.enabled).toBe(true);
      expect(result.data.priority).toBe(50);
      expect(result.data.eventTypes).toEqual(['warn', 'error', 'fatal']);
      expect(result.data.samplingRate).toBe(1.0);
    }
  });

  it('accepts full policy with targeting', () => {
    const result = CreatePolicySchema.safeParse({
      name: 'Debug iOS keyboard',
      description: 'Debug user X iOS keyboard dictation',
      enabled: true,
      priority: 100,
      eventTypes: ['debug', 'info', 'warn', 'error', 'fatal'],
      modules: ['keyboard_dictation'],
      samplingRate: 1.0,
      targeting: {
        userIds: ['usr_abc'],
        platforms: ['ios'],
        channels: ['keyboard_extension'],
        osFamilies: ['ios'],
        buildNumberRange: { min: 1, max: 25 },
        countryCodes: ['US'],
        regionCodes: ['US:WA'],
        releaseChannels: ['beta'],
        percentage: 100,
      },
      startsAt: '2026-02-17T00:00:00.000Z',
      expiresAt: '2026-02-20T00:00:00.000Z',
    });
    expect(result.success).toBe(true);
  });

  it('rejects empty name', () => {
    const result = CreatePolicySchema.safeParse({ name: '' });
    expect(result.success).toBe(false);
  });

  it('rejects samplingRate > 1', () => {
    const result = CreatePolicySchema.safeParse({ name: 'Test', samplingRate: 1.5 });
    expect(result.success).toBe(false);
  });

  it('rejects priority > 999', () => {
    const result = CreatePolicySchema.safeParse({ name: 'Test', priority: 1000 });
    expect(result.success).toBe(false);
  });
});

// ─── UpdatePolicySchema ─────────────────────────────────────────────

describe('UpdatePolicySchema', () => {
  it('accepts partial updates', () => {
    const result = UpdatePolicySchema.safeParse({ enabled: false, priority: 10 });
    expect(result.success).toBe(true);
  });

  it('accepts empty update', () => {
    const result = UpdatePolicySchema.safeParse({});
    expect(result.success).toBe(true);
  });

  it('accepts nullable startsAt/expiresAt (to clear them)', () => {
    const result = UpdatePolicySchema.safeParse({ startsAt: null, expiresAt: null });
    expect(result.success).toBe(true);
  });
});

// ─── TelemetryQuerySchema ───────────────────────────────────────────

describe('TelemetryQuerySchema', () => {
  it('accepts empty query (defaults)', () => {
    const result = TelemetryQuerySchema.safeParse({});
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.limit).toBe(50);
    }
  });

  it('accepts full filter set', () => {
    const result = TelemetryQuerySchema.safeParse({
      userId: 'usr_abc',
      platform: 'ios',
      channel: 'keyboard_extension',
      module: 'keyboard_dictation',
      eventType: 'error',
      from: '2026-02-01T00:00:00.000Z',
      to: '2026-02-17T23:59:59.000Z',
      limit: 100,
    });
    expect(result.success).toBe(true);
  });

  it('rejects limit > 200', () => {
    const result = TelemetryQuerySchema.safeParse({ limit: 201 });
    expect(result.success).toBe(false);
  });

  it('coerces string limit to number', () => {
    const result = TelemetryQuerySchema.safeParse({ limit: '25' });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.limit).toBe(25);
    }
  });
});

// ─── containsPII ────────────────────────────────────────────────────

describe('containsPII', () => {
  it('detects email addresses', () => {
    expect(containsPII('Contact user@example.com for help')).toBe(true);
  });

  it('detects US phone numbers', () => {
    expect(containsPII('Call 555-123-4567')).toBe(true);
    expect(containsPII('Call 5551234567')).toBe(true);
  });

  it('detects credit card numbers', () => {
    expect(containsPII('Card 4111 1111 1111 1111')).toBe(true);
    expect(containsPII('Card 4111-1111-1111-1111')).toBe(true);
  });

  it('detects SSN', () => {
    expect(containsPII('SSN 123-45-6789')).toBe(true);
  });

  it('allows clean text', () => {
    expect(containsPII('Recognition failed with code 209')).toBe(false);
  });

  it('allows empty string', () => {
    expect(containsPII('')).toBe(false);
  });
});

// ─── computePk ──────────────────────────────────────────────────────

describe('computePk', () => {
  it('computes correct pk for mid-year date', () => {
    expect(computePk('lysnrai', '2026-06-15T10:00:00.000Z', 'ios')).toBe('lysnrai:202606:ios');
  });

  it('pads single-digit months', () => {
    expect(computePk('lysnrai', '2026-01-01T00:00:00.000Z', 'desktop')).toBe(
      'lysnrai:202601:desktop'
    );
  });

  it('handles December correctly', () => {
    expect(computePk('mindlyst', '2026-12-31T23:59:59.000Z', 'web')).toBe('mindlyst:202612:web');
  });

  it('uses UTC month (not local)', () => {
    // Feb 1 00:00 UTC — should be 202602 regardless of local timezone
    expect(computePk('lysnrai', '2026-02-01T00:00:00.000Z', 'android')).toBe(
      'lysnrai:202602:android'
    );
  });
});

// ─── normalizeMessage ───────────────────────────────────────────────

describe('normalizeMessage', () => {
  it('replaces UUIDs', () => {
    expect(normalizeMessage('Error for 550e8400-e29b-41d4-a716-446655440000')).toContain('<uuid>');
  });

  it('replaces numbers', () => {
    expect(normalizeMessage('Timeout after 5000ms')).toBe('timeout after <n>ms');
  });

  it('replaces file paths', () => {
    expect(normalizeMessage('Failed at /usr/lib/speech.so')).toContain('<path>');
  });

  it('lowercases', () => {
    expect(normalizeMessage('FATAL ERROR')).toBe('fatal error');
  });

  it('trims whitespace', () => {
    expect(normalizeMessage('  error  ')).toBe('error');
  });

  it('handles empty string', () => {
    expect(normalizeMessage('')).toBe('');
  });

  it('is deterministic', () => {
    const msg = 'Error 550e8400-e29b-41d4-a716-446655440000 at /foo/bar.ts line 42';
    expect(normalizeMessage(msg)).toBe(normalizeMessage(msg));
  });
});

// ─── generateFingerprint ────────────────────────────────────────────

describe('generateFingerprint', () => {
  function makeDoc(overrides: Partial<TelemetryEventDoc> = {}): TelemetryEventDoc {
    return {
      id: '550e8400-e29b-41d4-a716-446655440000',
      productId: 'lysnrai',
      anonymousInstallId: '660e8400-e29b-41d4-a716-446655440001',
      sessionId: 'sess_abc',
      platform: 'ios',
      channel: 'keyboard_extension',
      osFamily: 'ios',
      appVersion: '1.0.0',
      buildNumber: '26',
      releaseChannel: 'beta',
      eventType: 'error',
      module: 'dictation',
      eventName: 'recognition_failed',
      occurredAt: '2026-02-17T08:00:00.000Z',
      pk: 'lysnrai:202602:ios',
      receivedAt: '2026-02-17T08:00:01.000Z',
      ttl: 2592000,
      ...overrides,
    };
  }

  it('returns 16-char hex string', () => {
    const fp = generateFingerprint(makeDoc());
    expect(fp).toMatch(/^[0-9a-f]{16}$/);
  });

  it('is deterministic', () => {
    const doc = makeDoc();
    expect(generateFingerprint(doc)).toBe(generateFingerprint(doc));
  });

  it('same error from different sessions produces same fingerprint', () => {
    const fp1 = generateFingerprint(makeDoc({ sessionId: 'a' }));
    const fp2 = generateFingerprint(makeDoc({ sessionId: 'b' }));
    expect(fp1).toBe(fp2);
  });

  it('same error from different versions produces same fingerprint', () => {
    const fp1 = generateFingerprint(makeDoc({ appVersion: '1.0.0' }));
    const fp2 = generateFingerprint(makeDoc({ appVersion: '1.1.0' }));
    expect(fp1).toBe(fp2);
  });

  it('different error name produces different fingerprint', () => {
    const fp1 = generateFingerprint(makeDoc({ eventName: 'recognition_failed' }));
    const fp2 = generateFingerprint(makeDoc({ eventName: 'recognition_timeout' }));
    expect(fp1).not.toBe(fp2);
  });

  it('different module produces different fingerprint', () => {
    const fp1 = generateFingerprint(makeDoc({ module: 'dictation' }));
    const fp2 = generateFingerprint(makeDoc({ module: 'audio' }));
    expect(fp1).not.toBe(fp2);
  });

  it('different errorDomain produces different fingerprint', () => {
    const fp1 = generateFingerprint(makeDoc({ errorDomain: 'kAFAssistant' }));
    const fp2 = generateFingerprint(makeDoc({ errorDomain: 'NSURLError' }));
    expect(fp1).not.toBe(fp2);
  });

  it('normalizes numeric differences in messages', () => {
    const fp1 = generateFingerprint(makeDoc({ message: 'Timeout after 5000ms' }));
    const fp2 = generateFingerprint(makeDoc({ message: 'Timeout after 3000ms' }));
    expect(fp1).toBe(fp2);
  });
});

// ─── policyMatchesContext ───────────────────────────────────────────

describe('policyMatchesContext', () => {
  function makePolicy(
    overrides: Partial<TelemetryCollectionPolicyDoc> = {}
  ): TelemetryCollectionPolicyDoc {
    return {
      id: 'pol_1',
      productId: 'lysnrai',
      name: 'Test',
      description: '',
      enabled: true,
      priority: 50,
      eventTypes: ['error', 'fatal'],
      modules: [],
      samplingRate: 1.0,
      targeting: {},
      createdAt: '2026-01-01T00:00:00.000Z',
      updatedAt: '2026-01-01T00:00:00.000Z',
      createdBy: 'admin',
      ...overrides,
    };
  }

  it('matches when targeting is empty (matches all)', () => {
    expect(policyMatchesContext(makePolicy(), { platform: 'ios' })).toBe(true);
  });

  it('matches platform targeting', () => {
    const policy = makePolicy({ targeting: { platforms: ['ios'] } });
    expect(policyMatchesContext(policy, { platform: 'ios' })).toBe(true);
    expect(policyMatchesContext(policy, { platform: 'android' })).toBe(false);
  });

  it('matches channel targeting', () => {
    const policy = makePolicy({ targeting: { channels: ['keyboard_extension'] } });
    expect(policyMatchesContext(policy, { channel: 'keyboard_extension' })).toBe(true);
    expect(policyMatchesContext(policy, { channel: 'mobile_app' })).toBe(false);
  });

  it('matches osFamily targeting', () => {
    const policy = makePolicy({ targeting: { osFamilies: ['ios', 'macos'] } });
    expect(policyMatchesContext(policy, { osFamily: 'ios' })).toBe(true);
    expect(policyMatchesContext(policy, { osFamily: 'android' })).toBe(false);
  });

  it('matches userId targeting', () => {
    const policy = makePolicy({ targeting: { userIds: ['usr_abc'] } });
    expect(policyMatchesContext(policy, { userId: 'usr_abc' })).toBe(true);
    expect(policyMatchesContext(policy, { userId: 'usr_xyz' })).toBe(false);
  });

  it('matches appVersion targeting', () => {
    const policy = makePolicy({ targeting: { appVersions: ['1.0.0', '1.1.0'] } });
    expect(policyMatchesContext(policy, { appVersion: '1.0.0' })).toBe(true);
    expect(policyMatchesContext(policy, { appVersion: '2.0.0' })).toBe(false);
  });

  it('matches buildNumber range targeting', () => {
    const policy = makePolicy({ targeting: { buildNumberRange: { min: 10, max: 50 } } });
    expect(policyMatchesContext(policy, { buildNumber: '25' })).toBe(true);
    expect(policyMatchesContext(policy, { buildNumber: '5' })).toBe(false);
    expect(policyMatchesContext(policy, { buildNumber: '100' })).toBe(false);
  });

  it('matches releaseChannel targeting', () => {
    const policy = makePolicy({ targeting: { releaseChannels: ['beta'] } });
    expect(policyMatchesContext(policy, { releaseChannel: 'beta' })).toBe(true);
    expect(policyMatchesContext(policy, { releaseChannel: 'prod' })).toBe(false);
  });

  it('rejects expired policy', () => {
    const policy = makePolicy({ expiresAt: '2020-01-01T00:00:00.000Z' });
    expect(policyMatchesContext(policy, { platform: 'ios' })).toBe(false);
  });

  it('rejects future-only policy', () => {
    const policy = makePolicy({ startsAt: '2099-01-01T00:00:00.000Z' });
    expect(policyMatchesContext(policy, { platform: 'ios' })).toBe(false);
  });

  it('matches countryCode targeting', () => {
    const policy = makePolicy({ targeting: { countryCodes: ['US', 'CA'] } });
    expect(policyMatchesContext(policy, { countryCode: 'US' })).toBe(true);
    expect(policyMatchesContext(policy, { countryCode: 'DE' })).toBe(false);
  });

  it('matches regionCode targeting', () => {
    const policy = makePolicy({ targeting: { regionCodes: ['US:WA', 'US:CA'] } });
    expect(policyMatchesContext(policy, { regionCode: 'US:WA' })).toBe(true);
    expect(policyMatchesContext(policy, { regionCode: 'US:TX' })).toBe(false);
  });
});

// ─── mergePolicies ──────────────────────────────────────────────────

describe('mergePolicies', () => {
  function makePolicy(
    overrides: Partial<TelemetryCollectionPolicyDoc> = {}
  ): TelemetryCollectionPolicyDoc {
    return {
      id: 'pol_1',
      productId: 'lysnrai',
      name: 'Test',
      description: '',
      enabled: true,
      priority: 50,
      eventTypes: ['error', 'fatal'],
      modules: [],
      samplingRate: 1.0,
      targeting: {},
      createdAt: '2026-01-01T00:00:00.000Z',
      updatedAt: '2026-01-01T00:00:00.000Z',
      createdBy: 'admin',
      ...overrides,
    };
  }

  it('returns defaults when no policies', () => {
    const config = mergePolicies([]);
    expect(config.enabled).toBe(true);
    expect(config.eventTypes).toEqual(['warn', 'error', 'fatal']);
    expect(config.samplingRates.error).toBe(1);
    expect(config.samplingRates.debug).toBe(0);
  });

  it('unions event types from multiple policies', () => {
    const config = mergePolicies([
      makePolicy({ eventTypes: ['error'], samplingRate: 1.0 }),
      makePolicy({ id: 'pol_2', eventTypes: ['debug', 'info'], samplingRate: 0.5 }),
    ]);
    expect(config.eventTypes).toContain('error');
    expect(config.eventTypes).toContain('debug');
    expect(config.eventTypes).toContain('info');
  });

  it('first policy sampling rate wins per event type', () => {
    const config = mergePolicies([
      makePolicy({ eventTypes: ['error'], samplingRate: 0.8 }),
      makePolicy({ id: 'pol_2', eventTypes: ['error'], samplingRate: 0.2 }),
    ]);
    expect(config.samplingRates.error).toBe(0.8);
  });

  it('priority 999 + samplingRate 0 disables everything', () => {
    const config = mergePolicies([
      makePolicy({ priority: 999, samplingRate: 0, eventTypes: ['error'] }),
    ]);
    expect(config.enabled).toBe(false);
  });

  it('unions modules from multiple policies', () => {
    const config = mergePolicies([
      makePolicy({ modules: ['dictation'] }),
      makePolicy({ id: 'pol_2', modules: ['audio', 'dictation'] }),
    ]);
    expect(config.modules).toContain('dictation');
    expect(config.modules).toContain('audio');
    expect(config.modules.length).toBe(2);
  });
});

// ─── checkRateLimit ─────────────────────────────────────────────

describe('checkRateLimit', () => {
  it('allows events within the limit', () => {
    const id = `test_rate_${Date.now()}`;
    expect(checkRateLimit(id, 50)).toBe(true);
    expect(checkRateLimit(id, 49)).toBe(true);
  });

  it('rejects events that exceed the limit', () => {
    const id = `test_rate_exceed_${Date.now()}`;
    expect(checkRateLimit(id, 80)).toBe(true);
    expect(checkRateLimit(id, 30)).toBe(false); // 80 + 30 > 100
  });

  it('different installIds have separate buckets', () => {
    const id1 = `test_rate_a_${Date.now()}`;
    const id2 = `test_rate_b_${Date.now()}`;
    expect(checkRateLimit(id1, 90)).toBe(true);
    expect(checkRateLimit(id2, 90)).toBe(true); // separate bucket
    expect(checkRateLimit(id1, 20)).toBe(false); // id1 exceeded
  });
});

// ─── UpdateClusterSchema ─────────────────────────────────────────

describe('UpdateClusterSchema', () => {
  it('accepts valid status values', () => {
    for (const status of ['open', 'resolved', 'ignored']) {
      const result = UpdateClusterSchema.safeParse({ status });
      expect(result.success).toBe(true);
    }
  });

  it('rejects invalid status', () => {
    const result = UpdateClusterSchema.safeParse({ status: 'deleted' });
    expect(result.success).toBe(false);
  });

  it('rejects missing status', () => {
    const result = UpdateClusterSchema.safeParse({});
    expect(result.success).toBe(false);
  });
});

// ─── ClusterStatusEnum ───────────────────────────────────────────

describe('ClusterStatusEnum', () => {
  it('defines exactly three values', () => {
    expect(ClusterStatusEnum.options).toEqual(['open', 'resolved', 'ignored']);
  });
});

// ─── extractClientIp ─────────────────────────────────────────────

describe('extractClientIp', () => {
  it('extracts first IP from X-Forwarded-For string', () => {
    expect(
      extractClientIp({
        headers: { 'x-forwarded-for': '1.2.3.4, 10.0.0.1, 172.16.0.1' },
      })
    ).toBe('1.2.3.4');
  });

  it('extracts from X-Forwarded-For array', () => {
    expect(
      extractClientIp({
        headers: { 'x-forwarded-for': ['5.6.7.8, 10.0.0.1'] },
      })
    ).toBe('5.6.7.8');
  });

  it('falls back to req.ip when no X-Forwarded-For', () => {
    expect(extractClientIp({ headers: {}, ip: '192.168.1.1' })).toBe('192.168.1.1');
  });

  it('returns empty string when no IP available', () => {
    expect(extractClientIp({ headers: {} })).toBe('');
  });

  it('trims whitespace from X-Forwarded-For', () => {
    expect(
      extractClientIp({
        headers: { 'x-forwarded-for': '  9.8.7.6  , 10.0.0.1' },
      })
    ).toBe('9.8.7.6');
  });

  it('handles single IP in X-Forwarded-For (no comma)', () => {
    expect(
      extractClientIp({
        headers: { 'x-forwarded-for': '203.0.113.50' },
      })
    ).toBe('203.0.113.50');
  });
});
