/**
 * Hook 系统测试
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as path from 'path';
import * as fs from 'fs/promises';
import * as os from 'os';
import {
  HookManager,
  initHookManager,
  getHookManager,
  resetHookManager,
  loadHookConfig,
  loadProjectConfig,
  type Hooks,
  type HookConfig,
} from '../../src/hooks/index.js';

describe('HookManager', () => {
  let tempDir: string;
  let manager: HookManager;

  beforeEach(async () => {
    tempDir = path.join(os.tmpdir(), `hooks-test-${Date.now()}`);
    await fs.mkdir(tempDir, { recursive: true });
    manager = new HookManager(tempDir, 'test-session');
  });

  afterEach(async () => {
    resetHookManager();
    try {
      await fs.rm(tempDir, { recursive: true, force: true });
    } catch {
      // ignore cleanup errors
    }
  });

  describe('Plugin Registration', () => {
    it('should register hooks from plugin', async () => {
      const hooks: Hooks = {
        'tool.execute.before': async (input, output) => {
          output.args = { ...output.args, injected: true };
        },
      };

      manager.registerHooks(hooks);
      expect(manager.getHookCount()).toBe(1);
    });

    it('should register multiple plugins', async () => {
      const hooks1: Hooks = {
        'tool.execute.before': async () => {},
      };
      const hooks2: Hooks = {
        'tool.execute.after': async () => {},
      };

      manager.registerHooks(hooks1);
      manager.registerHooks(hooks2);
      expect(manager.getHookCount()).toBe(2);
    });
  });

  describe('Tool Execute Before Hook', () => {
    it('should trigger tool.execute.before hook', async () => {
      let triggered = false;
      const hooks: Hooks = {
        'tool.execute.before': async (input, output) => {
          triggered = true;
          expect(input.tool).toBe('test_tool');
          expect(input.sessionId).toBe('test-session');
          expect(input.args).toEqual({ foo: 'bar' });
        },
      };

      manager.registerHooks(hooks);

      await manager.triggerToolExecuteBefore({
        tool: 'test_tool',
        sessionId: 'test-session',
        callId: 'call-1',
        args: { foo: 'bar' },
      });

      expect(triggered).toBe(true);
    });

    it('should allow hook to modify args', async () => {
      const hooks: Hooks = {
        'tool.execute.before': async (input, output) => {
          output.args = { ...output.args, modified: true };
        },
      };

      manager.registerHooks(hooks);

      const result = await manager.triggerToolExecuteBefore({
        tool: 'test_tool',
        sessionId: 'test-session',
        callId: 'call-1',
        args: { original: true },
      });

      expect(result.args).toEqual({ original: true, modified: true });
    });

    it('should allow hook to skip execution', async () => {
      const hooks: Hooks = {
        'tool.execute.before': async (input, output) => {
          output.skip = true;
          output.skipResult = {
            success: false,
            output: '',
            error: 'Blocked by hook',
          };
        },
      };

      manager.registerHooks(hooks);

      const result = await manager.triggerToolExecuteBefore({
        tool: 'test_tool',
        sessionId: 'test-session',
        callId: 'call-1',
        args: {},
      });

      expect(result.skip).toBe(true);
      expect(result.skipResult?.error).toBe('Blocked by hook');
    });
  });

  describe('Tool Execute After Hook', () => {
    it('should trigger tool.execute.after hook', async () => {
      let triggered = false;
      const hooks: Hooks = {
        'tool.execute.after': async (input, output) => {
          triggered = true;
          expect(input.tool).toBe('test_tool');
          expect(input.duration).toBeGreaterThanOrEqual(0);
        },
      };

      manager.registerHooks(hooks);

      await manager.triggerToolExecuteAfter(
        {
          tool: 'test_tool',
          sessionId: 'test-session',
          callId: 'call-1',
          args: {},
          duration: 100,
        },
        { success: true, output: 'test output' }
      );

      expect(triggered).toBe(true);
    });

    it('should allow hook to modify result', async () => {
      const hooks: Hooks = {
        'tool.execute.after': async (input, output) => {
          output.result = {
            ...output.result,
            output: output.result.output + ' (modified)',
          };
        },
      };

      manager.registerHooks(hooks);

      const result = await manager.triggerToolExecuteAfter(
        {
          tool: 'test_tool',
          sessionId: 'test-session',
          callId: 'call-1',
          args: {},
          duration: 100,
        },
        { success: true, output: 'original' }
      );

      expect(result.result.output).toBe('original (modified)');
    });
  });

  describe('Session Hooks', () => {
    it('should trigger session.start hook', async () => {
      let triggered = false;
      const hooks: Hooks = {
        'session.start': async (input) => {
          triggered = true;
          expect(input.sessionId).toBe('session-123');
          expect(input.workdir).toBe('/test/dir');
        },
      };

      manager.registerHooks(hooks);

      await manager.triggerSessionStart({
        sessionId: 'session-123',
        workdir: '/test/dir',
      });

      expect(triggered).toBe(true);
    });

    it('should trigger session.end hook', async () => {
      let triggered = false;
      const hooks: Hooks = {
        'session.end': async (input) => {
          triggered = true;
          expect(input.messageCount).toBe(10);
          expect(input.duration).toBe(5000);
        },
      };

      manager.registerHooks(hooks);

      await manager.triggerSessionEnd({
        sessionId: 'session-123',
        messageCount: 10,
        duration: 5000,
      });

      expect(triggered).toBe(true);
    });
  });

  describe('Message Hooks', () => {
    it('should trigger message.before hook', async () => {
      const hooks: Hooks = {
        'message.before': async (input, output) => {
          output.content = input.content.toUpperCase();
        },
      };

      manager.registerHooks(hooks);

      const result = await manager.triggerMessageBefore({
        sessionId: 'test-session',
        content: 'hello world',
      });

      expect(result.content).toBe('HELLO WORLD');
    });

    it('should allow message.before to skip', async () => {
      const hooks: Hooks = {
        'message.before': async (input, output) => {
          if (input.content.includes('forbidden')) {
            output.skip = true;
          }
        },
      };

      manager.registerHooks(hooks);

      const result = await manager.triggerMessageBefore({
        sessionId: 'test-session',
        content: 'this is forbidden',
      });

      expect(result.skip).toBe(true);
    });

    it('should trigger message.after hook', async () => {
      let triggered = false;
      const hooks: Hooks = {
        'message.after': async (input) => {
          triggered = true;
          expect(input.toolCalls).toBe(3);
        },
      };

      manager.registerHooks(hooks);

      await manager.triggerMessageAfter({
        sessionId: 'test-session',
        content: 'response',
        toolCalls: 3,
      });

      expect(triggered).toBe(true);
    });
  });

  describe('File Change Hooks', () => {
    it('should trigger file.edited hook', async () => {
      let triggered = false;
      const hooks: Hooks = {
        'file.edited': async (input, output) => {
          triggered = true;
          expect(input.path).toBe('/test/file.ts');
          expect(input.tool).toBe('edit_file');
        },
      };

      manager.registerHooks(hooks);

      await manager.triggerFileEdited({
        path: '/test/file.ts',
        tool: 'edit_file',
        sessionId: 'test-session',
      });

      expect(triggered).toBe(true);
    });

    it('should trigger file.created hook', async () => {
      let triggered = false;
      const hooks: Hooks = {
        'file.created': async (input, output) => {
          triggered = true;
          expect(input.path).toBe('/test/new-file.ts');
        },
      };

      manager.registerHooks(hooks);

      await manager.triggerFileCreated({
        path: '/test/new-file.ts',
        tool: 'write_file',
        sessionId: 'test-session',
      });

      expect(triggered).toBe(true);
    });

    it('should trigger file.deleted hook', async () => {
      let triggered = false;
      const hooks: Hooks = {
        'file.deleted': async (input, output) => {
          triggered = true;
          expect(input.path).toBe('/test/old-file.ts');
        },
      };

      manager.registerHooks(hooks);

      await manager.triggerFileDeleted({
        path: '/test/old-file.ts',
        tool: 'delete_file',
        sessionId: 'test-session',
      });

      expect(triggered).toBe(true);
    });
  });

  describe('Config Hooks', () => {
    it('should execute file hooks matching glob pattern', async () => {
      const config: HookConfig = {
        file_edited: {
          '*.ts': [
            {
              command: ['echo', 'TypeScript file edited'],
              timeout: 5000,
            },
          ],
        },
      };

      manager.setConfigHooks(config);

      const result = await manager.triggerFileEdited({
        path: 'test.ts',
        tool: 'edit_file',
        sessionId: 'test-session',
      });

      expect(result.commandResults).toBeDefined();
      expect(result.commandResults?.length).toBe(1);
      expect(result.commandResults?.[0].success).toBe(true);
      expect(result.commandResults?.[0].output).toContain('TypeScript file edited');
    });

    it('should not execute hooks for non-matching patterns', async () => {
      const config: HookConfig = {
        file_edited: {
          '*.ts': [
            {
              command: ['echo', 'TypeScript'],
            },
          ],
        },
      };

      manager.setConfigHooks(config);

      const result = await manager.triggerFileEdited({
        path: 'test.js',
        tool: 'edit_file',
        sessionId: 'test-session',
      });

      expect(result.commandResults).toBeDefined();
      expect(result.commandResults?.length).toBe(0);
    });
  });

  describe('Event Listeners', () => {
    it('should emit events to listeners', async () => {
      const events: any[] = [];
      manager.addEventListener((event) => {
        events.push(event);
      });

      await manager.triggerToolExecuteBefore({
        tool: 'test_tool',
        sessionId: 'test-session',
        callId: 'call-1',
        args: {},
      });

      expect(events.length).toBe(1);
      expect(events[0].type).toBe('tool.execute.before');
      expect(events[0].timestamp).toBeGreaterThan(0);
    });

    it('should remove event listener', async () => {
      const events: any[] = [];
      const listener = (event: any) => events.push(event);

      manager.addEventListener(listener);
      manager.removeEventListener(listener);

      await manager.triggerToolExecuteBefore({
        tool: 'test_tool',
        sessionId: 'test-session',
        callId: 'call-1',
        args: {},
      });

      expect(events.length).toBe(0);
    });
  });

  describe('Error Handling', () => {
    it('should continue execution when hook throws error', async () => {
      const hooks: Hooks = {
        'tool.execute.before': async () => {
          throw new Error('Hook error');
        },
      };

      manager.registerHooks(hooks);

      // Should not throw
      const result = await manager.triggerToolExecuteBefore({
        tool: 'test_tool',
        sessionId: 'test-session',
        callId: 'call-1',
        args: { foo: 'bar' },
      });

      // Args should remain unchanged
      expect(result.args).toEqual({ foo: 'bar' });
    });
  });
});

describe('Global Hook Manager', () => {
  afterEach(() => {
    resetHookManager();
  });

  it('should initialize global hook manager', () => {
    const manager = initHookManager('/test/dir', 'session-1');
    expect(manager).toBeInstanceOf(HookManager);
    expect(getHookManager()).toBe(manager);
  });

  it('should return null before initialization', () => {
    expect(getHookManager()).toBeNull();
  });

  it('should reset global hook manager', () => {
    initHookManager('/test/dir');
    resetHookManager();
    expect(getHookManager()).toBeNull();
  });
});

describe('Config Loader', () => {
  let tempDir: string;

  beforeEach(async () => {
    tempDir = path.join(os.tmpdir(), `config-test-${Date.now()}`);
    await fs.mkdir(tempDir, { recursive: true });
  });

  afterEach(async () => {
    try {
      await fs.rm(tempDir, { recursive: true, force: true });
    } catch {
      // ignore cleanup errors
    }
  });

  it('should load project config from .ai-assistant.json', async () => {
    const config = {
      hooks: {
        file_edited: {
          '*.ts': [{ command: ['echo', 'test'] }],
        },
      },
      plugins: ['plugin-a', 'plugin-b'],
    };

    await fs.writeFile(
      path.join(tempDir, '.ai-assistant.json'),
      JSON.stringify(config)
    );

    const loaded = await loadProjectConfig(tempDir);
    expect(loaded).not.toBeNull();
    expect(loaded?.hooks?.file_edited).toBeDefined();
    expect(loaded?.plugins).toEqual(['plugin-a', 'plugin-b']);
  });

  it('should load hook config', async () => {
    const config = {
      hooks: {
        file_edited: {
          '*.ts': [{ command: ['npm', 'run', 'lint'] }],
        },
        session_completed: [{ command: ['echo', 'done'] }],
      },
    };

    await fs.writeFile(
      path.join(tempDir, '.ai-assistant.json'),
      JSON.stringify(config)
    );

    const hookConfig = await loadHookConfig(tempDir);
    expect(hookConfig).not.toBeNull();
    expect(hookConfig?.file_edited?.['*.ts']).toHaveLength(1);
    expect(hookConfig?.session_completed).toHaveLength(1);
  });

  it('should return null for missing config', async () => {
    const config = await loadProjectConfig(tempDir);
    expect(config).toBeNull();
  });

  it('should support JSONC format with comments', async () => {
    const configContent = `{
      // This is a comment
      "hooks": {
        /* Multi-line
           comment */
        "file_edited": {
          "*.ts": [{ "command": ["echo", "test"] }]
        }
      }
    }`;

    await fs.writeFile(
      path.join(tempDir, '.ai-assistant.jsonc'),
      configContent
    );

    const loaded = await loadProjectConfig(tempDir);
    expect(loaded).not.toBeNull();
    expect(loaded?.hooks?.file_edited?.['*.ts']).toBeDefined();
  });
});
