/**
 * Tests for ToolManager
 */

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { ToolManager } from '../../../src/core/ToolManager.js';
import type { ToolFunction } from '../../../src/domain/entities/Tool.js';

describe('ToolManager', () => {
  let toolManager: ToolManager;
  let testTool1: ToolFunction;
  let testTool2: ToolFunction;
  let testTool3: ToolFunction;

  beforeEach(() => {
    toolManager = new ToolManager();

    testTool1 = {
      definition: {
        type: 'function',
        function: {
          name: 'test_tool_1',
          description: 'Test tool 1',
          parameters: { type: 'object', properties: {}, required: [] },
        },
      },
      execute: vi.fn(async () => ({ result: 'tool1' })),
    };

    testTool2 = {
      definition: {
        type: 'function',
        function: {
          name: 'test_tool_2',
          description: 'Test tool 2',
          parameters: { type: 'object', properties: {}, required: [] },
        },
      },
      execute: vi.fn(async () => ({ result: 'tool2' })),
    };

    testTool3 = {
      definition: {
        type: 'function',
        function: {
          name: 'test_tool_3',
          description: 'Test tool 3',
          parameters: { type: 'object', properties: {}, required: [] },
        },
      },
      execute: vi.fn(async () => ({ result: 'tool3' })),
    };
  });

  describe('register', () => {
    it('should register a tool', () => {
      toolManager.register(testTool1);
      expect(toolManager.has('test_tool_1')).toBe(true);
    });

    it('should register a tool with options', () => {
      toolManager.register(testTool1, {
        namespace: 'test',
        priority: 100,
        enabled: true,
      });

      expect(toolManager.has('test_tool_1')).toBe(true);
      expect(toolManager.isEnabled('test_tool_1')).toBe(true);
    });

    it('should emit tool:registered event', () => {
      const listener = vi.fn();
      toolManager.on('tool:registered', listener);

      toolManager.register(testTool1, { namespace: 'test', enabled: true });

      expect(listener).toHaveBeenCalledWith({
        name: 'test_tool_1',
        namespace: 'test',
        enabled: true,
      });
    });

    it('should replace existing tool with same name', () => {
      toolManager.register(testTool1);
      const firstTool = toolManager.get('test_tool_1');

      const updatedTool = { ...testTool1 };
      toolManager.register(updatedTool);
      const secondTool = toolManager.get('test_tool_1');

      expect(secondTool).toBe(updatedTool);
      expect(secondTool).not.toBe(firstTool);
    });

    it('should register disabled tool', () => {
      toolManager.register(testTool1, { enabled: false });
      expect(toolManager.has('test_tool_1')).toBe(true);
      expect(toolManager.isEnabled('test_tool_1')).toBe(false);
    });
  });

  describe('unregister', () => {
    it('should unregister a tool', () => {
      toolManager.register(testTool1);
      expect(toolManager.has('test_tool_1')).toBe(true);

      toolManager.unregister('test_tool_1');
      expect(toolManager.has('test_tool_1')).toBe(false);
    });

    it('should emit tool:unregistered event', () => {
      const listener = vi.fn();
      toolManager.on('tool:unregistered', listener);

      toolManager.register(testTool1);
      toolManager.unregister('test_tool_1');

      expect(listener).toHaveBeenCalledWith({ name: 'test_tool_1' });
    });

    it('should not throw when unregistering non-existent tool', () => {
      expect(() => toolManager.unregister('non_existent')).not.toThrow();
    });
  });

  describe('enable/disable', () => {
    beforeEach(() => {
      toolManager.register(testTool1);
    });

    it('should enable a tool', () => {
      toolManager.disable('test_tool_1');
      expect(toolManager.isEnabled('test_tool_1')).toBe(false);

      const result = toolManager.enable('test_tool_1');
      expect(result).toBe(true);
      expect(toolManager.isEnabled('test_tool_1')).toBe(true);
    });

    it('should disable a tool', () => {
      expect(toolManager.isEnabled('test_tool_1')).toBe(true);

      const result = toolManager.disable('test_tool_1');
      expect(result).toBe(true);
      expect(toolManager.isEnabled('test_tool_1')).toBe(false);
    });

    it('should emit tool:enabled event', () => {
      const listener = vi.fn();
      toolManager.on('tool:enabled', listener);

      toolManager.disable('test_tool_1');
      toolManager.enable('test_tool_1');

      expect(listener).toHaveBeenCalledWith({ name: 'test_tool_1' });
    });

    it('should emit tool:disabled event', () => {
      const listener = vi.fn();
      toolManager.on('tool:disabled', listener);

      toolManager.disable('test_tool_1');

      expect(listener).toHaveBeenCalledWith({ name: 'test_tool_1' });
    });

    it('should return false when enabling non-existent tool', () => {
      const result = toolManager.enable('non_existent');
      expect(result).toBe(false);
    });

    it('should return false when disabling non-existent tool', () => {
      const result = toolManager.disable('non_existent');
      expect(result).toBe(false);
    });
  });

  describe('get', () => {
    it('should get a tool by name', () => {
      toolManager.register(testTool1);
      const tool = toolManager.get('test_tool_1');
      expect(tool).toBe(testTool1);
    });

    it('should return undefined for non-existent tool', () => {
      const tool = toolManager.get('non_existent');
      expect(tool).toBeUndefined();
    });
  });

  describe('has', () => {
    it('should return true for registered tool', () => {
      toolManager.register(testTool1);
      expect(toolManager.has('test_tool_1')).toBe(true);
    });

    it('should return false for non-existent tool', () => {
      expect(toolManager.has('non_existent')).toBe(false);
    });
  });

  describe('isEnabled', () => {
    it('should return true for enabled tool', () => {
      toolManager.register(testTool1, { enabled: true });
      expect(toolManager.isEnabled('test_tool_1')).toBe(true);
    });

    it('should return false for disabled tool', () => {
      toolManager.register(testTool1, { enabled: false });
      expect(toolManager.isEnabled('test_tool_1')).toBe(false);
    });

    it('should return false for non-existent tool', () => {
      expect(toolManager.isEnabled('non_existent')).toBe(false);
    });
  });

  describe('list', () => {
    it('should list all tool names', () => {
      toolManager.register(testTool1);
      toolManager.register(testTool2);
      toolManager.register(testTool3);

      const names = toolManager.list();
      expect(names).toHaveLength(3);
      expect(names).toContain('test_tool_1');
      expect(names).toContain('test_tool_2');
      expect(names).toContain('test_tool_3');
    });

    it('should return empty array when no tools', () => {
      const names = toolManager.list();
      expect(names).toEqual([]);
    });
  });

  describe('listEnabled', () => {
    it('should list only enabled tools', () => {
      toolManager.register(testTool1, { enabled: true });
      toolManager.register(testTool2, { enabled: false });
      toolManager.register(testTool3, { enabled: true });

      const names = toolManager.listEnabled();
      expect(names).toHaveLength(2);
      expect(names).toContain('test_tool_1');
      expect(names).toContain('test_tool_3');
      expect(names).not.toContain('test_tool_2');
    });

    it('should return empty array when no enabled tools', () => {
      toolManager.register(testTool1, { enabled: false });
      const names = toolManager.listEnabled();
      expect(names).toEqual([]);
    });
  });

  describe('getEnabled', () => {
    it('should return enabled tools', () => {
      toolManager.register(testTool1, { enabled: true });
      toolManager.register(testTool2, { enabled: false });
      toolManager.register(testTool3, { enabled: true });

      const tools = toolManager.getEnabled();
      expect(tools).toHaveLength(2);
      expect(tools).toContainEqual(testTool1);
      expect(tools).toContainEqual(testTool3);
      expect(tools).not.toContainEqual(testTool2);
    });

    it('should return empty array when no enabled tools', () => {
      toolManager.register(testTool1, { enabled: false });
      const tools = toolManager.getEnabled();
      expect(tools).toEqual([]);
    });
  });

  describe('getAll', () => {
    it('should return all tools', () => {
      toolManager.register(testTool1);
      toolManager.register(testTool2);
      toolManager.register(testTool3);

      const tools = toolManager.getAll();
      expect(tools).toHaveLength(3);
      expect(tools).toContainEqual(testTool1);
      expect(tools).toContainEqual(testTool2);
      expect(tools).toContainEqual(testTool3);
    });

    it('should return empty array when no tools', () => {
      const tools = toolManager.getAll();
      expect(tools).toEqual([]);
    });
  });

  describe('selectForContext', () => {
    it('should select tools based on condition', () => {
      const conditionalTool: ToolFunction = {
        definition: {
          type: 'function',
          function: {
            name: 'conditional_tool',
            description: 'Conditional tool',
            parameters: { type: 'object', properties: {}, required: [] },
          },
        },
        execute: vi.fn(async () => ({ result: 'conditional' })),
      };

      toolManager.register(testTool1);
      toolManager.register(conditionalTool, {
        conditions: [
          {
            type: 'mode',
            predicate: (context) => context.mode === 'admin',
          },
        ],
      });

      // Without admin mode
      const tools1 = toolManager.selectForContext({ mode: 'user' });
      expect(tools1).toHaveLength(1);
      expect(tools1).toContainEqual(testTool1);

      // With admin mode
      const tools2 = toolManager.selectForContext({ mode: 'admin' });
      expect(tools2).toHaveLength(2);
      expect(tools2).toContainEqual(testTool1);
      expect(tools2).toContainEqual(conditionalTool);
    });

    it('should only return enabled tools', () => {
      toolManager.register(testTool1, { enabled: true });
      toolManager.register(testTool2, { enabled: false });

      const tools = toolManager.selectForContext({});
      expect(tools).toHaveLength(1);
      expect(tools).toContainEqual(testTool1);
    });

    it('should sort by priority descending', () => {
      toolManager.register(testTool1, { priority: 10 });
      toolManager.register(testTool2, { priority: 100 });
      toolManager.register(testTool3, { priority: 50 });

      const tools = toolManager.selectForContext({});
      expect(tools[0]).toBe(testTool2); // Priority 100
      expect(tools[1]).toBe(testTool3); // Priority 50
      expect(tools[2]).toBe(testTool1); // Priority 10
    });
  });

  describe('getStats', () => {
    it('should return statistics', () => {
      toolManager.register(testTool1, { enabled: true });
      toolManager.register(testTool2, { enabled: false });
      toolManager.register(testTool3, { enabled: true });

      const stats = toolManager.getStats();
      expect(stats.totalTools).toBe(3);
      expect(stats.enabledTools).toBe(2);
      expect(stats.disabledTools).toBe(1);
    });

    it('should include namespace counts', () => {
      toolManager.register(testTool1, { namespace: 'api' });
      toolManager.register(testTool2, { namespace: 'api' });
      toolManager.register(testTool3, { namespace: 'db' });

      const stats = toolManager.getStats();
      expect(stats.toolsByNamespace.api).toBe(2);
      expect(stats.toolsByNamespace.db).toBe(1);
    });
  });

  describe('getState / loadState', () => {
    it('should serialize and deserialize state', () => {
      toolManager.register(testTool1, {
        namespace: 'test',
        priority: 100,
        enabled: true,
      });
      toolManager.register(testTool2, {
        namespace: 'test',
        priority: 50,
        enabled: false,
      });

      const state = toolManager.getState();

      // Create new manager and load state
      const newManager = new ToolManager();
      newManager.register(testTool1); // Tools must exist
      newManager.register(testTool2);
      newManager.loadState(state);

      expect(newManager.isEnabled('test_tool_1')).toBe(true);
      expect(newManager.isEnabled('test_tool_2')).toBe(false);
    });

    it('should handle missing tools in state', () => {
      toolManager.register(testTool1);
      const state = toolManager.getState();

      const newManager = new ToolManager();
      // Don't register testTool1
      expect(() => newManager.loadState(state)).not.toThrow();
    });
  });

  describe('clear', () => {
    it('should remove all tools', () => {
      toolManager.register(testTool1);
      toolManager.register(testTool2);
      toolManager.register(testTool3);

      expect(toolManager.list()).toHaveLength(3);

      toolManager.clear();
      expect(toolManager.list()).toHaveLength(0);
    });
  });

  describe('executionPipeline', () => {
    it('should expose the execution pipeline', () => {
      expect(toolManager.executionPipeline).toBeDefined();
      expect(typeof toolManager.executionPipeline.use).toBe('function');
      expect(typeof toolManager.executionPipeline.remove).toBe('function');
      expect(typeof toolManager.executionPipeline.has).toBe('function');
      expect(typeof toolManager.executionPipeline.get).toBe('function');
      expect(typeof toolManager.executionPipeline.list).toBe('function');
    });

    it('should have ResultNormalizerPlugin registered by default', () => {
      expect(toolManager.executionPipeline.has('result-normalizer')).toBe(true);
      const plugin = toolManager.executionPipeline.get('result-normalizer');
      expect(plugin).toBeDefined();
      expect(plugin?.priority).toBe(0); // Runs last in afterExecute
    });

    it('should normalize undefined results to error objects', async () => {
      // Create a tool that returns undefined
      const undefinedTool: ToolFunction = {
        definition: {
          type: 'function',
          function: {
            name: 'undefined_tool',
            description: 'Returns undefined',
            parameters: { type: 'object', properties: {}, required: [] },
          },
        },
        execute: vi.fn(async () => undefined),
      };

      toolManager.register(undefinedTool);
      const result = await toolManager.execute('undefined_tool', {});

      // ResultNormalizerPlugin should convert undefined to error object
      expect(result).toEqual({
        success: false,
        error: "Tool 'undefined_tool' returned no result",
      });
    });

    it('should normalize null results to error objects', async () => {
      // Create a tool that returns null
      const nullTool: ToolFunction = {
        definition: {
          type: 'function',
          function: {
            name: 'null_tool',
            description: 'Returns null',
            parameters: { type: 'object', properties: {}, required: [] },
          },
        },
        execute: vi.fn(async () => null),
      };

      toolManager.register(nullTool);
      const result = await toolManager.execute('null_tool', {});

      // ResultNormalizerPlugin should convert null to error object
      expect(result).toEqual({
        success: false,
        error: "Tool 'null_tool' returned no result",
      });
    });

    it('should allow registering plugins', () => {
      const plugin = {
        name: 'test-plugin',
        priority: 100,
      };

      toolManager.executionPipeline.use(plugin);
      expect(toolManager.executionPipeline.has('test-plugin')).toBe(true);
    });

    it('should run tool execution through plugin pipeline', async () => {
      const beforeExecuteCalled = vi.fn();
      const afterExecuteCalled = vi.fn();

      const plugin = {
        name: 'tracking-plugin',
        beforeExecute: async () => {
          beforeExecuteCalled();
        },
        afterExecute: async (_: unknown, result: unknown) => {
          afterExecuteCalled();
          return result;
        },
      };

      toolManager.executionPipeline.use(plugin);
      toolManager.register(testTool1);

      await toolManager.execute('test_tool_1', { input: 'test' });

      expect(beforeExecuteCalled).toHaveBeenCalled();
      expect(afterExecuteCalled).toHaveBeenCalled();
    });

    it('should allow plugins to modify tool args', async () => {
      const plugin = {
        name: 'arg-modifier',
        beforeExecute: async () => {
          return { modifiedArgs: { modified: true } };
        },
      };

      toolManager.executionPipeline.use(plugin);
      toolManager.register(testTool1);

      await toolManager.execute('test_tool_1', { original: true });

      // ToolManager wraps tool.execute() to pass context as second arg
      // Context is undefined when not set via setToolContext()
      expect(testTool1.execute).toHaveBeenCalledWith(
        { modified: true },
        undefined
      );
    });

    it('should allow plugins to abort execution', async () => {
      const plugin = {
        name: 'aborter',
        beforeExecute: async () => {
          return { abort: true, result: { aborted: true } };
        },
      };

      toolManager.executionPipeline.use(plugin);
      toolManager.register(testTool1);

      const result = await toolManager.execute('test_tool_1', {});

      expect(result).toEqual({ aborted: true });
      expect(testTool1.execute).not.toHaveBeenCalled();
    });

    it('should allow plugins to transform results', async () => {
      const plugin = {
        name: 'transformer',
        afterExecute: async (_: unknown, result: unknown) => {
          return { ...(result as object), transformed: true };
        },
      };

      toolManager.executionPipeline.use(plugin);
      toolManager.register(testTool1);

      const result = await toolManager.execute('test_tool_1', {});

      expect(result).toEqual({ result: 'tool1', transformed: true });
    });
  });

  describe('destroy', () => {
    it('should mark as destroyed', () => {
      expect(toolManager.isDestroyed).toBe(false);
      toolManager.destroy();
      expect(toolManager.isDestroyed).toBe(true);
    });

    it('should be idempotent (safe to call multiple times)', () => {
      toolManager.destroy();
      toolManager.destroy();
      toolManager.destroy();
      expect(toolManager.isDestroyed).toBe(true);
    });

    it('should remove all tools', () => {
      toolManager.register(testTool1);
      toolManager.register(testTool2);
      expect(toolManager.list()).toHaveLength(2);

      toolManager.destroy();
      expect(toolManager.list()).toHaveLength(0);
    });

    it('should remove all event listeners', () => {
      const listener = vi.fn();
      toolManager.on('tool:registered', listener);

      toolManager.destroy();

      // Listener should have been removed - but since we're destroyed,
      // we can't really test by emitting. Just verify no errors.
      expect(toolManager.isDestroyed).toBe(true);
    });

    it('should clear circuit breakers', async () => {
      // Register a tool and execute it to create a circuit breaker
      toolManager.register(testTool1);
      await toolManager.execute('test_tool_1', {});

      // Should have circuit breaker state
      const states = toolManager.getCircuitBreakerStates();
      expect(states.size).toBe(1);

      toolManager.destroy();

      // Circuit breakers should be cleared
      const statesAfter = toolManager.getCircuitBreakerStates();
      expect(statesAfter.size).toBe(0);
    });
  });

  describe('toolExecutionTimeout (hard timeout safety net)', () => {
    it('should default to 0 (disabled)', () => {
      const tm = new ToolManager();
      expect(tm.toolExecutionTimeout).toBe(0);
    });

    it('should accept timeout via constructor config', () => {
      const tm = new ToolManager({ toolExecutionTimeout: 5000 });
      expect(tm.toolExecutionTimeout).toBe(5000);
    });

    it('should allow setting timeout via property', () => {
      const tm = new ToolManager();
      tm.toolExecutionTimeout = 10000;
      expect(tm.toolExecutionTimeout).toBe(10000);
    });

    it('should not interfere with fast tools when enabled', async () => {
      const tm = new ToolManager({ toolExecutionTimeout: 5000 });
      const fastTool: ToolFunction = {
        definition: {
          type: 'function',
          function: {
            name: 'fast_tool',
            description: 'Returns immediately',
            parameters: { type: 'object', properties: {}, required: [] },
          },
        },
        execute: async () => ({ result: 'fast' }),
      };

      tm.register(fastTool);
      const result = await tm.execute('fast_tool', {});
      expect(result).toEqual({ result: 'fast' });
    });

    it('should reject with ToolExecutionError when tool exceeds hard timeout', async () => {
      const tm = new ToolManager({ toolExecutionTimeout: 100 });
      const slowTool: ToolFunction = {
        definition: {
          type: 'function',
          function: {
            name: 'slow_tool',
            description: 'Takes forever',
            parameters: { type: 'object', properties: {}, required: [] },
          },
        },
        execute: () => new Promise(() => {}), // never resolves
      };

      tm.register(slowTool);
      await expect(tm.execute('slow_tool', {})).rejects.toThrow('hard timeout');
    }, 5000);

    it('should not enforce hard timeout when disabled (0)', async () => {
      const tm = new ToolManager({ toolExecutionTimeout: 0 });
      const tool: ToolFunction = {
        definition: {
          type: 'function',
          function: {
            name: 'quick_tool',
            description: 'Quick',
            parameters: { type: 'object', properties: {}, required: [] },
          },
        },
        execute: async () => {
          await new Promise((r) => setTimeout(r, 50));
          return { result: 'done' };
        },
      };

      tm.register(tool);
      const result = await tm.execute('quick_tool', {});
      expect(result).toEqual({ result: 'done' });
    });
  });
});
