// Tests for web/src/tinygrad.js — tinygrad server lifecycle manager

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

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

// Mock execSync
const mockExecSync = jest.spyOn(cp, 'execSync');

// Mock child_process.spawn
const mockSpawn = jest.spyOn(cp, 'spawn');

// Mock process.kill so tinygrad.js's _isAlive (process.kill(pid, 0)) reports
// the fake PID as alive, and stop()'s SIGTERM is a no-op rather than killing
// the test runner. The previous "use real PID" approach worked for liveness
// but caused stop() to actually try to terminate jest.
jest.spyOn(process, 'kill').mockImplementation((_pid, _signal) => true);

// Mock gpu-detect module
jest.mock('../src/gpu-detect', () => {
  const emitter = new (require('events').EventEmitter)();
  emitter.getState = () => ({ available: false, vendor: null });
  return { gpuDetector: emitter, detect: () => ({ available: false }) };
});

// Mock http.request for health checks
jest.spyOn(http, 'request');

const tinygrad = require('../src/tinygrad');
const { gpuDetector } = require('../src/gpu-detect');

const PRE_DIR = path.join(mockHome, '.pre');
const LOG_FILE = path.join(PRE_DIR, 'tinygrad-server.log');
const MODEL_PATH = path.join(mockHome, 'test-model.safetensors');

beforeAll(() => {
  fs.mkdirSync(PRE_DIR, { recursive: true });
  // Create a dummy model file
  fs.writeFileSync(MODEL_PATH, 'dummy model data');
});

afterAll(() => {
  tinygrad.shutdown();
  // Windows may hold file locks briefly after shutdown — retry cleanup
  try { fs.rmSync(mockHome, { recursive: true, force: true }); } catch {}
});

beforeEach(() => {
  mockExecSync.mockReset();
  mockSpawn.mockReset();
  http.request.mockReset();
  tinygrad.shutdown(); // Ensure clean state
});

// ── Python detection ─────────────────────────────────────────────────────

describe('getPythonPath', () => {
  test('finds Python 3.12', () => {
    mockExecSync.mockImplementation((cmd) => {
      if (cmd.includes('--version')) return 'Python 3.12.4';
      if (cmd.includes('which') || cmd.includes('where')) return '/usr/bin/python3';
      return '';
    });
    expect(tinygrad.getPythonPath()).toBe('/usr/bin/python3');
  });

  test('rejects Python 3.10 (too old)', () => {
    mockExecSync.mockImplementation((cmd) => {
      if (cmd.includes('--version')) return 'Python 3.10.2';
      throw new Error('not found');
    });
    expect(tinygrad.getPythonPath()).toBeNull();
  });

  test('returns null when no Python found', () => {
    mockExecSync.mockImplementation(() => { throw new Error('not found'); });
    expect(tinygrad.getPythonPath()).toBeNull();
  });
});

// ── tinygrad installation check ──────────────────────────────────────────

describe('isTinygradInstalled', () => {
  test('returns true when tinygrad importable', () => {
    mockExecSync.mockImplementation((cmd) => {
      if (cmd.includes('--version')) return 'Python 3.12.4';
      if (cmd.includes('which') || cmd.includes('where')) return '/usr/bin/python3';
      if (cmd.includes('import tinygrad')) return '';
      return '';
    });
    expect(tinygrad.isTinygradInstalled()).toBe(true);
  });

  test('returns false when import fails', () => {
    mockExecSync.mockImplementation((cmd) => {
      if (cmd.includes('--version')) return 'Python 3.12.4';
      if (cmd.includes('which') || cmd.includes('where')) return '/usr/bin/python3';
      if (cmd.includes('import tinygrad')) throw new Error('ModuleNotFoundError');
      return '';
    });
    expect(tinygrad.isTinygradInstalled()).toBe(false);
  });

  test('returns false when no Python', () => {
    mockExecSync.mockImplementation(() => { throw new Error('not found'); });
    expect(tinygrad.isTinygradInstalled()).toBe(false);
  });
});

// ── Server start ─────────────────────────────────────────────────────────

describe('start', () => {
  test('throws if model_path is missing', async () => {
    mockExecSync.mockImplementation((cmd) => {
      if (cmd.includes('--version')) return 'Python 3.12.4';
      if (cmd.includes('which') || cmd.includes('where')) return '/usr/bin/python3';
      return '';
    });
    await expect(tinygrad.start({ port: 8091 }))
      .rejects.toThrow('model_path is required');
  });

  test('throws if model file does not exist', async () => {
    mockExecSync.mockImplementation((cmd) => {
      if (cmd.includes('--version')) return 'Python 3.12.4';
      if (cmd.includes('which') || cmd.includes('where')) return '/usr/bin/python3';
      return '';
    });
    await expect(tinygrad.start({ model_path: '/nonexistent/model.safetensors' }))
      .rejects.toThrow('Model file not found');
  });

  test('throws if Python not found', async () => {
    mockExecSync.mockImplementation(() => { throw new Error('not found'); });
    await expect(tinygrad.start({ model_path: MODEL_PATH }))
      .rejects.toThrow('Python 3.11+ not found');
  });

  test('spawns process with correct args', async () => {
    mockExecSync.mockImplementation((cmd) => {
      if (cmd.includes('--version')) return 'Python 3.12.4';
      if (cmd.includes('which') || cmd.includes('where')) return '/usr/bin/python3';
      return '';
    });

    // Mock spawn to return a fake process
    const fakeProc = new EventEmitter();
    fakeProc.pid = 12345;
    fakeProc.stdout = new EventEmitter();
    fakeProc.stdout.pipe = jest.fn();
    fakeProc.stderr = new EventEmitter();
    fakeProc.stderr.pipe = jest.fn();
    fakeProc.kill = jest.fn();
    fakeProc.unref = jest.fn();
    mockSpawn.mockReturnValue(fakeProc);

    // Mock health check to succeed immediately
    http.request.mockImplementation((_opts, cb) => {
      const res = new EventEmitter();
      res.statusCode = 200;
      process.nextTick(() => {
        cb(res);
        res.emit('data', '{"data":[]}');
        res.emit('end');
      });
      const req = new EventEmitter();
      req.end = jest.fn();
      return req;
    });

    const result = await tinygrad.start({
      model_path: MODEL_PATH,
      port: 8091,
      context_size: 4096,
      startup_timeout: 5000,
    });

    expect(result.pid).toBe(12345);
    expect(result.port).toBe(8091);

    // Check spawn was called with correct args (direct mode, no bridge)
    expect(mockSpawn).toHaveBeenCalledTimes(1);
    const [binary, args, opts] = mockSpawn.mock.calls[0];
    expect(binary).toBe('/usr/bin/python3');
    expect(args[0]).toContain('tinygrad-server.py');
    expect(args).toContain('--model');
    expect(args).toContain('--port');
    expect(args).toContain('8091');
    expect(args).toContain('--max-context');
    expect(args).toContain('4096');

    // Clean up
    tinygrad.stop();
  });

  test('second start() adopts the running server instead of re-spawning', async () => {
    mockExecSync.mockImplementation((cmd) => {
      if (cmd.includes('--version')) return 'Python 3.12.4';
      if (cmd.includes('which') || cmd.includes('where')) return '/usr/bin/python3';
      return '';
    });
    const fakeProc = new EventEmitter();
    fakeProc.pid = 12345;
    fakeProc.stdout = new EventEmitter();
    fakeProc.stdout.pipe = jest.fn();
    fakeProc.stderr = new EventEmitter();
    fakeProc.stderr.pipe = jest.fn();
    fakeProc.kill = jest.fn();
    fakeProc.unref = jest.fn();
    mockSpawn.mockReturnValue(fakeProc);

    http.request.mockImplementation((_opts, cb) => {
      const res = new EventEmitter();
      res.statusCode = 200;
      process.nextTick(() => { cb(res); res.emit('data', '{}'); res.emit('end'); });
      const req = new EventEmitter();
      req.end = jest.fn();
      return req;
    });

    const first = await tinygrad.start({ model_path: MODEL_PATH, startup_timeout: 5000 });
    expect(first.pid).toBe(12345);
    expect(first.adopted).toBeUndefined(); // fresh spawn

    const second = await tinygrad.start({ model_path: MODEL_PATH });
    expect(second.pid).toBe(12345);
    expect(second.adopted).toBe(true); // adopted, not re-spawned
    // spawn must NOT have been called a second time — the whole point of adoption
    expect(mockSpawn).toHaveBeenCalledTimes(1);

    tinygrad.stop();
  });
});

// ── Server stop ──────────────────────────────────────────────────────────

describe('stop', () => {
  test('does nothing when no server running', () => {
    expect(() => tinygrad.stop()).not.toThrow();
  });
});

// ── Server status ────────────────────────────────────────────────────────

describe('getStatus', () => {
  test('returns not-running status when no server', async () => {
    mockExecSync.mockImplementation(() => { throw new Error('not found'); });
    const status = await tinygrad.getStatus();
    expect(status.running).toBe(false);
    expect(status.pid).toBeNull();
    expect(status.port).toBe(8091);
    expect(status.gpu).toEqual({ available: false, vendor: null });
  });
});

// ── Health check ─────────────────────────────────────────────────────────

describe('_healthPing', () => {
  test('returns true on 200 response', async () => {
    http.request.mockImplementation((_opts, cb) => {
      const res = new EventEmitter();
      res.statusCode = 200;
      process.nextTick(() => { cb(res); res.emit('data', '{}'); res.emit('end'); });
      const req = new EventEmitter();
      req.end = jest.fn();
      return req;
    });
    expect(await tinygrad._healthPing(8091)).toBe(true);
  });

  test('returns false on connection error', async () => {
    http.request.mockImplementation(() => {
      const req = new EventEmitter();
      req.end = jest.fn();
      process.nextTick(() => req.emit('error', new Error('ECONNREFUSED')));
      return req;
    });
    expect(await tinygrad._healthPing(8091)).toBe(false);
  });
});

// ── Auto-start integration ───────────────────────────────────────────────

describe('enableAutoStart', () => {
  test('auto-starts server when GPU becomes available', (done) => {
    mockExecSync.mockImplementation((cmd) => {
      if (cmd.includes('--version')) return 'Python 3.12.4';
      if (cmd.includes('which') || cmd.includes('where')) return '/usr/bin/python3';
      return '';
    });

    const fakeProc = new EventEmitter();
    fakeProc.pid = 99999;
    fakeProc.stdout = new EventEmitter();
    fakeProc.stdout.pipe = jest.fn();
    fakeProc.stderr = new EventEmitter();
    fakeProc.stderr.pipe = jest.fn();
    fakeProc.kill = jest.fn();
    fakeProc.unref = jest.fn();
    mockSpawn.mockReturnValue(fakeProc);

    http.request.mockImplementation((_opts, cb) => {
      const res = new EventEmitter();
      res.statusCode = 200;
      process.nextTick(() => { cb(res); res.emit('data', '{}'); res.emit('end'); });
      const req = new EventEmitter();
      req.end = jest.fn();
      return req;
    });

    // Register auto-start with a provider config that has auto_start=true
    tinygrad.enableAutoStart(() => ({
      type: 'tinygrad',
      model_path: MODEL_PATH,
      port: 8091,
      auto_start: true,
      startup_timeout: 5000,
    }));

    // Simulate GPU becoming available
    gpuDetector.emit('gpu-available', { available: true, name: 'RTX 4090', vram_gb: 24 });

    // Give async start time to complete
    setTimeout(() => {
      expect(mockSpawn).toHaveBeenCalled();
      tinygrad.shutdown();
      done();
    }, 100);
  });

  test('does not auto-start when auto_start is false', (done) => {
    tinygrad.enableAutoStart(() => ({
      type: 'tinygrad',
      model_path: MODEL_PATH,
      port: 8091,
      auto_start: false,
    }));

    gpuDetector.emit('gpu-available', { available: true, name: 'RTX 4090' });

    setTimeout(() => {
      expect(mockSpawn).not.toHaveBeenCalled();
      tinygrad.shutdown();
      done();
    }, 100);
  });

  test('does not auto-start when provider is not tinygrad', (done) => {
    tinygrad.enableAutoStart(() => ({
      type: 'ollama',
    }));

    gpuDetector.emit('gpu-available', { available: true, name: 'RTX 4090' });

    setTimeout(() => {
      expect(mockSpawn).not.toHaveBeenCalled();
      tinygrad.shutdown();
      done();
    }, 100);
  });
});
