#
# Author:: Claire McQuin (<claire@opscode.com>)
# Copyright:: Copyright (c) 2013 Opscode, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License"); you
# may not use this file except in compliance with the License. You may
# obtain a copy of the license at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License
#

require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')

describe Ohai::Runner, "run_plugin" do
  let(:safe_run) { true }

  before(:each) do
    @ohai = Ohai::System.new
    @runner = Ohai::Runner.new(@ohai, safe_run)
  end

  describe "when running an invalid plugin" do
    it "should raise error" do
      expect { @runner.run_plugin(double("Ohai::NotPlugin")) }.to raise_error(Ohai::Exceptions::InvalidPlugin)
    end
  end

  describe "when running a plugin" do
    let(:plugin) { double("Ohai::DSL::Plugin", :kind_of? => true, :version => version, :name => :Test, :has_run? => has_run, :dependencies => [ ]) }
    let(:version) { :version7 }
    let(:has_run) { false }

    describe "version 7" do
      it "should call run_v7_plugin" do
        expect(@runner).to receive(:run_v7_plugin)
        @runner.run_plugin(plugin)
      end

      describe "if the plugin has run before" do
        let(:has_run) { true }

        it "should not run the plugin" do
          expect(plugin).not_to receive(:safe_run)
          @runner.run_plugin(plugin)
        end
      end
    end

    describe "version 6" do
      let(:version) { :version6 }

      it "should call run_v6_plugin" do
        expect(@runner).to receive(:run_v6_plugin)
        @runner.run_plugin(plugin)
      end

      describe "if the plugin has not run before" do
        describe "if safe_run is not set" do
          it "safe_run should be called" do
            expect(plugin).to receive(:safe_run)
            @runner.run_plugin(plugin)
          end
        end

        describe "if safe_run is set" do
          let(:safe_run) { false }

          it "run should be called" do
            expect(plugin).to receive(:run)
            @runner.run_plugin(plugin)
          end
        end
      end

      describe "if the plugin has run before" do
        let(:has_run) { true }

        it "should not run" do
          expect(plugin).not_to receive(:safe_run)
          @runner.run_plugin(plugin)
        end

      end
    end

    describe "invalid version" do
      let(:version) { :versionBla }

      it "should raise error" do
        expect { @runner.run_plugin(plugin) }.to raise_error(Ohai::Exceptions::InvalidPlugin)
      end
    end
  end

  describe "when running a plugin with no dependencies, Ohai::Runner" do
    let(:plugin) {
      klass = Ohai.plugin(:Test) {
        provides("thing")
        collect_data {
          thing(Mash.new)
        }
      }
      klass.new(@ohai.data)
    }

    it "should run the plugin" do
      @runner.run_plugin(plugin)
      expect(plugin.has_run?).to be true
    end

    it "should add plugin data to Ohai::System.data" do
      @runner.run_plugin(plugin)
      expect(@ohai.data).to have_key(:thing)
      expect(@ohai.data[:thing]).to eql({})
    end
  end

  describe "when running a plugin with one dependency" do
    describe "when the dependency does not exist" do
      before(:each) do
        klass = Ohai.plugin(:Test) {
          provides("thing")
          depends("other_thing")
          collect_data {
            thing(other_thing)
          }
        }
        @plugin = klass.new(@ohai.data)
      end

      it "should raise Ohai::Excpetions::AttributeNotFound" do
        expect{ @runner.run_plugin(@plugin) }.to raise_error(Ohai::Exceptions::AttributeNotFound)
      end

      it "should not run the plugin" do
        expect{ @runner.run_plugin(@plugin) }.to raise_error(Ohai::Exceptions::AttributeNotFound)
        expect(@plugin.has_run?).to be false
      end
    end

    describe "when the dependency has a single provider" do
      before(:each) do
        klass1 = Ohai.plugin(:Thing) {
          provides("thing")
          collect_data {
            thing("thang")
          }
        }
        klass2 = Ohai.plugin(:Other) {
          provides("other")
          depends("thing")
          collect_data {
            other(thing)
          }
        }

        @plugins = []
        [klass1, klass2].each do |klass|
          @plugins << klass.new(@ohai.data)
        end
        @plugin1, @plugin2 = @plugins

        @ohai.provides_map.set_providers_for(@plugin1, ["thing"])
      end

      it "should run the plugins" do
        @runner.run_plugin(@plugin2)
        @plugins.each do |plugin|
          expect(plugin.has_run?).to be true
        end
      end
    end

    describe "when the dependency has multiple providers" do
      before(:each) do
        klass1 = Ohai.plugin(:Thing) {
          provides("thing")
          collect_data {
            thing(Mash.new)
          }
        }
        klass2 = Ohai.plugin(:Other) {
          provides("other")
          depends("thing")
          collect_data {
            other(thing)
          }
        }

        @plugins = []
        [klass1, klass1, klass2].each do |klass|
          @plugins << klass.new(@ohai.data)
        end
        @plugin1, @plugin2, @plugin3 = @plugins

        @ohai.provides_map.set_providers_for(@plugin1, ["thing"])
        @ohai.provides_map.set_providers_for(@plugin2, ["thing"])
      end

      it "should run the plugins" do
        @runner.run_plugin(@plugin3)
        @plugins.each do |plugin|
          expect(plugin.has_run?).to be true
        end
      end
    end
  end

  describe "when running a plugin with many dependencies" do
    before(:each) do
      @ohai = Ohai::System.new
      @runner = Ohai::Runner.new(@ohai, true)

      klass1 = Ohai.plugin(:One) {
        provides("one")
        collect_data {
          one(1)
        }
      }
      klass2 = Ohai.plugin(:Two) {
        provides("two")
        collect_data {
          two(2)
        }
      }
      klass3 = Ohai.plugin(:Three) {
        provides("three")
        depends("one", "two")
        collect_data {
          three(3)
        }
      }

      @plugins = []
      [klass1, klass2, klass3].each do |klass|
        @plugins << klass.new(@ohai.data)
      end
      @plugin1, @plugin2, @plugin3 = @plugins
      @ohai.provides_map.set_providers_for(@plugin1, ["one", "two"])
      @ohai.provides_map.set_providers_for(@plugin2, ["one", "two"])
    end

    it "should run the plugins" do
      @runner.run_plugin(@plugin3)
      @plugins.each do |plugin|
        expect(plugin.has_run?).to be true
      end
    end
  end

  describe "when a cycle is detected" do
    let(:runner) { Ohai::Runner.new(@ohai, true) }

    context "when there are no edges in the cycle (A->A)" do
      let(:plugin_class) do
        klass1 = Ohai.plugin(:Thing) do
          provides("thing")
          depends("thing")
          collect_data do
            thing(other)
          end
        end
      end
      let(:plugin) { plugin_class.new(@ohai.data) }

      it "ignores the cycle" do
        @ohai.provides_map.set_providers_for(plugin, ["thing"])

        expected_error_string = "Dependency cycle detected. Please refer to the following plugins: Thing, Other"
        runner.run_plugin(plugin) # should not raise
      end

    end

    context "when there is one edge in the cycle (A->B and B->A)" do
      before(:each) do
        klass1 = Ohai.plugin(:Thing) {
          provides("thing")
          depends("other")
          collect_data {
            thing(other)
          }
        }
        klass2 = Ohai.plugin(:Other) {
          provides("other")
          depends("thing")
          collect_data {
            other(thing)
          }
        }

        @plugins = []
        [klass1, klass2].each_with_index do |klass, idx|
          @plugins << klass.new(@ohai.data)
        end

        @plugin1, @plugin2 = @plugins
      end

      it "should raise Ohai::Exceptions::DependencyCycle" do
        allow(runner).to receive(:fetch_plugins).with(["thing"]).and_return([@plugin1])
        allow(runner).to receive(:fetch_plugins).with(["other"]).and_return([@plugin2])
        expected_error_string = "Dependency cycle detected. Please refer to the following plugins: Thing, Other"
        expect { runner.run_plugin(@plugin1) }.to raise_error(Ohai::Exceptions::DependencyCycle, expected_error_string)
      end
    end
  end

  describe "when A depends on B and C, and B depends on C" do
    before(:each) do
      @ohai = Ohai::System.new
      @runner = Ohai::Runner.new(@ohai, true)

      klassA = Ohai.plugin(:A) {
        provides("A")
        depends("B", "C")
        collect_data { }
      }
      klassB = Ohai.plugin(:B) {
        provides("B")
        depends("C")
        collect_data { }
      }
      klassC = Ohai.plugin(:C) {
        provides("C")
        collect_data { }
      }

      @plugins = []
      [klassA, klassB, klassC].each do |klass|
        @plugins << klass.new(@ohai.data)
      end
      @pluginA, @pluginB, @pluginC = @plugins
    end

    it "should not detect a cycle when B is the first provider returned" do
      @ohai.provides_map.set_providers_for(@pluginA, ["A"])
      @ohai.provides_map.set_providers_for(@pluginB, ["B"])
      @ohai.provides_map.set_providers_for(@pluginC, ["C"])

      expect(Ohai::Log).not_to receive(:error).with(/DependencyCycleError/)
      @runner.run_plugin(@pluginA)

      @plugins.each do |plugin|
        expect(plugin.has_run?).to be true
      end
    end

    it "should not detect a cycle when C is the first provider returned" do
      @ohai.provides_map.set_providers_for(@pluginA, ["A"])
      @ohai.provides_map.set_providers_for(@pluginC, ["C"])
      @ohai.provides_map.set_providers_for(@pluginB, ["B"])

      expect(Ohai::Log).not_to receive(:error).with(/DependencyCycleError/)
      @runner.run_plugin(@pluginA)

      @plugins.each do |plugin|
        expect(plugin.has_run?).to be true
      end
    end
  end
end

describe Ohai::Runner, "fetch_plugins" do
  before(:each) do
    @provides_map = Ohai::ProvidesMap.new
    @data = Mash.new
    @ohai = double('Ohai::System', :data => @data, :provides_map => @provides_map)
    @runner = Ohai::Runner.new(@ohai, true)
  end

  it "should collect the provider" do
    plugin = Ohai::DSL::Plugin.new(@ohai.data)
    @ohai.provides_map.set_providers_for(plugin, ["top/middle/bottom"])

    dependency_providers = @runner.fetch_plugins(["top/middle/bottom"])
    expect(dependency_providers).to eql([plugin])
  end

  describe "when the attribute is not provided by any plugin" do
    describe "and some parent attribute has providers" do
      it "should return the providers for the parent" do
        plugin = Ohai::DSL::Plugin.new(@ohai.data)
        @provides_map.set_providers_for(plugin, ["test/attribute"])
        expect(@runner.fetch_plugins(["test/attribute/too_far"])).to eql([plugin])
      end
    end

    describe "and no parent attribute has providers" do
      it "should raise Ohai::Exceptions::AttributeNotFound exception" do
        # provides map is empty
        expect{ @runner.fetch_plugins(["false/attribute"]) }.to raise_error(Ohai::Exceptions::AttributeNotFound, "No such attribute: 'false/attribute'")
      end
    end
  end

  it "should return unique providers" do
    plugin = Ohai::DSL::Plugin.new(@ohai.data)
    @provides_map.set_providers_for(plugin, ["test", "test/too_far/way_too_far"])
    expect(@runner.fetch_plugins(["test", "test/too_far/way_too_far"])).to eql([plugin])
  end
end

describe Ohai::Runner, "#get_cycle" do
  before(:each) do
    @ohai = Ohai::System.new
    @runner = Ohai::Runner.new(@ohai, true)

    klass1 = Ohai.plugin(:One) {
      provides("one")
      depends("two")
      collect_data {
        one(two)
      }
    }
    klass2 = Ohai.plugin(:Two) {
      provides("two")
      depends("one")
      collect_data {
        two(one)
      }
    }
    klass3 = Ohai.plugin(:Three) {
      provides("three")
      depends("two")
      collect_data {
        three(two)
      }
    }

    plugins = []
    [klass1, klass2, klass3].each_with_index do |klass, idx|
      plugins << klass.new(@ohai.data)
    end
    @plugin1, @plugin2, @plugin3 = plugins
  end

  it "should return the sources for the plugins in the cycle, when given an exact cycle" do
    cycle = [@plugin1, @plugin2]
    cycle_start = @plugin1

    cycle_names = @runner.get_cycle(cycle, cycle_start)
    expect(cycle_names).to eql([@plugin1.name, @plugin2.name])
  end

  it "should return the sources for only the plugins in the cycle, when there are plugins before the cycle begins" do
    cycle = [@plugin3, @plugin1, @plugin2]
    cycle_start = @plugin1

    cycle_names = @runner.get_cycle(cycle, cycle_start)
    expect(cycle_names).to eql([@plugin1.name, @plugin2.name])
  end
end
