import { describe, it, expect, vi, beforeEach } from 'vitest';
import { DockerSandbox, type DockerSandboxConfig } from './docker.js';
import * as childProcess from 'child_process';

// Mock child_process.execFile
vi.mock('child_process', () => ({
  execFile: vi.fn(),
}));

const mockedExecFile = vi.mocked(childProcess.execFile);

function mockExecFileSuccess(stdout = '', stderr = '') {
  mockedExecFile.mockImplementation(
    (_cmd: unknown, _args: unknown, _opts: unknown, callback: unknown) => {
      (callback as (err: null, stdout: string, stderr: string) => void)(null, stdout, stderr);
      return {} as ReturnType<typeof childProcess.execFile>;
    },
  );
}

function mockExecFileError(message: string) {
  mockedExecFile.mockImplementation(
    (_cmd: unknown, _args: unknown, _opts: unknown, callback: unknown) => {
      (callback as (err: Error) => void)(new Error(message));
      return {} as ReturnType<typeof childProcess.execFile>;
    },
  );
}

describe('DockerSandbox', () => {
  const defaultConfig: DockerSandboxConfig = {
    sessionId: 'test-session',
    image: 'node:22-slim',
    workspaceDir: '/workspace',
    network: 'none',
    memoryLimit: '512m',
    cpuLimit: '1.0',
    timeoutSeconds: 300,
  };

  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('create()', () => {
    it('creates a docker container with correct args', async () => {
      mockExecFileSuccess('container-abc123');
      const sandbox = new DockerSandbox(defaultConfig);
      await sandbox.create();

      expect(mockedExecFile).toHaveBeenCalledWith(
        'docker',
        expect.arrayContaining([
          'create',
          '--name', expect.stringContaining('flynn-test-session'),
          '--memory', '512m',
          '--cpus', '1.0',
          '--network', 'none',
          '-v', expect.stringContaining(':/workspace'),
          'node:22-slim',
          'sleep', 'infinity',
        ]),
        expect.any(Object),
        expect.any(Function),
      );
      expect(sandbox.containerId).toBe('container-abc123');
    });

    it('starts the container after creating', async () => {
      mockExecFileSuccess('container-abc123');
      const sandbox = new DockerSandbox(defaultConfig);
      await sandbox.create();

      // Second call should be docker start
      expect(mockedExecFile).toHaveBeenCalledTimes(2);
      expect(mockedExecFile).toHaveBeenNthCalledWith(
        2, 'docker', ['start', 'container-abc123'],
        expect.any(Object), expect.any(Function),
      );
    });

    it('throws if docker create fails', async () => {
      mockExecFileError('docker not found');
      const sandbox = new DockerSandbox(defaultConfig);
      await expect(sandbox.create()).rejects.toThrow('docker not found');
    });
  });

  describe('exec()', () => {
    it('runs command inside container', async () => {
      const sandbox = new DockerSandbox(defaultConfig);
      // Manually set container ID to skip create
      (sandbox as unknown as { _containerId: string })._containerId = 'container-abc';

      mockExecFileSuccess('hello world\n');
      const result = await sandbox.exec('echo hello world');

      expect(mockedExecFile).toHaveBeenCalledWith(
        'docker',
        ['exec', 'container-abc', 'bash', '-c', 'echo hello world'],
        expect.objectContaining({ timeout: expect.any(Number) }),
        expect.any(Function),
      );
      expect(result).toEqual({ stdout: 'hello world\n', stderr: '' });
    });

    it('passes cwd as workdir option', async () => {
      const sandbox = new DockerSandbox(defaultConfig);
      (sandbox as unknown as { _containerId: string })._containerId = 'container-abc';

      mockExecFileSuccess('');
      await sandbox.exec('ls', { cwd: '/workspace/project' });

      expect(mockedExecFile).toHaveBeenCalledWith(
        'docker',
        ['exec', '-w', '/workspace/project', 'container-abc', 'bash', '-c', 'ls'],
        expect.any(Object),
        expect.any(Function),
      );
    });

    it('throws if no container created', async () => {
      const sandbox = new DockerSandbox(defaultConfig);
      await expect(sandbox.exec('echo hi')).rejects.toThrow('not created');
    });
  });

  describe('destroy()', () => {
    it('force-removes the container', async () => {
      const sandbox = new DockerSandbox(defaultConfig);
      (sandbox as unknown as { _containerId: string })._containerId = 'container-abc';

      mockExecFileSuccess();
      await sandbox.destroy();

      expect(mockedExecFile).toHaveBeenCalledWith(
        'docker', ['rm', '-f', 'container-abc'],
        expect.any(Object), expect.any(Function),
      );
    });

    it('does nothing if no container', async () => {
      const sandbox = new DockerSandbox(defaultConfig);
      await sandbox.destroy(); // should not throw
      expect(mockedExecFile).not.toHaveBeenCalled();
    });
  });

  describe('isAvailable()', () => {
    it('returns true when docker is installed', async () => {
      mockExecFileSuccess('Docker version 27.0.0');
      const result = await DockerSandbox.isAvailable();
      expect(result).toBe(true);
    });

    it('returns false when docker is not installed', async () => {
      mockExecFileError('command not found');
      const result = await DockerSandbox.isAvailable();
      expect(result).toBe(false);
    });
  });
});
