require 'tempfile'
require 'rbconfig'

##
# UnitDiff makes reading Test::Unit output easy and fun.  Instead of a
# confusing jumble of text with nearly unnoticable changes like this:
#
#   1) Failure:
#   test_to_gpoints(RouteTest) [test/unit/route_test.rb:29]:
#   <"new GPolyline([\n  new GPoint(  47.00000, -122.00000),\n  new GPoint(  46.5000
#   0, -122.50000),\n  new GPoint(  46.75000, -122.75000),\n  new GPoint(  46.00000,
#    -123.00000)])"> expected but was
#   <"new Gpolyline([\n  new GPoint(  47.00000, -122.00000),\n  new GPoint(  46.5000
#   0, -122.50000),\n  new GPoint(  46.75000, -122.75000),\n  new GPoint(  46.00000,
#    -123.00000)])">.
#
#
# You get an easy-to-read diff output like this:
#
#   1) Failure:
#   test_to_gpoints(RouteTest) [test/unit/route_test.rb:29]:
#   1c1
#   < new GPolyline([
#   ---
#   > new Gpolyline([
#
# == Usage
#
#   test.rb | unit_diff [options]
#     options:
#     -b ignore whitespace differences
#     -c contextual diff
#     -h show usage
#     -k keep temp diff files around
#     -l prefix line numbers on the diffs
#     -u unified diff [default]
#     -p plain diff
#     -v display version

class UnitDiff

  WINDOZE = RbConfig::CONFIG['host_os'] =~ /mswin|mingw/

  DIFF = if WINDOZE
           'diff.exe'
         else
           if system("gdiff", __FILE__, __FILE__)
             'gdiff' # solaris and kin suck
           else
             'diff'
           end
         end unless defined? DIFF

  ##
  # Handy wrapper for UnitDiff#unit_diff.

  def self.unit_diff
    trap 'INT' do exit 1 end
    puts UnitDiff.new.unit_diff
  end

  def parse_input(input, output)
    current = []
    data = []
    data << current
    print_lines = true

    term = "\nFinished".split(//).map { |c| c[0] }
    term_length = term.size

    old_sync = output.sync
    output.sync = true
    while line = input.gets
      case line
      when /^(Loaded suite|Started|# Running tests:)/ then
        print_lines = true
        output.puts line
        chars = []
        while c = input.getc do
          output.putc c
          chars << c
          tail = chars[-term_length..-1]
          break if chars.size >= term_length and tail == term
        end
        output.puts input.gets # the rest of "Finished in..."
        output.puts
        next
      when /^\s*$/, /^\(?\s*\d+\) (Failure|Error):/, /^\d+\)/ then
        print_lines = false
        current = []
        data << current
      when /^Finished in \d/ then
        print_lines = false
      end
      output.puts line if print_lines
      current << line
    end
    output.sync = old_sync
    data = data.reject { |o| o == ["\n"] or o.empty? }
    footer = data.pop

    data.map do |result|
      break if result.any? { |l| l =~ / expected( but was|, not)/ }

      header = result.find do |l|
        l =~ /^\(?\s*\d+\) (Failure|Error):/
      end

      break unless header

      message_index = result.index(header) + 2

      result[message_index..-1] = result[message_index..-1].join
    end

    return data, footer
  end

  # Parses a single diff recording the header and what
  # was expected, and what was actually obtained.
  def parse_diff(result)
    header = []
    expect = []
    butwas = []
    footer = []
    state = :header

    until result.empty? do
      case state
      when :header then
        header << result.shift
        state = :expect if result.first =~ /^<|^Expected/
      when :expect then
        case result.first
        when /^Expected (.*?) to equal (.*?):$/ then
          expect << $1
          butwas << $2
          state = :footer
          result.shift
        when /^Expected (.*?), not (.*)$/m then
          expect << $1
          butwas << $2
          state = :footer
          result.shift
        when /^Expected (.*?)$/ then
          expect << "#{$1}\n"
          result.shift
        when /^to equal / then
          state = :spec_butwas
          bw = result.shift.sub(/^to equal (.*):?$/, '\1')
          butwas << bw
        else
          state = :butwas if result.first.sub!(/ expected( but was|, not)/, '')
          expect << result.shift
        end
      when :butwas then
        butwas = result[0..-1]
        result.clear
      when :spec_butwas then
        if result.first =~ /^\s+\S+ at |^:\s*$/
          state = :footer
        else
          butwas << result.shift
        end
      when :footer then
        butwas.last.sub!(/:$/, '')
        footer = result.map {|l| l.chomp }
        result.clear
      else
        raise "unknown state #{state}"
      end
    end

    return header, expect, nil, footer if butwas.empty?

    expect.last.chomp!
    expect.first.sub!(/^<\"/, '')
    expect.last.sub!(/\">$/, '')

    butwas.last.chomp!
    butwas.last.chop! if butwas.last =~ /\.$/
    butwas.first.sub!( /^<\"/, '')
    butwas.last.sub!(/\">$/, '')

    return header, expect, butwas, footer
  end

  ##
  # Scans Test::Unit output +input+ looking for comparison failures and makes
  # them easily readable by passing them through diff.

  def unit_diff(input=ARGF, output=$stdout)
    $b = false unless defined? $b
    $c = false unless defined? $c
    $k = false unless defined? $k
    $u = true  unless defined? $u
    $p = false unless defined? $p

    data, footer = self.parse_input(input, output)

    output = []

    # Output
    data.each do |result|
      if result.first =~ /Error/ then
        output.push result.join('')
        next
      end

      prefix, expect, butwas, result_footer = parse_diff(result)

      output.push prefix.compact.map {|line| line.strip}.join("\n")

      if butwas then
        output.push self.diff(expect, butwas)

        output.push result_footer
        output.push ''
      else
        output.push expect.join('')
      end
    end

    if footer then
      footer.shift if footer.first.strip.empty?
      output.push footer.compact.map {|line| line.strip}.join("\n")
    end

    return output.flatten.join("\n")
  end

  def diff expect, butwas
    output = nil

    Tempfile.open("expect") do |a|
      a.write(massage(expect))
      a.rewind
      Tempfile.open("butwas") do |b|
        b.write(massage(butwas))
        b.rewind

        diff_flags = $p ? "" : $c ? "-c" : "-u"
        diff_flags += " -b" if $b

        result = `#{DIFF} #{diff_flags} #{a.path} #{b.path}`
        result.sub!(/^\-\-\- .+/, "--- expected")
        result.sub!(/^\+\+\+ .+/, "+++ actual")

        output = if result.empty? then
                   "[no difference--suspect ==]"
                 else
                   result.split(/\n/)
                 end

        if $k then
          warn "moving #{a.path} to #{a.path}.keep"
          File.rename a.path, a.path + ".keep"
          warn "moving #{b.path} to #{b.path}.keep"
          File.rename b.path, b.path + ".keep"
        end
      end
    end

    output
  end

  def massage(data)
    # unescape newlines, strip <> from entire string
    data = data.join
    data = data.gsub(/\\n/, "\n").gsub(/0x[a-f0-9]+/m, '0xXXXXXX') + "\n"
    data += "\n" unless data[-1] == ?\n
    data
  end
end
