import { describe, expect, test } from 'bun:test'
import { parseGitNumstat, parseGitDiff, parseShortstat } from '../gitDiff'

describe('parseGitNumstat', () => {
  test('parses single file', () => {
    const result = parseGitNumstat('5\t3\tsrc/foo.ts')
    expect(result.stats).toEqual({
      filesCount: 1,
      linesAdded: 5,
      linesRemoved: 3,
    })
    expect(result.perFileStats.get('src/foo.ts')).toEqual({
      added: 5,
      removed: 3,
      isBinary: false,
    })
  })

  test('parses multiple files', () => {
    const input = '10\t2\ta.ts\n3\t0\tb.ts\n0\t7\tc.ts'
    const result = parseGitNumstat(input)
    expect(result.stats).toEqual({
      filesCount: 3,
      linesAdded: 13,
      linesRemoved: 9,
    })
    expect(result.perFileStats.size).toBe(3)
  })

  test('handles binary file with dash counts', () => {
    const result = parseGitNumstat('-\t-\timage.png')
    expect(result.perFileStats.get('image.png')).toEqual({
      added: 0,
      removed: 0,
      isBinary: true,
    })
  })

  test('handles rename format', () => {
    const result = parseGitNumstat('1\t0\told.txt => new.txt')
    const entry = result.perFileStats.get('old.txt => new.txt')
    expect(entry).not.toBeUndefined()
    expect(entry!.added).toBe(1)
    expect(entry!.isBinary).toBe(false)
  })

  test('handles filename with tabs', () => {
    const result = parseGitNumstat('1\t0\t"tab\tfile.txt"')
    // parts.slice(2).join('\t') preserves the rest
    expect(result.stats.filesCount).toBe(1)
  })

  test('returns empty for empty string', () => {
    const result = parseGitNumstat('')
    expect(result.stats).toEqual({
      filesCount: 0,
      linesAdded: 0,
      linesRemoved: 0,
    })
    expect(result.perFileStats.size).toBe(0)
  })

  test('skips lines with fewer than 3 tab-separated parts', () => {
    const result = parseGitNumstat('invalid-line\n5\t3\tsrc/foo.ts')
    expect(result.stats.filesCount).toBe(1)
  })

  test('handles zero additions and zero deletions', () => {
    const result = parseGitNumstat('0\t0\tempty-change.ts')
    expect(result.perFileStats.get('empty-change.ts')).toEqual({
      added: 0,
      removed: 0,
      isBinary: false,
    })
  })
})

describe('parseGitDiff', () => {
  test('parses single file with one hunk', () => {
    const input = [
      'diff --git a/foo.ts b/foo.ts',
      'index abc..def 100644',
      '--- a/foo.ts',
      '+++ b/foo.ts',
      '@@ -1,3 +1,4 @@',
      ' line1',
      '+added',
      ' line2',
      ' line3',
    ].join('\n')

    const result = parseGitDiff(input)
    expect(result.size).toBe(1)
    const hunks = result.get('foo.ts')!
    expect(hunks).toHaveLength(1)
    expect(hunks[0].oldStart).toBe(1)
    expect(hunks[0].oldLines).toBe(3)
    expect(hunks[0].newStart).toBe(1)
    expect(hunks[0].newLines).toBe(4)
    expect(hunks[0].lines).toEqual([' line1', '+added', ' line2', ' line3'])
  })

  test('parses multiple hunks in one file', () => {
    const input = [
      'diff --git a/bar.ts b/bar.ts',
      'index abc..def 100644',
      '--- a/bar.ts',
      '+++ b/bar.ts',
      '@@ -1,2 +1,3 @@',
      ' a',
      '+b',
      ' c',
      '@@ -10,2 +11,2 @@',
      ' d',
      '-e',
      '+f',
    ].join('\n')

    const result = parseGitDiff(input)
    const hunks = result.get('bar.ts')!
    expect(hunks).toHaveLength(2)
    expect(hunks[0].oldStart).toBe(1)
    expect(hunks[1].oldStart).toBe(10)
  })

  test('skips binary files marker', () => {
    const input = [
      'diff --git a/img.png b/img.png',
      'Binary files a/img.png and b/img.png differ',
    ].join('\n')

    const result = parseGitDiff(input)
    // Binary file has no hunks, so it's not in the result
    expect(result.size).toBe(0)
  })

  test('parses new file mode', () => {
    const input = [
      'diff --git a/new.ts b/new.ts',
      'new file mode 100644',
      '--- /dev/null',
      '+++ b/new.ts',
      '@@ -0,0 +1,2 @@',
      '+line1',
      '+line2',
    ].join('\n')

    const result = parseGitDiff(input)
    const hunks = result.get('new.ts')!
    expect(hunks).toHaveLength(1)
    expect(hunks[0].lines).toEqual(['+line1', '+line2'])
  })

  test('parses deleted file', () => {
    const input = [
      'diff --git a/old.ts b/old.ts',
      'deleted file mode 100644',
      '--- a/old.ts',
      '+++ /dev/null',
      '@@ -1,2 +0,0 @@',
      '-line1',
      '-line2',
    ].join('\n')

    const result = parseGitDiff(input)
    const hunks = result.get('old.ts')!
    expect(hunks).toHaveLength(1)
  })

  test('returns empty map for empty input', () => {
    const result = parseGitDiff('')
    expect(result.size).toBe(0)
  })

  test('handles multiple files', () => {
    const input = [
      'diff --git a/a.ts b/a.ts',
      '--- a/a.ts',
      '+++ b/a.ts',
      '@@ -1 +1 @@',
      '-old',
      '+new',
      'diff --git a/b.ts b/b.ts',
      '--- a/b.ts',
      '+++ b/b.ts',
      '@@ -1 +1 @@',
      '-x',
      '+y',
    ].join('\n')

    const result = parseGitDiff(input)
    expect(result.size).toBe(2)
    expect(result.has('a.ts')).toBe(true)
    expect(result.has('b.ts')).toBe(true)
  })

  test('skips hunk without comma (single line)', () => {
    const input = [
      'diff --git a/solo.ts b/solo.ts',
      '--- a/solo.ts',
      '+++ b/solo.ts',
      '@@ -1 +1 @@',
      '-old',
      '+new',
    ].join('\n')

    const result = parseGitDiff(input)
    const hunks = result.get('solo.ts')!
    expect(hunks[0].oldLines).toBe(1) // default when no comma
    expect(hunks[0].newLines).toBe(1)
  })
})

describe('parseShortstat', () => {
  test('parses full shortstat with insertions and deletions', () => {
    const result = parseShortstat(
      ' 3 files changed, 10 insertions(+), 5 deletions(-)',
    )
    expect(result).toEqual({
      filesCount: 3,
      linesAdded: 10,
      linesRemoved: 5,
    })
  })

  test('parses single file', () => {
    const result = parseShortstat(
      ' 1 file changed, 2 insertions(+), 1 deletion(-)',
    )
    expect(result).toEqual({
      filesCount: 1,
      linesAdded: 2,
      linesRemoved: 1,
    })
  })

  test('parses insertions only', () => {
    const result = parseShortstat(' 2 files changed, 5 insertions(+)')
    expect(result).toEqual({
      filesCount: 2,
      linesAdded: 5,
      linesRemoved: 0,
    })
  })

  test('parses deletions only', () => {
    const result = parseShortstat(' 1 file changed, 3 deletions(-)')
    expect(result).toEqual({
      filesCount: 1,
      linesAdded: 0,
      linesRemoved: 3,
    })
  })

  test('parses files changed only (no insertions or deletions)', () => {
    const result = parseShortstat(' 2 files changed')
    expect(result).toEqual({
      filesCount: 2,
      linesAdded: 0,
      linesRemoved: 0,
    })
  })

  test('returns null for empty string', () => {
    expect(parseShortstat('')).toBeNull()
  })

  test('returns null for non-matching string', () => {
    expect(parseShortstat('nothing to see here')).toBeNull()
  })

  test('handles large numbers', () => {
    const result = parseShortstat(
      ' 100 files changed, 50000 insertions(+), 30000 deletions(-)',
    )
    expect(result).toEqual({
      filesCount: 100,
      linesAdded: 50000,
      linesRemoved: 30000,
    })
  })

  test('handles zero insertions and deletions explicitly', () => {
    // git can output "0 insertions(+), 0 deletions(-)"
    const result = parseShortstat(
      ' 1 file changed, 0 insertions(+), 0 deletions(-)',
    )
    expect(result).toEqual({
      filesCount: 1,
      linesAdded: 0,
      linesRemoved: 0,
    })
  })
})
