import { describe, expect, it, vi, beforeEach } from 'vitest';
import { execFile } from 'child_process';

import {
  ExternalCliBackend,
  ClaudeCodeBackend,
  OpenCodeBackend,
  CodexBackend,
  GeminiBackend,
} from './external.js';

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

const mockExecFile = vi.mocked(execFile);

describe('ExternalCliBackend', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('uses inferred -p args for codex backend', async () => {
    mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => {
      if (typeof callback === 'function') {
        callback(null, 'ok', '');
      }
      return {} as never;
    });

    const backend = new CodexBackend('/usr/bin/codex', ['run']);
    const result = await backend.process({ prompt: 'hello', history: [] });

    expect(result).toBe('ok');
    expect(mockExecFile).toHaveBeenCalledWith(
      '/usr/bin/codex',
      ['run', '-p', 'USER: hello'],
      expect.any(Object),
      expect.any(Function),
    );
  });

  it('uses inferred --print args for claude_code backend', async () => {
    mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => {
      if (typeof callback === 'function') {
        callback(null, 'ok', '');
      }
      return {} as never;
    });

    const backend = new ClaudeCodeBackend('/usr/bin/claude', []);
    await backend.process({ prompt: 'hello', history: [] });

    expect(mockExecFile).toHaveBeenCalledWith(
      '/usr/bin/claude',
      ['--print', 'USER: hello'],
      expect.any(Object),
      expect.any(Function),
    );
  });

  it('uses inferred run args for opencode backend', async () => {
    mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => {
      if (typeof callback === 'function') {
        callback(null, 'ok', '');
      }
      return {} as never;
    });

    const backend = new OpenCodeBackend('/usr/bin/opencode', []);
    await backend.process({ prompt: 'hello', history: [] });

    expect(mockExecFile).toHaveBeenCalledWith(
      '/usr/bin/opencode',
      ['run', '--format', 'default', 'USER: hello'],
      expect.any(Object),
      expect.any(Function),
    );
  });

  it('supports {prompt} substitution in configured args', async () => {
    mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => {
      if (typeof callback === 'function') {
        callback(null, 'ok', '');
      }
      return {} as never;
    });

    const backend = new ExternalCliBackend({
      name: 'opencode',
      command: 'opencode',
      args: ['--message', '{prompt}'],
    });
    await backend.process({
      prompt: 'new question',
      history: [{ role: 'assistant', content: 'previous reply' }],
    });

    expect(mockExecFile).toHaveBeenCalledWith(
      'opencode',
      ['--message', 'ASSISTANT: previous reply\n\nUSER: new question'],
      expect.any(Object),
      expect.any(Function),
    );
  });

  it('throws when backend returns no output', async () => {
    mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => {
      if (typeof callback === 'function') {
        callback(null, '   ', '');
      }
      return {} as never;
    });

    const backend = new GeminiBackend('/usr/bin/gemini', []);
    await expect(backend.process({ prompt: 'hello', history: [] }))
      .rejects.toThrow('returned no output');
  });

  it('includes stdout details when backend process fails', async () => {
    mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => {
      if (typeof callback === 'function') {
        callback(new Error('Command failed: opencode'), 'default agent "code-planner" is a subagent', '');
      }
      return {} as never;
    });

    const backend = new OpenCodeBackend('/usr/bin/opencode', []);
    await expect(backend.process({ prompt: 'hello', history: [] }))
      .rejects.toThrow('default agent "code-planner" is a subagent');
  });

  it('constructs default commands for opencode and gemini backends', () => {
    const opencode = new OpenCodeBackend();
    const gemini = new GeminiBackend();
    expect(opencode.name).toBe('opencode');
    expect(gemini.name).toBe('gemini');
  });
});
