// file: plugins/plugin-codecall/src/__tests__/codecall.plugin.spec.ts

import { type ScopeEntry } from '@frontmcp/sdk';

import CodeCallPlugin from '../codecall.plugin';
import { type CodeCallMode, type CodeCallPluginOptionsInput } from '../codecall.types';
import CodeCallConfig from '../providers/code-call.config';
import { ToolSearchService } from '../services';
import EnclaveService from '../services/enclave.service';

describe('CodeCallPlugin', () => {
  describe('constructor', () => {
    it('should initialize with default options', () => {
      const plugin = new CodeCallPlugin();
      expect(plugin.options.mode).toBe('codecall_only');
      expect(plugin.options.topK).toBe(8);
    });

    it('should initialize with custom options', () => {
      const plugin = new CodeCallPlugin({
        mode: 'codecall_opt_in',
        topK: 15,
      });
      expect(plugin.options.mode).toBe('codecall_opt_in');
      expect(plugin.options.topK).toBe(15);
    });
  });

  describe('dynamicProviders', () => {
    it('should return array of providers', () => {
      const providers = CodeCallPlugin.dynamicProviders({});
      expect(providers).toHaveLength(3);
    });

    it('should include codecall:config provider', () => {
      const providers = CodeCallPlugin.dynamicProviders({});
      const configProvider = providers.find((p) => p.name === 'codecall:config');
      expect(configProvider).toBeDefined();
      expect(configProvider?.provide).toBe(CodeCallConfig);
      expect(configProvider?.useValue).toBeInstanceOf(CodeCallConfig);
    });

    it('should include codecall:enclave provider', () => {
      const providers = CodeCallPlugin.dynamicProviders({});
      const enclaveProvider = providers.find((p) => p.name === 'codecall:enclave');
      expect(enclaveProvider).toBeDefined();
      expect(enclaveProvider?.provide).toBe(EnclaveService);
      expect(typeof enclaveProvider?.useFactory).toBe('function');
      expect(typeof enclaveProvider?.inject).toBe('function');
    });

    it('should include codecall:tool-search provider', () => {
      const providers = CodeCallPlugin.dynamicProviders({});
      const searchProvider = providers.find((p) => p.name === 'codecall:tool-search');
      expect(searchProvider).toBeDefined();
      expect(searchProvider?.provide).toBe(ToolSearchService);
      expect(typeof searchProvider?.useFactory).toBe('function');
    });

    it('should create EnclaveService using factory', async () => {
      const providers = CodeCallPlugin.dynamicProviders({});
      const enclaveProvider = providers.find((p) => p.name === 'codecall:enclave');
      const config = new CodeCallConfig({});
      const service = await enclaveProvider?.useFactory?.(config);
      expect(service).toBeInstanceOf(EnclaveService);
    });

    it('should create ToolSearchService using factory', async () => {
      const providers = CodeCallPlugin.dynamicProviders({});
      const searchProvider = providers.find((p) => p.name === 'codecall:tool-search');
      const mockScope = {
        tools: {
          list: () => [],
          subscribe: () => () => {},
        },
      } as unknown as ScopeEntry;
      const service = await searchProvider?.useFactory?.(mockScope);
      expect(service).toBeInstanceOf(ToolSearchService);
    });

    it('should pass embedding options to ToolSearchService', async () => {
      const options = {
        embedding: {
          strategy: 'ml' as const,
          modelName: 'custom/model',
        },
        mode: 'codecall_opt_in' as const,
      };
      const providers = CodeCallPlugin.dynamicProviders(options);
      const searchProvider = providers.find((p) => p.name === 'codecall:tool-search');

      expect(searchProvider).toBeDefined();
      // The factory function should be callable
      expect(typeof searchProvider?.useFactory).toBe('function');
    });
  });

  describe('adjustListTools hook', () => {
    let plugin: CodeCallPlugin;
    let mockLogger: { verbose: jest.Mock; info: jest.Mock; warn: jest.Mock; error: jest.Mock; child: jest.Mock };

    const createMockFlowCtx = (tools: Array<{ tool: { name?: string; fullName?: string; metadata?: any } }>) => {
      let resolvedTools = tools;
      return {
        state: {
          resolvedTools,
          set: (key: string, value: any) => {
            if (key === 'resolvedTools') {
              resolvedTools = value;
            }
          },
        },
        get resolvedToolsAfter() {
          return resolvedTools;
        },
      } as any;
    };

    const createPluginWithLogger = (options: CodeCallPluginOptionsInput = { mode: 'codecall_only' }) => {
      const p = new CodeCallPlugin(options);
      jest.spyOn(p, 'get').mockReturnValue({ child: () => mockLogger } as unknown as never);
      return p;
    };

    beforeEach(() => {
      mockLogger = {
        verbose: jest.fn(),
        info: jest.fn(),
        warn: jest.fn(),
        error: jest.fn(),
        child: jest.fn().mockReturnThis(),
      };
      plugin = createPluginWithLogger({ mode: 'codecall_only' });
    });

    it('should return early when no tools present', async () => {
      const flowCtx = createMockFlowCtx([]);
      await plugin.adjustListTools(flowCtx);
      expect(mockLogger.verbose).toHaveBeenCalledWith('adjustListTools: no tools to filter, returning early');
    });

    it('should return early when resolvedTools is undefined', async () => {
      const flowCtx = {
        state: {
          resolvedTools: undefined,
          set: jest.fn(),
        },
      } as any;
      await plugin.adjustListTools(flowCtx);
      expect(mockLogger.verbose).toHaveBeenCalledWith('adjustListTools: no tools to filter, returning early');
    });

    it('should always show codecall: meta-tools in codecall_only mode', async () => {
      plugin = createPluginWithLogger({ mode: 'codecall_only' });
      const flowCtx = createMockFlowCtx([
        { tool: { name: 'codecall:search' } },
        { tool: { name: 'codecall:describe' } },
        { tool: { name: 'other:tool' } },
      ]);

      await plugin.adjustListTools(flowCtx);

      const filtered = flowCtx.resolvedToolsAfter;
      expect(filtered).toHaveLength(2);
      expect(filtered.map((t: any) => t.tool.name)).toContain('codecall:search');
      expect(filtered.map((t: any) => t.tool.name)).toContain('codecall:describe');
    });

    it('should show tools with visibleInListTools=true in codecall_only mode', async () => {
      plugin = createPluginWithLogger({ mode: 'codecall_only' });
      const flowCtx = createMockFlowCtx([
        { tool: { name: 'visible:tool', metadata: { codecall: { visibleInListTools: true } } } },
        { tool: { name: 'hidden:tool', metadata: { codecall: { visibleInListTools: false } } } },
        { tool: { name: 'default:tool', metadata: {} } },
      ]);

      await plugin.adjustListTools(flowCtx);

      const filtered = flowCtx.resolvedToolsAfter;
      expect(filtered).toHaveLength(1);
      expect(filtered[0].tool.name).toBe('visible:tool');
    });

    it('should show all tools in codecall_opt_in mode', async () => {
      plugin = createPluginWithLogger({ mode: 'codecall_opt_in' });
      const flowCtx = createMockFlowCtx([
        { tool: { name: 'tool1' } },
        { tool: { name: 'tool2' } },
        { tool: { name: 'codecall:search' } },
      ]);

      await plugin.adjustListTools(flowCtx);

      const filtered = flowCtx.resolvedToolsAfter;
      expect(filtered).toHaveLength(3);
    });

    it('should hide tools with visibleInListTools=false in metadata_driven mode', async () => {
      plugin = createPluginWithLogger({ mode: 'metadata_driven' });
      const flowCtx = createMockFlowCtx([
        { tool: { name: 'visible:tool', metadata: { codecall: { visibleInListTools: true } } } },
        { tool: { name: 'hidden:tool', metadata: { codecall: { visibleInListTools: false } } } },
        { tool: { name: 'default:tool', metadata: {} } },
      ]);

      await plugin.adjustListTools(flowCtx);

      const filtered = flowCtx.resolvedToolsAfter;
      expect(filtered).toHaveLength(2);
      expect(filtered.map((t: any) => t.tool.name)).toContain('visible:tool');
      expect(filtered.map((t: any) => t.tool.name)).toContain('default:tool');
      expect(filtered.map((t: any) => t.tool.name)).not.toContain('hidden:tool');
    });

    it('should show tools by default in metadata_driven mode', async () => {
      plugin = createPluginWithLogger({ mode: 'metadata_driven' });
      const flowCtx = createMockFlowCtx([
        { tool: { name: 'tool-no-metadata' } },
        { tool: { name: 'tool-empty-metadata', metadata: {} } },
        { tool: { name: 'tool-empty-codecall', metadata: { codecall: {} } } },
      ]);

      await plugin.adjustListTools(flowCtx);

      const filtered = flowCtx.resolvedToolsAfter;
      expect(filtered).toHaveLength(3);
    });

    it('should handle tools with fullName instead of name', async () => {
      plugin = createPluginWithLogger({ mode: 'codecall_only' });
      const flowCtx = createMockFlowCtx([
        { tool: { fullName: 'codecall:execute' } },
        { tool: { fullName: 'other:tool' } },
      ]);

      await plugin.adjustListTools(flowCtx);

      const filtered = flowCtx.resolvedToolsAfter;
      expect(filtered).toHaveLength(1);
      expect(filtered[0].tool.fullName).toBe('codecall:execute');
    });

    it('should log before and after tool counts', async () => {
      plugin = createPluginWithLogger({ mode: 'codecall_only' });
      const flowCtx = createMockFlowCtx([{ tool: { name: 'codecall:search' } }, { tool: { name: 'other:tool' } }]);

      await plugin.adjustListTools(flowCtx);

      expect(mockLogger.verbose).toHaveBeenCalledWith('adjustListTools: tools before filter', { count: 2 });
      expect(mockLogger.verbose).toHaveBeenCalledWith('adjustListTools: tools after filter', { count: 1 });
    });

    it('should handle unknown mode by showing all tools (fail-open)', async () => {
      plugin = createPluginWithLogger({});
      // Force an unknown mode via private property
      (plugin as any).options.mode = 'unknown_mode' as CodeCallMode;

      const flowCtx = createMockFlowCtx([{ tool: { name: 'tool1' } }, { tool: { name: 'tool2' } }]);

      await plugin.adjustListTools(flowCtx);

      const filtered = flowCtx.resolvedToolsAfter;
      expect(filtered).toHaveLength(2);
    });
  });

  describe('multi-app scoping with appIds', () => {
    let mockLogger: { verbose: jest.Mock; info: jest.Mock; warn: jest.Mock; error: jest.Mock; child: jest.Mock };

    const createMockFlowCtx = (tools: Array<{ tool: Record<string, unknown> }>) => {
      let resolvedTools = tools;
      return {
        state: {
          resolvedTools,
          set: (key: string, value: any) => {
            if (key === 'resolvedTools') {
              resolvedTools = value;
            }
          },
        },
        get resolvedToolsAfter() {
          return resolvedTools;
        },
      } as any;
    };

    const createPluginWithLogger = (options: CodeCallPluginOptionsInput = {}) => {
      const p = new CodeCallPlugin(options);
      jest.spyOn(p, 'get').mockReturnValue({ child: () => mockLogger } as unknown as never);
      return p;
    };

    beforeEach(() => {
      mockLogger = {
        verbose: jest.fn(),
        info: jest.fn(),
        warn: jest.fn(),
        error: jest.fn(),
        child: jest.fn().mockReturnThis(),
      };
    });

    it('should accept appIds option', () => {
      const plugin = new CodeCallPlugin({ appIds: ['ecommerce'] });
      expect(plugin.options.appIds).toEqual(['ecommerce']);
    });

    it('without appIds: codecall_only hides ALL non-codecall tools (root-level = all apps)', async () => {
      const plugin = createPluginWithLogger({ mode: 'codecall_only' });
      const flowCtx = createMockFlowCtx([
        { tool: { name: 'codecall:search' } },
        { tool: { name: 'add', owner: { kind: 'app', id: 'calc' } } },
        { tool: { name: 'get_products', owner: { kind: 'app', id: 'ecommerce' } } },
      ]);

      await plugin.adjustListTools(flowCtx);

      const names = flowCtx.resolvedToolsAfter.map((t: any) => t.tool.name);
      expect(names).toEqual(['codecall:search']);
    });

    it('with appIds: only hides tools from managed apps, others remain visible', async () => {
      const plugin = createPluginWithLogger({ mode: 'codecall_only', appIds: ['ecommerce'] });
      const flowCtx = createMockFlowCtx([
        { tool: { name: 'codecall:search' } },
        { tool: { name: 'codecall:execute' } },
        { tool: { name: 'add', owner: { kind: 'app', id: 'calc' } } },
        { tool: { name: 'get_products', owner: { kind: 'app', id: 'ecommerce' } } },
        { tool: { name: 'post_checkout', owner: { kind: 'app', id: 'ecommerce' } } },
      ]);

      await plugin.adjustListTools(flowCtx);

      const names = flowCtx.resolvedToolsAfter.map((t: any) => t.tool.name);
      // Calc app tools remain visible, ecommerce tools hidden, codecall meta-tools visible
      expect(names).toContain('codecall:search');
      expect(names).toContain('codecall:execute');
      expect(names).toContain('add');
      expect(names).not.toContain('get_products');
      expect(names).not.toContain('post_checkout');
    });

    it('with appIds covering multiple apps: hides tools from all managed apps', async () => {
      const plugin = createPluginWithLogger({ mode: 'codecall_only', appIds: ['ecommerce', 'calc'] });
      const flowCtx = createMockFlowCtx([
        { tool: { name: 'codecall:search' } },
        { tool: { name: 'add', owner: { kind: 'app', id: 'calc' } } },
        { tool: { name: 'get_products', owner: { kind: 'app', id: 'ecommerce' } } },
        { tool: { name: 'list_users', owner: { kind: 'app', id: 'admin' } } },
      ]);

      await plugin.adjustListTools(flowCtx);

      const names = flowCtx.resolvedToolsAfter.map((t: any) => t.tool.name);
      // Only admin app tools and codecall meta-tools remain
      expect(names).toEqual(['codecall:search', 'list_users']);
    });

    it('with appIds: tools without owner are not hidden', async () => {
      const plugin = createPluginWithLogger({ mode: 'codecall_only', appIds: ['ecommerce'] });
      const flowCtx = createMockFlowCtx([
        { tool: { name: 'codecall:search' } },
        { tool: { name: 'orphan_tool' } },
        { tool: { name: 'get_products', owner: { kind: 'app', id: 'ecommerce' } } },
      ]);

      await plugin.adjustListTools(flowCtx);

      const names = flowCtx.resolvedToolsAfter.map((t: any) => t.tool.name);
      // Orphan tool (no owner) stays visible
      expect(names).toContain('codecall:search');
      expect(names).toContain('orphan_tool');
      expect(names).not.toContain('get_products');
    });

    it('with appIds: plugin-owned tools are not hidden by app scoping', async () => {
      const plugin = createPluginWithLogger({ mode: 'codecall_only', appIds: ['ecommerce'] });
      const flowCtx = createMockFlowCtx([
        { tool: { name: 'codecall:search' } },
        { tool: { name: 'remember_this', owner: { kind: 'plugin', id: 'remember' } } },
        { tool: { name: 'get_products', owner: { kind: 'app', id: 'ecommerce' } } },
      ]);

      await plugin.adjustListTools(flowCtx);

      const names = flowCtx.resolvedToolsAfter.map((t: any) => t.tool.name);
      // Plugin-owned tools remain visible (owner.kind !== 'app')
      expect(names).toContain('codecall:search');
      expect(names).toContain('remember_this');
      expect(names).not.toContain('get_products');
    });

    it('with empty appIds array: behaves like no appIds (hides all)', async () => {
      const plugin = createPluginWithLogger({ mode: 'codecall_only', appIds: [] });
      const flowCtx = createMockFlowCtx([
        { tool: { name: 'codecall:search' } },
        { tool: { name: 'add', owner: { kind: 'app', id: 'calc' } } },
      ]);

      await plugin.adjustListTools(flowCtx);

      const names = flowCtx.resolvedToolsAfter.map((t: any) => t.tool.name);
      expect(names).toEqual(['codecall:search']);
    });

    it('appIds does not affect codecall_opt_in mode (all tools shown)', async () => {
      const plugin = createPluginWithLogger({ mode: 'codecall_opt_in', appIds: ['ecommerce'] });
      const flowCtx = createMockFlowCtx([
        { tool: { name: 'codecall:search' } },
        { tool: { name: 'add', owner: { kind: 'app', id: 'calc' } } },
        { tool: { name: 'get_products', owner: { kind: 'app', id: 'ecommerce' } } },
      ]);

      await plugin.adjustListTools(flowCtx);

      const names = flowCtx.resolvedToolsAfter.map((t: any) => t.tool.name);
      expect(names).toHaveLength(3);
    });

    it('appIds does not affect metadata_driven mode', async () => {
      const plugin = createPluginWithLogger({ mode: 'metadata_driven', appIds: ['ecommerce'] });
      const flowCtx = createMockFlowCtx([
        { tool: { name: 'add', owner: { kind: 'app', id: 'calc' } } },
        { tool: { name: 'get_products', owner: { kind: 'app', id: 'ecommerce' } } },
        {
          tool: {
            name: 'hidden',
            owner: { kind: 'app', id: 'ecommerce' },
            metadata: { codecall: { visibleInListTools: false } },
          },
        },
      ]);

      await plugin.adjustListTools(flowCtx);

      const names = flowCtx.resolvedToolsAfter.map((t: any) => t.tool.name);
      // metadata_driven respects metadata, not appIds
      expect(names).toContain('add');
      expect(names).toContain('get_products');
      expect(names).not.toContain('hidden');
    });

    it('with appIds: managed app tools with visibleInListTools=true still shown', async () => {
      const plugin = createPluginWithLogger({ mode: 'codecall_only', appIds: ['ecommerce'] });
      const flowCtx = createMockFlowCtx([
        { tool: { name: 'codecall:search' } },
        { tool: { name: 'get_products', owner: { kind: 'app', id: 'ecommerce' } } },
        {
          tool: {
            name: 'featured_products',
            owner: { kind: 'app', id: 'ecommerce' },
            metadata: { codecall: { visibleInListTools: true } },
          },
        },
      ]);

      await plugin.adjustListTools(flowCtx);

      const names = flowCtx.resolvedToolsAfter.map((t: any) => t.tool.name);
      // featured_products stays visible because visibleInListTools=true overrides appIds hiding
      expect(names).toContain('codecall:search');
      expect(names).toContain('featured_products');
      expect(names).not.toContain('get_products');
    });
  });

  describe('private methods', () => {
    let plugin: CodeCallPlugin;

    beforeEach(() => {
      plugin = new CodeCallPlugin();
    });

    describe('isCodeCallTool', () => {
      it('should return true for codecall: prefixed tools', () => {
        const isCodeCall = (plugin as any).isCodeCallTool({ name: 'codecall:search' });
        expect(isCodeCall).toBe(true);
      });

      it('should return false for non-codecall tools', () => {
        const isCodeCall = (plugin as any).isCodeCallTool({ name: 'other:tool' });
        expect(isCodeCall).toBe(false);
      });

      it('should work with fullName property', () => {
        const isCodeCall = (plugin as any).isCodeCallTool({ fullName: 'codecall:describe' });
        expect(isCodeCall).toBe(true);
      });

      // Defensive guard: external tool entries can arrive with both name +
      // fullName absent (or non-string), and a bare `.startsWith` would
      // throw, taking down the whole list_tools filter. Pin the guarded
      // behaviour so a regression that removes the `typeof` check fails
      // here loudly.
      it('returns false (not throw) when both name and fullName are missing', () => {
        const isCodeCall = (plugin as any).isCodeCallTool({});
        expect(isCodeCall).toBe(false);
      });

      it('returns false when both name and fullName are non-strings', () => {
        // A pathological caller could set these to numbers/objects via JS
        // (TS types don't help at runtime). The guard must reject loudly.
        const isCodeCall = (plugin as any).isCodeCallTool({ name: 42, fullName: null });
        expect(isCodeCall).toBe(false);
      });
    });

    describe('getCodeCallMetadata', () => {
      it('should extract codecall metadata', () => {
        const metadata = (plugin as any).getCodeCallMetadata({
          metadata: { codecall: { visibleInListTools: true } },
        });
        expect(metadata).toEqual({ visibleInListTools: true });
      });

      it('should return undefined when no codecall metadata', () => {
        const metadata = (plugin as any).getCodeCallMetadata({ metadata: {} });
        expect(metadata).toBeUndefined();
      });

      it('should return undefined when no metadata', () => {
        const metadata = (plugin as any).getCodeCallMetadata({});
        expect(metadata).toBeUndefined();
      });
    });

    describe('shouldShowInListTools', () => {
      it('should always show codecall tools regardless of mode', () => {
        const codecallTool = { name: 'codecall:invoke' };

        expect((plugin as any).shouldShowInListTools(codecallTool, 'codecall_only')).toBe(true);
        expect((plugin as any).shouldShowInListTools(codecallTool, 'codecall_opt_in')).toBe(true);
        expect((plugin as any).shouldShowInListTools(codecallTool, 'metadata_driven')).toBe(true);
      });

      it('should show only explicitly visible tools in codecall_only mode', () => {
        const visibleTool = { name: 'test:tool', metadata: { codecall: { visibleInListTools: true } } };
        const hiddenTool = { name: 'test:tool2', metadata: { codecall: { visibleInListTools: false } } };
        const defaultTool = { name: 'test:tool3' };

        expect((plugin as any).shouldShowInListTools(visibleTool, 'codecall_only')).toBe(true);
        expect((plugin as any).shouldShowInListTools(hiddenTool, 'codecall_only')).toBe(false);
        expect((plugin as any).shouldShowInListTools(defaultTool, 'codecall_only')).toBe(false);
      });

      it('should show all tools in codecall_opt_in mode', () => {
        const anyTool = { name: 'any:tool' };
        expect((plugin as any).shouldShowInListTools(anyTool, 'codecall_opt_in')).toBe(true);
      });

      it('should hide explicitly hidden tools in metadata_driven mode', () => {
        const hiddenTool = { name: 'test:tool', metadata: { codecall: { visibleInListTools: false } } };
        expect((plugin as any).shouldShowInListTools(hiddenTool, 'metadata_driven')).toBe(false);
      });

      it('should show tools by default in metadata_driven mode', () => {
        const defaultTool = { name: 'test:tool' };
        const undefinedVisibility = { name: 'test:tool2', metadata: { codecall: {} } };

        expect((plugin as any).shouldShowInListTools(defaultTool, 'metadata_driven')).toBe(true);
        expect((plugin as any).shouldShowInListTools(undefinedVisibility, 'metadata_driven')).toBe(true);
      });
    });
  });
});
