import * as fs from 'fs';
import * as fsp from 'fs/promises';
import iconv = require('iconv-lite');
import { bashTool, createBashTool, type BashOperations } from '../src/modules/netaclaw/tools/builtin/bash.js';

describe('bash tool', () => {
  const tempLogs: string[] = [];
  const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
  const originalEnv = process.env;

  afterEach(() => {
    tempLogs.splice(0).forEach(file => fs.rmSync(file, { force: true }));
    process.env = originalEnv;
    if (originalPlatform) {
      Object.defineProperty(process, 'platform', originalPlatform);
    }
  });

  it('returns normal command output unchanged', async () => {
    const tool = createBashTool({
      operations: createMockOperations(({ onStdout }) => {
        onStdout(Buffer.from('hello'));
        return { exitCode: 0 };
      }),
    });

    const result = await tool.execute('bash-1', {
      command: 'node -e "process.stdout.write(\'hello\')"',
      timeout: 5000,
    });

    expect((result as any).text).toBe('hello');
  });

  it('keeps the tail of long command output and writes a full log', async () => {
    const tool = createBashTool({
      operations: createMockOperations(({ onStdout }) => {
        const output = Array.from({ length: 2001 }, (_, index) => `line ${index + 1}`).join('\n');
        onStdout(Buffer.from(output));
        return { exitCode: 0 };
      }),
    });

    const result = await tool.execute('bash-2', {
      command: 'long-output',
      timeout: 5000,
    });
    const text = (result as any).text as string;
    const fullOutputPath = extractFullOutputPath(text);
    tempLogs.push(fullOutputPath);
    await waitForFile(fullOutputPath);

    expect(text).not.toContain('line 1\n');
    expect(text).toContain('line 2001');
    expect(text).toContain('完整输出:');
    expect(fs.readFileSync(fullOutputPath, 'utf8')).toContain('line 1');
    expect(fs.readFileSync(fullOutputPath, 'utf8')).toContain('line 2001');
  });

  it('includes stderr output for failed commands', async () => {
    const tool = createBashTool({
      operations: createMockOperations(({ onStderr }) => {
        onStderr(Buffer.from('bad\n'));
        throw { message: '命令退出码 2', exitCode: 2 };
      }),
    });

    const result = await tool.execute('bash-3', {
      command: 'failing-command',
      timeout: 5000,
    });

    expect((result as any).text).toContain('命令执行失败:');
    expect((result as any).text).toContain('[stderr]: bad');
  });

  it('decodes localized Windows shell stderr instead of showing mojibake', async () => {
    const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
    Object.defineProperty(process, 'platform', { value: 'win32' });
    const tool = createBashTool({
      operations: createMockOperations(({ onStderr }) => {
        onStderr(iconv.encode("'pwd' 不是内部或外部命令，也不是可运行的程序或批处理文件。\r\n", 'gb18030'));
        return { exitCode: 1 };
      }),
    });

    try {
      const result = await tool.execute('bash-encoding', {
        command: 'pwd',
        timeout: 5000,
      });

      expect((result as any).text).toContain("'pwd' 不是内部或外部命令");
      expect((result as any).text).not.toContain('ڲ');
    } finally {
      if (originalPlatform) {
        Object.defineProperty(process, 'platform', originalPlatform);
      }
    }
  });

  it('exports the default bash tool instance', () => {
    expect(bashTool.name).toBe('bash');
  });
});

function createMockOperations(
  runner: (options: {
    timeout?: number;
    onStdout: (chunk: Buffer) => void;
    onStderr: (chunk: Buffer) => void;
  }) => { exitCode: number | null } | Promise<{ exitCode: number | null }>,
): BashOperations {
  return {
    async exec(_command, _cwd, options) {
      return await runner(options);
    },
  };
}

function extractFullOutputPath(text: string): string {
  const match = text.match(/完整输出: (.+?)\]/);
  if (!match) throw new Error(`full output path not found in: ${text}`);
  return match[1];
}

async function waitForFile(filePath: string) {
  for (let i = 0; i < 20; i++) {
    try {
      await fsp.access(filePath);
      return;
    } catch {
      await new Promise(resolve => setTimeout(resolve, 10));
    }
  }
  throw new Error(`file not ready: ${filePath}`);
}
