require 'helper'

class TestSourceRewriter < Minitest::Test
  def setup
    @buf = Parser::Source::Buffer.new('(rewriter)')
    @buf.source = 'foo bar baz'

    @rewriter = Parser::Source::Rewriter.new(@buf)
  end

  def range(from, len)
    Parser::Source::Range.new(@buf, from, from + len)
  end

  def test_remove
    assert_equal 'foo  baz',
                 @rewriter.
                    remove(range(4, 3)).
                    process
  end

  def test_insert_before
    assert_equal 'foo quux bar baz',
                 @rewriter.
                    insert_before(range(4, 3), 'quux ').
                    process
  end

  def test_insert_after
    assert_equal 'foo bar quux baz',
                 @rewriter.
                    insert_after(range(4, 3), ' quux').
                    process
  end

  def test_replace
    assert_equal 'foo quux baz',
                 @rewriter.
                    replace(range(4, 3), 'quux').
                    process
  end

  def test_composing_asc
    assert_equal 'foo---bar---baz',
                 @rewriter.
                    replace(range(3, 1), '---').
                    replace(range(7, 1), '---').
                    process
  end

  def test_composing_desc
    assert_equal 'foo---bar---baz',
                 @rewriter.
                    replace(range(7, 1), '---').
                    replace(range(3, 1), '---').
                    process
  end

  #
  # Merging/clobbering of overlapping edits
  #

  def test_insertion_just_before_replace
    assert_equal 'foostrawberry jam---bar baz',
      @rewriter.
        replace(range(3, 1), '---').
        insert_before(range(3, 1), 'strawberry jam').
        process
  end

  def test_insertion_just_after_replace
    assert_equal 'foo---strawberry jam baz',
      @rewriter.
        replace(range(3, 4), '---').
        insert_after(range(3, 4), 'strawberry jam').
        process
  end

  def test_insertion_just_before_remove
    assert_equal 'foostrawberry jambar baz',
      @rewriter.
        remove(range(3, 1)).
        insert_before(range(3, 1), 'strawberry jam').
        process
  end

  def test_insertion_just_after_remove
    assert_equal 'foostrawberry jam baz',
      @rewriter.
        remove(range(3, 4)).
        insert_after(range(3, 4), 'strawberry jam').
        process
  end

  def test_insertion_just_before_replace_at_buffer_start
    assert_equal 'strawberry jam--- bar baz',
      @rewriter.
        replace(range(0, 3), '---').
        insert_before(range(0, 1), 'strawberry jam').
        process
  end

  def test_insertion_just_after_replace_at_buffer_end
    assert_equal 'foo bar ---strawberry jam',
      @rewriter.
        replace(range(8, 3), '---').
        insert_after(range(9, 2), 'strawberry jam').
        process
  end

  def test_insertion_just_before_remove_at_buffer_start
    assert_equal 'strawberry bar baz',
      @rewriter.
        remove(range(0, 3)).
        insert_before(range(0, 1), 'strawberry').
        process
  end

  def test_insertion_just_after_remove_at_buffer_end
    assert_equal 'foo bar strawberry',
      @rewriter.
        remove(range(8, 3)).
        insert_after(range(10, 1), 'strawberry').
        process
  end

  def test_multiple_insertions_at_same_location_clobber
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        insert_before(range(0, 11), '<').
        insert_after( range(0, 11), '>').
        insert_before(range(0, 7), '(')
    end
  end

  def test_intentional_multiple_insertions_at_same_location
    assert_equal 'foo [(bar)] baz',
      @rewriter.
        insert_before_multi(range(4, 0), '(').
        insert_after_multi(range(7, 0), ')').
        insert_before_multi(range(4, 0), '[').
        insert_after_multi(range(7, 0), ']').
        process
  end

  def test_insertion_within_replace_clobber
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        replace(range(3, 2), '<').
        insert_after(range(3, 1), '>')
    end
  end

  def test_insertion_within_remove_clobber
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        remove(range(3, 2)).
        insert_after(range(3, 1), '>')
    end
  end

  def test_replace_overlapping_insertion_clobber
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        insert_after(range(3, 1), '>').
        replace(range(3, 2), '<')
    end
  end

  def test_remove_overlapping_insertion_clobber
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        insert_after(range(3, 1), '>').
        remove(range(3, 2))
    end
  end

  def test_multi_insertion_within_replace_clobber
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        replace(range(3, 2), '<').
        insert_after_multi(range(3, 1), '>')
    end
  end

  def test_multi_insertion_within_remove_clobber
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        remove(range(3, 2)).
        insert_after_multi(range(3, 1), '>')
    end
  end

  def test_replace_overlapping_multi_insertion_clobber
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        insert_after_multi(range(3, 1), '>').
        replace(range(3, 2), '<')
    end
  end

  def test_remove_overlapping_multi_insertion_clobber
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        insert_after_multi(range(3, 1), '>').
        remove(range(3, 2))
    end
  end

  def test_insertion_on_merged_insertion_clobber
    # 2 insertions at the same point clobber each other, even if the 1st one
    # was merged with an adjacent edit, and even if the same text is being
    # inserted

    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        insert_before(range(3, 1), '>').
        remove(range(3, 2)).
        insert_after(range(2, 1), '>')
    end
  end

  def test_insertion_merge_with_overlapping_replace
    assert_equal 'fo abc bar baz',
      @rewriter.
        insert_before(range(3, 1), 'abc').
        replace(range(2, 2), ' abc ').
        process
  end

  def test_replace_merge_with_overlapped_insertion
    assert_equal 'fo abc bar baz',
      @rewriter.
        replace(range(2, 2), ' abc ').
        insert_before(range(3, 1), 'abc').
        process
  end

  def test_replace_same_begin_larger_than_replaced_range_matching
    assert_equal 'foo supercalifragilistic baz',
      @rewriter.
        replace(range(4, 3), 'super').
        replace(range(4, 3), 'supercalifragilistic').
        process
  end

  def test_replace_same_begin_larger_than_replaced_range_non_matching
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        replace(range(4, 3), 'super!').
        replace(range(4, 3), 'supercalifragilistic')
    end
  end

  def test_overlapping_replace_left_smaller_than_replaced_matching
    assert_equal 'superbaz',
      @rewriter.
        replace(range(0, 7), 'super').
        replace(range(2, 6), 'per').
        process
  end

  def test_overlapping_replace_left_smaller_than_replaced_non_matching
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        replace(range(0, 7), 'super').
        replace(range(2, 8), 'perk')
    end
  end

  def test_overlapping_replace_left_larger_right_smaller_matching
    assert_equal 'foods baz',
      @rewriter.
        replace(range(1, 3), 'oods ').
        replace(range(3, 6), 'ds b').
        process
  end

  def test_overlapping_replace_left_larger_right_larger_matching
    assert_equal 'foods abcdefghijklm',
      @rewriter.
        replace(range(1, 3), 'oods ').
        replace(range(3, 8), 'ds abcdefghijklm').
        process
  end

  def test_overlapping_replace_left_larger_right_smaller_non_matching
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        replace(range(1, 3), 'oods ').
        replace(range(3, 6), 'ds')
    end
  end

  def test_overlapping_replace_left_larger_right_larger_non_matching
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        replace(range(1, 3), 'oods b').
        replace(range(3, 8), 'ds abcdefghijklm')
    end
  end

  def test_subsuming_replace_both_smaller_matching
    assert_equal 'food baz',
      @rewriter.
        replace(range(0, 7), 'food').
        replace(range(3, 3), 'd').
        process
  end

  def test_subsuming_replace_both_smaller_non_matching
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        replace(range(0, 7), 'foo').
        replace(range(3, 3), 'd')
    end
  end

  def test_subsuming_replace_both_larger_matching
    assert_equal 'foo barr baz',
      @rewriter.
        replace(range(0, 7), 'foo barr').
        replace(range(3, 3), ' bar').
        process
  end

  def test_subsuming_replace_both_larger_non_matching
    silence_diagnostics

    assert_raises Parser::ClobberingError do
      @rewriter.
        replace(range(0, 7), 'foo barr').
        replace(range(3, 3), ' bar ')
    end
  end

  def test_replaced_ranges_merge_when_furthest_right_range_is_not_furthest_left
    # regression test; previously, when actions were merged, the resulting
    # replaced range could be too small sometimes
    assert_equal 'foo_***_***',
      @rewriter.
        replace(range(3, 1), '_').
        replace(range(7, 1), '_').
        replace(range(4, 7), '***_***').
        process
  end

  def test_clobber
    diagnostics = []
    @rewriter.diagnostics.consumer = lambda do |diag|
      diagnostics << diag
    end

    assert_raises Parser::ClobberingError do
      @rewriter.
        replace(range(3, 1), '---').
        remove(range(3, 1))
    end

    assert_equal 2, diagnostics.count

    assert_equal :error, diagnostics.first.level
    assert_equal 'cannot remove 1 character(s)',
                 diagnostics.first.message
    assert_equal range(3, 1), diagnostics.first.location

    assert_equal :note, diagnostics.last.level
    assert_equal "clobbered by: replace 1 character(s) with \"---\"",
                 diagnostics.last.message
    assert_equal range(3, 1), diagnostics.last.location
  end

  def test_clobbering_error_backward_compatibility
    silence_diagnostics

    rescued = false

    # We use begin..rescue..end here rather than #assert_raises
    # since #assert_raises expects exact error class.
    begin
      @rewriter.
        replace(range(3, 1), '---').
        remove(range(3, 1))
    rescue RuntimeError => error
      rescued = true if error.message.include?('clobber')
    end

    assert rescued
  end


  def test_overlapping_delete
    assert_equal 'faz',
                 @rewriter.
                   remove(range(1, 4)).
                   remove(range(6, 3)).
                   remove(range(4, 3)).
                   process
  end

  def test_overlapping_replace
    assert_equal 'flippin flyin flapjackz',
                 @rewriter.
                   replace(range(1, 4), 'lippin f').
                   replace(range(4, 4), 'pin flyin flap').
                   replace(range(7, 3), ' flyin flapjack').
                   process
  end

  def test_subsuming_delete
    assert_equal 'foo',
                 @rewriter.
                   remove(range(6, 3)).
                   remove(range(7, 2)).
                   remove(range(3, 8)).
                   process
  end

  def test_subsuming_replace
    assert_equal 'freebie',
                 @rewriter.
                   replace(range(3, 3), 'ebi').
                   replace(range(1, 10), 'reebie').
                   replace(range(5, 2), 'ie').
                   process
  end

  def test_equivalent_delete_insert_replace
    # A deletion + insertion just before or after the deleted range is
    # identical in every way to a replacement! So logically, they shouldn't
    # conflict.
    assert_equal 'tin bar baz',
      @rewriter.
        remove(range(0, 3)).  # ' bar baz'
        insert_before(range(0, 1), 'tin'). # 'tin bar baz'
        replace(range(0, 3), 'tin').
        process
  end

  def test_transaction_returns_self
    assert_equal @rewriter, @rewriter.transaction {}
  end

  def test_transaction_commit
    silence_diagnostics

    # Original: 'foo bar baz'

    # Rewrite as 'foo BAR baz'
    @rewriter.replace(range(4, 3), 'BAR')

    # Rewrite as '( bar )'
    @rewriter.transaction do
      @rewriter.replace(range(0, 3), '(')
      @rewriter.replace(range(8, 3), ')')
    end

    @rewriter.replace(range(3, 1), '_')
    @rewriter.replace(range(7, 1), '_')

    assert_equal '(_BAR_)', @rewriter.process
  end

  def test_transaction_rollback
    silence_diagnostics

    # Original: 'foo bar baz'

    # Rewrite as 'foo bar BAZ'
    @rewriter.replace(range(8, 3), 'BAZ')

    assert_raises Parser::ClobberingError do
      # Trying to rewrite as '( bar )', but it fails
      @rewriter.transaction do
        @rewriter.replace(range(0, 3), '(')
        @rewriter.replace(range(8, 3), ')')
      end
    end

    @rewriter.replace(range(0, 3), 'FOO')

    assert_equal 'FOO bar BAZ', @rewriter.process
  end

  def test_nested_transaction_raises_error
    error = assert_raises RuntimeError do
      @rewriter.transaction do
        @rewriter.transaction do
        end
      end
    end

    assert_match /nested/i, error.message
  end

  def test_process_in_transaction_raises_error
    error = assert_raises RuntimeError do
      @rewriter.transaction do
        @rewriter.process
      end
    end

    assert_match /transaction/, error.message
  end

  def silence_diagnostics
    @rewriter.diagnostics.consumer = proc {}
  end
end
