/**
 * Unit tests for PluginRegistry
 */

import 'reflect-metadata';
import PluginRegistry, { PluginScopeInfo } from '../plugin.registry';
import { FlowCtxOf } from '../../common/interfaces';
import { FrontMcpPlugin } from '../../common/decorators/plugin.decorator';
import { FrontMcpProvider } from '../../common/decorators/provider.decorator';
import { FlowHooksOf } from '../../common/decorators/hook.decorator';
import { createClassProvider } from '../../__test-utils__/fixtures/provider.fixtures';
import { createProviderRegistryWithScope, createMockScope } from '../../__test-utils__/fixtures/scope.fixtures';
import { InvalidPluginScopeError } from '../../errors';
import { Scope } from '../../scope';

// Create ToolHook for tests (same as exported from index.ts)
const ToolHook = FlowHooksOf('tools:call-tool');

describe('PluginRegistry', () => {
  describe('Basic Registration', () => {
    it('should register a plugin with metadata', async () => {
      @FrontMcpPlugin({
        name: 'TestPlugin',
        description: 'A test plugin',
      })
      class TestPlugin {
        constructor() {}
      }

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [TestPlugin]);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toBeInstanceOf(TestPlugin);
    });

    it('should register multiple plugins', async () => {
      @FrontMcpPlugin({
        name: 'PluginA',
        description: 'First plugin',
      })
      class PluginA {}

      @FrontMcpPlugin({
        name: 'PluginB',
        description: 'Second plugin',
      })
      class PluginB {}

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [PluginA, PluginB]);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(2);
      expect(plugins[0]).toBeInstanceOf(PluginA);
      expect(plugins[1]).toBeInstanceOf(PluginB);
    });

    it('should register a plugin using useClass', async () => {
      const PLUGIN_TOKEN = Symbol('PLUGIN_TOKEN');

      class TestPluginImpl {
        constructor() {}
      }

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [
        {
          provide: PLUGIN_TOKEN,
          useClass: TestPluginImpl,
          name: 'TestPlugin',
          description: 'A test plugin',
        },
      ]);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toBeInstanceOf(TestPluginImpl);
    });

    it('should register a plugin using useFactory', async () => {
      const PLUGIN_TOKEN = Symbol('PLUGIN_TOKEN');

      const factoryFn = () => ({
        name: 'FactoryPlugin',
      });

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [
        {
          provide: PLUGIN_TOKEN,
          inject: () => [] as const,
          useFactory: factoryFn,
          name: 'FactoryPlugin',
          description: 'A factory plugin',
        },
      ]);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect((plugins[0] as any).name).toBe('FactoryPlugin');
      expect(typeof plugins[0].get).toBe('function');
    });

    it('should register a plugin using useValue', async () => {
      const PLUGIN_TOKEN = Symbol('PLUGIN_TOKEN');

      const pluginValue = {
        name: 'ValuePlugin',
        getValue: () => 'value',
      };

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [
        {
          provide: PLUGIN_TOKEN,
          useValue: pluginValue,
          name: 'ValuePlugin',
          description: 'A value plugin',
        },
      ]);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toEqual(pluginValue);
    });
  });

  describe('Plugin with Providers', () => {
    it('should register plugin-scoped providers', async () => {
      const SERVICE_TOKEN = Symbol('SERVICE');

      @FrontMcpPlugin({
        name: 'PluginWithProviders',
        description: 'Plugin that provides services',
        providers: [],
      })
      class PluginWithProviders {
        get: any;

        getService() {
          return this.get(SERVICE_TOKEN);
        }
      }

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [PluginWithProviders]);
      await registry.ready;

      const plugins = registry.getPlugins();
      const plugin = plugins[0] as any as PluginWithProviders;

      expect(plugin).toBeInstanceOf(PluginWithProviders);
      expect(typeof plugin.get).toBe('function');
    });

    it('should export providers to parent registry', async () => {
      const EXPORTED_SERVICE_TOKEN = Symbol('EXPORTED_SERVICE');

      @FrontMcpPlugin({
        name: 'PluginWithExports',
        description: 'Plugin that exports services',
        providers: [],
        exports: [],
      })
      class PluginWithExports {}

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [PluginWithExports]);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toBeInstanceOf(PluginWithExports);
    });
  });

  describe('Dependency Resolution', () => {
    it('should resolve plugin dependencies from parent provider registry', async () => {
      @FrontMcpPlugin({
        name: 'DependentPlugin',
        description: 'Plugin with dependencies',
      })
      class DependentPlugin {
        constructor() {}
      }

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [DependentPlugin]);
      await registry.ready;

      const plugins = registry.getPlugins();
      const plugin = plugins[0] as any as DependentPlugin;

      expect(plugin).toBeInstanceOf(DependentPlugin);
    });

    it('should create plugin even if optional dependencies are not registered', async () => {
      @FrontMcpPlugin({
        name: 'SimplePlugin',
        description: 'Plugin without dependencies',
      })
      class SimplePlugin {
        constructor() {}
      }

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [SimplePlugin]);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toBeInstanceOf(SimplePlugin);
    });

    it('should resolve factory plugin dependencies', async () => {
      const PLUGIN_TOKEN = Symbol('FACTORY_PLUGIN');
      const SERVICE_TOKEN = Symbol('SERVICE');

      class Service {
        getName() {
          return 'TestService';
        }
      }

      const providers = await createProviderRegistryWithScope([createClassProvider(Service, { name: 'Service' })]);

      const registry = new PluginRegistry(providers, [
        {
          provide: PLUGIN_TOKEN,
          inject: () => [Service] as const,
          useFactory: ((service: Service) => {
            return { serviceName: service.getName() };
          }) as any,
          name: 'FactoryPlugin',
          description: 'Factory plugin with deps',
        },
      ]);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect((plugins[0] as any).serviceName).toBe('TestService');
      expect(typeof plugins[0].get).toBe('function');
    });
  });

  describe('Nested Plugins', () => {
    it('should register plugins with nested plugins', async () => {
      @FrontMcpPlugin({
        name: 'NestedPlugin',
        description: 'A nested plugin',
      })
      class NestedPlugin {}

      @FrontMcpPlugin({
        name: 'ParentPlugin',
        description: 'Parent plugin with nested plugins',
        plugins: [NestedPlugin],
      })
      class ParentPlugin {}

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [ParentPlugin]);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toBeInstanceOf(ParentPlugin);
    });
  });

  describe('Plugin Tools and Resources', () => {
    it('should register plugin with tools', async () => {
      @FrontMcpPlugin({
        name: 'PluginWithTools',
        description: 'Plugin that provides tools',
        tools: [],
      })
      class PluginWithTools {}

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [PluginWithTools]);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toBeInstanceOf(PluginWithTools);
    });

    it('should expose tool registries via getToolRegistries()', async () => {
      @FrontMcpPlugin({
        name: 'PluginExposingTools',
        description: 'Plugin with tools for getToolRegistries test',
        tools: [],
      })
      class PluginExposingTools {}

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [PluginExposingTools]);
      await registry.ready;

      const toolRegistries = registry.getToolRegistries();
      expect(toolRegistries).toHaveLength(1);
    });

    it('should return empty array from getToolRegistries() when no plugins', async () => {
      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, []);
      await registry.ready;

      const toolRegistries = registry.getToolRegistries();
      expect(toolRegistries).toHaveLength(0);
    });

    it('should register plugin with resources', async () => {
      @FrontMcpPlugin({
        name: 'PluginWithResources',
        description: 'Plugin that provides resources',
        resources: [],
      })
      class PluginWithResources {}

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [PluginWithResources]);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toBeInstanceOf(PluginWithResources);
    });

    it('should register plugin with prompts', async () => {
      @FrontMcpPlugin({
        name: 'PluginWithPrompts',
        description: 'Plugin that provides prompts',
        prompts: [],
      })
      class PluginWithPrompts {}

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [PluginWithPrompts]);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toBeInstanceOf(PluginWithPrompts);
    });

    it('should register plugin with adapters', async () => {
      @FrontMcpPlugin({
        name: 'PluginWithAdapters',
        description: 'Plugin that provides adapters',
        adapters: [],
      })
      class PluginWithAdapters {}

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [PluginWithAdapters]);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toBeInstanceOf(PluginWithAdapters);
    });
  });

  describe('Error Handling', () => {
    it('should throw error for invalid plugin type', async () => {
      const providers = await createProviderRegistryWithScope();

      expect(() => {
        new PluginRegistry(providers, [null as any]);
      }).toThrow(/Invalid plugin/);
    });

    it('should throw error for plugin without provide', async () => {
      const providers = await createProviderRegistryWithScope();

      expect(() => {
        new PluginRegistry(providers, [
          {
            useValue: { name: 'test' },
            name: 'NoProvidePlugin',
          } as any,
        ]);
      }).toThrow(/missing 'provide'/);
    });

    it('should throw error for plugin with invalid useClass', async () => {
      const PLUGIN_TOKEN = Symbol('PLUGIN');
      const providers = await createProviderRegistryWithScope();

      expect(() => {
        new PluginRegistry(providers, [
          {
            provide: PLUGIN_TOKEN,
            useClass: 'not a class' as any,
            name: 'InvalidPlugin',
          },
        ]);
      }).toThrow(/must be a class/);
    });

    it('should throw error for plugin with invalid useFactory', async () => {
      const PLUGIN_TOKEN = Symbol('PLUGIN');
      const providers = await createProviderRegistryWithScope();

      expect(() => {
        new PluginRegistry(providers, [
          {
            provide: PLUGIN_TOKEN,
            inject: () => [] as const,
            useFactory: 'not a function' as any,
            name: 'InvalidPlugin',
          },
        ]);
      }).toThrow(/must be a function/);
    });

    it('should throw error for plugin with null useValue', async () => {
      const PLUGIN_TOKEN = Symbol('PLUGIN');
      const providers = await createProviderRegistryWithScope();

      expect(() => {
        new PluginRegistry(providers, [
          {
            provide: PLUGIN_TOKEN,
            useValue: null,
            name: 'NullValuePlugin',
          } as any,
        ]);
      }).toThrow(/must be defined/);
    });

    it('should throw error for plugin with undefined useValue', async () => {
      const PLUGIN_TOKEN = Symbol('PLUGIN');
      const providers = await createProviderRegistryWithScope();

      expect(() => {
        new PluginRegistry(providers, [
          {
            provide: PLUGIN_TOKEN,
            useValue: undefined,
            name: 'UndefinedValuePlugin',
          } as any,
        ]);
      }).toThrow(/must be defined/);
    });
  });

  describe('Plugin get method', () => {
    it('should provide get method to access plugin-scoped providers', async () => {
      const SERVICE_TOKEN = Symbol('SERVICE');

      @FrontMcpPlugin({
        name: 'PluginWithGet',
        description: 'Plugin that uses get method',
        providers: [],
      })
      class PluginWithGet {
        get: any;
      }

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [PluginWithGet]);
      await registry.ready;

      const plugins = registry.getPlugins();
      const plugin = plugins[0] as any as PluginWithGet;

      expect(typeof plugin.get).toBe('function');
    });
  });

  describe('Dynamic Providers', () => {
    it('should handle plugins with dynamic providers', async () => {
      const DYNAMIC_TOKEN = Symbol('DYNAMIC');

      class DynamicPlugin {
        get: any;
      }

      const providers = await createProviderRegistryWithScope();

      const registry = new PluginRegistry(providers, [
        {
          provide: DynamicPlugin,
          useValue: new DynamicPlugin(),
          name: 'DynamicPlugin',
        },
      ]);
      await registry.ready;

      const plugins = registry.getPlugins();
      const plugin = plugins[0] as any as DynamicPlugin;

      expect(plugin).toBeInstanceOf(DynamicPlugin);
      expect(typeof plugin.get).toBe('function');
    });

    it('should not throw ProviderNotRegisteredError when decorator providers are used with DynamicPlugin.init pattern', async () => {
      @FrontMcpProvider({ name: 'ExportedService' })
      class ExportedService {
        getValue() {
          return 'test';
        }
      }

      @FrontMcpPlugin({
        name: 'PluginWithExportedProvider',
        description: 'Plugin that declares providers and exports in decorator',
        providers: [ExportedService],
        exports: [ExportedService],
      })
      class PluginWithExportedProvider {
        get: any;
      }

      const providers = await createProviderRegistryWithScope();

      // Simulates what DynamicPlugin.init({}) returns:
      // providers: [] is empty dynamic providers that must NOT clobber decorator providers
      const registry = new PluginRegistry(providers, [
        {
          provide: PluginWithExportedProvider,
          useValue: new PluginWithExportedProvider(),
          providers: [],
          name: 'PluginWithExportedProvider',
        },
      ]);

      // This used to throw: ProviderNotRegisteredError: Provider "ExportedService" is not registered
      await expect(registry.ready).resolves.not.toThrow();

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);

      // Exported provider should be accessible from the parent providers registry
      const exportedInstance = providers.get(ExportedService);
      expect(exportedInstance).toBeInstanceOf(ExportedService);
    });
  });

  describe('Plugin Scope', () => {
    it('should default to app scope when no scope is specified', async () => {
      @FrontMcpPlugin({
        name: 'DefaultScopePlugin',
        description: 'Plugin without explicit scope',
      })
      class DefaultScopePlugin {}

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: false,
      };

      const registry = new PluginRegistry(providers, [DefaultScopePlugin], undefined, scopeInfo);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toBeInstanceOf(DefaultScopePlugin);
    });

    it('should register plugin with explicit app scope', async () => {
      @FrontMcpPlugin({
        name: 'AppScopePlugin',
        description: 'Plugin with app scope',
        scope: 'app',
      })
      class AppScopePlugin {}

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: false,
      };

      const registry = new PluginRegistry(providers, [AppScopePlugin], undefined, scopeInfo);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toBeInstanceOf(AppScopePlugin);
    });

    it('should register plugin with server scope in non-standalone app', async () => {
      @FrontMcpPlugin({
        name: 'ServerScopePlugin',
        description: 'Plugin with server scope',
        scope: 'server',
      })
      class ServerScopePlugin {}

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);
      const parentScope = createMockScope();

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope,
        isStandaloneApp: false,
      };

      const registry = new PluginRegistry(providers, [ServerScopePlugin], undefined, scopeInfo);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toBeInstanceOf(ServerScopePlugin);
    });

    it('should throw InvalidPluginScopeError when server scope plugin is used in standalone app', async () => {
      @FrontMcpPlugin({
        name: 'ServerScopePlugin',
        description: 'Plugin with server scope',
        scope: 'server',
      })
      class ServerScopePlugin {}

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: true, // Standalone app
      };

      const registry = new PluginRegistry(providers, [ServerScopePlugin], undefined, scopeInfo);

      await expect(registry.ready).rejects.toThrow(InvalidPluginScopeError);
      await expect(registry.ready).rejects.toThrow(/scope='server'/);
      await expect(registry.ready).rejects.toThrow(/standalone app/);
    });

    it('should allow app scope plugin in standalone app', async () => {
      @FrontMcpPlugin({
        name: 'AppScopePlugin',
        description: 'Plugin with app scope',
        scope: 'app',
      })
      class AppScopePlugin {}

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: true, // Standalone app
      };

      const registry = new PluginRegistry(providers, [AppScopePlugin], undefined, scopeInfo);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toBeInstanceOf(AppScopePlugin);
    });

    it('should register mixed scope plugins correctly', async () => {
      @FrontMcpPlugin({
        name: 'AppPlugin',
        description: 'App scope plugin',
        scope: 'app',
      })
      class AppPlugin {}

      @FrontMcpPlugin({
        name: 'ServerPlugin',
        description: 'Server scope plugin',
        scope: 'server',
      })
      class ServerPlugin {}

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);
      const parentScope = createMockScope();

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope,
        isStandaloneApp: false,
      };

      const registry = new PluginRegistry(providers, [AppPlugin, ServerPlugin], undefined, scopeInfo);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(2);
      expect(plugins[0]).toBeInstanceOf(AppPlugin);
      expect(plugins[1]).toBeInstanceOf(ServerPlugin);
    });

    it('should work without scopeInfo (backward compatibility)', async () => {
      @FrontMcpPlugin({
        name: 'LegacyPlugin',
        description: 'Plugin without scopeInfo',
      })
      class LegacyPlugin {}

      const providers = await createProviderRegistryWithScope();

      // No scopeInfo passed - should use default behavior
      const registry = new PluginRegistry(providers, [LegacyPlugin]);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
      expect(plugins[0]).toBeInstanceOf(LegacyPlugin);
    });

    it('should validate server scope plugin with object-based registration', async () => {
      const PLUGIN_TOKEN = Symbol('SERVER_PLUGIN');

      class ServerPlugin {
        get: any;
      }

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: true, // Standalone app
      };

      const registry = new PluginRegistry(
        providers,
        [
          {
            provide: PLUGIN_TOKEN,
            useValue: new ServerPlugin(),
            name: 'ServerPlugin',
            scope: 'server',
          },
        ],
        undefined,
        scopeInfo,
      );

      await expect(registry.ready).rejects.toThrow(InvalidPluginScopeError);
    });

    it('should validate server scope plugin with useClass registration', async () => {
      const PLUGIN_TOKEN = Symbol('SERVER_PLUGIN');

      @FrontMcpPlugin({
        name: 'DecoratedServerPlugin',
        scope: 'app', // Decorator says app
      })
      class DecoratedServerPlugin {}

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: true,
      };

      // Inline config overrides decorator - sets scope to 'server'
      const registry = new PluginRegistry(
        providers,
        [
          {
            provide: PLUGIN_TOKEN,
            useClass: DecoratedServerPlugin,
            name: 'ServerPlugin',
            scope: 'server', // Override decorator's 'app' scope
          },
        ],
        undefined,
        scopeInfo,
      );

      await expect(registry.ready).rejects.toThrow(InvalidPluginScopeError);
    });

    it('should use decorator scope when inline scope is not specified for useClass', async () => {
      const PLUGIN_TOKEN = Symbol('APP_PLUGIN');

      @FrontMcpPlugin({
        name: 'DecoratedAppPlugin',
        scope: 'app',
      })
      class DecoratedAppPlugin {}

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: true,
      };

      // No scope in inline config - should use decorator's 'app' scope
      const registry = new PluginRegistry(
        providers,
        [
          {
            provide: PLUGIN_TOKEN,
            useClass: DecoratedAppPlugin,
            name: 'AppPlugin',
            // scope not specified - should inherit from decorator
          },
        ],
        undefined,
        scopeInfo,
      );

      await registry.ready;
      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
    });
  });

  describe('Nested Plugins Scope Inheritance', () => {
    it('should propagate scopeInfo to nested plugins', async () => {
      @FrontMcpPlugin({
        name: 'NestedServerPlugin',
        scope: 'server',
      })
      class NestedServerPlugin {}

      @FrontMcpPlugin({
        name: 'ParentPlugin',
        plugins: [NestedServerPlugin],
      })
      class ParentPlugin {}

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: true, // Standalone app - nested server-scoped plugin should fail
      };

      const registry = new PluginRegistry(providers, [ParentPlugin], undefined, scopeInfo);

      // Should fail because nested plugin has scope='server' in a standalone app
      await expect(registry.ready).rejects.toThrow(InvalidPluginScopeError);
      await expect(registry.ready).rejects.toThrow(/NestedServerPlugin/);
    });

    it('should allow nested app-scoped plugins in standalone app', async () => {
      @FrontMcpPlugin({
        name: 'NestedAppPlugin',
        scope: 'app',
      })
      class NestedAppPlugin {}

      @FrontMcpPlugin({
        name: 'ParentPlugin',
        plugins: [NestedAppPlugin],
      })
      class ParentPlugin {}

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: true,
      };

      const registry = new PluginRegistry(providers, [ParentPlugin], undefined, scopeInfo);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
    });

    it('should allow nested server-scoped plugins in non-standalone app', async () => {
      @FrontMcpPlugin({
        name: 'NestedServerPlugin',
        scope: 'server',
      })
      class NestedServerPlugin {}

      @FrontMcpPlugin({
        name: 'ParentPlugin',
        plugins: [NestedServerPlugin],
      })
      class ParentPlugin {}

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);
      const parentScope = createMockScope();

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope,
        isStandaloneApp: false,
      };

      const registry = new PluginRegistry(providers, [ParentPlugin], undefined, scopeInfo);
      await registry.ready;

      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
    });

    it('should validate deeply nested plugins with server scope', async () => {
      @FrontMcpPlugin({
        name: 'DeeplyNestedServerPlugin',
        scope: 'server',
      })
      class DeeplyNestedServerPlugin {}

      @FrontMcpPlugin({
        name: 'MiddlePlugin',
        plugins: [DeeplyNestedServerPlugin],
      })
      class MiddlePlugin {}

      @FrontMcpPlugin({
        name: 'TopPlugin',
        plugins: [MiddlePlugin],
      })
      class TopPlugin {}

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: true,
      };

      const registry = new PluginRegistry(providers, [TopPlugin], undefined, scopeInfo);

      // Should fail because deeply nested plugin has scope='server' in standalone app
      await expect(registry.ready).rejects.toThrow(InvalidPluginScopeError);
      await expect(registry.ready).rejects.toThrow(/DeeplyNestedServerPlugin/);
    });
  });

  describe('Scope Fallback Behavior', () => {
    it('should register hooks to own scope when scope is app', async () => {
      @FrontMcpPlugin({
        name: 'AppScopePluginWithHooks',
        scope: 'app',
      })
      class AppScopePluginWithHooks {
        @ToolHook.Will('execute')
        async beforeExecute(_ctx: FlowCtxOf<'tools:call-tool'>) {
          // App-scoped hook
        }
      }

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);
      const parentScope = createMockScope();

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope,
        isStandaloneApp: false,
      };

      const registry = new PluginRegistry(providers, [AppScopePluginWithHooks], undefined, scopeInfo);
      await registry.ready;

      // Hooks should be registered to own scope, not parent scope
      expect(ownScope.hooks.registerHooks).toHaveBeenCalled();
      expect(parentScope.hooks.registerHooks).not.toHaveBeenCalled();
    });

    it('should register hooks to parent scope when scope is server and parentScope exists', async () => {
      @FrontMcpPlugin({
        name: 'ServerScopePluginWithHooks',
        scope: 'server',
      })
      class ServerScopePluginWithHooks {
        @ToolHook.Will('execute')
        async beforeExecute(_ctx: FlowCtxOf<'tools:call-tool'>) {
          // Server-scoped hook
        }
      }

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);
      const parentScope = createMockScope();

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope,
        isStandaloneApp: false,
      };

      const registry = new PluginRegistry(providers, [ServerScopePluginWithHooks], undefined, scopeInfo);
      await registry.ready;

      // Hooks should be registered to parent scope, not own scope
      expect(parentScope.hooks.registerHooks).toHaveBeenCalled();
      expect(ownScope.hooks.registerHooks).not.toHaveBeenCalled();
    });

    it('should fall back to own scope when scope is server but no parentScope exists', async () => {
      @FrontMcpPlugin({
        name: 'ServerScopePluginNoParent',
        scope: 'server',
      })
      class ServerScopePluginNoParent {
        @ToolHook.Will('execute')
        async beforeExecute(_ctx: FlowCtxOf<'tools:call-tool'>) {
          // Server-scoped hook with no parent
        }
      }

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);
      const warnSpy = jest.spyOn(ownScope.logger, 'warn').mockImplementation(() => {});

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined, // No parent scope
        isStandaloneApp: false,
      };

      const registry = new PluginRegistry(providers, [ServerScopePluginNoParent], undefined, scopeInfo);
      await registry.ready;

      // Should log a warning and register to own scope
      expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('ServerScopePluginNoParent'));
      expect(ownScope.hooks.registerHooks).toHaveBeenCalled();

      warnSpy.mockRestore();
    });
  });

  describe('Metadata Merging Edge Cases', () => {
    it('should prefer inline scope over decorator scope for useValue', async () => {
      const PLUGIN_TOKEN = Symbol('PLUGIN');

      @FrontMcpPlugin({
        name: 'DecoratorAppPlugin',
        scope: 'app',
      })
      class DecoratorAppPlugin {
        get: any;
      }

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: true,
      };

      // Inline scope='server' should override decorator's scope='app'
      // This should fail in standalone app
      const registry = new PluginRegistry(
        providers,
        [
          {
            provide: PLUGIN_TOKEN,
            useValue: new DecoratorAppPlugin(),
            name: 'OverriddenPlugin',
            scope: 'server',
          },
        ],
        undefined,
        scopeInfo,
      );

      await expect(registry.ready).rejects.toThrow(InvalidPluginScopeError);
    });

    it('should use decorator scope when inline metadata is empty for useValue', async () => {
      const PLUGIN_TOKEN = Symbol('PLUGIN');

      @FrontMcpPlugin({
        name: 'DecoratorAppPlugin',
        scope: 'app',
      })
      class DecoratorAppPlugin {
        get: any;
      }

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: true,
      };

      // No inline scope - should use decorator's 'app' scope
      const registry = new PluginRegistry(
        providers,
        [
          {
            provide: PLUGIN_TOKEN,
            useValue: new DecoratorAppPlugin(),
            name: 'InheritedScopePlugin',
            // scope not specified
          },
        ],
        undefined,
        scopeInfo,
      );

      await registry.ready;
      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
    });

    it('should default to app scope when no scope is defined anywhere', async () => {
      const PLUGIN_TOKEN = Symbol('PLUGIN');

      // No @FrontMcpPlugin decorator
      class PlainPlugin {
        get: any;
      }

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: true,
      };

      // No scope in decorator or inline - should default to 'app'
      const registry = new PluginRegistry(
        providers,
        [
          {
            provide: PLUGIN_TOKEN,
            useValue: new PlainPlugin(),
            name: 'NoScopePlugin',
          },
        ],
        undefined,
        scopeInfo,
      );

      // Should succeed because default scope is 'app'
      await registry.ready;
      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
    });
  });

  describe('Factory Plugin Edge Cases', () => {
    it('should use inline scope for factory plugins', async () => {
      const PLUGIN_TOKEN = Symbol('FACTORY_PLUGIN');

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: true,
      };

      const registry = new PluginRegistry(
        providers,
        [
          {
            provide: PLUGIN_TOKEN,
            inject: () => [] as const,
            useFactory: () => ({ name: 'FactoryPlugin' }),
            name: 'FactoryPlugin',
            scope: 'server', // Factory plugin with server scope
          },
        ],
        undefined,
        scopeInfo,
      );

      await expect(registry.ready).rejects.toThrow(InvalidPluginScopeError);
    });

    it('should default to app scope for factory plugins without scope', async () => {
      const PLUGIN_TOKEN = Symbol('FACTORY_PLUGIN');

      const providers = await createProviderRegistryWithScope();
      const ownScope = providers.get(Scope);

      const scopeInfo: PluginScopeInfo = {
        ownScope,
        parentScope: undefined,
        isStandaloneApp: true,
      };

      const registry = new PluginRegistry(
        providers,
        [
          {
            provide: PLUGIN_TOKEN,
            inject: () => [] as const,
            useFactory: () => ({ name: 'FactoryPlugin' }),
            name: 'FactoryPlugin',
            // No scope - should default to 'app'
          },
        ],
        undefined,
        scopeInfo,
      );

      await registry.ready;
      const plugins = registry.getPlugins();
      expect(plugins).toHaveLength(1);
    });
  });
});
