require 'spec_helper'
require 'puppet/module_tool/applications'
require 'puppet_spec/module_tool/shared_functions'
require 'puppet_spec/module_tool/stub_source'
require 'semver'

describe Puppet::ModuleTool::Applications::Installer do
  include PuppetSpec::ModuleTool::SharedFunctions
  include PuppetSpec::Files
  include PuppetSpec::Fixtures

  before do
    FileUtils.mkdir_p(primary_dir)
    FileUtils.mkdir_p(secondary_dir)
  end

  let(:vardir)        { tmpdir('installer') }
  let(:primary_dir)   { File.join(vardir, "primary") }
  let(:secondary_dir) { File.join(vardir, "secondary") }
  let(:remote_source) { PuppetSpec::ModuleTool::StubSource.new }

  let(:install_dir) do
    mock("Puppet::ModuleTool::InstallDirectory").tap do |dir|
      dir.stubs(:prepare)
      dir.stubs(:target).returns(primary_dir)
    end
  end

  before do
    Semantic::Dependency.clear_sources
    installer = Puppet::ModuleTool::Applications::Installer.any_instance
    installer.stubs(:module_repository).returns(remote_source)
  end

  def installer(modname, target_dir, options)
    Puppet::ModuleTool.set_option_defaults(options)
    Puppet::ModuleTool::Applications::Installer.new(modname, target_dir, options)
  end

  let(:environment) do
    Puppet.lookup(:current_environment).override_with(
      :vardir     => vardir,
      :modulepath => [ primary_dir, secondary_dir ]
    )
  end

  context '#run' do
    let(:module) { 'pmtacceptance-stdlib' }

    def options
      { :environment => environment }
    end

    let(:application) { installer(self.module, install_dir, options) }
    subject { application.run }

    it 'installs the specified module' do
      subject.should include :result => :success
      graph_should_include 'pmtacceptance-stdlib', nil => v('4.1.0')
    end

    context 'with a tarball file' do
      let(:module) { fixtures('stdlib.tgz') }

      it 'installs the specified tarball' do
        subject.should include :result => :success
        graph_should_include 'puppetlabs-stdlib', nil => v('3.2.0')
      end

      context 'with --ignore-dependencies' do
        def options
          super.merge(:ignore_dependencies => true)
        end

        it 'installs the specified tarball' do
          remote_source.expects(:fetch).never
          subject.should include :result => :success
          graph_should_include 'puppetlabs-stdlib', nil => v('3.2.0')
        end
      end

      context 'with dependencies' do
        let(:module) { fixtures('java.tgz') }

        it 'installs the specified tarball' do
          subject.should include :result => :success
          graph_should_include 'puppetlabs-java', nil => v('1.0.0')
          graph_should_include 'puppetlabs-stdlib', nil => v('4.1.0')
        end

        context 'with --ignore-dependencies' do
          def options
            super.merge(:ignore_dependencies => true)
          end

          it 'installs the specified tarball without dependencies' do
            remote_source.expects(:fetch).never
            subject.should include :result => :success
            graph_should_include 'puppetlabs-java', nil => v('1.0.0')
            graph_should_include 'puppetlabs-stdlib', nil
          end
        end
      end
    end

    context 'with dependencies' do
      let(:module) { 'pmtacceptance-apache' }

      it 'installs the specified module and its dependencies' do
        subject.should include :result => :success
        graph_should_include 'pmtacceptance-apache', nil => v('0.10.0')
        graph_should_include 'pmtacceptance-stdlib', nil => v('4.1.0')
      end

      context 'and using --ignore_dependencies' do
        def options
          super.merge(:ignore_dependencies => true)
        end

        it 'installs only the specified module' do
          subject.should include :result => :success
          graph_should_include 'pmtacceptance-apache', nil => v('0.10.0')
          graph_should_include 'pmtacceptance-stdlib', nil
        end
      end

      context 'that are already installed' do
        context 'and satisfied' do
          before { preinstall('pmtacceptance-stdlib', '4.1.0') }

          it 'installs only the specified module' do
            subject.should include :result => :success
            graph_should_include 'pmtacceptance-apache', nil => v('0.10.0')
            graph_should_include 'pmtacceptance-stdlib', :path => primary_dir
          end

          context '(outdated but suitable version)' do
            before { preinstall('pmtacceptance-stdlib', '2.4.0') }

            it 'installs only the specified module' do
              subject.should include :result => :success
              graph_should_include 'pmtacceptance-apache', nil => v('0.10.0')
              graph_should_include 'pmtacceptance-stdlib', v('2.4.0') => v('2.4.0'), :path => primary_dir
            end
          end

          context '(outdated and unsuitable version)' do
            before { preinstall('pmtacceptance-stdlib', '1.0.0') }

            it 'installs a version that is compatible with the installed dependencies' do
              subject.should include :result => :success
              graph_should_include 'pmtacceptance-apache', nil => v('0.0.4')
              graph_should_include 'pmtacceptance-stdlib', nil
            end
          end
        end

        context 'but not satisfied' do
          let(:module) { 'pmtacceptance-keystone' }

          def options
            super.merge(:version => '2.0.0')
          end

          before { preinstall('pmtacceptance-mysql', '2.1.0') }

          it 'installs only the specified module' do
            subject.should include :result => :success
            graph_should_include 'pmtacceptance-keystone', nil => v('2.0.0')
            graph_should_include 'pmtacceptance-mysql', v('2.1.0') => v('2.1.0')
            graph_should_include 'pmtacceptance-stdlib', nil
          end
        end
      end

      context 'that are already installed in other modulepath directories' do
        before { preinstall('pmtacceptance-stdlib', '1.0.0', :into => secondary_dir) }
        let(:module) { 'pmtacceptance-apache' }

        context 'without dependency updates' do
          it 'installs the module only' do
            subject.should include :result => :success
            graph_should_include 'pmtacceptance-apache', nil => v('0.0.4')
            graph_should_include 'pmtacceptance-stdlib', nil
          end
        end

        context 'with dependency updates' do
          before { preinstall('pmtacceptance-stdlib', '2.0.0', :into => secondary_dir) }

          it 'installs the module and upgrades dependencies in-place' do
            subject.should include :result => :success
            graph_should_include 'pmtacceptance-apache', nil => v('0.10.0')
            graph_should_include 'pmtacceptance-stdlib', v('2.0.0') => v('2.6.0'), :path => secondary_dir
          end
        end
      end
    end

    context 'with a specified' do

      context 'version' do
        def options
          super.merge(:version => '3.0.0')
        end

        it 'installs the specified release (or a prerelease thereof)' do
          subject.should include :result => :success
          graph_should_include 'pmtacceptance-stdlib', nil => v('3.0.0')
        end
      end

      context 'version range' do
        def options
          super.merge(:version => '3.x')
        end

        it 'installs the greatest available version matching that range' do
          subject.should include :result => :success
          graph_should_include 'pmtacceptance-stdlib', nil => v('3.2.0')
        end
      end
    end

    context 'when depended upon' do
      before { preinstall('pmtacceptance-keystone', '2.1.0') }
      let(:module)  { 'pmtacceptance-mysql' }

      it 'installs the greatest available version meeting the dependency constraints' do
        subject.should include :result => :success
        graph_should_include 'pmtacceptance-mysql', nil => v('0.9.0')
      end

      context 'with a --version that can satisfy' do
        def options
          super.merge(:version => '0.8.0')
        end

        it 'installs the greatest available version satisfying both constraints' do
          subject.should include :result => :success
          graph_should_include 'pmtacceptance-mysql', nil => v('0.8.0')
        end
      end

      context 'with a --version that cannot satisfy' do
        def options
          super.merge(:version => '> 1.0.0')
        end

        it 'fails to install, since there is no version that can satisfy both constraints' do
          subject.should include :result => :failure
        end

        context 'with --ignore-dependencies' do
          def options
            super.merge(:ignore_dependencies => true)
          end

          it 'fails to install, since ignore_dependencies should still respect dependencies from installed modules' do
            subject.should include :result => :failure
          end
        end

        context 'with --force' do
          def options
            super.merge(:force => true)
          end

          it 'installs the greatest available version, ignoring dependencies' do
            subject.should include :result => :success
            graph_should_include 'pmtacceptance-mysql', nil => v('2.1.0')
          end
        end
      end
    end

    context 'when already installed' do
      before { preinstall('pmtacceptance-stdlib', '1.0.0') }

      context 'but matching the requested version' do
        it 'does nothing, since the installed version satisfies' do
          subject.should include :result => :noop
        end

        context 'with --force' do
          def options
            super.merge(:force => true)
          end

          it 'does reinstall the module' do
            subject.should include :result => :success
            graph_should_include 'pmtacceptance-stdlib', v('1.0.0') => v('4.1.0')
          end
        end

        context 'with local changes' do
          before do
            release = application.send(:installed_modules)['pmtacceptance-stdlib']
            mark_changed(release.mod.path)
          end

          it 'does nothing, since local changes do not affect that' do
            subject.should include :result => :noop
          end

          context 'with --force' do
            def options
              super.merge(:force => true)
            end

            it 'does reinstall the module, since --force ignores local changes' do
              subject.should include :result => :success
              graph_should_include 'pmtacceptance-stdlib', v('1.0.0') => v('4.1.0')
            end
          end
        end
      end

      context 'but not matching the requested version' do
        def options
          super.merge(:version => '2.x')
        end

        it 'fails to install the module, since it is already installed' do
          subject.should include :result => :failure
          subject[:error].should include :oneline => "'pmtacceptance-stdlib' (v2.x) requested; 'pmtacceptance-stdlib' (v1.0.0) already installed"
        end

        context 'with --force' do
          def options
            super.merge(:force => true)
          end

          it 'installs the greatest version matching the new version range' do
            subject.should include :result => :success
            graph_should_include 'pmtacceptance-stdlib', v('1.0.0') => v('2.6.0')
          end
        end
      end
    end

    context 'when a module with the same name is already installed' do
      let(:module) { 'pmtacceptance-stdlib' }
      before { preinstall('puppetlabs-stdlib', '4.1.0') }

      it 'fails to install, since two modules with the same name cannot be installed simultaneously' do
        subject.should include :result => :failure
      end

      context 'using --force' do
        def options
          super.merge(:force => true)
        end

        it 'overwrites the existing module with the greatest version of the requested module' do
          subject.should include :result => :success
          graph_should_include 'pmtacceptance-stdlib', nil => v('4.1.0')
        end
      end
    end

  end
end
