import { describe, expect, it, vi, beforeEach } from 'vitest';
import { IpcBridge, getIpcBridge, setIpcBridge } from './ipc-bridge.js';

type TestableIpcBridge = IpcBridge & {
  handleLine(line: string): void;
  sendResponse(id: number, result?: unknown, error?: { code: number; message: string }): void;
};

function asTestableBridge(bridge: IpcBridge): TestableIpcBridge {
  return bridge as unknown as TestableIpcBridge;
}

vi.mock('./config.js', () => ({
  config: {
    RUST_RUNTIME_BIN: 'echo',
    RUST_RUNTIME_TIMEOUT_MS: 5_000,
    ANTHROPIC_API_KEY: undefined,
  },
}));

describe('IpcBridge', () => {
  it('constructs with default options', () => {
    const bridge = new IpcBridge();
    expect(bridge.isRunning).toBe(false);
  });

  it('constructs with custom options', () => {
    const bridge = new IpcBridge({
      bin: '/usr/bin/custom-binary',
      args: ['--extra'],
      timeoutMs: 1000,
      env: { CUSTOM: 'value' },
    });
    expect(bridge.isRunning).toBe(false);
  });

  it('throws when calling call() before start()', async () => {
    const bridge = new IpcBridge();
    await expect(bridge.call('test', {})).rejects.toThrow('IPC bridge not started');
  });

  it('throws on double start()', async () => {
    const bridge = new IpcBridge({ bin: 'cat', timeoutMs: 500 });
    // Start will spawn but initialization will timeout — that's fine, we just
    // want to verify the double-start guard
    const startPromise = bridge.start().catch(() => {});
    await expect(bridge.start()).rejects.toThrow('IPC bridge already started');
    // Clean up
    await bridge.shutdown();
    await startPromise;
  });

  it('shutdown() is safe to call when not started', async () => {
    const bridge = new IpcBridge();
    await expect(bridge.shutdown()).resolves.toBeUndefined();
  });
});

describe('singleton', () => {
  beforeEach(() => {
    setIpcBridge(null);
  });

  it('getIpcBridge returns a consistent instance', () => {
    const a = getIpcBridge();
    const b = getIpcBridge();
    expect(a).toBe(b);
  });

  it('setIpcBridge replaces the singleton', () => {
    const custom = new IpcBridge({ bin: 'custom' });
    setIpcBridge(custom);
    expect(getIpcBridge()).toBe(custom);
  });
});

describe('IpcBridge convenience methods', () => {
  it('submitTask builds correct params', async () => {
    const bridge = new IpcBridge();
    const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
      jsonrpc: '2.0',
      id: 1,
      result: { id: 'task-1', status: 'pending' },
    });

    const auth = { userId: 'u1', role: 'admin' };
    await bridge.submitTask('do stuff', '/tmp', auth, { model: 'claude-sonnet-4-20250514' });

    expect(callSpy).toHaveBeenCalledWith('submit_task', {
      goal: 'do stuff',
      folder: '/tmp',
      auth,
      model: 'claude-sonnet-4-20250514',
    });
  });

  it('getTaskStatus builds correct params', async () => {
    const bridge = new IpcBridge();
    const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
      jsonrpc: '2.0',
      id: 2,
      result: { id: 'task-1', status: 'running' },
    });

    const auth = { userId: 'u1' };
    await bridge.getTaskStatus('task-1', auth);
    expect(callSpy).toHaveBeenCalledWith('get_task_status', { taskId: 'task-1', auth });
  });

  it('cancelTask builds correct params', async () => {
    const bridge = new IpcBridge();
    const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
      jsonrpc: '2.0',
      id: 3,
      result: { status: 'cancelled' },
    });

    await bridge.cancelTask('task-1', { userId: 'u1' });
    expect(callSpy).toHaveBeenCalledWith('cancel_task', {
      taskId: 'task-1',
      auth: { userId: 'u1' },
    });
  });

  it('listTasks with status filter', async () => {
    const bridge = new IpcBridge();
    const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
      jsonrpc: '2.0',
      id: 4,
      result: { tasks: [] },
    });

    await bridge.listTasks({ userId: 'u1' }, 'pending');
    expect(callSpy).toHaveBeenCalledWith('list_tasks', {
      auth: { userId: 'u1' },
      status: 'pending',
    });
  });

  it('listTasks without status filter', async () => {
    const bridge = new IpcBridge();
    const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
      jsonrpc: '2.0',
      id: 5,
      result: { tasks: [] },
    });

    await bridge.listTasks({ userId: 'u1' });
    expect(callSpy).toHaveBeenCalledWith('list_tasks', { auth: { userId: 'u1' } });
  });

  it('updateFlags builds correct params', async () => {
    const bridge = new IpcBridge();
    const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
      jsonrpc: '2.0',
      id: 6,
      result: { updated: 2 },
    });

    await bridge.updateFlags(
      { sandbox_enabled: true, plugins_enabled: false },
      { userId: 'admin' }
    );
    expect(callSpy).toHaveBeenCalledWith('update_flags', {
      flags: { sandbox_enabled: true, plugins_enabled: false },
      auth: { userId: 'admin' },
    });
  });

  it('flushAudit builds correct params', async () => {
    const bridge = new IpcBridge();
    const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
      jsonrpc: '2.0',
      id: 7,
      result: { count: 0, entries: [] },
    });

    await bridge.flushAudit({ userId: 'admin' });
    expect(callSpy).toHaveBeenCalledWith('flush_audit', { auth: { userId: 'admin' } });
  });

  it('recordSpend builds correct params', async () => {
    const bridge = new IpcBridge();
    const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
      jsonrpc: '2.0',
      id: 8,
      result: { totalCostUsd: 0.015, allowed: true },
    });

    await bridge.recordSpend('gpt-4o', 1000, 500, 0.015, { userId: 'u1' }, 'task-1');
    expect(callSpy).toHaveBeenCalledWith('record_spend', {
      model: 'gpt-4o',
      inputTokens: 1000,
      outputTokens: 500,
      costUsd: 0.015,
      auth: { userId: 'u1' },
      taskId: 'task-1',
    });
  });

  it('updateVerdict builds correct params', async () => {
    const bridge = new IpcBridge();
    const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
      jsonrpc: '2.0',
      id: 9,
      result: { allowed: false },
    });

    await bridge.updateVerdict({ verdict: 'block', spent_usd: 10 }, { userId: 'admin' });
    expect(callSpy).toHaveBeenCalledWith('update_verdict', {
      verdict: { verdict: 'block', spent_usd: 10 },
      auth: { userId: 'admin' },
    });
  });
});

describe('reverse IPC (incoming requests)', () => {
  it('onIncomingRequest registers a handler', () => {
    const bridge = new IpcBridge();
    const handler = vi.fn().mockResolvedValue({ ok: true });
    bridge.onIncomingRequest(handler);
    // Handler is registered (no public getter, but we test via handleLine below)
    expect(handler).not.toHaveBeenCalled();
  });

  it('handleLine routes incoming requests to the registered handler', async () => {
    const bridge = new IpcBridge();
    const handler = vi
      .fn()
      .mockResolvedValue({ response: 'hello', provider: 'test', model: 'test-model' });
    bridge.onIncomingRequest(handler);

    // Simulate an incoming JSON-RPC request line from Rust
    const incomingLine = JSON.stringify({
      jsonrpc: '2.0',
      id: 42,
      method: 'intercept_llm',
      params: { messages: [{ role: 'user', content: 'hi' }], model: 'test-model' },
    });

    // Access private handleLine through the typed test bridge.
    asTestableBridge(bridge).handleLine(incomingLine);

    // Wait for async handler
    await new Promise(r => setTimeout(r, 10));

    expect(handler).toHaveBeenCalledWith('intercept_llm', {
      messages: [{ role: 'user', content: 'hi' }],
      model: 'test-model',
    });
  });

  it('handleLine sends error response when no handler is registered', () => {
    const bridge = new IpcBridge();
    const sendSpy = vi.spyOn(asTestableBridge(bridge), 'sendResponse');

    const incomingLine = JSON.stringify({
      jsonrpc: '2.0',
      id: 99,
      method: 'unknown_method',
      params: {},
    });

    asTestableBridge(bridge).handleLine(incomingLine);

    expect(sendSpy).toHaveBeenCalledWith(99, undefined, {
      code: -32601,
      message: "No handler for 'unknown_method'",
    });
  });

  it('handleLine sends error response when handler throws', async () => {
    const bridge = new IpcBridge();
    const handler = vi.fn().mockRejectedValue(new Error('LLM router not initialized'));
    bridge.onIncomingRequest(handler);
    const sendSpy = vi.spyOn(asTestableBridge(bridge), 'sendResponse');

    const incomingLine = JSON.stringify({
      jsonrpc: '2.0',
      id: 50,
      method: 'intercept_llm',
      params: { messages: [] },
    });

    asTestableBridge(bridge).handleLine(incomingLine);
    await new Promise(r => setTimeout(r, 10));

    expect(sendSpy).toHaveBeenCalledWith(50, undefined, {
      code: -32000,
      message: 'LLM router not initialized',
    });
  });

  it('handleLine distinguishes incoming requests from responses', () => {
    const bridge = new IpcBridge();
    const handler = vi.fn().mockResolvedValue({});
    bridge.onIncomingRequest(handler);

    // Response (no method field) — should not call handler
    const responseLine = JSON.stringify({ jsonrpc: '2.0', id: 1, result: { ok: true } });
    asTestableBridge(bridge).handleLine(responseLine);
    expect(handler).not.toHaveBeenCalled();

    // Request (has method field) — should call handler
    const requestLine = JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'test', params: {} });
    asTestableBridge(bridge).handleLine(requestLine);
    expect(handler).toHaveBeenCalledWith('test', {});
  });
});
