/// <reference types="jest" />

import { ChromeDetector, ChromeDetectionResult, CHROME_DOWNLOAD_URL } from '../../src/desktop/chrome-detector';
import * as fs from 'fs';
import * as os from 'os';

// Mock fs and child_process so tests are hermetic
jest.mock('fs');
jest.mock('child_process');

const mockExistsSync = fs.existsSync as jest.MockedFunction<typeof fs.existsSync>;

import { execFileSync } from 'child_process';
const mockExecFileSync = execFileSync as jest.MockedFunction<typeof execFileSync>;

function mockChromeFound(chromePath: string): void {
  mockExistsSync.mockImplementation((p: fs.PathLike | number) => String(p) === chromePath);
}

function mockChromeNotFound(): void {
  mockExistsSync.mockReturnValue(false);
  mockExecFileSync.mockImplementation(() => { throw new Error('not found'); });
}

describe('ChromeDetector', () => {
  let detector: ChromeDetector;

  beforeEach(() => {
    jest.clearAllMocks();
    // Suppress CHROME_PATH env override so tests control detection fully
    delete process.env['CHROME_PATH'];
    detector = new ChromeDetector();
  });

  afterEach(() => {
    detector.stopPolling();
  });

  // -------------------------------------------------------------------------
  // Detection result shape
  // -------------------------------------------------------------------------

  describe('detect() — result shape', () => {
    test('returns downloadUrl pointing to the Chrome website', async () => {
      mockChromeNotFound();
      const result = await detector.detect();
      expect(result.downloadUrl).toBe(CHROME_DOWNLOAD_URL);
      expect(result.downloadUrl).toBe('https://www.google.com/chrome/');
    });

    test('result includes the current platform', async () => {
      mockChromeNotFound();
      const result = await detector.detect();
      expect(result.platform).toBe(os.platform());
    });
  });

  // -------------------------------------------------------------------------
  // Chrome found
  // -------------------------------------------------------------------------

  describe('detect() — Chrome present', () => {
    test('returns found:true when Chrome exists', async () => {
      const platform = os.platform();
      let chromePath: string;
      if (platform === 'darwin') {
        chromePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
      } else if (platform === 'win32') {
        chromePath = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
        process.env['PROGRAMFILES'] = 'C:\\Program Files';
      } else {
        chromePath = '/usr/bin/google-chrome-stable';
      }
      mockChromeFound(chromePath);

      const result = await detector.detect();
      expect(result.found).toBe(true);
      expect(result.path).toBe(chromePath);
    });

    test('emits "detected" event when Chrome is found', async () => {
      const platform = os.platform();
      let chromePath: string;
      if (platform === 'darwin') {
        chromePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
      } else if (platform === 'win32') {
        chromePath = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
        process.env['PROGRAMFILES'] = 'C:\\Program Files';
      } else {
        chromePath = '/usr/bin/google-chrome-stable';
      }
      mockChromeFound(chromePath);

      const handler = jest.fn();
      detector.on('detected', handler);

      await detector.detect();
      expect(handler).toHaveBeenCalledTimes(1);
      expect(handler).toHaveBeenCalledWith(expect.objectContaining({ found: true }));
    });

    test('message contains no technical jargon when Chrome is found', async () => {
      const platform = os.platform();
      let chromePath: string;
      if (platform === 'darwin') {
        chromePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
      } else if (platform === 'win32') {
        chromePath = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
        process.env['PROGRAMFILES'] = 'C:\\Program Files';
      } else {
        chromePath = '/usr/bin/google-chrome-stable';
      }
      mockChromeFound(chromePath);

      const result = await detector.detect();
      // Should not contain paths or error codes
      expect(result.message).not.toMatch(/\/usr|\.exe|ENOENT|spawn/i);
      expect(result.message.length).toBeGreaterThan(0);
    });
  });

  // -------------------------------------------------------------------------
  // Chrome not found
  // -------------------------------------------------------------------------

  describe('detect() — Chrome missing', () => {
    test('returns found:false when Chrome is not installed', async () => {
      mockChromeNotFound();
      const result = await detector.detect();
      expect(result.found).toBe(false);
      expect(result.path).toBeNull();
    });

    test('emits "not-found" event when Chrome is missing', async () => {
      mockChromeNotFound();

      const handler = jest.fn();
      detector.on('not-found', handler);

      await detector.detect();
      expect(handler).toHaveBeenCalledTimes(1);
      expect(handler).toHaveBeenCalledWith(expect.objectContaining({ found: false }));
    });

    test('user-friendly message mentions installing Chrome with no jargon', async () => {
      mockChromeNotFound();
      const result = await detector.detect();
      // Must mention Chrome and be actionable
      expect(result.message).toMatch(/chrome/i);
      // Must not contain file paths or error codes
      expect(result.message).not.toMatch(/\/usr|\.exe|ENOENT|spawn|execSync/i);
    });

    test('does not emit "detected" when Chrome is missing', async () => {
      mockChromeNotFound();

      const detectedHandler = jest.fn();
      detector.on('detected', detectedHandler);

      await detector.detect();
      expect(detectedHandler).not.toHaveBeenCalled();
    });
  });

  // -------------------------------------------------------------------------
  // retry()
  // -------------------------------------------------------------------------

  describe('retry()', () => {
    test('returns not-found on first call when Chrome is absent', async () => {
      mockChromeNotFound();
      const result = await detector.retry();
      expect(result.found).toBe(false);
    });

    test('returns found after Chrome becomes available', async () => {
      // First call: not found
      mockChromeNotFound();
      const first = await detector.detect();
      expect(first.found).toBe(false);

      // Simulate user installing Chrome — use CHROME_PATH for cross-platform reliability
      const chromePath = os.platform() === 'win32' ? 'C:\\test-chrome.exe' : '/tmp/test-chrome';
      process.env['CHROME_PATH'] = chromePath;
      mockChromeFound(chromePath);

      const second = await detector.retry();
      expect(second.found).toBe(true);
      expect(second.path).toBe(chromePath);
    });

    test('emits "detected" on retry when Chrome is now present', async () => {
      mockChromeNotFound();
      await detector.detect();

      // Use CHROME_PATH for cross-platform reliability
      const chromePath = os.platform() === 'win32' ? 'C:\\test-chrome.exe' : '/tmp/test-chrome';
      process.env['CHROME_PATH'] = chromePath;
      mockChromeFound(chromePath);

      const detectedHandler = jest.fn();
      detector.on('detected', detectedHandler);

      await detector.retry();
      expect(detectedHandler).toHaveBeenCalledTimes(1);
    });
  });

  // -------------------------------------------------------------------------
  // Polling
  // -------------------------------------------------------------------------

  describe('startPolling() / stopPolling()', () => {
    test('isPolling() returns false before polling starts', () => {
      expect(detector.isPolling()).toBe(false);
    });

    test('isPolling() returns true after startPolling()', () => {
      mockChromeNotFound();
      detector.startPolling(10000);
      expect(detector.isPolling()).toBe(true);
    });

    test('isPolling() returns false after stopPolling()', () => {
      mockChromeNotFound();
      detector.startPolling(10000);
      detector.stopPolling();
      expect(detector.isPolling()).toBe(false);
    });

    test('startPolling is idempotent — calling twice does not create duplicate timers', () => {
      mockChromeNotFound();
      detector.startPolling(10000);
      detector.startPolling(10000); // second call should clear first
      expect(detector.isPolling()).toBe(true);
      detector.stopPolling();
      expect(detector.isPolling()).toBe(false);
    });

    test('polling emits "not-found" events periodically', async () => {
      jest.useFakeTimers();
      mockChromeNotFound();

      const handler = jest.fn();
      detector.on('not-found', handler);

      try {
        detector.startPolling(50);
        jest.advanceTimersByTime(100);
        await Promise.resolve();
        detector.stopPolling();

        // Two elapsed intervals should deterministically produce two polls;
        // fake timers avoid CI scheduler starvation on saturated macOS runners.
        expect(handler.mock.calls.length).toBeGreaterThanOrEqual(2);
      } finally {
        detector.stopPolling();
        jest.useRealTimers();
      }
    });

    test('polling emits "detected" when Chrome appears during polling', async () => {
      jest.useFakeTimers();
      // Start with Chrome absent
      mockChromeNotFound();

      const notFoundHandler = jest.fn();
      const detectedHandler = jest.fn();
      detector.on('not-found', notFoundHandler);
      detector.on('detected', detectedHandler);

      try {
        detector.startPolling(50);

        // After first poll, simulate Chrome being installed.
        jest.advanceTimersByTime(50);
        await Promise.resolve();

        const platform = os.platform();
        let chromePath: string;
        if (platform === 'darwin') {
          chromePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
        } else if (platform === 'win32') {
          chromePath = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
          process.env['PROGRAMFILES'] = 'C:\\Program Files';
        } else {
          chromePath = '/usr/bin/google-chrome-stable';
        }
        mockChromeFound(chromePath);

        jest.advanceTimersByTime(50);
        await Promise.resolve();
        detector.stopPolling();

        expect(notFoundHandler).toHaveBeenCalled();
        expect(detectedHandler).toHaveBeenCalled();
      } finally {
        detector.stopPolling();
        jest.useRealTimers();
      }
    });

    test('accepts custom interval via startPolling parameter', async () => {
      jest.useFakeTimers();
      mockChromeNotFound();

      const handler = jest.fn();
      detector.on('not-found', handler);

      try {
        // Use constructor default but override in startPolling.
        detector.startPolling(60);
        jest.advanceTimersByTime(180);
        await Promise.resolve();
        detector.stopPolling();

        // Three elapsed custom intervals should produce at least two polls.
        expect(handler.mock.calls.length).toBeGreaterThanOrEqual(2);
      } finally {
        detector.stopPolling();
        jest.useRealTimers();
      }
    });
  });

  // -------------------------------------------------------------------------
  // CHROME_PATH env override
  // -------------------------------------------------------------------------

  describe('CHROME_PATH environment variable', () => {
    afterEach(() => {
      delete process.env['CHROME_PATH'];
    });

    test('uses CHROME_PATH when set and path exists', async () => {
      const customPath = '/custom/path/to/chrome';
      process.env['CHROME_PATH'] = customPath;
      mockExistsSync.mockImplementation((p: fs.PathLike | number) => String(p) === customPath);

      const result = await detector.detect();
      expect(result.found).toBe(true);
      expect(result.path).toBe(customPath);
    });

    test('ignores CHROME_PATH when path does not exist', async () => {
      process.env['CHROME_PATH'] = '/nonexistent/chrome';
      mockChromeNotFound(); // existsSync returns false for everything

      const result = await detector.detect();
      expect(result.found).toBe(false);
    });
  });
});
