import { describe, it, expect, vi } from 'vitest';
import { PassThrough } from 'node:stream';
import { PersistentStdioClient } from '../src/services/transport/persistent-stdio.js';
import type { InteractiveExec, McpOrchestrator } from '../src/services/orchestrator.js';

function makeFakeExec(): {
  iexec: InteractiveExec;
  written: string[];
  emit: (line: unknown) => void;
} {
  const stdout = new PassThrough();
  const written: string[] = [];
  const iexec: InteractiveExec = {
    stdout,
    write(data) { written.push(data); },
    close() { stdout.destroy(); },
  };
  const emit = (msg: unknown) => {
    stdout.write(JSON.stringify(msg) + '\n');
  };
  return { iexec, written, emit };
}

function makeOrchestrator(overrides: Partial<McpOrchestrator> = {}): McpOrchestrator {
  return {
    pullImage: vi.fn(),
    createContainer: vi.fn(),
    stopContainer: vi.fn(),
    removeContainer: vi.fn(),
    inspectContainer: vi.fn(),
    getContainerLogs: vi.fn(),
    execInContainer: vi.fn(),
    ping: vi.fn(),
    ...overrides,
  } as McpOrchestrator;
}

describe('PersistentStdioClient', () => {
  it('exec mode calls execInteractive with the command', async () => {
    const fake = makeFakeExec();
    const execInteractive = vi.fn(async () => fake.iexec);
    const orch = makeOrchestrator({ execInteractive });

    const client = new PersistentStdioClient(
      orch,
      'container-1',
      { kind: 'exec', command: ['node', 'index.js'] },
    );

    // Drive the handshake: respond to the first init request (id=1)
    // then to the subsequent tools/list request (id=2).
    const sendPromise = client.send('tools/list');
    await new Promise((r) => setTimeout(r, 10));

    const init = JSON.parse(fake.written[0]!);
    expect(init.method).toBe('initialize');
    fake.emit({ jsonrpc: '2.0', id: init.id, result: { capabilities: {} } });
    await new Promise((r) => setTimeout(r, 150));

    // Second written msg is notifications/initialized; third is tools/list
    const toolsReq = JSON.parse(fake.written[2]!);
    expect(toolsReq.method).toBe('tools/list');
    fake.emit({ jsonrpc: '2.0', id: toolsReq.id, result: { tools: [] } });

    const res = await sendPromise;
    expect(res.result).toEqual({ tools: [] });
    expect(execInteractive).toHaveBeenCalledWith('container-1', ['node', 'index.js']);
    client.close();
  });

  it('attach mode calls attachInteractive and never execInteractive', async () => {
    const fake = makeFakeExec();
    const attachInteractive = vi.fn(async () => fake.iexec);
    const execInteractive = vi.fn();
    const orch = makeOrchestrator({ attachInteractive, execInteractive });

    const client = new PersistentStdioClient(
      orch,
      'container-gitea',
      { kind: 'attach' },
    );

    const sendPromise = client.send('tools/list');
    await new Promise((r) => setTimeout(r, 10));

    const init = JSON.parse(fake.written[0]!);
    fake.emit({ jsonrpc: '2.0', id: init.id, result: { capabilities: {} } });
    await new Promise((r) => setTimeout(r, 150));

    const req = JSON.parse(fake.written[2]!);
    fake.emit({ jsonrpc: '2.0', id: req.id, result: { tools: [{ name: 'list_repos' }] } });

    const res = await sendPromise;
    expect((res.result as { tools: unknown[] }).tools).toHaveLength(1);
    expect(attachInteractive).toHaveBeenCalledWith('container-gitea');
    expect(execInteractive).not.toHaveBeenCalled();
    client.close();
  });

  it('attach mode throws if orchestrator does not support attach', async () => {
    const orch = makeOrchestrator({}); // no attachInteractive
    const client = new PersistentStdioClient(orch, 'c', { kind: 'attach' });
    await expect(client.send('tools/list')).rejects.toThrow(/attach/i);
  });

  it('exec mode throws if orchestrator does not support execInteractive', async () => {
    const orch = makeOrchestrator({}); // no execInteractive
    const client = new PersistentStdioClient(orch, 'c', { kind: 'exec', command: ['x'] });
    await expect(client.send('tools/list')).rejects.toThrow(/interactive exec/i);
  });
});
