import { Test } from '@nestjs/testing';
import { describe, it, expect, beforeEach } from 'vitest';

import { DeviceEnrichmentService } from './device-enrichment.service';

describe('DeviceEnrichmentService', () => {
  let service: DeviceEnrichmentService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [DeviceEnrichmentService],
    }).compile();

    service = module.get<DeviceEnrichmentService>(DeviceEnrichmentService);
  });

  describe('enrich', () => {
    it('enriches device data from User-Agent and client data', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0',
        '192.168.1.1',
        {
          screenWidth: 1920,
          screenHeight: 1080,
          viewportWidth: 1440,
          viewportHeight: 900,
          language: 'en-US',
          timezone: 'America/New_York',
        },
      );

      expect(result.deviceType).toBe('desktop');
      expect(result.isBot).toBe(false);
      expect(result.browser).toBe('Chrome');
      expect(result.browserVersion).toBe('120.0');
      expect(result.browserMajor).toBe(120);
      expect(result.os).toBe('macOS');
      expect(result.osVersion).toBe('10.15.7');
      expect(result.screenWidth).toBe(1920);
      expect(result.screenHeight).toBe(1080);
      expect(result.viewportWidth).toBe(1440);
      expect(result.viewportHeight).toBe(900);
      expect(result.language).toBe('en-US');
      expect(result.timezone).toBe('America/New_York');
      expect(result.ipHash).toBeTruthy();
    });

    it('handles missing User-Agent gracefully', async () => {
      const result = await service.enrich(undefined, undefined, {
        screenWidth: 1024,
        screenHeight: 768,
      });

      expect(result.deviceType).toBe('desktop');
      expect(result.isBot).toBe(false);
      expect(result.browser).toBeNull();
      expect(result.browserVersion).toBeNull();
      expect(result.os).toBeNull();
      expect(result.osVersion).toBeNull();
      expect(result.ipHash).toBeNull();
    });

    it('hashes IP address for privacy', async () => {
      const result1 = await service.enrich(undefined, '192.168.1.1');
      const result2 = await service.enrich(undefined, '192.168.1.2');
      const result3 = await service.enrich(undefined, '192.168.1.1');

      expect(result1.ipHash).toBeTruthy();
      expect(result2.ipHash).toBeTruthy();
      expect(result1.ipHash).not.toBe(result2.ipHash);
      expect(result1.ipHash).toBe(result3.ipHash); // Same IP on same day = same hash
    });
  });

  describe('parseUserAgent', () => {
    it('parses Chrome User-Agent correctly', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
      );

      expect(result.browser).toBe('Chrome');
      expect(result.browserVersion).toBe('120.0');
      expect(result.browserMajor).toBe(120);
      expect(result.os).toBe('Windows');
      expect(result.osVersion).toBe('10/11');
    });

    it('parses Firefox User-Agent correctly', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
      );

      expect(result.browser).toBe('Firefox');
      expect(result.browserVersion).toBe('122.0');
      expect(result.browserMajor).toBe(122);
      expect(result.os).toBe('Windows');
    });

    it('parses Safari User-Agent correctly', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
      );

      expect(result.browser).toBe('Safari');
      expect(result.browserVersion).toBe('17.2');
      expect(result.browserMajor).toBe(17);
      expect(result.os).toBe('macOS');
      expect(result.osVersion).toBe('10.15.7');
    });

    it('parses Edge User-Agent correctly', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Edge/120.0.0.0',
      );

      expect(result.browser).toBe('Edge');
      expect(result.browserVersion).toBe('120.0');
      expect(result.browserMajor).toBe(120);
    });

    it('parses mobile Safari User-Agent correctly', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1',
      );

      expect(result.browser).toBe('Safari');
      expect(result.os).toBe('iOS');
      expect(result.osVersion).toBe('17.2');
      expect(result.deviceType).toBe('mobile');
    });

    it('parses Android Chrome User-Agent correctly', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 Chrome/120.0.0.0 Mobile Safari/537.36',
      );

      expect(result.browser).toBe('Chrome');
      expect(result.os).toBe('Android');
      expect(result.osVersion).toBe('14');
      expect(result.deviceType).toBe('mobile');
    });

    it('handles unknown User-Agent', async () => {
      const result = await service.enrich(
        'UnknownBot/1.0',
      );

      expect(result.browser).toBeNull();
      expect(result.browserVersion).toBeNull();
      expect(result.os).toBeNull();
      expect(result.osVersion).toBeNull();
    });
  });

  describe('detectDeviceType', () => {
    it('detects desktop from no touch points', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (Windows NT 10.0) Chrome/120.0.0.0',
        undefined,
        {
          touchPoints: 0,
          viewportWidth: 1920,
        },
      );

      expect(result.deviceType).toBe('desktop');
    });

    it('detects mobile from touch points and small viewport', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (iPhone) Safari/604.1',
        undefined,
        {
          touchPoints: 5,
          viewportWidth: 375,
        },
      );

      expect(result.deviceType).toBe('mobile');
    });

    it('detects tablet from touch points and large viewport', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (iPad) Safari/604.1',
        undefined,
        {
          touchPoints: 5,
          viewportWidth: 1024,
        },
      );

      expect(result.deviceType).toBe('tablet');
    });

    it('detects tablet from iPad User-Agent', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (iPad; CPU OS 17_2 like Mac OS X) AppleWebKit/605.1.15',
      );

      expect(result.deviceType).toBe('tablet');
    });

    it('detects mobile from iPhone User-Agent', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) Mobile/15E148',
      );

      expect(result.deviceType).toBe('mobile');
    });

    it('detects mobile from Android Mobile User-Agent', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (Linux; Android 14) Mobile Safari/537.36',
      );

      expect(result.deviceType).toBe('mobile');
    });

    it('detects tablet from Android without Mobile keyword', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (Linux; Android 14; Tablet) AppleWebKit/537.36',
      );

      expect(result.deviceType).toBe('tablet');
    });
  });

  describe('detectBot', () => {
    it('detects Googlebot', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
      );

      expect(result.isBot).toBe(true);
      expect(result.deviceType).toBe('bot');
    });

    it('detects Bingbot', async () => {
      const result = await service.enrich(
        'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)',
      );

      expect(result.isBot).toBe(true);
      expect(result.deviceType).toBe('bot');
    });

    it('detects generic crawlers', async () => {
      const result = await service.enrich('SomeCrawler/1.0');

      expect(result.isBot).toBe(true);
      expect(result.deviceType).toBe('bot');
    });

    it('detects headless browsers', async () => {
      const result = await service.enrich('HeadlessChrome/120.0.0.0');

      expect(result.isBot).toBe(true);
      expect(result.deviceType).toBe('bot');
    });

    it('detects Puppeteer', async () => {
      const result = await service.enrich('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 HeadlessChrome/120.0.0.0 Safari/537.36 Puppeteer');

      expect(result.isBot).toBe(true);
      expect(result.deviceType).toBe('bot');
    });

    it('detects Playwright', async () => {
      const result = await service.enrich('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36 Playwright');

      expect(result.isBot).toBe(true);
      expect(result.deviceType).toBe('bot');
    });

    it('detects Selenium', async () => {
      const result = await service.enrich('Mozilla/5.0 (Windows NT 10.0) Chrome/120.0.0.0 Selenium');

      expect(result.isBot).toBe(true);
      expect(result.deviceType).toBe('bot');
    });

    it('detects social media bots', async () => {
      const facebookBot = await service.enrich('facebookexternalhit/1.1');
      const twitterBot = await service.enrich('Twitterbot/1.0');
      const linkedinBot = await service.enrich('LinkedInBot/1.0');

      expect(facebookBot.isBot).toBe(true);
      expect(twitterBot.isBot).toBe(true);
      expect(linkedinBot.isBot).toBe(true);
    });

    it('does not detect normal browsers as bots', async () => {
      const chrome = await service.enrich('Mozilla/5.0 (Windows NT 10.0) Chrome/120.0.0.0');
      const firefox = await service.enrich('Mozilla/5.0 (Windows NT 10.0) Firefox/122.0');
      const safari = await service.enrich('Mozilla/5.0 (Macintosh) Safari/605.1.15');

      expect(chrome.isBot).toBe(false);
      expect(firefox.isBot).toBe(false);
      expect(safari.isBot).toBe(false);
    });
  });

  describe('mapWindowsVersion', () => {
    it('maps Windows NT 10.0 to Windows 10/11', async () => {
      const result = await service.enrich('Mozilla/5.0 (Windows NT 10.0)');
      expect(result.osVersion).toBe('10/11');
    });

    it('maps Windows NT 6.3 to Windows 8.1', async () => {
      const result = await service.enrich('Mozilla/5.0 (Windows NT 6.3)');
      expect(result.osVersion).toBe('8.1');
    });

    it('maps Windows NT 6.2 to Windows 8', async () => {
      const result = await service.enrich('Mozilla/5.0 (Windows NT 6.2)');
      expect(result.osVersion).toBe('8');
    });

    it('maps Windows NT 6.1 to Windows 7', async () => {
      const result = await service.enrich('Mozilla/5.0 (Windows NT 6.1)');
      expect(result.osVersion).toBe('7');
    });

    it('handles unknown Windows NT version', async () => {
      const result = await service.enrich('Mozilla/5.0 (Windows NT 5.0)');
      expect(result.osVersion).toBe('5.0');
    });
  });

  describe('OS detection', () => {
    it('detects macOS with version', async () => {
      const result = await service.enrich('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)');
      expect(result.os).toBe('macOS');
      expect(result.osVersion).toBe('10.15.7');
    });

    it('detects Linux', async () => {
      const result = await service.enrich('Mozilla/5.0 (X11; Linux x86_64)');
      expect(result.os).toBe('Linux');
    });

    it('detects iOS with version', async () => {
      const result = await service.enrich('Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X)');
      expect(result.os).toBe('iOS');
      expect(result.osVersion).toBe('17.2');
    });

    it('detects Android with version', async () => {
      const result = await service.enrich('Mozilla/5.0 (Linux; Android 14)');
      expect(result.os).toBe('Android');
      expect(result.osVersion).toBe('14');
    });

    it('handles Android version with decimals', async () => {
      const result = await service.enrich('Mozilla/5.0 (Linux; Android 13.5)');
      expect(result.os).toBe('Android');
      expect(result.osVersion).toBe('13.5');
    });
  });

  describe('client device data preservation', () => {
    it('preserves all client device data', async () => {
      const clientData = {
        screenWidth: 1920,
        screenHeight: 1080,
        viewportWidth: 1440,
        viewportHeight: 900,
        pixelRatio: 2,
        colorDepth: 24,
        language: 'en-US',
        languages: ['en-US', 'en', 'es'],
        timezone: 'America/New_York',
        timezoneOffset: -300,
        deviceMemory: 8,
        hardwareConcurrency: 8,
        touchPoints: 0,
        cookiesEnabled: true,
        doNotTrack: false,
      };

      const result = await service.enrich(
        'Mozilla/5.0 (Macintosh) Chrome/120.0.0.0',
        undefined,
        clientData,
      );

      expect(result.screenWidth).toBe(1920);
      expect(result.screenHeight).toBe(1080);
      expect(result.viewportWidth).toBe(1440);
      expect(result.viewportHeight).toBe(900);
      expect(result.pixelRatio).toBe(2);
      expect(result.colorDepth).toBe(24);
      expect(result.language).toBe('en-US');
      expect(result.languages).toEqual(['en-US', 'en', 'es']);
      expect(result.timezone).toBe('America/New_York');
      expect(result.timezoneOffset).toBe(-300);
      expect(result.deviceMemory).toBe(8);
      expect(result.hardwareConcurrency).toBe(8);
      expect(result.touchPoints).toBe(0);
      expect(result.cookiesEnabled).toBe(true);
      expect(result.doNotTrack).toBe(false);
    });

    it('handles missing client device data', async () => {
      const result = await service.enrich('Mozilla/5.0 (Macintosh) Chrome/120.0.0.0');

      expect(result.screenWidth).toBeUndefined();
      expect(result.screenHeight).toBeUndefined();
      expect(result.viewportWidth).toBeUndefined();
      expect(result.viewportHeight).toBeUndefined();
      expect(result.pixelRatio).toBeUndefined();
      expect(result.colorDepth).toBeUndefined();
    });
  });
});
