// Tests for ElectrobunAdapter — verifies RPC call names match pty-rpc-schema.ts

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ElectrobunAdapter } from '../ElectrobunAdapter';
import type { BackendAdapter } from '@agor/types';

// Mock RPC handle matching the interface in ElectrobunAdapter
function createMockRpc() {
  const request: Record<string, ReturnType<typeof vi.fn>> = {};
  const listeners: Array<{ event: string; handler: (payload: unknown) => void }> = [];

  return {
    request: new Proxy(request, {
      get: (_target, prop: string) => {
        if (!request[prop]) request[prop] = vi.fn().mockResolvedValue({ ok: true });
        return request[prop];
      },
    }),
    addMessageListener: vi.fn((event: string, handler: (payload: unknown) => void) => {
      listeners.push({ event, handler });
    }),
    removeMessageListener: vi.fn(),
    _listeners: listeners,
    _calls: request,
  };
}

let adapter: ElectrobunAdapter;
let mockRpc: ReturnType<typeof createMockRpc>;

beforeEach(() => {
  adapter = new ElectrobunAdapter();
  mockRpc = createMockRpc();
  adapter.setRpc(mockRpc as any);
});

describe('ElectrobunAdapter', () => {
  // ── Interface compliance ──────────────────────────────────────────────

  it('implements BackendAdapter interface', () => {
    const ba: BackendAdapter = adapter;
    expect(ba.getSetting).toBeTypeOf('function');
    expect(ba.setSetting).toBeTypeOf('function');
    expect(ba.startAgent).toBeTypeOf('function');
    expect(ba.stopAgent).toBeTypeOf('function');
    expect(ba.createPty).toBeTypeOf('function');
    expect(ba.btmsgSend).toBeTypeOf('function');
    expect(ba.bttaskList).toBeTypeOf('function');
    expect(ba.memoraAvailable).toBeTypeOf('function');
  });

  it('has correct capabilities (no OS features)', () => {
    expect(adapter.capabilities.supportsPtyMultiplexing).toBe(true);
    expect(adapter.capabilities.supportsNativeMenus).toBe(false);
    expect(adapter.capabilities.supportsOsKeychain).toBe(false);
    expect(adapter.capabilities.supportsDesktopNotifications).toBe(false);
    expect(adapter.capabilities.supportsTelemetry).toBe(false);
  });

  it('throws when RPC not initialized', () => {
    const fresh = new ElectrobunAdapter();
    expect(() => fresh.getSetting('x')).rejects.toThrow('RPC not initialized');
  });

  // ── Settings (matches pty-rpc-schema "settings.*") ────────────────────

  describe('settings', () => {
    it('getSetting calls settings.get', async () => {
      mockRpc.request['settings.get'].mockResolvedValue({ value: 'mocha' });
      const result = await adapter.getSetting('theme');
      expect(mockRpc.request['settings.get']).toHaveBeenCalledWith({ key: 'theme' });
      expect(result).toBe('mocha');
    });

    it('setSetting calls settings.set', async () => {
      await adapter.setSetting('theme', 'mocha');
      expect(mockRpc.request['settings.set']).toHaveBeenCalledWith({ key: 'theme', value: 'mocha' });
    });

    it('getAllSettings calls settings.getAll', async () => {
      mockRpc.request['settings.getAll'].mockResolvedValue({
        settings: { theme: 'mocha', shell: '/bin/bash' },
      });
      const result = await adapter.getAllSettings();
      expect(result).toEqual({ theme: 'mocha', shell: '/bin/bash' });
    });
  });

  // ── PTY (matches pty-rpc-schema "pty.*") ──────────────────────────────

  describe('pty', () => {
    it('createPty calls pty.create', async () => {
      mockRpc.request['pty.create'].mockResolvedValue({ ok: true });
      const id = await adapter.createPty({ sessionId: 's1', cols: 80, rows: 24 });
      expect(mockRpc.request['pty.create']).toHaveBeenCalledWith(
        expect.objectContaining({ sessionId: 's1', cols: 80, rows: 24 }),
      );
      expect(id).toBe('s1');
    });

    it('writePty calls pty.write', async () => {
      await adapter.writePty('s1', 'hello');
      expect(mockRpc.request['pty.write']).toHaveBeenCalledWith({ sessionId: 's1', data: 'hello' });
    });

    it('resizePty calls pty.resize', async () => {
      await adapter.resizePty('s1', 120, 40);
      expect(mockRpc.request['pty.resize']).toHaveBeenCalledWith({ sessionId: 's1', cols: 120, rows: 40 });
    });

    it('closePty calls pty.close', async () => {
      await adapter.closePty('s1');
      expect(mockRpc.request['pty.close']).toHaveBeenCalledWith({ sessionId: 's1' });
    });
  });

  // ── Agent (matches pty-rpc-schema "agent.*") ──────────────────────────

  describe('agent', () => {
    it('startAgent calls agent.start', async () => {
      const result = await adapter.startAgent({
        sessionId: 's1',
        prompt: 'hello',
        provider: 'claude',
        cwd: '/tmp',
      });
      expect(mockRpc.request['agent.start']).toHaveBeenCalledWith(
        expect.objectContaining({ sessionId: 's1', prompt: 'hello', provider: 'claude' }),
      );
      expect(result).toEqual({ ok: true });
    });

    it('stopAgent calls agent.stop', async () => {
      const result = await adapter.stopAgent('s1');
      expect(mockRpc.request['agent.stop']).toHaveBeenCalledWith({ sessionId: 's1' });
      expect(result).toEqual({ ok: true });
    });

    it('sendPrompt calls agent.prompt', async () => {
      const result = await adapter.sendPrompt('s1', 'do something');
      expect(mockRpc.request['agent.prompt']).toHaveBeenCalledWith({
        sessionId: 's1',
        prompt: 'do something',
      });
      expect(result).toEqual({ ok: true });
    });
  });

  // ── Files (matches pty-rpc-schema "files.*") ──────────────────────────

  describe('files', () => {
    it('listDirectory calls files.list', async () => {
      mockRpc.request['files.list'].mockResolvedValue({
        entries: [{ name: 'src', type: 'dir', size: 0 }],
      });
      const result = await adapter.listDirectory('/project');
      expect(mockRpc.request['files.list']).toHaveBeenCalledWith({ path: '/project' });
      expect(result).toEqual([{ name: 'src', path: '/project/src', isDir: true, size: 0 }]);
    });

    it('readFile calls files.read', async () => {
      mockRpc.request['files.read'].mockResolvedValue({
        content: 'hello', encoding: 'utf8', size: 5,
      });
      const result = await adapter.readFile('/tmp/file.ts');
      expect(result).toEqual({ type: 'Text', content: 'hello' });
    });

    it('writeFile calls files.write', async () => {
      await adapter.writeFile('/tmp/file.ts', 'content');
      expect(mockRpc.request['files.write']).toHaveBeenCalledWith({
        path: '/tmp/file.ts',
        content: 'content',
      });
    });
  });

  // ── Event listeners ───────────────────────────────────────────────────

  describe('events', () => {
    it('onSidecarMessage subscribes to agent.sidecar', () => {
      const callback = vi.fn();
      adapter.onSidecarMessage(callback);
      expect(mockRpc.addMessageListener).toHaveBeenCalledWith('agent.sidecar', expect.any(Function));
    });

    it('onPtyData filters by session ID', () => {
      const callback = vi.fn();
      adapter.onPtyData('pty-1', callback);
      expect(mockRpc.addMessageListener).toHaveBeenCalledWith('pty.output', expect.any(Function));

      // Simulate event
      const handler = mockRpc.addMessageListener.mock.calls[0][1];
      handler({ sessionId: 'pty-1', data: 'hello' });
      expect(callback).toHaveBeenCalledWith('hello');

      // Different session — should not trigger
      callback.mockClear();
      handler({ sessionId: 'pty-2', data: 'world' });
      expect(callback).not.toHaveBeenCalled();
    });

    it('unsubscribe removes listener', () => {
      const unsub = adapter.onSidecarMessage(vi.fn());
      unsub();
      expect(mockRpc.removeMessageListener).toHaveBeenCalled();
    });
  });

  // ── Stub methods return safe defaults ─────────────────────────────────

  describe('stub methods', () => {
    it('btmsg stubs return empty arrays/strings', async () => {
      expect(await adapter.btmsgGetAgents('g1' as any)).toEqual([]);
      expect(await adapter.btmsgSend('a1' as any, 'a2' as any, 'hi')).toBe('');
      expect(await adapter.btmsgGetChannels('g1' as any)).toEqual([]);
    });

    it('bttask stubs return empty/zero', async () => {
      expect(await adapter.bttaskList('g1' as any)).toEqual([]);
      expect(await adapter.bttaskReviewQueueCount('g1' as any)).toBe(0);
    });

    it('memora stubs return empty/false', async () => {
      expect(await adapter.memoraAvailable()).toBe(false);
      expect(await adapter.memoraList()).toEqual({ nodes: [], total: 0 });
    });

    it('secrets stubs return null/empty', async () => {
      expect(await adapter.getSecret('x')).toBeNull();
      expect(await adapter.hasKeyring()).toBe(false);
    });
  });

  // ── Cleanup ───────────────────────────────────────────────────────────

  describe('destroy', () => {
    it('removes all registered listeners', async () => {
      adapter.onSidecarMessage(vi.fn());
      adapter.onPtyData('p1', vi.fn());
      await adapter.destroy();
      // removeMessageListener should have been called for each listener
      expect(mockRpc.removeMessageListener.mock.calls.length).toBeGreaterThanOrEqual(2);
    });
  });
});
