#
# Author:: Adam Jacob (<adam@chef.io>)
# 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"

describe Chef::Provider::Package::Chocolatey, :windows_only do
  let(:timeout) { 900 }

  let(:new_resource) { Chef::Resource::ChocolateyPackage.new("git") }

  let(:provider) do
    node = Chef::Node.new
    events = Chef::EventDispatch::Dispatcher.new
    run_context = Chef::RunContext.new(node, {}, events)
    Chef::Provider::Package::Chocolatey.new(new_resource, run_context)
  end

  let(:choco_install_path) { "C:\\ProgramData\\chocolatey" }
  let(:choco_exe) { "#{choco_install_path}\\bin\\choco.exe" }

  # installed packages (ConEmu is upgradable)
  let(:local_list_stdout) do
    <<~EOF
      Chocolatey v0.9.9.10
      chocolatey|0.9.9.12
      ConEmu|15.10.25.2
    EOF
  end

  before do
    allow(provider).to receive(:choco_install_path).and_return(choco_install_path)
    allow(provider).to receive(:choco_exe).and_return(choco_exe)
    local_list_obj = double(stdout: local_list_stdout)
    allow(provider).to receive(:shell_out_compacted!).with(choco_exe, "list", "-l", "-r", { returns: [0, 2], timeout: timeout }).and_return(local_list_obj)
    allow(provider).to receive(:powershell_exec!).with("Get-ItemProperty #{choco_exe} | select-object -expandproperty versioninfo | select-object -expandproperty productversion").and_return(double(result: "2.1.0"))
    # Mock the local file system choco queries
    allow(provider).to receive(:get_local_pkg_dirs).and_return(%w{chocolatey ConEmu})
    allow(provider).to receive(:fetch_package_versions).and_return({ "chocolatey" => "0.9.9.11", "conemu" => "15.10.25.0" })
  end

  after(:each) do
    provider.instance_variable_set(:@get_choco_version, nil)
  end

  def allow_remote_list(package_names, args = nil)
    # Ensure that when we set this, we invalidate any cache since we're changing
    # the returned data on purpose
    provider.invalidate_cache
    remote_list_stdout = <<~EOF
      Chocolatey v0.9.9.11
      chocolatey|0.9.9.11
      ConEmu|15.10.25.1
      Git|2.6.1
      Git|2.6.2
      munin-node|1.6.1.20130823
    EOF
    remote_list_obj = double(stdout: remote_list_stdout)

    package_names.each do |pkg|
      if args
        allow(provider).to receive(:shell_out_compacted!).with(choco_exe, provider.query_command, "-r", *([pkg] + args), { returns: [0, 2], timeout: timeout }).and_return(remote_list_obj)
      else
        allow(provider).to receive(:shell_out_compacted!).with(choco_exe, provider.query_command, "-r", pkg, { returns: [0, 2], timeout: timeout }).and_return(remote_list_obj)
      end
    end
  end

  describe "#initialize" do
    it "should return the correct class" do
      expect(provider).to be_kind_of(Chef::Provider::Package::Chocolatey)
    end

    it "should support arrays" do
      expect(provider.use_multipackage_api?).to be true
    end
  end

  describe "choco searches change with the version" do
    it "Choco V1 uses List" do
      allow(provider).to receive(:powershell_exec!).with("Get-ItemProperty #{choco_exe} | select-object -expandproperty versioninfo | select-object -expandproperty productversion").and_return(double(result: "1.3.0"))
      expect(provider.query_command).to eql("list")
    end

    it "Choco V2 uses Search" do
      allow(provider).to receive(:powershell_exec!).with("Get-ItemProperty #{choco_exe} | select-object -expandproperty versioninfo | select-object -expandproperty productversion").and_return(double(result: "2.1.0"))
      expect(provider.query_command).to eql("search")
    end
  end

  describe "bulk_query changes the search behaviour" do
    it "should respect bulk_query when getting working out what to search" do
      new_resource.bulk_query(true)
      expect(provider.collect_package_requests).to eql(["*"])
    end
  end

  describe "#candidate_version" do
    it "should set the candidate_version to the latest version when not pinning" do
      allow_remote_list(["git"])
      expect(provider.candidate_version).to eql(["2.6.2"])
    end

    it "should set the candidate_version to pinned version if available" do
      allow_remote_list(["git"])
      new_resource.version("2.6.1")
      expect(provider.candidate_version).to eql(["2.6.1"])
    end

    it "should set the candidate_version to nil if there is no candidate" do
      allow_remote_list(["vim"])
      new_resource.package_name("vim")
      expect(provider.candidate_version).to eql([nil])
    end

    it "should set the candidate_version correctly when there are two packages to install" do
      allow_remote_list(%w{ConEmu chocolatey})
      new_resource.package_name(%w{ConEmu chocolatey})
      expect(provider.candidate_version).to eql(["15.10.25.1", "0.9.9.11"])
    end

    it "should set the candidate_version correctly when only the first is installable" do
      allow_remote_list(%w{ConEmu vim})
      new_resource.package_name(%w{ConEmu vim})
      expect(provider.candidate_version).to eql(["15.10.25.1", nil])
    end

    it "should set the candidate_version correctly when only the last is installable" do
      allow_remote_list(%w{vim chocolatey})
      new_resource.package_name(%w{vim chocolatey})
      expect(provider.candidate_version).to eql([nil, "0.9.9.11"])
    end

    it "should set the candidate_version correctly when neither are is installable" do
      allow_remote_list(%w{vim ruby})
      new_resource.package_name(%w{vim ruby})
      expect(provider.candidate_version).to eql([nil, nil])
    end
  end

  describe "the query cache should be invalidated if the config is clean" do
    it "should return false to cache_is_valid? by default" do
      provider.invalidate_cache
      expect(provider.cache_is_valid?).to eql(false)
    end
  end

  describe "#load_current_resource" do
    it "should return a current_resource" do
      expect(provider.load_current_resource).to be_kind_of(Chef::Resource::ChocolateyPackage)
    end

    it "should set the current_resource#package_name" do
      provider.load_current_resource
      expect(provider.current_resource.package_name).to eql(["git"])
    end

    it "should load and downcase names in the installed_packages hash (with disk provider)" do
      new_resource.use_choco_list(false)
      provider.invalidate_cache
      provider.load_current_resource
      expect(provider.send(:installed_packages)).to eql(
        { "chocolatey" => "0.9.9.11", "conemu" => "15.10.25.0" }
      )
    end

    it "should load and downcase names in the installed_packages hash (with choco list provider)" do
      new_resource.use_choco_list(true)
      provider.invalidate_cache
      provider.load_current_resource
      expect(provider.send(:installed_packages)).to eql(
        { "chocolatey" => "0.9.9.12", "conemu" => "15.10.25.2" }
      )
    end

    it "should load and downcase names in the available_packages hash" do
      allow_remote_list(["git"])
      provider.load_current_resource
      expect(provider.send(:available_packages)).to eql(
        { "chocolatey" => "0.9.9.11", "conemu" => "15.10.25.1", "git" => "2.6.2", "munin-node" => "1.6.1.20130823" }
      )
    end

    it "installing a package that does not exist throws an error" do
      new_resource.package_name("package-does-not-exist")
      new_resource.returns([0])
      allow(provider).to receive(:shell_out_compacted!)
        .with(choco_exe, provider.query_command, "-r", new_resource.package_name.first, { returns: new_resource.returns, timeout: timeout })
        .and_raise(Mixlib::ShellOut::ShellCommandFailed, "Expected process to exit with [0], but received '2'")
      expect { provider.send(:available_packages) }.to raise_error(Mixlib::ShellOut::ShellCommandFailed, "Expected process to exit with [0], but received '2'")
    end

    it "should set the current_resource.version to nil when the package is not installed" do
      provider.load_current_resource
      expect(provider.current_resource.version).to eql([nil])
    end

    it "should set the current_resource.version to the installed version when the package is installed" do
      new_resource.package_name("ConEmu")
      provider.load_current_resource
      expect(provider.current_resource.version).to eql(["15.10.25.0"])
    end

    it "should set the current_resource.version when there are two packages that are installed" do
      new_resource.package_name(%w{ConEmu chocolatey})
      provider.load_current_resource
      expect(provider.current_resource.version).to eql(["15.10.25.0", "0.9.9.11"])
    end

    it "should set the current_resource.version correctly when only the first is installed" do
      new_resource.package_name(%w{ConEmu git})
      provider.load_current_resource
      expect(provider.current_resource.version).to eql(["15.10.25.0", nil])
    end

    it "should set the current_resource.version correctly when only the last is installed" do
      new_resource.package_name(%w{git chocolatey})
      provider.load_current_resource
      expect(provider.current_resource.version).to eql([nil, "0.9.9.11"])
    end

    it "should set the current_resource.version correctly when none are installed" do
      new_resource.package_name(%w{git vim})
      provider.load_current_resource
      expect(provider.current_resource.version).to eql([nil, nil])
    end
  end

  describe "#action_install" do
    it "should install a single package" do
      allow_remote_list(["git"])
      provider.load_current_resource
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "install", "-y", "git", { returns: [0, 2], timeout: timeout }).and_return(double)
      provider.run_action(:install)
      expect(new_resource).to be_updated_by_last_action
    end

    context "when changing the timeout to 3600" do
      let(:timeout) { 3600 }
      it "sets the timeout on shell_out commands" do
        allow_remote_list(["git"])
        new_resource.timeout(timeout)
        provider.load_current_resource
        expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "install", "-y", "git", { returns: [0, 2], timeout: timeout }).and_return(double)
        provider.run_action(:install)
        expect(new_resource).to be_updated_by_last_action
      end
    end

    it "should not install packages that are up-to-date" do
      allow_remote_list(["chocolatey"])
      new_resource.package_name("chocolatey")
      provider.load_current_resource
      expect(provider).not_to receive(:install_package)
      provider.run_action(:install)
      expect(new_resource).not_to be_updated_by_last_action
    end

    it "should not upgrade packages" do
      allow_remote_list(["ConEmu"])
      new_resource.package_name("ConEmu")
      provider.load_current_resource
      expect(provider).not_to receive(:install_package)
      provider.run_action(:install)
      expect(new_resource).not_to be_updated_by_last_action
    end

    it "should upgrade packages when given a version pin" do
      allow_remote_list(["ConEmu"])
      new_resource.package_name("ConEmu")
      new_resource.version("15.10.25.1")
      provider.load_current_resource
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "install", "-y", "--version", "15.10.25.1", "conemu", { returns: [0, 2], timeout: timeout }).and_return(double)
      provider.run_action(:install)
      expect(new_resource).to be_updated_by_last_action
    end

    it "should handle complicated cases when the name/version array is pruned" do
      # chocolatey will be pruned by the superclass out of the args to install_package and we
      # implicitly test that we correctly pick up new_resource.version[1] instead of
      # new_version.resource[0]
      allow_remote_list(%w{chocolatey ConEmu})
      new_resource.package_name(%w{chocolatey ConEmu})
      new_resource.version([nil, "15.10.25.1"])
      provider.load_current_resource
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "install", "-y", "--version", "15.10.25.1", "conemu", { returns: [0, 2], timeout: timeout }).and_return(double)
      provider.run_action(:install)
      expect(new_resource).to be_updated_by_last_action
    end

    it "should be case-insensitive" do
      allow_remote_list(["conemu"])
      new_resource.package_name("conemu")
      new_resource.version("15.10.25.1")
      provider.load_current_resource
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "install", "-y", "--version", "15.10.25.1", "conemu", { returns: [0, 2], timeout: timeout }).and_return(double)
      provider.run_action(:install)
      expect(new_resource).to be_updated_by_last_action
    end

    it "should split up commands when given two packages, one with a version pin" do
      allow_remote_list(%w{ConEmu git})
      new_resource.package_name(%w{ConEmu git})
      new_resource.version(["15.10.25.1", nil])
      provider.load_current_resource
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "install", "-y", "--version", "15.10.25.1", "conemu", { returns: [0, 2], timeout: timeout }).and_return(double)
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "install", "-y", "git", { returns: [0, 2], timeout: timeout }).and_return(double)
      provider.run_action(:install)
      expect(new_resource).to be_updated_by_last_action
    end

    it "should do multipackage installs when given two packages without constraints" do
      allow_remote_list(%w{git munin-node})
      new_resource.package_name(%w{git munin-node})
      provider.load_current_resource
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "install", "-y", "git", "munin-node", { returns: [0, 2], timeout: timeout }).and_return(double)
      provider.run_action(:install)
      expect(new_resource).to be_updated_by_last_action
    end

    context "when passing a source argument" do
      it "should pass options into the install command" do
        allow_remote_list(["git"], ["-source", "localpackages"])
        new_resource.source("localpackages")
        provider.load_current_resource
        expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "install", "-y", "-source", "localpackages", "git", { returns: [0, 2], timeout: timeout }).and_return(double)
        provider.run_action(:install)
        expect(new_resource).to be_updated_by_last_action
      end
    end

    it "should pass options into the install command" do
      allow_remote_list(["git"])
      new_resource.options("-force")
      provider.load_current_resource
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "install", "-y", "-force", "git", { returns: [0, 2], timeout: timeout }).and_return(double)
      provider.run_action(:install)
      expect(new_resource).to be_updated_by_last_action
    end

    it "installing a package that does not exist throws an error" do
      allow_remote_list(["package-does-not-exist"])
      new_resource.package_name("package-does-not-exist")
      provider.load_current_resource
      expect { provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package)
    end

    it "installing multiple packages with a package that does not exist throws an error" do
      allow_remote_list(%w{git package-does-not-exist})
      new_resource.package_name(%w{git package-does-not-exist})
      provider.load_current_resource
      expect { provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package)
    end

    context "alternate source" do
      it "installing a package that does not exist throws an error" do
        allow_remote_list(["package-does-not-exist"], ["-source", "alternate_source"])
        new_resource.package_name("package-does-not-exist")
        new_resource.source("alternate_source")
        provider.load_current_resource
        expect { provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package)
      end
    end

    context "private source" do
      it "installing a package with valid credentials" do
        allow_remote_list(["git"], ["-source", "auth_source", "--user", "ubuntu", "--password", "ubuntu@123"])
        new_resource.source("auth_source")
        new_resource.user("ubuntu")
        new_resource.password("ubuntu@123")
        provider.load_current_resource
        expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "install", "-y", "-source", "auth_source", "--user", "ubuntu", "--password", "ubuntu@123", "git", { returns: [0, 2], timeout: timeout }).and_return(double)
        provider.run_action(:install)
        expect(new_resource).to be_updated_by_last_action
      end

      it "installing a package with invalid credentials throws an error" do
        allow_remote_list(["package-invalid-auth"], ["-source", "auth_source", "--user", "ubuntu", "--password", "ubuntu@123"])
        new_resource.package_name("package-invalid-auth")
        new_resource.source("auth_source")
        new_resource.user("ubuntu")
        new_resource.password("ubuntu@123")
        provider.load_current_resource
        expect { provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package)
      end

      it "only credentials and list options pass into the list command" do
        allow_remote_list(["git"], ["-source", "auth_source", "--user", "ubuntu", "--password", "ubuntu@123", "--local-only"])
        new_resource.source("auth_source")
        new_resource.list_options("--local-only")
        new_resource.user("ubuntu")
        new_resource.password("ubuntu@123")
        provider.load_current_resource
        expect(provider.send(:available_packages)).to eql(
          { "chocolatey" => "0.9.9.11", "conemu" => "15.10.25.1", "git" => "2.6.2", "munin-node" => "1.6.1.20130823" }
        )
      end
    end
  end

  describe "#action_upgrade" do
    it "should install a package that is not installed" do
      allow_remote_list(["git"])
      provider.load_current_resource
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "upgrade", "-y", "git", { returns: [0, 2], timeout: timeout }).and_return(double)
      provider.run_action(:upgrade)
      expect(new_resource).to be_updated_by_last_action
    end

    it "should upgrade a package that is installed but upgradable" do
      allow_remote_list(["ConEmu"])
      new_resource.package_name("ConEmu")
      provider.load_current_resource
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "upgrade", "-y", "conemu", { returns: [0, 2], timeout: timeout }).and_return(double)
      provider.run_action(:upgrade)
      expect(new_resource).to be_updated_by_last_action
    end

    it "should be case insensitive" do
      allow_remote_list(["conemu"])
      new_resource.package_name("conemu")
      provider.load_current_resource
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "upgrade", "-y", "conemu", { returns: [0, 2], timeout: timeout }).and_return(double)
      provider.run_action(:upgrade)
      expect(new_resource).to be_updated_by_last_action
    end

    it "should not install a package that is up-to-date" do
      allow_remote_list(["chocolatey"])
      new_resource.package_name("chocolatey")
      provider.load_current_resource
      expect(provider).not_to receive(:shell_out_compacted!).with(choco_exe, "upgrade", "-y", "chocolatey", { returns: [0, 2], timeout: timeout })
      provider.run_action(:upgrade)
      expect(new_resource).not_to be_updated_by_last_action
    end

    it "version pins work as well" do
      allow_remote_list(["git"])
      new_resource.version("2.6.2")
      provider.load_current_resource
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "upgrade", "-y", "--version", "2.6.2", "git", { returns: [0, 2], timeout: timeout })
      provider.run_action(:upgrade)
      expect(new_resource).to be_updated_by_last_action
    end

    it "upgrading multiple packages uses a single command" do
      allow_remote_list(%w{conemu git})
      new_resource.package_name(%w{conemu git})
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "upgrade", "-y", "conemu", "git", { returns: [0, 2], timeout: timeout }).and_return(double)
      provider.run_action(:upgrade)
      expect(new_resource).to be_updated_by_last_action
    end

    it "upgrading a package that does not exist throws an error" do
      allow_remote_list(["package-does-not-exist"])
      new_resource.package_name("package-does-not-exist")
      provider.load_current_resource
      expect { provider.run_action(:upgrade) }.to raise_error(Chef::Exceptions::Package)
    end

    it "upgrading multiple packages with a package that does not exist throws an error" do
      allow_remote_list(%w{git package-does-not-exist})
      new_resource.package_name(%w{git package-does-not-exist})
      provider.load_current_resource
      expect { provider.run_action(:upgrade) }.to raise_error(Chef::Exceptions::Package)
    end

    context "alternate source" do
      it "installing a package that does not exist throws an error" do
        allow_remote_list(["package-does-not-exist"], ["-source", "alternate_source"])
        new_resource.package_name("package-does-not-exist")
        new_resource.source("alternate_source")
        provider.load_current_resource
        expect { provider.run_action(:upgrade) }.to raise_error(Chef::Exceptions::Package)
      end
    end
  end

  describe "#action_remove" do
    it "does nothing when the package is already removed" do
      allow_remote_list(["git"])
      provider.load_current_resource
      expect(provider).not_to receive(:remove_package)
      provider.run_action(:remove)
      expect(new_resource).not_to be_updated_by_last_action
    end

    it "does nothing when all the packages are already removed" do
      allow_remote_list(%w{git package-does-not-exist})
      new_resource.package_name(%w{git package-does-not-exist})
      provider.load_current_resource
      expect(provider).not_to receive(:remove_package)
      provider.run_action(:remove)
      expect(new_resource).not_to be_updated_by_last_action
    end

    it "removes a package" do
      allow_remote_list(["ConEmu"])
      new_resource.package_name("ConEmu")
      provider.load_current_resource
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "uninstall", "-y", "ConEmu", { returns: [0, 2], timeout: timeout }).and_return(double)
      provider.run_action(:remove)
      expect(new_resource).to be_updated_by_last_action
    end

    it "is case-insensitive" do
      allow_remote_list(["conemu"])
      new_resource.package_name("conemu")
      provider.load_current_resource
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "uninstall", "-y", "conemu", { returns: [0, 2], timeout: timeout }).and_return(double)
      provider.run_action(:remove)
      expect(new_resource).to be_updated_by_last_action
    end

    it "removes a single package when its the only one installed" do
      pending "this is a bug in the superclass"
      allow_remote_list(%w{git conemu})
      new_resource.package_name(%w{git conemu})
      provider.load_current_resource
      expect(provider).to receive(:shell_out_compacted!).with(choco_exe, "uninstall", "-y", "conemu", { returns: [0, 2], timeout: timeout }).and_return(double)
      provider.run_action(:remove)
      expect(new_resource).to be_updated_by_last_action
    end
  end
end

describe "behavior when Chocolatey is not installed" do
  let(:new_resource) { Chef::Resource::ChocolateyPackage.new("git") }

  let(:provider) do
    node = Chef::Node.new
    events = Chef::EventDispatch::Dispatcher.new
    run_context = Chef::RunContext.new(node, {}, events)
    Chef::Provider::Package::Chocolatey.new(new_resource, run_context)
  end

  before do
    # the shellout sometimes returns "", but test nil to be safe.
    allow(provider).to receive(:choco_install_path).and_return("")

    # we don't care what this returns, but we have to let it be called.
    allow(provider).to receive(:shell_out_compacted!).and_return(double(stdout: ""))
  end

  let(:error_regex) do
    /Could not locate.*installer.*resource.*PowerShell.*GetEnvironmentVariable/m
  end

  context "#choco_exe" do
    it "triggers a MissingLibrary exception when Chocolatey is not installed" do
      expect { provider.send(:choco_exe) }.to raise_error(Chef::Exceptions::MissingLibrary, error_regex)
    end
  end

  context "#load_current_resource" do
    it "triggers a MissingLibrary exception when Chocolatey is not installed" do
      expect { provider.load_current_resource }.to raise_error(Chef::Exceptions::MissingLibrary, error_regex)
    end
  end
end
