require "reflexive/reflexive_ripper"

Rspec::Matchers.define :have_scopes do |*expected_scopes|
  match do |actual_scanner_events|
    @actual_scopes = actual_scanner_events.
                       map { |e| e[:meta_scope] }.compact
    @actual_scopes == expected_scopes
  end

  failure_message_for_should do |actual_scanner_events|
    "expected that scanner events: #{ actual_scanner_events } \n" <<
            "(with scopes:                #{ @actual_scopes }) \n" <<
            "would have following scopes: #{ expected_scopes[0] }"
  end
end

Rspec::Matchers.define :have_tags do |*expected_tags|
  match do |actual_scanner_events|
    @actual_tags = actual_scanner_events.map { |e| e[:tags] }.compact
    (expected_tags - @actual_tags).empty?
  end

  failure_message_for_should do |actual_scanner_events|
    "expected that scanner events: #{ actual_scanner_events } \n" <<
            "(with tags:                #{ @actual_tags }) \n" <<
            "would have following tags: #{ expected_tags }"
  end
end

Rspec::Matchers.define :have_exact_tags do |*expected_tags|
  match do |actual_scanner_events|
    @actual_tags = actual_scanner_events.map { |e| e[:tags] }.compact
    expected_tags == @actual_tags  
  end

  failure_message_for_should do |actual_scanner_events|
    "expected that scanner events: #{ actual_scanner_events } \n" <<
            "(with tags:                #{ @actual_tags }) \n" <<
            "would have exact tags: #{ expected_tags[0] }"
  end
end

describe Reflexive::ReflexiveRipper do
  def scanner_events_for(source)
    parser = Reflexive::ReflexiveRipper.new(source)
    parser.parse
    parser.scanner_events
  end

  it "collects scanner events as hashes" do
    scanner_events_for("a = 42").should == [
            {:ident=>"a", :tags=>{:local_variable_assignment=>"1:a"}},
            {:sp=>" "}, {:op=>"="}, {:sp=>" "}, {:int=>"42"}
    ]
  end

  describe "injects method call scanner event tags" do
    specify "m!" do
      scanner_events_for("m!").should(have_tags(
              {:method_call=>{:name=>"m!", :receiver=>:class, :scope => []}}
      ))
    end

    specify "A.b!()" do
      scanner_events_for("A.b!").should(have_tags(
              {:method_call=>{:name=>"b!", :receiver=>["A"], :scope => []}}
      ))
    end

    specify "A::B.c!()" do
      scanner_events_for("A::B.c!()").should(have_tags(
              {:method_call=>{:name=>"c!", :receiver=>["A::B"], :scope => []}}
      ))
    end

    specify "module A; B.c!(); end" do
      scanner_events_for("module A; B.c!(); end").should(have_tags(
              {:method_call=>{:name=>"c!", :receiver=>["B"], :scope => ["A"]}}
      ))
    end
  end

  describe "injects local variable assignment event tags" do
    specify "v = 1" do
      scanner_events_for("a = 42").should(have_tags(
              :local_variable_assignment=>"1:a"))
    end

    specify "def m(arg) end" do
      scanner_events_for("def m(arg); end").should(have_tags(
              :local_variable_assignment=>"2:arg"))
    end

    specify "a, b = 1, 2" do
      scanner_events_for("a, b = 1, 2").should(have_tags(
              {:local_variable_assignment=>"1:a"},
              {:local_variable_assignment=>"1:b"}))
    end

    specify "a, b, *c, d = 1, 2" do
      scanner_events_for("a, b, *c, d = 1, 2").should(have_tags(
              {:local_variable_assignment=>"1:a"},
              {:local_variable_assignment=>"1:b"},
              {:local_variable_assignment=>"1:c"},
              {:local_variable_assignment=>"1:d"}
      ))
    end

    specify "1.tap { (dv = 1).tap { puts dv } }" do
      scanner_events_for("1.tap { (dv = 1).tap { puts dv } }").should(have_exact_tags(
              {:local_variable_assignment=>"2:dv"},
              {:method_call=>{:name=>"puts", :receiver=>:class, :scope => []}},
              {:local_variable_access=>"2:dv"}
      ))
    end

    specify "1.tap { x = 1; 1.tap { puts x } }" do
      scanner_events_for("1.tap { x = 1; 1.tap { puts x } }").should(have_tags(
              {:local_variable_assignment=>"2:x"},
              {:local_variable_assignment=>"2:x"}))
    end

    specify "tv = 1; def m; puts tv; end" do
      scanner_events_for("tv = 1; def m; puts tv; end").should(have_tags(
              {:local_variable_assignment=>"1:tv"},
              {:method_call=>{:name=>"puts", :receiver=>:instance, :scope => []}},
              {:method_call=>{:name=>"tv", :receiver=>:instance, :scope => []}}
      ))
    end

  end

  describe "scope handling" do
    describe "for method calls" do
      specify "from top level" do
        scanner_events_for("m()").should(have_tags(
              {:method_call=>{:name=>"m", :receiver=>:class, :scope => []}}
        ))
      end

      specify "class definition level" do
        scanner_events_for("class C; cm(); end").should(have_tags(
              {:method_call=>{:name=>"cm", :receiver=>:class, :scope => ["C"]}}
        ))
        scanner_events_for("module M; cm(); end").should(have_tags(
              {:method_call=>{:name=>"cm", :receiver=>:class, :scope => ["M"]}}
        ))
      end

      specify "class instance level" do
        scanner_events_for("class C; def im1; im2; end end").should(have_tags(
              {:method_call=>{:name=>"im2", :receiver=>:instance, :scope => ["C"]}}
        ))
        scanner_events_for("module M; def im1; im2; end end").should(have_tags(
              {:method_call=>{:name=>"im2", :receiver=>:instance, :scope => ["M"]}}
        ))
      end

      specify "class singleton level" do
        scanner_events_for("class C; def self.cm1() cm2; end end").should(have_tags(
              {:method_call=>{:name=>"cm2", :receiver=>:class, :scope => ["C"]}}
        ))
        scanner_events_for("module M; def self.cm1() cm2; end end").should(have_tags(
              {:method_call=>{:name=>"cm2", :receiver=>:class, :scope => ["M"]}}
        ))
      end

      specify "class singleton level for methods defined with class << self" do
        scanner_events_for("class C; class << self; def cm1(); cm2; end end end").should(have_tags(
              {:method_call=>{:name=>"cm2", :receiver=>:class, :scope => ["C"]}}
        ))
      end

      specify "nested method calls with constant references" do
        src = <<-RUBY
          module M
            class C
            end
            C.new
          end
        RUBY
        scanner_events_for(src).should(have_tags(
              {:method_call=>{:name=>"new", :receiver=>["C"], :scope => ["M"]}}
        ))
      end
    end

    describe "for constant references" do
      specify "top level" do
        scanner_events_for("C").should(have_tags(
              :constant_access=>{:name=>"C", :scope=>[]}
        ))
      end

      specify "path" do
        scanner_events_for("A::B").should(have_tags(
              {:constant_access=>{:name=>"A", :scope=>[]}},
              {:constant_access=>{:name=>"A::B", :scope=>[]}}
        ))
        scanner_events_for("A::B::C").should(have_tags(
              {:constant_access=>{:name=>"A", :scope=>[]}},
              {:constant_access=>{:name=>"A::B", :scope=>[]}},
              {:constant_access=>{:name=>"A::B::C", :scope=>[]}}
        ))
      end

      specify "top path" do
        scanner_events_for("::A::B").should(have_tags(
              {:constant_access=>{:name=>"::A::B", :scope=>[]}}
        ))
      end

      specify "nested" do
        scanner_events_for("module A; module B; C; end end").should(have_tags(
              {:constant_access=>{:name=>"A", :scope=>[]}},
              {:constant_access=>{:name=>"B", :scope=>["A"]}},
              {:constant_access=>{:name=>"C", :scope=>["A", "B"]}}
        ))
        scanner_events_for("module A; class B; C; end end").should(have_tags(
              {:constant_access=>{:name=>"A", :scope=>[]}},
              {:constant_access=>{:name=>"B", :scope=>["A"]}},
              {:constant_access=>{:name=>"C", :scope=>["A", "B"]}}
        ))
      end

      specify "combined (nested with path)" do
        scanner_events_for("module M; C::D; end").should(have_tags(
                {:constant_access=>{:name=>"M", :scope=>[]}},
                {:constant_access=>{:name=>"C", :scope=>["M"]}},
                {:constant_access=>{:name=>"C::D", :scope=>["M"]}}
        ))
      end

      specify "combined (nested with top level)" do
        scanner_events_for("module M; ::C::D; end").should(have_tags(
              {:constant_access=>{:name=>"M", :scope=>[]}},
              {:constant_access=>{:name=>"::C::D", :scope=>["M"]}}
        ))
      end

      specify "from instance methods" do
        scanner_events_for("def m; C; end").should(have_tags(
              {:constant_access=>{:name=>"C", :scope=>[]}}
        ))

      end

      specify "from instance methods params" do
        scanner_events_for("def m(a = C.new(m1), b = D.new(m2)); end").should(have_tags(
              {:constant_access=>{:name=>"C", :scope=>[]}},
              {:constant_access=>{:name=>"D", :scope=>[]}}
        ))
      end

      specify "for superclass" do
        scanner_events_for("class C < D; end").should(have_tags(
              {:constant_access=>{:name=>"D", :scope=>[]}}
        ))
      end

      specify "autoload hint" do
        scanner_events_for("module M; autoload :C; end").should(have_tags(
              {:constant_access=>{:name=>"C", :scope=>["M"]}}
        ))

        scanner_events_for("module M; autoload 'C'; end").should(have_tags(
              {:constant_access=>{:name=>"C", :scope=>["M"]}}
        ))

        scanner_events_for("module M; autoload('C'); end").should(have_tags(
              {:constant_access=>{:name=>"C", :scope=>["M"]}}
        ))
      end
    end
  end

  describe "injects scope meta events" do
    example "simple class def" do
      scanner_events_for("class C; end").should(
        have_scopes(["C"], []))
    end

    example "nested class def" do
      scanner_events_for("class C; class D; end end").should(
        have_scopes(["C"], ["C", "D"], ["C"], []))
    end

    example "class inside module def" do
      scanner_events_for("module M; class C; end end").should(
              have_scopes(["M"], ["M", "C"], ["M"], []))
    end

    it "handles const_path_ref" do
      scanner_events_for("class Const::PathRef < ::TopConstRef; end").should(
              have_scopes(["Const::PathRef"], []))
    end

    it "handles top_const_ref" do
      scanner_events_for("class ::TopConstRef; end").should(
              have_scopes(["::TopConstRef"], []))
    end
  end

  #  describe "injects require/load argument tag" do
  #    specify "require" do
  #      scanner_events_for("require 'f'").should(have_tags(
  #          {:local_variable_assignment=>"1:tv"}
  #      ))
  #    end
  #  end

  it "shouldn't get confused with non-scope changing const refs" do
    scanner_events_for("v = :class; SomeConst = 123").should(
              have_scopes())
  end

  it "should handle complex arbitrary nesting" do
    src = <<-RUBY
      class TC
        def m
        end
      end

      module M1
        def m
        end

        module M2
          def m
          end
          class C1

          end
          class C1
            def m
            end
          end
        end

        class C3
          def m
          end
        end

      end

      module M1::M2
        class C4
          module M4
            def m
            end
          end

          class C5
            module M3
            end
            class C6
            end
          end
        end
      end
    RUBY
    scanner_events_for(src).should(
              have_scopes(["TC"],
                           [], 
                           ["M1"],
                           ["M1", "M2"],
                           ["M1", "M2", "C1"],
                           ["M1", "M2"],
                           ["M1", "M2", "C1"],
                           ["M1", "M2"],
                           ["M1"],
                           ["M1", "C3"],
                           ["M1"],
                           [],
                           ["M1::M2"],
                           ["M1::M2", "C4"],
                           ["M1::M2", "C4", "M4"],
                           ["M1::M2", "C4"],
                           ["M1::M2", "C4", "C5"],
                           ["M1::M2", "C4", "C5", "M3"],
                           ["M1::M2", "C4", "C5"],
                           ["M1::M2", "C4", "C5", "C6"],
                           ["M1::M2", "C4", "C5"],
                           ["M1::M2", "C4"],
                           ["M1::M2"],
                           []))
  end
end
