import { describe, it, expect } from 'vitest';
import type { ProxyModelPlugin, PluginSessionContext } from '../src/proxymodel/plugin.js';
import { PluginRegistry, resolveInheritance, loadPlugins } from '../src/proxymodel/plugin-loader.js';

function stubCtx(): PluginSessionContext {
  return {} as PluginSessionContext;
}

describe('PluginRegistry', () => {
  it('registers and resolves plugins', () => {
    const registry = new PluginRegistry();
    const plugin: ProxyModelPlugin = { name: 'test' };
    registry.register({ name: 'test', plugin, source: 'built-in' });

    expect(registry.resolve('test')).toBe(plugin);
    expect(registry.has('test')).toBe(true);
    expect(registry.resolve('nonexistent')).toBeNull();
    expect(registry.has('nonexistent')).toBe(false);
  });

  it('lists all registered plugins', () => {
    const registry = new PluginRegistry();
    registry.register({ name: 'a', plugin: { name: 'a' }, source: 'built-in' });
    registry.register({ name: 'b', plugin: { name: 'b' }, source: 'local' });

    const list = registry.list();
    expect(list).toHaveLength(2);
    expect(list.map((e) => e.name)).toEqual(['a', 'b']);
  });
});

describe('resolveInheritance', () => {
  it('returns plugin unchanged when no extends', () => {
    const registry = new PluginRegistry();
    const plugin: ProxyModelPlugin = { name: 'simple' };
    const resolved = resolveInheritance(plugin, registry);
    expect(resolved.name).toBe('simple');
  });

  it('inherits hooks from single parent', async () => {
    const registry = new PluginRegistry();
    const parent: ProxyModelPlugin = {
      name: 'parent',
      async onToolsList(tools) {
        return tools.filter((t) => t.name !== 'hidden');
      },
    };
    registry.register({ name: 'parent', plugin: parent, source: 'built-in' });

    const child: ProxyModelPlugin = { name: 'child', extends: ['parent'] };
    registry.register({ name: 'child', plugin: child, source: 'built-in' });

    const resolved = resolveInheritance(child, registry);
    expect(resolved.onToolsList).toBeDefined();

    const tools = [
      { name: 'visible', description: 'yes', inputSchema: {} },
      { name: 'hidden', description: 'no', inputSchema: {} },
    ];
    const result = await resolved.onToolsList!(tools, stubCtx());
    expect(result).toHaveLength(1);
    expect(result[0]!.name).toBe('visible');
  });

  it('child hook overrides parent hook', async () => {
    const registry = new PluginRegistry();
    const parent: ProxyModelPlugin = {
      name: 'parent',
      async onToolsList(tools) {
        return tools.filter((t) => t.name !== 'hidden');
      },
    };
    registry.register({ name: 'parent', plugin: parent, source: 'built-in' });

    const child: ProxyModelPlugin = {
      name: 'child',
      extends: ['parent'],
      async onToolsList(tools) {
        return [...tools, { name: 'added', description: 'new', inputSchema: {} }];
      },
    };
    registry.register({ name: 'child', plugin: child, source: 'built-in' });

    const resolved = resolveInheritance(child, registry);
    const result = await resolved.onToolsList!(
      [{ name: 'existing', description: 'yes', inputSchema: {} }],
      stubCtx(),
    );
    expect(result).toHaveLength(2);
    expect(result.map((t) => t.name)).toEqual(['existing', 'added']);
  });

  it('detects conflict when two parents define the same non-chainable hook', () => {
    const registry = new PluginRegistry();
    const parentA: ProxyModelPlugin = {
      name: 'parent-a',
      async onToolsList(tools) { return tools; },
    };
    const parentB: ProxyModelPlugin = {
      name: 'parent-b',
      async onToolsList(tools) { return tools; },
    };
    registry.register({ name: 'parent-a', plugin: parentA, source: 'built-in' });
    registry.register({ name: 'parent-b', plugin: parentB, source: 'built-in' });

    const child: ProxyModelPlugin = {
      name: 'child',
      extends: ['parent-a', 'parent-b'],
    };
    registry.register({ name: 'child', plugin: child, source: 'built-in' });

    expect(() => resolveInheritance(child, registry)).toThrow(/onToolsList.*parent-a.*parent-b/);
  });

  it('resolves conflict when child overrides the conflicting hook', async () => {
    const registry = new PluginRegistry();
    const parentA: ProxyModelPlugin = {
      name: 'parent-a',
      async onToolsList(tools) { return tools.slice(0, 1); },
    };
    const parentB: ProxyModelPlugin = {
      name: 'parent-b',
      async onToolsList(tools) { return tools.slice(1); },
    };
    registry.register({ name: 'parent-a', plugin: parentA, source: 'built-in' });
    registry.register({ name: 'parent-b', plugin: parentB, source: 'built-in' });

    const child: ProxyModelPlugin = {
      name: 'child',
      extends: ['parent-a', 'parent-b'],
      async onToolsList(tools) { return tools; },
    };
    registry.register({ name: 'child', plugin: child, source: 'built-in' });

    const resolved = resolveInheritance(child, registry);
    const tools = [
      { name: 'a', description: '', inputSchema: {} },
      { name: 'b', description: '', inputSchema: {} },
    ];
    const result = await resolved.onToolsList!(tools, stubCtx());
    expect(result).toHaveLength(2);
  });

  it('chains lifecycle hooks from multiple parents', async () => {
    const registry = new PluginRegistry();
    const order: string[] = [];
    const parentA: ProxyModelPlugin = {
      name: 'parent-a',
      async onSessionCreate() { order.push('a'); },
    };
    const parentB: ProxyModelPlugin = {
      name: 'parent-b',
      async onSessionCreate() { order.push('b'); },
    };
    registry.register({ name: 'parent-a', plugin: parentA, source: 'built-in' });
    registry.register({ name: 'parent-b', plugin: parentB, source: 'built-in' });

    const child: ProxyModelPlugin = {
      name: 'child',
      extends: ['parent-a', 'parent-b'],
    };
    registry.register({ name: 'child', plugin: child, source: 'built-in' });

    const resolved = resolveInheritance(child, registry);
    await resolved.onSessionCreate!(stubCtx());
    expect(order).toEqual(['a', 'b']);
  });

  it('detects circular inheritance', () => {
    const registry = new PluginRegistry();
    const a: ProxyModelPlugin = { name: 'a', extends: ['b'] };
    const b: ProxyModelPlugin = { name: 'b', extends: ['a'] };
    registry.register({ name: 'a', plugin: a, source: 'built-in' });
    registry.register({ name: 'b', plugin: b, source: 'built-in' });

    expect(() => resolveInheritance(a, registry)).toThrow(/Circular/);
  });

  it('errors when extending unknown parent', () => {
    const registry = new PluginRegistry();
    const child: ProxyModelPlugin = { name: 'child', extends: ['nonexistent'] };
    registry.register({ name: 'child', plugin: child, source: 'built-in' });

    expect(() => resolveInheritance(child, registry)).toThrow(/unknown plugin 'nonexistent'/);
  });

  it('resolves deep inheritance (grandparent)', async () => {
    const registry = new PluginRegistry();
    const grandparent: ProxyModelPlugin = {
      name: 'grandparent',
      async onToolCallAfter(_toolName, _args, response) { return response; },
    };
    const parent: ProxyModelPlugin = { name: 'parent', extends: ['grandparent'] };
    const child: ProxyModelPlugin = { name: 'child', extends: ['parent'] };

    registry.register({ name: 'grandparent', plugin: grandparent, source: 'built-in' });
    registry.register({ name: 'parent', plugin: parent, source: 'built-in' });
    registry.register({ name: 'child', plugin: child, source: 'built-in' });

    const resolved = resolveInheritance(child, registry);
    expect(resolved.onToolCallAfter).toBeDefined();
  });
});

describe('loadPlugins', () => {
  it('loads built-in plugins into registry', async () => {
    const pluginA: ProxyModelPlugin = { name: 'a' };
    const pluginB: ProxyModelPlugin = { name: 'b', extends: ['a'] };

    const registry = await loadPlugins([pluginA, pluginB], '/tmp/nonexistent-plugins-dir');
    expect(registry.has('a')).toBe(true);
    expect(registry.has('b')).toBe(true);
    expect(registry.list()).toHaveLength(2);
  });

  it('resolves inheritance during load', async () => {
    const parent: ProxyModelPlugin = {
      name: 'parent',
      async onToolsList(tools) { return tools; },
    };
    const child: ProxyModelPlugin = { name: 'child', extends: ['parent'] };

    const registry = await loadPlugins([parent, child], '/tmp/nonexistent-plugins-dir');
    const resolved = registry.resolve('child');
    expect(resolved).toBeDefined();
    expect(resolved!.onToolsList).toBeDefined();
  });
});
