// Tests for web/src/gpu-detect.js — GPU hot-plug detection module

const path = require('path');
const fs = require('fs');
const os = require('os');
const cp = require('child_process');

// Mock os.homedir for state persistence tests
const mockHome = path.join(os.tmpdir(), `pre-gpu-test-${process.pid}`);
jest.spyOn(os, 'homedir').mockReturnValue(mockHome);

// Mock execSync at module level — keep reference to the spy
const mockExecSync = jest.spyOn(cp, 'execSync');

const gpuMod = require('../src/gpu-detect');
const {
  detect,
  GpuDetector,
  _detectTinyGpuDriver,
  _detectTinyGpuApp,
  _detectThunderboltGpu,
  _detectViaDocker,
  _detectNvidiaSmi,
  _saveState,
  _loadState,
} = gpuMod;

const STATE_FILE = path.join(mockHome, '.pre', 'gpu-state.json');

beforeAll(() => {
  fs.mkdirSync(path.join(mockHome, '.pre'), { recursive: true });
});

afterAll(() => {
  fs.rmSync(mockHome, { recursive: true, force: true });
});

beforeEach(() => {
  mockExecSync.mockReset();
  try { fs.unlinkSync(STATE_FILE); } catch {}
});

// ── Platform-specific detection tests ────────────────────────────────────
// These test the individual detection functions on the CURRENT platform.
// macOS-only functions return false/null on non-macOS platforms.

describe('TinyGPU driver detection', () => {
  test('returns boolean (platform-dependent behavior)', () => {
    mockExecSync.mockImplementation(() => '');
    const result = _detectTinyGpuDriver();
    expect(typeof result).toBe('boolean');
  });

  if (process.platform === 'darwin') {
    test('returns true when systemextensionsctl lists tinygpu (macOS)', () => {
      mockExecSync.mockReturnValue('enabled\tteamID\tcom.tinycorp.TinyGPU (1.0/1)\t[activated enabled]');
      expect(_detectTinyGpuDriver()).toBe(true);
    });

    test('returns false when no tinygpu in systemextensionsctl output (macOS)', () => {
      mockExecSync.mockReturnValue('enabled\tteamID\tcom.apple.driver (1.0)\t[activated enabled]');
      expect(_detectTinyGpuDriver()).toBe(false);
    });
  } else {
    test('returns false on non-macOS platform', () => {
      expect(_detectTinyGpuDriver()).toBe(false);
    });
  }
});

describe('TinyGPU app detection', () => {
  if (process.platform !== 'darwin') {
    test('returns false on non-macOS platform', () => {
      expect(_detectTinyGpuApp()).toBe(false);
    });
  }
  // TinyGPU.app tests only run on macOS (filesystem check)
});

describe('Thunderbolt GPU detection', () => {
  if (process.platform === 'darwin') {
    test('extracts NVIDIA GPU name from system_profiler output', () => {
      mockExecSync.mockReturnValue('Vendor Name: NVIDIA RTX 4090\nSome other line');
      const result = _detectThunderboltGpu();
      expect(result).not.toBeNull();
      expect(result.vendor).toBe('nvidia');
      expect(result.name).toMatch(/RTX 4090/);
    });

    test('detects AMD Radeon', () => {
      mockExecSync.mockReturnValue('Device: Radeon RX 7900 XTX\nMore info');
      const result = _detectThunderboltGpu();
      expect(result).not.toBeNull();
      expect(result.vendor).toBe('amd');
    });

    test('returns null when no GPU on Thunderbolt bus', () => {
      mockExecSync.mockReturnValue('Apple Thunderbolt Bus\nSpeed: 40 Gb/s');
      expect(_detectThunderboltGpu()).toBeNull();
    });
  } else {
    test('returns null on non-macOS platform', () => {
      expect(_detectThunderboltGpu()).toBeNull();
    });
  }
});

describe('nvidia-smi direct detection', () => {
  if (process.platform !== 'darwin') {
    test('parses nvidia-smi CSV output', () => {
      mockExecSync.mockReturnValue('NVIDIA GeForce RTX 4090, 24564 MiB, 550.120');
      const result = _detectNvidiaSmi();
      expect(result).not.toBeNull();
      expect(result.name).toBe('NVIDIA GeForce RTX 4090');
      expect(result.vram_gb).toBe(23); // 24564 / 1024 ≈ 23
      expect(result.driver).toBe('550.120');
    });

    test('returns null when nvidia-smi fails', () => {
      mockExecSync.mockImplementation(() => { throw new Error('not found'); });
      expect(_detectNvidiaSmi()).toBeNull();
    });

    test('handles 16GB VRAM card', () => {
      mockExecSync.mockReturnValue('NVIDIA GeForce RTX 4080, 16384 MiB, 545.23');
      const result = _detectNvidiaSmi();
      expect(result.vram_gb).toBe(16);
    });
  } else {
    // On macOS, nvidia-smi detection is skipped (uses Docker path)
    test('nvidia-smi detection runs on current platform', () => {
      mockExecSync.mockImplementation(() => { throw new Error('not found'); });
      // macOS: the function still runs, just nvidia-smi won't be there
      const result = _detectNvidiaSmi();
      expect(result).toBeNull();
    });
  }
});

describe('Docker VRAM detection', () => {
  if (process.platform === 'darwin') {
    test('parses Docker nvidia-smi output', () => {
      mockExecSync.mockImplementation((cmd) => {
        if (cmd.includes('docker info')) return 'Server Version: 20.10';
        if (cmd.includes('docker run')) return 'NVIDIA RTX 4090, 24564 MiB';
        return '';
      });
      const result = _detectViaDocker();
      expect(result).not.toBeNull();
      expect(result.name).toBe('NVIDIA RTX 4090');
      expect(result.vram_gb).toBe(23);
    });

    test('returns null when Docker not running', () => {
      mockExecSync.mockImplementation(() => { throw new Error('docker not running'); });
      expect(_detectViaDocker()).toBeNull();
    });
  } else {
    test('returns null on non-macOS (uses nvidia-smi directly)', () => {
      expect(_detectViaDocker()).toBeNull();
    });
  }
});

// ── Full detect() pipeline ───────────────────────────────────────────────

describe('detect() full pipeline', () => {
  test('returns state object with required fields', () => {
    mockExecSync.mockImplementation(() => { throw new Error('not found'); });
    const state = detect();
    expect(state).toHaveProperty('available');
    expect(state).toHaveProperty('vendor');
    expect(state).toHaveProperty('name');
    expect(state).toHaveProperty('vram_gb');
    expect(state).toHaveProperty('backend');
    expect(state).toHaveProperty('driver');
    expect(state).toHaveProperty('lastCheck');
    expect(typeof state.lastCheck).toBe('number');
  });

  test('returns available=false when no GPU found', () => {
    mockExecSync.mockImplementation(() => { throw new Error('not found'); });
    const state = detect();
    expect(state.available).toBe(false);
    expect(state.vendor).toBeNull();
    expect(state.name).toBeNull();
  });

  if (process.platform !== 'darwin') {
    test('returns available=true with NVIDIA on Linux/Windows', () => {
      mockExecSync.mockReturnValue('NVIDIA GeForce RTX 4090, 24564 MiB, 550.120');
      const state = detect();
      expect(state.available).toBe(true);
      expect(state.vendor).toBe('nvidia');
      expect(state.backend).toBe('cuda');
      expect(state.driver).toBe('native');
      expect(state.vram_gb).toBe(23);
    });
  }

  test('lastCheck is a recent timestamp', () => {
    mockExecSync.mockImplementation(() => { throw new Error('not found'); });
    const before = Date.now();
    const state = detect();
    expect(state.lastCheck).toBeGreaterThanOrEqual(before);
    expect(state.lastCheck).toBeLessThanOrEqual(Date.now());
  });
});

// ── GpuDetector class ────────────────────────────────────────────────────

describe('GpuDetector EventEmitter', () => {
  let det;

  afterEach(() => {
    if (det) det.stop();
    det = null;
  });

  test('getState returns null before start', () => {
    det = new GpuDetector();
    expect(det.getState()).toBeNull();
  });

  test('getState returns state after start', () => {
    mockExecSync.mockImplementation(() => { throw new Error('not found'); });
    det = new GpuDetector();
    det.start(60000);
    expect(det.getState()).not.toBeNull();
    expect(det.getState().available).toBe(false);
  });

  test('forceCheck runs detection immediately without start', () => {
    mockExecSync.mockImplementation(() => { throw new Error('not found'); });
    det = new GpuDetector();
    const state = det.forceCheck();
    expect(state).not.toBeNull();
    expect(state.available).toBe(false);
  });

  test('stop clears the interval', () => {
    mockExecSync.mockImplementation(() => { throw new Error('not found'); });
    det = new GpuDetector();
    det.start(60000);
    expect(det._interval).not.toBeNull();
    det.stop();
    expect(det._interval).toBeNull();
  });

  if (process.platform !== 'darwin') {
    test('emits gpu-available on first poll when GPU found', (done) => {
      mockExecSync.mockReturnValue('NVIDIA RTX 4090, 24564 MiB, 550.120');
      det = new GpuDetector();
      det.on('gpu-available', (state) => {
        expect(state.available).toBe(true);
        expect(state.name).toBe('NVIDIA RTX 4090');
        done();
      });
      det.start(60000);
    });

    test('emits gpu-unavailable when GPU disappears', (done) => {
      let callCount = 0;
      mockExecSync.mockImplementation(() => {
        callCount++;
        if (callCount <= 1) return 'NVIDIA RTX 4090, 24564 MiB, 550.120';
        throw new Error('not found');
      });
      det = new GpuDetector();
      det.on('gpu-unavailable', (state) => {
        expect(state.available).toBe(false);
        done();
      });
      det.start(50);
    });

    test('emits gpu-changed when GPU specs change', (done) => {
      let callCount = 0;
      mockExecSync.mockImplementation(() => {
        callCount++;
        if (callCount <= 1) return 'NVIDIA RTX 3080, 10240 MiB, 535.129';
        return 'NVIDIA RTX 4090, 24564 MiB, 550.120';
      });
      det = new GpuDetector();
      det.on('gpu-changed', (state) => {
        expect(state.name).toBe('NVIDIA RTX 4090');
        done();
      });
      det.start(50);
    });
  }

  test('does not emit when state stays the same', () => {
    mockExecSync.mockImplementation(() => { throw new Error('not found'); });
    det = new GpuDetector();
    const handler = jest.fn();
    det.on('gpu-available', handler);
    det.on('gpu-unavailable', handler);
    det.on('gpu-changed', handler);
    det.start(60000);
    // Force a second poll with same result
    det._poll();
    // Only one event at most (none if no GPU)
    expect(handler).toHaveBeenCalledTimes(0);
  });
});

// ── State persistence ────────────────────────────────────────────────────

describe('State persistence', () => {
  test('_saveState and _loadState round-trip', () => {
    const state = { available: true, vendor: 'nvidia', name: 'RTX 4090', vram_gb: 24, lastCheck: Date.now() };
    _saveState(state);
    const loaded = _loadState();
    expect(loaded).toEqual(state);
  });

  test('_loadState returns null when file missing', () => {
    try { fs.unlinkSync(STATE_FILE); } catch {}
    expect(_loadState()).toBeNull();
  });

  test('_saveState creates .pre directory if needed', () => {
    const subDir = path.join(mockHome, '.pre');
    // Dir already exists from beforeAll, but saveState should handle it gracefully
    const state = { available: false, vendor: null, lastCheck: Date.now() };
    _saveState(state);
    expect(fs.existsSync(STATE_FILE)).toBe(true);
  });
});
