import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { filePatchTool } from './file-patch.js';
import { writeFileSync, readFileSync, mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';

let testDir: string;

beforeEach(() => {
  testDir = mkdtempSync(join(tmpdir(), 'flynn-patch-test-'));
});

afterEach(() => {
  rmSync(testDir, { recursive: true, force: true });
});

describe('file.patch tool', () => {
  it('replaces lines in a single hunk', async () => {
    const filePath = join(testDir, 'replace.txt');
    writeFileSync(filePath, 'line1\nline2\nline3\nline4\nline5');

    const result = await filePatchTool.execute({
      patches: [
        {
          path: filePath,
          hunks: [{ start_line: 2, end_line: 3, content: 'new2\nnew3' }],
        },
      ],
    });

    expect(result.success).toBe(true);
    expect(result.output).toBe('Applied 1 hunk(s) to 1 file(s)');
    expect(readFileSync(filePath, 'utf-8')).toBe('line1\nnew2\nnew3\nline4\nline5');
  });

  it('applies multiple hunks to a single file', async () => {
    const filePath = join(testDir, 'multi-hunk.txt');
    writeFileSync(filePath, 'line1\nline2\nline3\nline4\nline5');

    const result = await filePatchTool.execute({
      patches: [
        {
          path: filePath,
          hunks: [
            { start_line: 1, end_line: 1, content: 'FIRST' },
            { start_line: 5, end_line: 5, content: 'LAST' },
          ],
        },
      ],
    });

    expect(result.success).toBe(true);
    expect(result.output).toBe('Applied 2 hunk(s) to 1 file(s)');
    expect(readFileSync(filePath, 'utf-8')).toBe('FIRST\nline2\nline3\nline4\nLAST');
  });

  it('inserts lines when end_line < start_line', async () => {
    const filePath = join(testDir, 'insert.txt');
    writeFileSync(filePath, 'line1\nline2\nline3\nline4');

    const result = await filePatchTool.execute({
      patches: [
        {
          path: filePath,
          hunks: [{ start_line: 3, end_line: 2, content: 'inserted' }],
        },
      ],
    });

    expect(result.success).toBe(true);
    expect(readFileSync(filePath, 'utf-8')).toBe('line1\nline2\ninserted\nline3\nline4');
  });

  it('deletes lines when content is empty', async () => {
    const filePath = join(testDir, 'delete.txt');
    writeFileSync(filePath, 'line1\nline2\nline3\nline4\nline5');

    const result = await filePatchTool.execute({
      patches: [
        {
          path: filePath,
          hunks: [{ start_line: 2, end_line: 3, content: '' }],
        },
      ],
    });

    expect(result.success).toBe(true);
    expect(readFileSync(filePath, 'utf-8')).toBe('line1\nline4\nline5');
  });

  it('patches multiple files in one call', async () => {
    const file1 = join(testDir, 'file1.txt');
    const file2 = join(testDir, 'file2.txt');
    writeFileSync(file1, 'aaa\nbbb\nccc');
    writeFileSync(file2, 'xxx\nyyy\nzzz');

    const result = await filePatchTool.execute({
      patches: [
        {
          path: file1,
          hunks: [{ start_line: 2, end_line: 2, content: 'BBB' }],
        },
        {
          path: file2,
          hunks: [{ start_line: 1, end_line: 1, content: 'XXX' }],
        },
      ],
    });

    expect(result.success).toBe(true);
    expect(result.output).toBe('Applied 2 hunk(s) to 2 file(s)');
    expect(readFileSync(file1, 'utf-8')).toBe('aaa\nBBB\nccc');
    expect(readFileSync(file2, 'utf-8')).toBe('XXX\nyyy\nzzz');
  });

  it('fails on overlapping hunks', async () => {
    const filePath = join(testDir, 'overlap.txt');
    writeFileSync(filePath, 'line1\nline2\nline3\nline4\nline5');

    const result = await filePatchTool.execute({
      patches: [
        {
          path: filePath,
          hunks: [
            { start_line: 2, end_line: 4, content: 'a' },
            { start_line: 3, end_line: 5, content: 'b' },
          ],
        },
      ],
    });

    expect(result.success).toBe(false);
    expect(result.error).toContain('Overlapping hunks');
  });

  it('fails on out of bounds line numbers', async () => {
    const filePath = join(testDir, 'oob.txt');
    writeFileSync(filePath, 'line1\nline2\nline3');

    const result = await filePatchTool.execute({
      patches: [
        {
          path: filePath,
          hunks: [{ start_line: 10, end_line: 10, content: 'nope' }],
        },
      ],
    });

    expect(result.success).toBe(false);
    expect(result.error).toContain('out of bounds');
  });

  it('fails on file not found', async () => {
    const result = await filePatchTool.execute({
      patches: [
        {
          path: join(testDir, 'nonexistent.txt'),
          hunks: [{ start_line: 1, end_line: 1, content: 'nope' }],
        },
      ],
    });

    expect(result.success).toBe(false);
    expect(result.error).toBeTruthy();
  });

  it('fails on empty patches array', async () => {
    const result = await filePatchTool.execute({ patches: [] });

    expect(result.success).toBe(false);
    expect(result.error).toContain('empty');
  });

  it('applies hunks bottom-up preserving line numbers', async () => {
    const filePath = join(testDir, 'bottomup.txt');
    writeFileSync(filePath, 'line1\nline2\nline3\nline4\nline5');

    // Replace line 2 with two lines (expanding the file) and replace line 4
    // If hunks were applied top-down, the line 4 replacement would target the wrong line
    const result = await filePatchTool.execute({
      patches: [
        {
          path: filePath,
          hunks: [
            { start_line: 2, end_line: 2, content: 'new2a\nnew2b' },
            { start_line: 4, end_line: 4, content: 'new4' },
          ],
        },
      ],
    });

    expect(result.success).toBe(true);
    // Line 4 was replaced first (bottom-up), so it correctly targets original line 4
    // Then line 2 is replaced, expanding to two lines
    expect(readFileSync(filePath, 'utf-8')).toBe('line1\nnew2a\nnew2b\nline3\nnew4\nline5');
  });
});
