import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import { describe, expect, test } from 'bun:test'
import {
  stripEmptyLines,
  isImageOutput,
  parseDataUri,
  formatOutput,
  createContentSummary,
  selectFailureOutput,
} from './utils.js'

// =============================================================================
// stripEmptyLines — removes leading/trailing blank lines
// =============================================================================

describe('stripEmptyLines', () => {
  test('strips leading empty lines', () => {
    expect(stripEmptyLines('\n\n\nhello')).toBe('hello')
  })

  test('strips trailing empty lines', () => {
    expect(stripEmptyLines('hello\n\n\n')).toBe('hello')
  })

  test('strips both ends', () => {
    expect(stripEmptyLines('\n\nhello\n\n')).toBe('hello')
  })

  test('preserves internal empty lines', () => {
    expect(stripEmptyLines('a\n\nb')).toBe('a\n\nb')
  })

  test('all empty lines returns empty string', () => {
    expect(stripEmptyLines('\n\n\n')).toBe('')
  })

  test('empty string returns empty string', () => {
    expect(stripEmptyLines('')).toBe('')
  })

  test('preserves whitespace-only lines in the middle', () => {
    expect(stripEmptyLines('a\n   \nb')).toBe('a\n   \nb')
  })

  test('single line no change', () => {
    expect(stripEmptyLines('hello')).toBe('hello')
  })
})

// =============================================================================
// isImageOutput — detects base64 data URIs
// =============================================================================

describe('isImageOutput', () => {
  test('detects PNG data URI', () => {
    expect(isImageOutput('data:image/png;base64,iVBORw0KGgo=')).toBe(true)
  })

  test('detects JPEG data URI', () => {
    expect(isImageOutput('data:image/jpeg;base64,/9j/4AAQ')).toBe(true)
  })

  test('detects GIF data URI', () => {
    expect(isImageOutput('data:image/gif;base64,R0lGODlhAQABAIAAAP')).toBe(true)
  })

  test('detects SVG data URI', () => {
    expect(isImageOutput('data:image/svg+xml;base64,PHN2Zz4=')).toBe(true)
  })

  test('rejects plain text', () => {
    expect(isImageOutput('hello world')).toBe(false)
  })

  test('rejects empty string', () => {
    expect(isImageOutput('')).toBe(false)
  })

  test('rejects non-image data URI', () => {
    expect(isImageOutput('data:text/plain;base64,aGVsbG8=')).toBe(false)
  })

  test('rejects partial data URI', () => {
    expect(isImageOutput('data:image/png,')).toBe(false)
  })
})

// =============================================================================
// parseDataUri — extracts media type and base64 payload
// =============================================================================

describe('parseDataUri', () => {
  test('parses valid PNG data URI', () => {
    const result = parseDataUri('data:image/png;base64,iVBORw0KGgo=')
    expect(result).toEqual({
      mediaType: 'image/png',
      data: 'iVBORw0KGgo=',
    })
  })

  test('parses valid JPEG data URI', () => {
    const result = parseDataUri('data:image/jpeg;base64,/9j/4AAQ')
    expect(result).toEqual({
      mediaType: 'image/jpeg',
      data: '/9j/4AAQ',
    })
  })

  test('handles whitespace around URI', () => {
    const result = parseDataUri('  data:image/png;base64,abc123  ')
    expect(result).toEqual({
      mediaType: 'image/png',
      data: 'abc123',
    })
  })

  test('returns null for non-data URI', () => {
    expect(parseDataUri('https://example.com/image.png')).toBeNull()
  })

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

  test('returns null for incomplete data URI', () => {
    expect(parseDataUri('data:image/png')).toBeNull()
  })

  test('returns null for non-base64 data URI', () => {
    expect(parseDataUri('data:text/plain,hello')).toBeNull()
  })
})

// =============================================================================
// formatOutput — truncates long output with line count
// =============================================================================

describe('formatOutput', () => {
  test('short output passes through unchanged', () => {
    const result = formatOutput('line1\nline2\nline3')
    expect(result.truncatedContent).toBe('line1\nline2\nline3')
    expect(result.totalLines).toBe(3)
    expect(result.isImage).toBe(false)
  })

  test('empty output', () => {
    const result = formatOutput('')
    expect(result.truncatedContent).toBe('')
    expect(result.totalLines).toBe(1)
  })

  test('image output is passed through', () => {
    const img = 'data:image/png;base64,iVBORw0KGgo='
    const result = formatOutput(img)
    expect(result.truncatedContent).toBe(img)
    expect(result.totalLines).toBe(1)
    expect(result.isImage).toBe(true)
  })

  test('single line no trailing newline', () => {
    const result = formatOutput('hello')
    expect(result.totalLines).toBe(1)
  })

  test('truncated output counts only fully-omitted lines', () => {
    const prev = process.env.BASH_MAX_OUTPUT_LENGTH
    process.env.BASH_MAX_OUTPUT_LENGTH = '20'
    try {
      // First line is exactly 20 chars, so it is shown in full and the cut
      // lands on its trailing newline. Only B, C and D are fully omitted.
      const content = `${'A'.repeat(20)}\nB\nC\nD`
      const result = formatOutput(content)
      expect(result.truncatedContent).toContain('[3 lines truncated]')
      expect(result.totalLines).toBe(4)
    } finally {
      if (prev === undefined) {
        delete process.env.BASH_MAX_OUTPUT_LENGTH
      } else {
        process.env.BASH_MAX_OUTPUT_LENGTH = prev
      }
    }
  })

  test('truncated output counts the first line omitted at a line boundary', () => {
    const prev = process.env.BASH_MAX_OUTPUT_LENGTH
    process.env.BASH_MAX_OUTPUT_LENGTH = '20'
    try {
      // The first line is 19 chars + its newline = exactly 20, so the cut lands
      // right after that newline (a line boundary). B, C and D are all fully
      // omitted, so the count must include B even though no newline precedes it
      // in the omitted suffix.
      const content = `${'A'.repeat(19)}\nB\nC\nD`
      const result = formatOutput(content)
      expect(result.truncatedContent).toContain('[3 lines truncated]')
      expect(result.totalLines).toBe(4)
    } finally {
      if (prev === undefined) {
        delete process.env.BASH_MAX_OUTPUT_LENGTH
      } else {
        process.env.BASH_MAX_OUTPUT_LENGTH = prev
      }
    }
  })
})

// =============================================================================
// createContentSummary — MCP content block summaries
// =============================================================================

describe('createContentSummary', () => {
  test('summarizes text blocks', () => {
    const content = [
      { type: 'text' as const, text: 'Hello world' },
    ]
    const result = createContentSummary(content)
    expect(result).toContain('MCP Result')
    expect(result).toContain('1 text block')
    expect(result).toContain('Hello world')
  })

  test('summarizes image blocks', () => {
    // MCP-shaped image blocks (data/mimeType) rather than the SDK's
    // source-wrapped shape; createContentSummary only reads `type`/`text`.
    const content = [
      { type: 'image' as const, data: 'base64data', mimeType: 'image/png' },
    ] as unknown as ContentBlockParam[]
    const result = createContentSummary(content)
    expect(result).toContain('1 image')
  })

  test('summarizes mixed content', () => {
    const content = [
      { type: 'text' as const, text: 'Description' },
      { type: 'image' as const, data: 'base64data', mimeType: 'image/png' },
      { type: 'text' as const, text: 'More text' },
    ] as unknown as ContentBlockParam[]
    const result = createContentSummary(content)
    expect(result).toContain('1 image')
    expect(result).toContain('2 text blocks')
  })

  test('truncates long text preview at 200 chars', () => {
    const longText = 'x'.repeat(300)
    const content = [
      { type: 'text' as const, text: longText },
    ]
    const result = createContentSummary(content)
    expect(result).toContain('...')
    expect(result).toContain('x'.repeat(200))
  })

  test('empty content array', () => {
    const result = createContentSummary([])
    expect(result).toContain('MCP Result')
  })
})

// =============================================================================
// selectFailureOutput — picks the best source for a non-zero-exit failure body
// =============================================================================

describe('selectFailureOutput (#1231)', () => {
  test('prefers the accumulator when it has content', () => {
    expect(selectFailureOutput('build failed\nerror: missing tsc', 'unused', 'also unused'))
      .toBe('build failed\nerror: missing tsc')
  })

  test('falls back to result.stdout when accumulator is empty', () => {
    expect(selectFailureOutput('', 'stdout body', 'unused')).toBe('stdout body')
  })

  test('falls back to result.stdout when accumulator is whitespace-only', () => {
    expect(selectFailureOutput('   \n\n  ', 'real stdout', 'unused')).toBe('real stdout')
  })

  test('recovers from progress.fullOutput when accumulator and result.stdout are both empty', () => {
    expect(selectFailureOutput('', '', 'streamed line 1\nstreamed line 2'))
      .toBe('streamed line 1\nstreamed line 2')
  })

  test('recovers from progress.fullOutput when result.stdout is undefined (shell runner left slot empty)', () => {
    // Reproduces #1231: accumulator was only `\nExit code N` (stripped to ''),
    // result.stdout was empty because the shell runner streamed everything
    // through progress callbacks, but lastProgressFullOutput captured every
    // line on the way through.
    expect(selectFailureOutput('', undefined, 'tsc error TS2305\nbuild halted'))
      .toBe('tsc error TS2305\nbuild halted')
  })

  test('returns empty string when all three sources are empty', () => {
    expect(selectFailureOutput('', undefined, '')).toBe('')
    expect(selectFailureOutput('', '', '')).toBe('')
  })

  test('whitespace-only progressFullOutput does not win over empty result.stdout', () => {
    // Pure whitespace from a progress flush should not be treated as a
    // recoverable failure body — would otherwise leak just "\n" or "   ".
    expect(selectFailureOutput('', '', '   \n  \n')).toBe('')
  })
})
