require 'spec_helper'

describe Berkshelf::Berksfile do
  describe '.from_file' do
    let(:cookbook_file) { fixtures_path.join('lockfile_spec', 'with_lock', 'Berksfile') }

    it 'reads a Berksfile and returns an instance Berksfile' do
      expect(Berkshelf::Berksfile.from_file(cookbook_file)).to be_a(Berkshelf::Berksfile)
    end

    context 'when Berksfile does not exist at given path' do
      let(:bad_path) { tmp_path.join('thisdoesnotexist') }

      it 'raises BerksfileNotFound' do
        expect {
          Berkshelf::Berksfile.from_file(bad_path)
        }.to raise_error(Berkshelf::BerksfileNotFound)
      end
    end
  end

  describe '.vendor' do
    let(:cached_cookbooks) { [] }
    let(:tmpdir) { Dir.mktmpdir(nil, tmp_path) }

    it 'returns the expanded filepath of the vendor directory' do
      expect(Berkshelf::Berksfile.vendor(cached_cookbooks, tmpdir)).to eql(tmpdir)
    end

    context 'with a chefignore' do
      before do
        File.stub(:exists?).and_return(true)
        Berkshelf::Chef::Cookbook::Chefignore.any_instance.stub(:remove_ignores_from).and_return(['metadata.rb'])
      end

      it 'finds a chefignore file' do
        Berkshelf::Chef::Cookbook::Chefignore.should_receive(:new).with(File.expand_path('chefignore'))
        Berkshelf::Berksfile.vendor(cached_cookbooks, tmpdir)
      end

      it 'removes files in chefignore' do
        cached_cookbooks = [ Berkshelf::CachedCookbook.from_path(fixtures_path.join('cookbooks/example_cookbook')) ]
        FileUtils.should_receive(:cp_r).with(['metadata.rb'], anything()).exactly(1).times
        FileUtils.should_receive(:cp_r).with(anything(), anything(), anything()).once
        Berkshelf::Berksfile.vendor(cached_cookbooks, tmpdir)
      end
    end
  end



  let(:source_one) { double('source_one', name: 'nginx') }
  let(:source_two) { double('source_two', name: 'mysql') }

  subject do
    berksfile_path = tmp_path.join('Berksfile').to_s
    FileUtils.touch(berksfile_path)
    Berkshelf::Berksfile.new(berksfile_path)
  end

  describe '#cookbook' do
    let(:name) { 'artifact' }
    let(:constraint) { double('constraint') }
    let(:default_options) { { group: [] } }

    it 'sends the add_source message with the name, constraint, and options to the instance of the includer' do
      subject.should_receive(:add_source).with(name, constraint, default_options)
      subject.cookbook(name, constraint, default_options)
    end

    it 'merges the default options into specified options' do
      subject.should_receive(:add_source).with(name, constraint, path: '/Users/reset', group: [])
      subject.cookbook(name, constraint, path: '/Users/reset')
    end

    it 'converts a single specified group option into an array of groups' do
      subject.should_receive(:add_source).with(name, constraint, group: [:production])
      subject.cookbook(name, constraint, group: :production)
    end

    context 'when no constraint specified' do
      it 'sends the add_source message with a nil value for constraint' do
        subject.should_receive(:add_source).with(name, nil, default_options)
        subject.cookbook(name, default_options)
      end
    end

    context 'when no options specified' do
      it 'sends the add_source message with an empty Hash for the value of options' do
        subject.should_receive(:add_source).with(name, constraint, default_options)
        subject.cookbook(name, constraint)
      end
    end
  end

  describe '#group' do
    let(:name) { 'artifact' }
    let(:group) { 'production' }

    it 'sends the add_source message with an array of groups determined by the parameter passed to the group block' do
      subject.should_receive(:add_source).with(name, nil, group: [group])

      subject.group(group) do
        subject.cookbook(name)
      end
    end
  end

  describe '#metadata' do
    let(:path) { fixtures_path.join('cookbooks/example_cookbook') }
    subject { Berkshelf::Berksfile.new(path.join('Berksfile')) }

    before { Dir.chdir(path) }

    it 'sends the add_source message with an explicit version constraint and the path to the cookbook' do
      subject.should_receive(:add_source).with('example_cookbook', '= 0.5.0', path: path.to_s)
      subject.metadata
    end
  end

  describe '#site' do
    let(:uri) { 'http://opscode/v1' }

    it 'sends the add_location to the instance of the implementing class with a SiteLocation' do
      subject.should_receive(:add_location).with(:site, uri)
      subject.site(uri)
    end

    context 'given the symbol :opscode' do
      it 'sends an add_location message with the default Opscode Community API as the first parameter' do
        subject.should_receive(:add_location).with(:site, :opscode)
        subject.site(:opscode)
      end
    end
  end

  describe '#chef_api' do
    let(:uri) { 'http://chef:8080/' }

    it 'sends and add_location message with the type :chef_api and the given URI' do
      subject.should_receive(:add_location).with(:chef_api, uri, {})
      subject.chef_api(uri)
    end

    it 'also sends any options passed' do
      options = { node_name: 'reset', client_key: '/Users/reset/.chef/reset.pem' }
      subject.should_receive(:add_location).with(:chef_api, uri, options)
      subject.chef_api(uri, options)
    end

    context 'given the symbol :config' do
      it 'sends an add_location message with the the type :chef_api and the URI :config' do
        subject.should_receive(:add_location).with(:chef_api, :config, {})
        subject.chef_api(:config)
      end
    end
  end

  describe '#sources' do
    let(:groups) do
      [
        :nautilus,
        :skarner
      ]
    end

    it 'returns all CookbookSources added to the instance of Berksfile' do
      subject.add_source(source_one.name)
      subject.add_source(source_two.name)

      expect(subject.sources).to have(2).items
      expect(subject).to have_source(source_one.name)
      expect(subject).to have_source(source_two.name)
    end

    context 'given the option :except' do
      before do
        source_one.stub(:groups) { [:default, :skarner] }
        source_two.stub(:groups) { [:default, :nautilus] }
      end

      it 'returns all of the sources except the ones in the given groups' do
        subject.add_source(source_one.name, nil, group: [:default, :skarner])
        subject.add_source(source_two.name, nil, group: [:default, :nautilus])
        filtered = subject.sources(except: :nautilus)

        expect(filtered).to have(1).item
        expect(filtered.first.name).to eq(source_one.name)
      end
    end

    context 'given the option :only' do
      before do
        source_one.stub(:groups) { [:default, :skarner] }
        source_two.stub(:groups) { [:default, :nautilus] }
      end

      it 'returns only the sources in the givne groups' do
        subject.add_source(source_one.name, nil, group: [:default, :skarner])
        subject.add_source(source_two.name, nil, group: [:default, :nautilus])
        filtered = subject.sources(only: :nautilus)

        expect(filtered).to have(1).item
        expect(filtered.first.name).to eq(source_two.name)
      end
    end

    context 'when a value for :only and :except is given' do
      it 'raises an ArgumentError' do
        expect {
          subject.sources(only: [:default], except: [:other])
        }.to raise_error(Berkshelf::ArgumentError, "Cannot specify both :except and :only")
      end
    end
  end

  describe '#groups' do
    before do
      subject.stub(:sources) { [source_one, source_two] }
      source_one.stub(:groups) { [:nautilus, :skarner] }
      source_two.stub(:groups) { [:nautilus, :riven] }
    end

    it 'returns a hash containing keys for every group a source is a member of' do
      expect(subject.groups.keys).to have(3).items
      expect(subject.groups).to have_key(:nautilus)
      expect(subject.groups).to have_key(:skarner)
      expect(subject.groups).to have_key(:riven)
    end

    it 'returns an Array of CookbookSources who are members of the group for value' do
      expect(subject.groups[:nautilus]).to have(2).items
      expect(subject.groups[:riven]).to have(1).item
    end
  end

  describe '#resolve' do
    let(:resolver) { double('resolver') }
    let(:sources) { [source_one, source_two] }
    let(:cached) { [double('cached_one'), double('cached_two')] }

    before do
      Berkshelf::Resolver.stub(:new).and_return(resolver)
    end

    it 'resolves the Berksfile' do
      resolver.should_receive(:resolve).and_return(cached)
      resolver.should_receive(:sources).and_return(sources)

      expect(subject.resolve).to eq({ solution: cached, sources: sources })
    end
  end

  describe '#install' do
    let(:resolver) { double('resolver') }
    let(:lockfile) { double('lockfile') }

    let(:cached_cookbooks) { [double('cached_one'), double('cached_two')] }
    let(:sources) { [source_one, source_two] }

    before do
      Berkshelf::Resolver.stub(:new).and_return(resolver)
      Berkshelf::Lockfile.stub(:new).and_return(lockfile)

      subject.stub(:sha).and_return('abc123')

      lockfile.stub(:sources).and_return([])
      lockfile.stub(:sha).and_return('xyz456')

      resolver.stub(:sources).and_return([])
      lockfile.stub(:update)
    end

    context 'when a lockfile is not present' do
      it 'returns the result from sending the message resolve to resolver' do
        resolver.should_receive(:resolve).and_return(cached_cookbooks)
        expect(subject.install).to eql(cached_cookbooks)
      end

      it 'sets a value for self.cached_cookbooks equivalent to the return value' do
        resolver.should_receive(:resolve).and_return(cached_cookbooks)
        subject.install

        expect(subject.cached_cookbooks).to eql(cached_cookbooks)
      end

      it 'creates a new resolver and finds a solution by calling resolve on the resolver' do
        resolver.should_receive(:resolve)
        subject.install
      end

      it 'writes a lockfile with the resolvers sources' do
        resolver.should_receive(:resolve)
        lockfile.should_receive(:update).with([], sha: 'abc123')

        subject.install
      end
    end

    context 'when a value for :path is given' do
      before do
        resolver.should_receive(:resolve)
        resolver.should_receive(:sources).and_return([])
      end

      it 'sends the message :vendor to Berksfile with the value for :path' do
        path = double('path')
        subject.class.should_receive(:vendor).with(subject.cached_cookbooks, path)

        subject.install(path: path)
      end
    end

    context 'when a value for :except is given' do
      before do
        resolver.should_receive(:resolve)
        resolver.should_receive(:sources).and_return([])
        subject.stub(:sources).and_return(sources)
        subject.stub(:apply_lockfile).and_return(sources)
      end

      it 'filters the sources and gives the results to the Resolver initializer' do
        subject.should_receive(:sources).with(except: [:skip_me]).and_return(sources)
        subject.install(except: [:skip_me])
      end
    end

    context 'when a value for :only is given' do
      before do
        resolver.should_receive(:resolve)
        resolver.should_receive(:sources).and_return([])
        subject.stub(:sources).and_return(sources)
        subject.stub(:apply_lockfile).and_return(sources)
      end

      it 'filters the sources and gives the results to the Resolver initializer' do
        subject.should_receive(:sources).with(only: [:skip_me]).and_return(sources)
        subject.install(only: [:skip_me])
      end
    end
  end

  describe '#load' do
    let(:content) do
      <<-EOF.strip
      cookbook 'ntp', '<= 1.0.0'
      cookbook 'mysql'
      cookbook 'nginx', '< 0.101.2'
      cookbook 'ssh_known_hosts2', :git => 'https://github.com/erikh/chef-ssh_known_hosts2.git'
      EOF
    end

    it 'reads the content of a Berksfile and adds the sources to the Shelf' do
      subject.load(content)

      %w(ntp mysql nginx ssh_known_hosts2).each do |name|
        expect(subject).to have_source(name)
      end
    end

    it 'returns an instance of Berksfile' do
      expect(subject.load(content)).to be_a(Berkshelf::Berksfile)
    end
  end

  describe '#add_source' do
    let(:name) { 'cookbook_one' }
    let(:constraint) { '= 1.2.0' }
    let(:location) { { site: 'http://site' } }

    before(:each) do
      subject.add_source(name, constraint, location)
    end

    it 'adds new cookbook source to the list of sources' do
      expect(subject.sources).to have(1).source
    end

    it "adds a cookbook source with a 'name' of the given name" do
      expect(subject.sources.first.name).to eq(name)
    end

    it "adds a cookbook source with a 'version_constraint' of the given constraint" do
      expect(subject.sources.first.version_constraint.to_s).to eq(constraint)
    end

    it 'raises DuplicateSourceDefined if multiple sources of the same name are found' do
      expect {
        subject.add_source(name)
      }.to raise_error(Berkshelf::DuplicateSourceDefined)
    end
  end

  describe '#add_location' do
    let(:type) { :site }
    let(:value) { double('value') }
    let(:options) { double('options') }

    it 'delegates :add_location to the downloader' do
      subject.downloader.should_receive(:add_location).with(type, value, options)
      subject.add_location(type, value, options)
    end
  end

  describe '#upload' do
    let(:upload) { subject.upload(options) }
    let(:options) { Hash.new }
    let(:ssl) { double('ssl', verify: true) }
    let(:chef) {
      double('chef',
        node_name: 'fake-client',
        client_key: 'client-key',
        chef_server_url: 'http://configured-chef-server/'
      )
    }
    let(:berkshelf_config) { double('berks', ssl: ssl, chef: chef) }
    let(:default_ridley_options) {
      {
        client_name: 'fake-client',
        client_key: 'client-key',
        ssl: {
          verify: true
        }
      }
    }

    before do
      Berkshelf::Config.stub(:instance).and_return(berkshelf_config)
      subject.stub(:resolve).and_return(solution: [], sources: [])
    end

    context 'when there is no :server_url' do
      let(:chef) do
        double('chef',
          node_name: 'fake-client',
          client_key: 'client-key',
          chef_server_url: nil
        )
      end
      let(:message) { 'Missing required attribute in your Berkshelf configuration: chef.server_url' }

      it 'raises an error' do
        expect {
          upload
        }.to raise_error(Berkshelf::ChefConnectionError, message)
      end
    end

    context 'when there is no :client_name' do
      let(:chef) do
        double('chef',
          node_name: nil,
          client_key: 'client-key',
          chef_server_url: 'http://configured-chef-server/'
        )
      end
      let(:message) { 'Missing required attribute in your Berkshelf configuration: chef.node_name' }

      it 'raises an error' do
        expect {
          upload
        }.to raise_error(Berkshelf::ChefConnectionError, message)
      end
    end

    context 'when there is no :client_key' do
      let(:chef) do
        double('chef',
          node_name: 'fake-client',
          client_key: nil,
          chef_server_url: 'http://configured-chef-server/'
        )
      end
      let(:message) { 'Missing required attribute in your Berkshelf configuration: chef.client_key' }

      it 'raises an error' do
        expect {
          upload
        }.to raise_error(Berkshelf::ChefConnectionError, message)
      end
    end

    context 'when a Chef Server url is not passed as an option' do
      let(:ridley_options) do
        { server_url: 'http://configured-chef-server/' }.merge(default_ridley_options)
      end

      it 'uses Berkshelf::Config configured server_url' do
        Ridley.should_receive(:new).with(ridley_options)
        upload
      end
    end

    context 'when a Chef Server url is passed as an option' do
      let(:options) do
        {
          server_url: 'http://fake-chef-server.com/'
        }
      end
      let(:ridley_options) do
        { server_url: 'http://fake-chef-server.com/'}.merge(default_ridley_options)
      end

      it 'uses the passed in :server_url' do
        Ridley.should_receive(:new).with(ridley_options)
        upload
      end
    end
  end

  describe '#apply' do
    let(:env_name)    { 'berkshelf-test' }
    let(:server_url)  { Berkshelf::RSpec::ChefServer.server_url }
    let(:client_name) { 'reset' }
    let(:client_key)  { fixtures_path.join('reset.pem').to_s }
    let(:ridley)      { Ridley.new(server_url: server_url, client_name: client_name, client_key: client_key) }

    before do
      subject.stub(:ridley_connection).and_return(ridley)
      subject.add_source('nginx', '>= 0.1.2', chef_api: server_url, node_name: client_name, client_key: client_key)
      subject.stub(install: nil)
    end

    context 'when the chef environment exists' do
      let(:sources) do
        [
          double(name: 'nginx', locked_version: '1.2.3'),
          double(name: 'artifact', locked_version: '1.4.0')
        ]
      end

      before do
        chef_environment('berkshelf')
        subject.lockfile.stub(:sources).and_return(sources)
      end

      it 'installs the Berksfile' do
        subject.should_receive(:install)
        subject.apply('berkshelf')
      end

      it 'applys the locked_versions of the Lockfile sources to the given Chef environment' do
        subject.apply('berkshelf')

        environment = ::JSON.parse(chef_server.data['environments']['berkshelf'])
        expect(environment['cookbook_versions']).to have(2).items
        expect(environment['cookbook_versions']['nginx']).to eq('1.2.3')
        expect(environment['cookbook_versions']['artifact']).to eq('1.4.0')
      end
    end

    context 'when the environment does not exist' do
      it 'raises an EnvironmentNotFound error' do
        expect {
          subject.apply(env_name)
        }.to raise_error(Berkshelf::EnvironmentNotFound)
      end
    end

    context 'when Ridley throw an exception' do
      before { ridley.stub_chain(:environment, :find).and_raise(Ridley::Errors::RidleyError) }

      it 'raises a ChefConnectionError' do
        expect {
          subject.apply(env_name)
        }.to raise_error(Berkshelf::ChefConnectionError)
      end
    end
  end

  describe '#package' do
    context 'when the source does not exist' do
      it 'raises a CookbookNotFound exception' do
        expect {
          subject.package('non-existent', output: '/tmp')
        }.to raise_error(Berkshelf::CookbookNotFound)
      end
    end

    context 'when the source exists' do
      let(:source) { double('source') }
      let(:cached) { double('cached', path: '/foo/bar', cookbook_name: 'cookbook') }
      let(:options) { { output: '/tmp' } }

      before do
        FileUtils.stub(:cp_r)
        FileUtils.stub(:mkdir_p)
        subject.stub(:find).with('non-existent').and_return(source)
        subject.stub(:resolve).with(source, options).and_return({ solution: [cached], sources: [source] })
      end

      it 'resolves the sources' do
        subject.should_receive(:resolve).with(source, options)
        subject.package('non-existent', options)
      end

      it 'returns the output path' do
        expect(subject.package('non-existent', options)).to eq('/tmp/non-existent.tar.gz')
      end
    end
  end
end
