require "spec_helper"

describe Berkshelf::Berksfile do
  describe "ClassMethods" do
    describe "::from_file" 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'
        solver :foo, :required
        EOF
      end
      let(:berksfile) { tmp_path.join("Berksfile") }

      before { File.open(berksfile, "w+") { |f| f.write(content) } }
      subject(:from_file) { described_class.from_file(berksfile) }

      it "reads the content of the Berksfile and binds them to a new instance" do
        %w{ntp mysql nginx ssh_known_hosts2}.each do |name|
          expect(subject).to have_dependency(name)
        end
      end

      it "returns an instance of Berkshelf::Berksfile" do
        expect(subject).to be_a(described_class)
      end

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

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

  let(:dependency_one) { double("dependency_one", name: "nginx") }
  let(:dependency_two) { double("dependency_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_dependency message with the name, constraint, and options to the instance of the includer" do
      expect(subject).to receive(:add_dependency).with(name, constraint, default_options)
      subject.cookbook(name, constraint, default_options)
    end

    it "merges the default options into specified options" do
      expect(subject).to receive(:add_dependency) do |arg_name, arg_constraint, arg_options|
        expect(arg_name).to eq(name)
        expect(arg_constraint).to eq(constraint)
        expect(arg_options[:path]).to match(%r{/Users/reset})
        expect(arg_options[:group]).to eq([])
      end

      subject.cookbook(name, constraint, path: "/Users/reset")
    end

    it "converts a single specified group option into an array of groups" do
      expect(subject).to receive(:add_dependency).with(name, constraint, group: [:production])
      subject.cookbook(name, constraint, group: :production)
    end

    context "when no constraint specified" do
      it "sends the add_dependency message with a nil value for constraint" do
        expect(subject).to receive(:add_dependency).with(name, nil, default_options)
        subject.cookbook(name, default_options)
      end
    end

    context "when no options specified" do
      it "sends the add_dependency message with an empty Hash for the value of options" do
        expect(subject).to receive(:add_dependency).with(name, constraint, default_options)
        subject.cookbook(name, constraint)
      end
    end

    it "is a DSL method" do
      expect(subject).to have_exposed_method(:cookbook)
    end
  end

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

    it "sends the add_dependency message with an array of groups determined by the parameter to the group block" do
      expect(subject).to receive(:add_dependency).with(name, nil, group: [group])

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

    it "is a DSL method" do
      expect(subject).to have_exposed_method(:group)
    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_dependency message with an explicit version constraint and the path to the cookbook" do
      expect(subject).to receive(:add_dependency).with("example_cookbook", nil, path: path.to_s, metadata: true)
      subject.metadata
    end

    it "is a DSL method" do
      expect(subject).to have_exposed_method(:metadata)
    end
  end

  describe "#source" do
    let(:new_source) { "http://berks.riotgames.com" }

    it "is a DSL method" do
      expect(subject).to have_exposed_method(:source)
    end

    it "adds a source to the sources" do
      subject.source(new_source)
      expect(subject.sources.map(&:to_s)).to include(new_source)
    end

    it "converts the string to a Source" do
      subject.source(new_source)
      subject.sources.each do |source|
        expect(source).to be_a(Berkshelf::Source)
      end
    end

    it "adds each source in order they appear" do
      subject.source(new_source)
      subject.source("http://berks.other.com")
      expect(subject.sources[0].to_s).to eq(new_source)
      expect(subject.sources[1].to_s).to eq("http://berks.other.com")
    end

    it "does not add duplicate entries" do
      subject.source(new_source)
      subject.source(new_source)
      expect(subject.sources[0].to_s).to eq(new_source)
      expect(subject.sources[1].to_s).to_not eq(new_source)
    end

    context "adding an invalid source" do
      let(:invalid_uri) { ".....$1233...." }

      it "raises an InvalidSourceURI" do
        expect { subject.source(invalid_uri).uri }.to raise_error(Berkshelf::InvalidSourceURI)
      end
    end
  end

  describe "#sources" do
    context "when there are no sources" do
      it "raises an exception" do
        expect do
          subject.sources
        end.to raise_error(Berkshelf::NoAPISourcesDefined)
      end
    end

    context "when there are sources" do
      before { subject.source("https://api.berkshelf.org") }

      it "returns an Array" do
        expect(subject.sources).to be_a(Array)
      end

      it "contains a collection of Berkshelf::Source" do
        subject.sources.each do |source|
          expect(source).to be_a(Berkshelf::Source)
        end
      end
    end
  end

  describe "#extension" do
    it "is a DSL method" do
      expect(subject).to have_exposed_method(:extension)
    end
  end

  describe "#dependencies" do
    let(:groups) do
      %i{
        nautilus
        skarner
      }
    end

    it "returns all Berkshelf::Dependencys added to the instance of Berksfile" do
      subject.add_dependency(dependency_one.name)
      subject.add_dependency(dependency_two.name)

      expect(subject.dependencies.size).to eq(2)
      expect(subject).to have_dependency(dependency_one.name)
      expect(subject).to have_dependency(dependency_two.name)
    end
  end

  describe "#cookbooks" do
    it "raises an exception if a cookbook is not installed" do
      subject.add_dependency("bacon", nil)
      expect { subject.cookbooks }.to raise_error(Berkshelf::DependencyNotFound)
    end

    it "retrieves the locked (cached) cookbook for each dependency" do
      subject.add_dependency("bacon", nil)
      subject.add_dependency("ham", nil)
      allow(subject).to receive(:retrive_locked)

      expect(subject).to receive(:retrieve_locked).twice
      subject.cookbooks
    end
  end

  describe "#groups" do
    before do
      allow(subject).to receive(:dependencies) { [dependency_one, dependency_two] }
      allow(dependency_one).to receive(:groups) { %i{nautilus skarner} }
      allow(dependency_two).to receive(:groups) { %i{nautilus riven} }
    end

    it "returns a hash containing keys for every group a dependency is a member of" do
      expect(subject.groups.keys.size).to eq(3)
      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 Berkshelf::Dependencys who are members of the group for value" do
      expect(subject.groups[:nautilus].size).to eq(2)
      expect(subject.groups[:riven].size).to eq(1)
    end
  end

  describe "#add_dependency" do
    let(:name) { "cookbook_one" }
    let(:constraint) { "= 1.2.0" }
    let(:options) { {} }

    before(:each) do
      subject.add_dependency(name, constraint, options)
    end

    let(:dependency) { subject.dependencies.first }

    it "adds new dependency to the list of dependencies" do
      expect(subject.dependencies.size).to eq(1)
    end

    it "is a Berkshelf::Dependency" do
      expect(dependency).to be_a(Berkshelf::Dependency)
    end

    it "has a name matching the given name" do
      expect(dependency.name).to eq(name)
    end

    it "has a version_constraint matching the given constraint" do
      expect(dependency.version_constraint.to_s).to eq(constraint)
    end

    it "raises DuplicateDependencyDefined if multiple dependencies of the same name are found" do
      expect do
        subject.add_dependency(name)
      end.to raise_error(Berkshelf::DuplicateDependencyDefined)
    end

    it "has a nil location if no location options are provided" do
      expect(dependency.location).to be_nil
    end

    context "when given the :git option" do
      let(:options) { { git: "git@github.com:berkshelf/berkshelf.git" } }

      it "has a GitLocation location" do
        expect(dependency.location).to be_a(Berkshelf::GitLocation)
      end
    end

    context "when given the :github option" do
      let(:options) { { github: "berkshelf/berkshelf" } }

      it "has a GithubLocation location" do
        expect(dependency.location).to be_a(Berkshelf::GithubLocation)
      end
    end

    context "when given the :path option" do
      let(:options) { { path: fixtures_path.join("cookbooks", "example_cookbook") } }

      it "has a PathLocation location" do
        expect(dependency.location).to be_a(Berkshelf::PathLocation)
      end
    end
  end

  describe "#retrieve_locked" do
    let(:lockfile) { double("lockfile") }
    let(:dependency) { double("dependency", name: "bacon") }
    let(:locked) { double("locked", cached_cookbook: cached, locked_version: "1.0.0") }
    let(:cached) { double("cached") }

    before do
      allow(subject).to receive(:lockfile).and_return(lockfile)
    end

    it "delegates to the lockfile" do
      expect(lockfile).to receive(:retrieve).with(dependency)
      subject.retrieve_locked(dependency)
    end
  end

  describe "#upload" do
    let(:uploader) { double(Berkshelf::Uploader, run: nil) }

    before do
      allow(subject).to receive(:validate_lockfile_present!)
      allow(subject).to receive(:validate_lockfile_trusted!)
      allow(subject).to receive(:validate_dependencies_installed!)

      allow(Berkshelf::Uploader).to receive(:new).and_return(uploader)
    end

    it "validates the lockfile is present" do
      expect(subject).to receive(:validate_lockfile_present!).once
      subject.upload
    end

    it "validates the lockfile is trusted" do
      expect(subject).to receive(:validate_lockfile_trusted!).once
      subject.upload
    end

    it "validates the dependencies are installed" do
      expect(subject).to receive(:validate_dependencies_installed!).once
      subject.upload
    end

    it "creates a new Uploader" do
      expect(Berkshelf::Uploader).to receive(:new).with(subject)
      expect(uploader).to receive(:run)

      subject.upload
    end
  end

  describe "#vendor" do
    let(:cached_cookbook)    { instance_double(Berkshelf::CachedCookbook, cookbook_name: "my_cookbook", path: "/my_cookbook/path", compile_metadata: true, reload: nil) }
    let(:installer)          { instance_double(Berkshelf::Installer, run: [cached_cookbook]) }
    let(:raw_metadata_files) { [File.join(cached_cookbook.cookbook_name, "metadata.rb")] }

    let(:destination) { "/a/destination/path" }
    let(:options) { { exclude: Berkshelf::Berksfile::EXCLUDED_VCS_FILES_WHEN_VENDORING, delete: nil } }

    before do
      allow(Berkshelf::Installer).to receive(:new).and_return(installer)
    end

    it "invokes FileSyncer with correct arguments" do
      expect(Berkshelf::FileSyncer).to receive(:sync).with(/vendor/, destination, options)

      subject.vendor(destination)
    end

    it "excludes the top-level metadata.rb file" do
      expect(options[:exclude].any? { |exclude| File.fnmatch?(exclude, "my_cookbook/recipes/metadata.rb", File::FNM_DOTMATCH) }).to be(false)
    end
  end

  describe "#solver" do

    let(:solver_precedence) { :please_dont_exist }

    it "defaults to nil required solver and :gecode preferred solver" do
      expect(subject.preferred_solver).to equal(:gecode)
      expect(subject.required_solver).to equal(nil)
    end

    it "adds preferred and required solvers" do
      subject.solver(:a, :preferred)
      subject.solver(:b, :required)
      expect(subject.preferred_solver).to equal(:a)
      expect(subject.required_solver).to equal(:b)
    end

    it "raises an exception with a bad precedence" do
      expect { subject.solver(:foo, :bar) }.to raise_error(/Invalid solver precedence ':bar'/)
    end

    it "is a DSL method" do
      expect(subject).to have_exposed_method(:solver)
    end
  end
end
