require "../../../spec_helper"

module Ameba::Rule::Lint
  describe UselessAssign do
    subject = UselessAssign.new

    it "does not report used assigments" do
      s = Source.new %(
        def method
          a = 2
          a
        end
      )
      subject.catch(s).should be_valid
    end

    it "reports a useless assignment in a method" do
      s = Source.new %(
        def method
          a = 2
        end
      )
      subject.catch(s).should_not be_valid
    end

    it "reports a useless assignment in a proc" do
      s = Source.new %(
        ->() {
          a = 2
        }
      )
      subject.catch(s).should_not be_valid
    end

    it "reports a useless assignment in a block" do
      s = Source.new %(
        def method
          3.times do
            a = 1
          end
        end
      )
      subject.catch(s).should_not be_valid
    end

    it "reports a useless assignment in a proc inside def" do
      s = Source.new %(
        def method
          ->() {
            a = 2
          }
        end
      )
      subject.catch(s).should_not be_valid
    end

    it "does not report ignored assigments" do
      s = Source.new %(
        payload, _header = decode
        puts payload
      )
      subject.catch(s).should be_valid
    end

    it "reports a useless assignment in a proc inside a block" do
      s = Source.new %(
        def method
          3.times do
            ->() {
              a = 2
            }
          end
        end
      )
      subject.catch(s).should_not be_valid
    end

    it "reports rule, position and a message" do
      s = Source.new %(
        def method
          a = 2
        end
      ), "source.cr"
      subject.catch(s).should_not be_valid

      issue = s.issues.first
      issue.rule.should_not be_nil
      issue.location.to_s.should eq "source.cr:2:3"
      issue.message.should eq "Useless assignment to variable `a`"
    end

    it "does not report useless assignment of instance var" do
      s = Source.new %(
        class Cls
          def initialize(@name)
          end
        end
      )
      subject.catch(s).should be_valid
    end

    it "does not report if assignment used in the inner block scope" do
      s = Source.new %(
        def method
          var = true
          3.times { var = false }
        end
      )
      subject.catch(s).should be_valid
    end

    it "reports if assigned is not referenced in the inner block scope" do
      s = Source.new %(
        def method
          var = true
          3.times {}
        end
      )
      subject.catch(s).should_not be_valid
    end

    it "doesn't report if assignment in referenced in inner block" do
      s = Source.new %(
        def method
          two = true

          3.times do
            mutex.synchronize do
              two = 2
            end
          end

          two.should be_true
        end
      )
      subject.catch(s).should be_valid
    end

    it "reports if first assignment is useless" do
      s = Source.new %(
        def method
          var = true
          var = false
          var
        end
      )
      subject.catch(s).should_not be_valid
      s.issues.first.location.to_s.should eq ":2:3"
    end

    it "reports if variable reassigned and not used" do
      s = Source.new %(
        def method
          var = true
          var = false
        end
      )
      subject.catch(s).should_not be_valid
    end

    it "does not report if variable used in a condition" do
      s = Source.new %(
        def method
          a = 1
          if a
            nil
          end
        end
      )
      subject.catch(s).should be_valid
    end

    it "reports second assignment as useless" do
      s = Source.new %(
        def method
          a = 1
          a = a + 1
        end
      )
      subject.catch(s).should_not be_valid
    end

    it "does not report if variable is referenced in other assignment" do
      s = Source.new %(
        def method
          if f = get_something
            @f = f
          end
        end
      )
      subject.catch(s).should be_valid
    end

    it "does not report if variable is referenced in a setter" do
      s = Source.new %(
        def method
          foo = 2
          table[foo] ||= "bar"
        end
      )
      subject.catch(s).should be_valid
    end

    it "does not report if variable is reassigned but not referenced" do
      s = Source.new %(
        def method
          foo = 1
          puts foo
          foo = 2
        end
      )
      subject.catch(s).should_not be_valid
    end

    it "does not report if variable is referenced in a call" do
      s = Source.new %(
        def method
          if f = FORMATTER
            @formatter = f.new
          end
        end
      )
      subject.catch(s).should be_valid
    end

    it "does not report if a setter is invoked with operator assignment" do
      s = Source.new %(
        def method
          obj = {} of Symbol => Int32
          obj[:name] = 3
        end
      )
      subject.catch(s).should be_valid
    end

    context "when transformed" do
      it "does not report if the first arg is transformed and not used" do
        s = Source.new %(
          collection.each do |(a, b)|
            puts b
          end
        )
        subject.catch(s).should be_valid
      end

      it "does not report if the second arg is transformed and not used" do
        s = Source.new %(
          collection.each do |(a, b)|
            puts a
          end
        )
        subject.catch(s).should be_valid
      end

      it "does not report if all transformed args are not used in a block" do
        s = Source.new %(
          collection.each do |(foo, bar), (baz, _qux), index, object|
          end
        )
        subject.catch(s).should be_valid
      end
    end

    it "does not report if assignment is referenced in a proc" do
      s = Source.new %(
        def method
          called = false
          ->() { called = true }
          called
        end
      )
      subject.catch(s).should be_valid
    end

    it "reports if variable is shadowed in inner scope" do
      s = Source.new %(
        def method
          i = 1
          3.times do |i|
            i + 1
          end
        end
      )
      subject.catch(s).should_not be_valid
    end

    it "does not report if parameter is referenced after the branch" do
      s = Source.new %(
        def method(param)
          3.times do
            param = 3
          end
          param
        end
      )
      subject.catch(s).should be_valid
    end

    context "op assigns" do
      it "does not report if variable is referenced below the op assign" do
        s = Source.new %(
          def method
            a = 1
            a += 1
            a
          end
        )
        subject.catch(s).should be_valid
      end

      it "does not report if variable is referenced in op assign few times" do
        s = Source.new %(
          def method
            a = 1
            a += 1
            a += 1
            a = a + 1
            a
          end
        )
        subject.catch(s).should be_valid
      end

      it "reports if variable is not referenced below the op assign" do
        s = Source.new %(
          def method
            a = 1
            a += 1
          end
        )
        subject.catch(s).should_not be_valid
      end

      it "reports rule, location and a message" do
        s = Source.new %(
          def method
            b = 2
            a = 3
            a += 1
          end
        ), "source.cr"
        subject.catch(s).should_not be_valid

        issue = s.issues.last
        issue.rule.should_not be_nil
        issue.location.to_s.should eq "source.cr:4:3"
        issue.message.should eq "Useless assignment to variable `a`"
      end
    end

    context "multi assigns" do
      it "does not report if all assigns are referenced" do
        s = Source.new %(
          def method
            a, b = {1, 2}
            a + b
          end
        )
        subject.catch(s).should be_valid
      end

      it "reports if one assign is not referenced" do
        s = Source.new %(
          def method
            a, b = {1, 2}
            a
          end
        )
        subject.catch(s).should_not be_valid
        issue = s.issues.first
        issue.location.to_s.should eq ":2:6"
        issue.message.should eq "Useless assignment to variable `b`"
      end

      it "reports if both assigns are reassigned and useless" do
        s = Source.new %(
          def method
            a, b = {1, 2}
            a, b = {3, 4}
          end
        )
        subject.catch(s).should_not be_valid
      end

      it "reports if both assigns are not referenced" do
        s = Source.new %(
          def method
            a, b = {1, 2}
          end
        )
        subject.catch(s).should_not be_valid

        issue = s.issues.first
        issue.location.to_s.should eq ":2:3"
        issue.message.should eq "Useless assignment to variable `a`"

        issue = s.issues.last
        issue.location.to_s.should eq ":2:6"
        issue.message.should eq "Useless assignment to variable `b`"
      end
    end

    context "top level" do
      it "reports if assignment is not referenced" do
        s = Source.new %(
          a = 1
          a = 2
        )
        subject.catch(s).should_not be_valid
        s.issues.size.should eq 2
        s.issues.first.location.to_s.should eq ":1:1"
        s.issues.last.location.to_s.should eq ":2:1"
      end

      it "doesn't report if assignments are referenced" do
        s = Source.new %(
          a = 1
          a += 1
          a

          b, c = {1, 2}
          b
          c
        )
        subject.catch(s).should be_valid
      end

      it "doesn't report if assignment is captured by block" do
        s = Source.new %(
          a = 1

          3.times do
            a = 2
          end
        )
        subject.catch(s).should be_valid
      end

      it "doesn't report if assignment initialized and captured by block" do
        s = Source.new %(
          a : String? = nil

          1.times do
            a = "Fotis"
          end
        )
        subject.catch(s).should be_valid
      end

      it "doesn't report if this is a record declaration" do
        s = Source.new %(
          record Foo, foo = "foo"
        )
        subject.catch(s).should be_valid
      end

      it "does not report if assignment is referenced after the record declaration" do
        s = Source.new %(
          foo = 2
          record Bar, foo = 3 # foo = 3 is not parsed as assignment
          puts foo
        )
        subject.catch(s).should be_valid
      end

      it "reports if assignment is not referenced after the record declaration" do
        s = Source.new %(
          foo = 2
          record Bar, foo = 3
        ), "source.cr"
        subject.catch(s).should_not be_valid
        s.issues.size.should eq 1

        issue = s.issues.first
        issue.location.to_s.should eq "source.cr:1:1"
        issue.message.should eq "Useless assignment to variable `foo`"
      end
    end

    context "branching" do
      context "if-then-else" do
        it "doesn't report if assignment is consumed by branches" do
          s = Source.new %(
            def method
              a = 0
              if something
                a = 1
              else
                a = 2
              end
              a
            end
          )
          subject.catch(s).should be_valid
        end

        it "doesn't report if assignment is in one branch" do
          s = Source.new %(
            def method
              a = 0
              if something
                a = 1
              else
                nil
              end
              a
            end
          )
          subject.catch(s).should be_valid
        end

        it "doesn't report if assignment is in one line branch" do
          s = Source.new %(
            def method
              a = 0
              a = 1 if something
              a
            end
          )
          subject.catch(s).should be_valid
        end

        it "reports if assignment is useless in the branch" do
          s = Source.new %(
            def method(a)
              if a
                a = 2
              end
            end
          )
          subject.catch(s).should_not be_valid
        end

        it "reports if only last assignment is referenced in a branch" do
          s = Source.new %(
            def method(a)
              a = 1
              if a
                a = 2
                a = 3
              end
              a
            end
          )
          subject.catch(s).should_not be_valid
          s.issues.size.should eq 1
          s.issues.first.location.to_s.should eq ":4:5"
        end

        it "does not report of assignments are referenced in all branches" do
          s = Source.new %(
            def method
              if matches
                matches = owner.lookup_matches signature
              else
                matches = owner.lookup_matches signature
              end

              matches
            end
          )
          subject.catch(s).should be_valid
        end

        it "does not report referenced assignments in inner branches" do
          s = Source.new %(
            def method
              has_newline = false

              if something
                do_something unless false
                has_newline = false
              else
                do_something if true
                has_newline = true
              end

              has_newline
            end
          )
          subject.catch(s).should be_valid
        end
      end

      context "unless-then-else" do
        it "doesn't report if assignment is consumed by branches" do
          s = Source.new %(
            def method
              a = 0
              unless something
                a = 1
              else
                a = 2
              end
              a
            end
          )
          subject.catch(s).should be_valid
        end

        it "reports if there is a useless assignment in a branch" do
          s = Source.new %(
            def method
              a = 0
              unless something
                a = 1
                a = 2
              else
                a = 2
              end
              a
            end
          )
          subject.catch(s).should_not be_valid
          s.issues.size.should eq 1
          s.issues.first.location.to_s.should eq ":4:5"
        end
      end

      context "case" do
        it "does not report if assignment is referenced" do
          s = Source.new %(
            def method(a)
              case a
              when /foo/
                a = 1
              when /bar/
                a = 2
              end
              puts a
            end
          )
          subject.catch(s).should be_valid
        end

        it "reports if assignment is useless" do
          s = Source.new %(
            def method(a)
              case a
              when /foo/
                a = 1
              when /bar/
                a = 2
              end
            end
          )
          subject.catch(s).should_not be_valid
          s.issues.size.should eq 2
          s.issues.first.location.to_s.should eq ":4:5"
          s.issues.last.location.to_s.should eq ":6:5"
        end

        it "doesn't report if assignment is referenced in cond" do
          s = Source.new %(
            def method
              a = 2
              case a
              when /foo/
              end
            end
          )
          subject.catch(s).should be_valid
        end
      end

      context "binary operator" do
        it "does not report if assignment is referenced" do
          s = Source.new %(
            def method(a)
              (a = 1) && (b = 1)
              a + b
            end
          )
          subject.catch(s).should be_valid
        end

        it "reports if assignment is useless" do
          s = Source.new %(
            def method(a)
              (a = 1) || (b = 1)
              a
            end
          )
          subject.catch(s).should_not be_valid
          s.issues.size.should eq 1
          s.issues.first.location.to_s.should eq ":2:15"
        end
      end

      context "while" do
        it "does not report if assignment is referenced" do
          s = Source.new %(
            def method(a)
              while a < 10
                a = a + 1
              end
              a
            end
          )
          subject.catch(s).should be_valid
        end

        it "reports if assignment is useless" do
          s = Source.new %(
            def method(a)
              while a < 10
                b = a
              end
            end
          )
          subject.catch(s).should_not be_valid
          s.issues.size.should eq 1
          s.issues.first.location.to_s.should eq ":3:5"
        end

        it "does not report if assignment is referenced in a loop" do
          s = Source.new %(
            def method
              a = 3
              result = 0

              while result < 10
                result += a
                a = a + 1
              end
              result
            end
          )
          subject.catch(s).should be_valid
        end

        it "does not report if assignment is referenced as param in a loop" do
          s = Source.new %(
            def method(a)
              result = 0

              while result < 10
                result += a
                a = a + 1
              end
              result
            end
          )
          subject.catch(s).should be_valid
        end

        it "does not report if assignment is referenced in loop and inner branch" do
          s = Source.new %(
            def method(a)
              result = 0

              while result < 10
                result += a
                if result > 0
                  a = a + 1
                else
                  a = 3
                end
              end
              result
            end
          )
          subject.catch(s).should be_valid
        end

        it "works properly if there is branch with blank node" do
          s = Source.new %(
            def visit
              count = 0
              while true
                break if count == 1
                case something
                when :any
                else
                  :anything_else
                end
                count += 1
              end
            end
          )
          subject.catch(s).should be_valid
        end
      end

      context "until" do
        it "does not report if assignment is referenced" do
          s = Source.new %(
            def method(a)
              until a > 10
                a = a + 1
              end
              a
            end
          )
          subject.catch(s).should be_valid
        end

        it "reports if assignment is useless" do
          s = Source.new %(
            def method(a)
              until a > 10
                b = a + 1
              end
            end
          )
          subject.catch(s).should_not be_valid
          s.issues.size.should eq 1
          s.issues.first.location.to_s.should eq ":3:5"
        end
      end

      context "exception handler" do
        it "does not report if assignment is referenced in body" do
          s = Source.new %(
            def method(a)
              a = 2
            rescue
              a
            end
          )
          subject.catch(s).should be_valid
        end

        it "doesn't report if assignment is referenced in ensure" do
          s = Source.new %(
            def method(a)
              a = 2
            ensure
              a
            end
          )
          subject.catch(s).should be_valid
        end

        it "doesn't report if assignment is referenced in else" do
          s = Source.new %(
            def method(a)
              a = 2
            rescue
            else
              a
            end
          )
          subject.catch(s).should be_valid
        end

        it "reports if assignment is useless" do
          s = Source.new %(
            def method(a)
            rescue
              a = 2
            end
          )
          subject.catch(s).should_not be_valid
          s.issues.size.should eq 1
          s.issues.first.location.to_s.should eq ":3:3"
        end
      end
    end

    context "typeof" do
      it "reports useless assigments in typeof" do
        s = Source.new %(
          typeof(begin
            foo = 1
            bar = 2
          end)
        )
        subject.catch(s).should_not be_valid
        s.issues.size.should eq 2
        s.issues.first.location.to_s.should eq ":2:3"
        s.issues.first.message.should eq "Useless assignment to variable `foo`"

        s.issues.last.location.to_s.should eq ":3:3"
        s.issues.last.message.should eq "Useless assignment to variable `bar`"
      end
    end

    context "macro" do
      it "doesn't report if assignment is referenced in macro" do
        s = Source.new %(
          def method
            a = 2
            {% if flag?(:bits64) %}
              a.to_s
            {% else %}
              a
            {% end %}
          end
        )
        subject.catch(s).should be_valid
      end

      it "doesn't report referenced assignments in macro literal" do
        s = Source.new %(
          def method
            a = 2
            {% if flag?(:bits64) %}
              a = 3
            {% else %}
              a = 4
            {% end %}
            puts a
          end
        )
        subject.catch(s).should be_valid
      end

      it "doesn't report if assignment is referenced in macro def" do
        s = Source.new %(
          macro macro_call
            puts x
          end

          def foo
            x = 1
            macro_call
          end
        )
        subject.catch(s).should be_valid
      end

      it "doesn't report if assignment is referenced in a macro below" do
        s = Source.new %(
          class Foo
            def foo
              a = 1
              macro_call
            end

            macro macro_call
              puts a
            end
          end
        )
        subject.catch(s).should be_valid
      end

      it "doesn't report if assignment is referenced in a macro expression as string" do
        s = Source.new %(
          foo = 1
          puts {{ "foo".id }}
        )
        subject.catch(s).should be_valid
      end

      it "doesn't report if assignement is referenced in for macro in exp" do
        s = Source.new %(
          foo = 22

          {% for x in %w(foo) %}
            add({{ x.id }})
          {% end %}
        )
        subject.catch(s).should be_valid
      end

      it "doesn't report if assignement is referenced in for macro in body" do
        s = Source.new %(
          foo = 22

          {% for x in %w(bar) %}
            puts {{ "foo".id }}
          {% end %}
        )
        subject.catch(s).should be_valid
      end

      it "doesn't report if assignement is referenced in if macro in cond" do
        s = Source.new %(
          foo = 22
          {% if "foo".id %}
          {% end %}
        )
        subject.catch(s).should be_valid
      end

      it "doesn't report if assignement is referenced in if macro in then" do
        s = Source.new %(
          foo = 22
          {% if true %}
             puts {{ "foo".id }}
          {% end %}
        )
        subject.catch(s).should be_valid
      end

      it "doesn't report if assignement is referenced in if macro in else" do
        s = Source.new %(
          foo = 22
          {% if true %}
          {% else %}
             puts {{ "foo".id }}
          {% end %}
        )
        subject.catch(s).should be_valid
      end
    end

    it "does not report if variable is referenced and there is a deep level scope" do
      s = Source.new %(
        response = JSON.build do |json|
          json.object do
            json.object do
              json.object do
                json.object do
                  json.object do
                    json.object do
                      json.object do
                        json.object do
                          json.object do
                            json.object do
                              json.object do
                                json.object do
                                  json.object do
                                    json.object do
                                      json.object do
                                        anything
                                      end
                                    end
                                  end
                                end
                              end
                            end
                          end
                        end
                      end
                    end
                  end
                end
              end
            end
          end
        end

        response = JSON.parse(response)
        response
       )
      subject.catch(s).should be_valid
    end

    context "uninitialized" do
      it "reports if uninitialized assignment is not referenced at a top level" do
        s = Source.new %(
          a = uninitialized U
        )
        subject.catch(s).should_not be_valid
      end

      it "reports if uninitialized assignment is not referenced in a method" do
        s = Source.new %(
          def foo
            a = uninitialized U
          end
        )
        subject.catch(s).should_not be_valid
      end

      it "doesn't report if uninitialized assignment is referenced" do
        s = Source.new %(
          def foo
            a = uninitialized U
            a
          end
        )
        subject.catch(s).should be_valid
      end
    end
  end
end
