#
# Copyright:: Copyright (c) Chef Software 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 express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "spec_helper"
require "chef/mixin/shell_out"

describe Chef::Resource::DpkgPackage, :requires_root, :debian_family_only, arch: "x86_64" do
  include Chef::Mixin::ShellOut

  let(:apt_data) { File.join(CHEF_SPEC_DATA, "apt") }

  let(:test1_0) { File.join(apt_data, "chef-integration-test_1.0-1_amd64.deb") }
  let(:test1_1) { File.join(apt_data, "chef-integration-test_1.1-1_amd64.deb") }
  let(:test2_0) { File.join(apt_data, "chef-integration-test2_1.0-1_amd64.deb") }

  let(:run_context) do
    node = TEST_NODE.dup
    events = Chef::EventDispatch::Dispatcher.new
    Chef::RunContext.new(node, {}, events)
  end

  let(:dpkg_package) { Chef::Resource::DpkgPackage.new(test1_0, run_context) }

  before(:each) do
    shell_out("dpkg -P chef-integration-test chef-integration-test2")
  end

  # handles setting the name property after the initializer runs
  def set_dpkg_package_name(name)
    dpkg_package.name name
    dpkg_package.package_name name
  end

  def should_be_purged_or_removed(package, action = nil)
    status = shell_out("dpkg -s #{package}")
    output = status.stdout + status.stderr
    if action.nil? || action == :purge
      expect(output).to match(/no info|not-installed|not installed/)
    elsif action == :remove
      expect(output).to match(/deinstall ok config-files/)
    else
      raise "Unknown action"
    end
  end

  shared_examples_for "common behavior for upgrade or install" do
    it "installs a package when given only the filename as a name argument (no source)" do
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      shell_out!("dpkg -s chef-integration-test")
    end

    it "installs a package when given the name and a source argument" do
      set_dpkg_package_name "chef-integration-test"
      dpkg_package.source test1_0
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      shell_out!("dpkg -s chef-integration-test")
    end

    it "installs a package when given a different name and a source argument" do
      set_dpkg_package_name "some other name"
      dpkg_package.source test1_0
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      shell_out!("dpkg -s chef-integration-test")
    end

    it "installs a package when given a path as a package_name and no source" do
      set_dpkg_package_name "chef-integration-test"
      dpkg_package.package_name test1_0
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      shell_out!("dpkg -s chef-integration-test")
    end

    it "raises an error when the name is not a path and the source is not given" do
      set_dpkg_package_name "chef-integration-test"
      dpkg_package.package_name "chef-integration-test"
      expect { dpkg_package.run_action(action) }.to raise_error(Chef::Exceptions::Package)
    end

    it "raises an error when passed a package_name that does not exist" do
      set_dpkg_package_name File.join(test1_0, "make.it.fail")
      expect { dpkg_package.run_action(action) }.to raise_error(Chef::Exceptions::Package)
    end

    it "raises an error when passed a source that does not exist" do
      set_dpkg_package_name "chef-integration-test"
      dpkg_package.source File.join(test1_0, "make.it.fail")
      expect { dpkg_package.run_action(action) }.to raise_error(Chef::Exceptions::Package)
    end

    it "should not install an already installed package" do
      shell_out!("dpkg -i #{test1_0}")
      dpkg_package.run_action(action)
      expect(dpkg_package).not_to be_updated_by_last_action
      shell_out!("dpkg -s chef-integration-test")
    end

    it "should handle a multipackage install" do
      set_dpkg_package_name [ test1_0, test2_0 ]
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      shell_out!("dpkg -s chef-integration-test")
      shell_out!("dpkg -s chef-integration-test2")
    end

    it "should not update multipackages that are up-to-date" do
      shell_out!("dpkg -i #{test1_0} #{test2_0}")
      set_dpkg_package_name [ test1_0, test2_0 ]
      dpkg_package.run_action(action)
      expect(dpkg_package).not_to be_updated_by_last_action
      shell_out!("dpkg -s chef-integration-test")
      shell_out!("dpkg -s chef-integration-test2")
    end

    it "should install the second if the first is installed" do
      shell_out!("dpkg -i #{test1_0}")
      set_dpkg_package_name [ test1_0, test2_0 ]
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      shell_out!("dpkg -s chef-integration-test")
      shell_out!("dpkg -s chef-integration-test2")
    end

    it "should install the first if the second is installed" do
      shell_out!("dpkg -i #{test2_0}")
      set_dpkg_package_name [ test1_0, test2_0 ]
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      shell_out!("dpkg -s chef-integration-test")
      shell_out!("dpkg -s chef-integration-test2")
    end
  end

  context "action :install" do
    let(:action) { :install }
    it_behaves_like "common behavior for upgrade or install"

    it "should not upgrade a package" do
      shell_out!("dpkg -i #{test1_0}")
      set_dpkg_package_name test1_1
      dpkg_package.run_action(action)
      expect(dpkg_package).not_to be_updated_by_last_action
    end

    it "should not upgrade on a multipackage install" do
      shell_out!("dpkg -i #{test1_0} #{test2_0}")
      set_dpkg_package_name [ test1_1, test2_0 ]
      dpkg_package.run_action(action)
      expect(dpkg_package).not_to be_updated_by_last_action
    end
  end

  context "action :upgrade" do
    let(:action) { :upgrade }
    it_behaves_like "common behavior for upgrade or install"

    it "should upgrade a package" do
      shell_out!("dpkg -i #{test1_0}")
      set_dpkg_package_name test1_1
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
    end

    it "should upgrade on a multipackage install" do
      shell_out!("dpkg -i #{test1_0} #{test2_0}")
      set_dpkg_package_name [ test1_1, test2_0 ]
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
    end

    it "should not allow downgrade a package" do
      dpkg_package.allow_downgrade false
      shell_out!("dpkg -i #{test1_1}")
      set_dpkg_package_name test1_0
      dpkg_package.run_action(action)
      expect(dpkg_package).not_to be_updated_by_last_action
    end

    it "should allow downgrade a package" do
      dpkg_package.allow_downgrade true
      shell_out!("dpkg -i #{test1_1}")
      set_dpkg_package_name test1_0
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
    end
  end

  shared_examples_for "common behavior for remove or purge" do
    it "should remove a package that is installed when the name is a source" do
      shell_out!("dpkg -i #{test1_0}")
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test")
    end

    it "should do nothing if the package is not installed when the name is a source" do
      dpkg_package.run_action(action)
      expect(dpkg_package).not_to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test")
    end

    it "should remove a package that is installed when the name is the package name and source is nil" do
      shell_out!("dpkg -i #{test1_0}")
      set_dpkg_package_name "chef-integration-test"
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test")
    end

    it "should do nothing if the package is not installed when the name is the package name and the source is nil" do
      set_dpkg_package_name "chef-integration-test"
      dpkg_package.run_action(action)
      expect(dpkg_package).not_to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test")
    end

    it "should remove a package that is installed when the name is changed but the source is a package" do
      shell_out!("dpkg -i #{test1_0}")
      set_dpkg_package_name "some other name"
      dpkg_package.source test1_0
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test")
    end

    it "should do nothing if the package is not installed when the name is changed but the source is a package" do
      set_dpkg_package_name "some other name"
      dpkg_package.source test1_0
      dpkg_package.run_action(action)
      expect(dpkg_package).not_to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test")
    end

    it "should remove a package if the name is a file that does not exist, but the source exists" do
      shell_out!("dpkg -i #{test1_0}")
      dpkg_package.name "whatever"
      dpkg_package.package_name File.join(test1_0, "make.it.fail")
      dpkg_package.source test1_0
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test")
    end

    it "should do nothing if the package is not installed when the name is a file that does not exist, but the source exists" do
      set_dpkg_package_name "some other name"
      dpkg_package.name "whatever"
      dpkg_package.package_name File.join(test1_0, "make.it.fail")
      dpkg_package.source test1_0
      dpkg_package.run_action(action)
      expect(dpkg_package).not_to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test")
    end

    it "should remove a package if the package_name is correct, but the source does not exist" do
      shell_out!("dpkg -i #{test1_0}")
      dpkg_package.name "whatever"
      dpkg_package.package_name "chef-integration-test"
      dpkg_package.source File.join(test1_0, "make.it.fail")
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test")
    end

    it "should do nothing if the package_name is correct, but the source does not exist, and the package is not installed" do
      dpkg_package.name "whatever"
      dpkg_package.package_name "chef-integration-test"
      dpkg_package.source File.join(test1_0, "make.it.fail")
      dpkg_package.run_action(action)
      expect(dpkg_package).not_to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test")
    end

    it "should remove both packages when called with two" do
      shell_out!("dpkg -i #{test1_0} #{test2_0}")
      set_dpkg_package_name %w{chef-integration-test chef-integration-test2}
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test")
      should_be_purged_or_removed("chef-integration-test2", action)
    end

    it "should remove a package when only the first one is installed" do
      shell_out!("dpkg -i #{test1_0}")
      set_dpkg_package_name %w{chef-integration-test chef-integration-test2}
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test")
      should_be_purged_or_removed("chef-integration-test2")
    end

    it "should remove a package when only the second one is installed" do
      shell_out!("dpkg -i #{test2_0}")
      set_dpkg_package_name %w{chef-integration-test chef-integration-test2}
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test")
      should_be_purged_or_removed("chef-integration-test2", action)
    end

    it "should do nothing when both packages are not installed" do
      set_dpkg_package_name %w{chef-integration-test chef-integration-test2}
      dpkg_package.run_action(action)
      expect(dpkg_package).not_to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test")
      should_be_purged_or_removed("chef-integration-test2")
    end
  end

  context "action :remove" do
    let(:action) { :remove }
    it_behaves_like "common behavior for remove or purge"

    it "should not remove a removed package when the name is a source" do
      # the "test2" file has a conffile declared in it
      shell_out!("dpkg -i #{test2_0}")
      shell_out!("dpkg -r chef-integration-test2")
      set_dpkg_package_name "chef-integration-test2"
      dpkg_package.run_action(action)
      expect(dpkg_package).not_to be_updated_by_last_action
      shell_out!("dpkg -s chef-integration-test2") # its still 'installed'
    end
  end

  context "action :purge" do
    let(:action) { :purge }
    it_behaves_like "common behavior for remove or purge"

    it "should purge a removed package when the name is a source" do
      # the "test2" file has a conffile declared in it
      shell_out!("dpkg -i #{test2_0}")
      shell_out!("dpkg -r chef-integration-test2")
      set_dpkg_package_name "chef-integration-test2"
      dpkg_package.run_action(action)
      expect(dpkg_package).to be_updated_by_last_action
      should_be_purged_or_removed("chef-integration-test2", action)
    end
  end
end
