import { Request } from 'express';

import {
  generateRandomString,
  hashString,
  isValidEmail,
  sanitizeInput,
  getClientIP,
  parseExpiration,
  isExpired,
} from '../helpers';

describe('Helpers', () => {
  describe('generateRandomString', () => {
    it('should generate a random string of default length (32)', () => {
      const result = generateRandomString();

      expect(result).toBeDefined();
      expect(typeof result).toBe('string');
      expect(result.length).toBe(64); // 32 bytes * 2 hex chars per byte
      expect(/^[a-f0-9]+$/.test(result)).toBe(true);
    });

    it('should generate a random string of specified length', () => {
      const length = 16;
      const result = generateRandomString(length);

      expect(result).toBeDefined();
      expect(typeof result).toBe('string');
      expect(result.length).toBe(32); // 16 bytes * 2 hex chars per byte
      expect(/^[a-f0-9]+$/.test(result)).toBe(true);
    });

    it('should generate different strings on multiple calls', () => {
      const result1 = generateRandomString();
      const result2 = generateRandomString();

      expect(result1).not.toBe(result2);
    });
  });

  describe('hashString', () => {
    it('should hash a string using SHA256', () => {
      const input = 'test-input';
      const result = hashString(input);

      expect(result).toBeDefined();
      expect(typeof result).toBe('string');
      expect(result.length).toBe(64); // SHA256 produces 64 character hex string
      expect(/^[a-f0-9]+$/.test(result)).toBe(true);
    });

    it('should produce consistent hashes for same input', () => {
      const input = 'consistent-input';
      const result1 = hashString(input);
      const result2 = hashString(input);

      expect(result1).toBe(result2);
    });

    it('should produce different hashes for different inputs', () => {
      const result1 = hashString('input1');
      const result2 = hashString('input2');

      expect(result1).not.toBe(result2);
    });
  });

  describe('isValidEmail', () => {
    it('should return true for valid email addresses', () => {
      const validEmails = [
        'test@example.com',
        'user.name@domain.co.uk',
        'test+tag@gmail.com',
        '123@test-domain.com',
      ];

      validEmails.forEach(email => {
        expect(isValidEmail(email)).toBe(true);
      });
    });

    it('should return false for invalid email addresses', () => {
      const invalidEmails = [
        'invalid-email',
        '@example.com',
        'test@',
        'test..test@example.com',
        'test @example.com',
        '',
        'test@example',
        'test@.com',
      ];

      invalidEmails.forEach(email => {
        expect(isValidEmail(email)).toBe(false);
      });
    });

    it('should return false for non-string inputs', () => {
      expect(isValidEmail(null as any)).toBe(false);
      expect(isValidEmail(undefined as any)).toBe(false);
      expect(isValidEmail(123 as any)).toBe(false);
      expect(isValidEmail({} as any)).toBe(false);
    });
  });

  describe('sanitizeInput', () => {
    it('should return empty string for null or undefined input', () => {
      expect(sanitizeInput(null as any)).toBe('');
      expect(sanitizeInput(undefined as any)).toBe('');
    });

    it('should return empty string for non-string input', () => {
      expect(sanitizeInput(123 as any)).toBe('');
      expect(sanitizeInput({} as any)).toBe('');
      expect(sanitizeInput([] as any)).toBe('');
    });

    it('should trim whitespace from input', () => {
      expect(sanitizeInput('  test input  ')).toBe('test input');
    });

    it('should remove HTML tags and prevent XSS', () => {
      const maliciousInput = '<script>alert("xss")</script><b>bold</b>normal text';
      const result = sanitizeInput(maliciousInput);

      expect(result).not.toContain('<script>');
      expect(result).not.toContain('<b>');
      expect(result).not.toContain('</b>');
      expect(result).toContain('bold');
      expect(result).toContain('normal text');
    });

    it('should handle empty string input', () => {
      expect(sanitizeInput('')).toBe('');
      expect(sanitizeInput('   ')).toBe('');
    });

    it('should preserve safe text content', () => {
      const safeInput = 'This is a safe string with normal text & symbols!';
      expect(sanitizeInput(safeInput)).toBe(safeInput);
    });
  });

  describe('getClientIP', () => {
    it('should return x-forwarded-for header as first priority', () => {
      const mockReq = {
        headers: {
          'x-forwarded-for': '192.168.1.100, 10.0.0.1',
        },
        ip: '127.0.0.1',
        socket: { remoteAddress: '127.0.0.1' },
      } as unknown as Request;

      const result = getClientIP(mockReq);
      expect(result).toBe('192.168.1.100');
    });

    it('should handle array x-forwarded-for header', () => {
      const mockReq = {
        headers: {
          'x-forwarded-for': ['192.168.1.100', '10.0.0.1'],
        },
        ip: '127.0.0.1',
      } as unknown as Request;

      const result = getClientIP(mockReq);
      expect(result).toBe('192.168.1.100');
    });

    it('should return x-real-ip header as second priority', () => {
      const mockReq = {
        headers: {
          'x-real-ip': '10.0.0.50',
        },
        ip: '127.0.0.1',
        socket: { remoteAddress: '127.0.0.1' },
      } as unknown as Request;

      const result = getClientIP(mockReq);
      expect(result).toBe('10.0.0.50');
    });

    it('should handle array x-real-ip header', () => {
      const mockReq = {
        headers: {
          'x-real-ip': ['10.0.0.50'],
        },
        ip: '127.0.0.1',
      } as unknown as Request;

      const result = getClientIP(mockReq);
      expect(result).toBe('10.0.0.50');
    });

    it('should return req.ip as third priority', () => {
      const mockReq = {
        headers: {},
        ip: '172.16.0.25',
        socket: { remoteAddress: '127.0.0.1' },
      } as unknown as Request;

      const result = getClientIP(mockReq);
      expect(result).toBe('172.16.0.25');
    });

    it('should return remoteAddress as fourth priority', () => {
      const mockReq = {
        headers: {},
        socket: { remoteAddress: '203.0.113.1' },
      } as unknown as Request;

      const result = getClientIP(mockReq);
      expect(result).toBe('203.0.113.1');
    });

    it('should return unknown when no IP found', () => {
      const mockReq = {
        headers: {},
      } as unknown as Request;

      const result = getClientIP(mockReq);
      expect(result).toBe('unknown');
    });
  });

  describe('parseExpiration', () => {
    it('should parse ISO date string to Date object', () => {
      const dateString = '2024-12-31T23:59:59.999Z';
      const result = parseExpiration(dateString);

      expect(result).toBeInstanceOf(Date);
      expect(result.toISOString()).toBe(dateString);
    });

    it('should parse date string in different formats', () => {
      const dateString = '2024-01-15';
      const result = parseExpiration(dateString);

      expect(result).toBeInstanceOf(Date);
      expect(result.getFullYear()).toBe(2024);
      expect(result.getMonth()).toBe(0); // January is 0
      expect(result.getDate()).toBe(15);
    });
  });

  describe('isExpired', () => {
    beforeAll(() => {
      // Freeze time for consistent testing
      jest.useFakeTimers();
      jest.setSystemTime(new Date('2024-01-15T12:00:00Z'));
    });

    afterAll(() => {
      jest.useRealTimers();
    });

    it('should return true for past dates', () => {
      const pastDate = new Date('2024-01-14T12:00:00Z');
      expect(isExpired(pastDate)).toBe(true);
    });

    it('should return false for future dates', () => {
      const futureDate = new Date('2024-01-16T12:00:00Z');
      expect(isExpired(futureDate)).toBe(false);
    });

    it('should return false for current time (not expired)', () => {
      const currentDate = new Date('2024-01-15T12:00:00Z');
      expect(isExpired(currentDate)).toBe(false);
    });

    it('should return true for dates just before current time', () => {
      const justExpired = new Date('2024-01-15T11:59:59Z');
      expect(isExpired(justExpired)).toBe(true);
    });
  });
});