require "../spec_helper"

module Ameba
  private def runner(files = [__FILE__], formatter = DummyFormatter.new)
    config = Config.load
    config.formatter = formatter
    config.globs = files

    config.update_rule ErrorRule.rule_name, enabled: false
    config.update_rule PerfRule.rule_name, enabled: false
    config.update_rule AtoAA.rule_name, enabled: false
    config.update_rule AtoB.rule_name, enabled: false
    config.update_rule BtoA.rule_name, enabled: false
    config.update_rule BtoC.rule_name, enabled: false
    config.update_rule CtoA.rule_name, enabled: false
    config.update_rule ClassToModule.rule_name, enabled: false
    config.update_rule ModuleToClass.rule_name, enabled: false

    Runner.new(config)
  end

  describe Runner do
    formatter = DummyFormatter.new
    default_severity = Severity::Convention

    describe "#run" do
      it "returns self" do
        runner.run.should_not be_nil
      end

      it "calls started callback" do
        runner(formatter: formatter).run
        formatter.started_sources.should_not be_nil
      end

      it "calls finished callback" do
        runner(formatter: formatter).run
        formatter.finished_sources.should_not be_nil
      end

      it "calls source_started callback" do
        runner(formatter: formatter).run
        formatter.started_source.should_not be_nil
      end

      it "calls source_finished callback" do
        runner(formatter: formatter).run
        formatter.finished_source.should_not be_nil
      end

      it "skips rule check if source is excluded" do
        path = "source.cr"
        source = Source.new "", path

        all_rules = ([] of Rule::Base).tap do |rules|
          rule = ErrorRule.new
          rule.excluded = [path]
          rules << rule
        end

        Runner.new(all_rules, [source], formatter, default_severity).run.success?.should be_true
      end

      it "aborts because of an infinite loop" do
        rules = [AtoAA.new] of Rule::Base
        source = Source.new "class A; end", "source.cr"
        message = "Infinite loop in source.cr caused by Ameba/AtoAA"

        expect_raises(Runner::InfiniteCorrectionLoopError, message) do
          Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
        end
      end

      context "exception in rule" do
        it "raises an exception raised in fiber while running a rule" do
          rule = RaiseRule.new
          rule.should_raise = true
          rules = [rule] of Rule::Base
          source = Source.new "", "source.cr"

          expect_raises(Exception, "something went wrong") do
            Runner.new(rules, [source], formatter, default_severity).run
          end
        end
      end

      context "invalid syntax" do
        it "reports a syntax error" do
          rules = [Rule::Lint::Syntax.new] of Rule::Base
          source = Source.new "def bad_syntax"

          Runner.new(rules, [source], formatter, default_severity).run
          source.should_not be_valid
          source.issues.first.rule.name.should eq Rule::Lint::Syntax.rule_name
        end

        it "does not run other rules" do
          rules = [Rule::Lint::Syntax.new, Rule::Style::ConstantNames.new] of Rule::Base
          source = Source.new <<-CRYSTAL
            MyBadConstant = 1

            when my_bad_syntax
            CRYSTAL

          Runner.new(rules, [source], formatter, default_severity).run
          source.should_not be_valid
          source.issues.size.should eq 1
        end
      end

      context "unneeded disables" do
        it "reports an issue if such disable exists" do
          rules = [Rule::Lint::UnneededDisableDirective.new] of Rule::Base
          source = Source.new <<-CRYSTAL
            a = 1 # ameba:disable LineLength
            CRYSTAL

          Runner.new(rules, [source], formatter, default_severity).run
          source.should_not be_valid
          source.issues.first.rule.name.should eq Rule::Lint::UnneededDisableDirective.rule_name
        end
      end
    end

    describe "#explain" do
      io = IO::Memory.new

      it "writes nothing if sources are valid" do
        io.clear
        runner = runner(formatter: formatter).run
        runner.explain({file: "source.cr", line: 1, column: 2}, io)
        io.to_s.should be_empty
      end

      it "writes the explanation if sources are not valid and location found" do
        io.clear
        rules = [ErrorRule.new] of Rule::Base
        source = Source.new "a = 1", "source.cr"

        runner = Runner.new(rules, [source], formatter, default_severity).run
        runner.explain({file: "source.cr", line: 1, column: 1}, io)
        io.to_s.should_not be_empty
      end

      it "writes nothing if sources are not valid and location is not found" do
        io.clear
        rules = [ErrorRule.new] of Rule::Base
        source = Source.new "a = 1", "source.cr"

        runner = Runner.new(rules, [source], formatter, default_severity).run
        runner.explain({file: "source.cr", line: 1, column: 2}, io)
        io.to_s.should be_empty
      end
    end

    describe "#success?" do
      it "returns true if runner has not been run" do
        runner.success?.should be_true
      end

      it "returns true if all sources are valid" do
        runner.run.success?.should be_true
      end

      it "returns false if there are invalid sources" do
        rules = Rule.rules.map &.new.as(Rule::Base)
        source = Source.new "WrongConstant = 5"

        Runner.new(rules, [source], formatter, default_severity).run.success?.should be_false
      end

      it "depends on the level of severity" do
        rules = Rule.rules.map &.new.as(Rule::Base)
        source = Source.new "WrongConstant = 5\n"

        Runner.new(rules, [source], formatter, :error).run.success?.should be_true
        Runner.new(rules, [source], formatter, :warning).run.success?.should be_true
        Runner.new(rules, [source], formatter, :convention).run.success?.should be_false
      end

      it "returns false if issue is disabled" do
        rules = [NamedRule.new] of Rule::Base
        source = Source.new <<-CRYSTAL
          def foo
            bar = 1 # ameba:disable #{NamedRule.name}
          end
          CRYSTAL
        source.add_issue NamedRule.new, location: {2, 1},
          message: "Useless assignment"

        Runner
          .new(rules, [source], formatter, default_severity)
          .run.success?.should be_true
      end
    end

    describe "#run with rules autocorrecting each other" do
      context "with two conflicting rules" do
        context "if there is an offense in an inspected file" do
          it "aborts because of an infinite loop" do
            rules = [AtoB.new, BtoA.new]
            source = Source.new "class A; end", "source.cr"
            message = "Infinite loop in source.cr caused by Ameba/AtoB -> Ameba/BtoA"

            expect_raises(Runner::InfiniteCorrectionLoopError, message) do
              Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
            end
          end
        end

        context "if there are multiple offenses in an inspected file" do
          it "aborts because of an infinite loop" do
            rules = [AtoB.new, BtoA.new]
            source = Source.new <<-CRYSTAL, "source.cr"
              class A; end
              class A_A; end
              CRYSTAL
            message = "Infinite loop in source.cr caused by Ameba/AtoB -> Ameba/BtoA"

            expect_raises(Runner::InfiniteCorrectionLoopError, message) do
              Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
            end
          end
        end
      end

      context "with two pairs of conflicting rules" do
        it "aborts because of an infinite loop" do
          rules = [ClassToModule.new, ModuleToClass.new, AtoB.new, BtoA.new]
          source = Source.new "class A_A; end", "source.cr"
          message = "Infinite loop in source.cr caused by Ameba/ClassToModule, Ameba/AtoB -> Ameba/ModuleToClass, Ameba/BtoA"

          expect_raises(Runner::InfiniteCorrectionLoopError, message) do
            Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
          end
        end
      end

      context "with three rule cycle" do
        it "aborts because of an infinite loop" do
          rules = [AtoB.new, BtoC.new, CtoA.new]
          source = Source.new "class A; end", "source.cr"
          message = "Infinite loop in source.cr caused by Ameba/AtoB -> Ameba/BtoC -> Ameba/CtoA"

          expect_raises(Runner::InfiniteCorrectionLoopError, message) do
            Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
          end
        end
      end
    end
  end
end
