import { describe, it, expect, vi } from 'vitest';
import { composePlugins } from '../src/proxymodel/plugins/compose.js';
import type { ProxyModelPlugin, PluginSessionContext } from '../src/proxymodel/plugin.js';
import type { JsonRpcRequest, JsonRpcResponse } from '../src/types.js';

const fakeCtx = {} as PluginSessionContext;

function plugin(name: string, hooks: Partial<ProxyModelPlugin> = {}): ProxyModelPlugin {
  return { name, ...hooks };
}

describe('composePlugins', () => {
  it('returns the single plugin when given one', () => {
    const p = plugin('only');
    expect(composePlugins([p])).toBe(p);
  });

  it('throws when given an empty list', () => {
    expect(() => composePlugins([])).toThrow();
  });

  it('chains onSessionCreate / onSessionDestroy in order', async () => {
    const calls: string[] = [];
    const a = plugin('a', {
      onSessionCreate: async () => { calls.push('a-create'); },
      onSessionDestroy: async () => { calls.push('a-destroy'); },
    });
    const b = plugin('b', {
      onSessionCreate: async () => { calls.push('b-create'); },
      onSessionDestroy: async () => { calls.push('b-destroy'); },
    });
    const composed = composePlugins([a, b]);
    await composed.onSessionCreate!(fakeCtx);
    await composed.onSessionDestroy!(fakeCtx);
    expect(calls).toEqual(['a-create', 'b-create', 'a-destroy', 'b-destroy']);
  });

  it('first non-null onToolCallBefore short-circuits the chain', async () => {
    const aSpy = vi.fn(async () => null);
    const bSpy = vi.fn(async (): Promise<JsonRpcResponse> => ({ jsonrpc: '2.0', id: 1, result: 'B' }));
    const cSpy = vi.fn(async (): Promise<JsonRpcResponse> => ({ jsonrpc: '2.0', id: 1, result: 'C' }));
    const composed = composePlugins([
      plugin('a', { onToolCallBefore: aSpy }),
      plugin('b', { onToolCallBefore: bSpy }),
      plugin('c', { onToolCallBefore: cSpy }),
    ]);
    const req: JsonRpcRequest = { jsonrpc: '2.0', id: 1, method: 'tools/call' };
    const res = await composed.onToolCallBefore!('foo', {}, req, fakeCtx);
    expect(res?.result).toBe('B');
    expect(cSpy).not.toHaveBeenCalled();
  });

  it('onToolsList pipelines through plugins (each transforms the previous output)', async () => {
    const composed = composePlugins([
      plugin('a', { onToolsList: async (tools) => [...tools, { name: 'a-added', description: '' }] }),
      plugin('b', { onToolsList: async (tools) => [...tools, { name: 'b-added', description: '' }] }),
    ]);
    const out = await composed.onToolsList!([{ name: 'orig', description: '' }], fakeCtx);
    expect(out.map((t) => t.name)).toEqual(['orig', 'a-added', 'b-added']);
  });

  it('does not declare hooks that no plugin provides (no-op composition stays minimal)', () => {
    const composed = composePlugins([plugin('a'), plugin('b')]);
    expect(composed.onSessionCreate).toBeUndefined();
    expect(composed.onToolsList).toBeUndefined();
  });
});
