# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1

require File.expand_path("../../base", __FILE__)

require "pathname"
require "tmpdir"

require "vagrant/vagrantfile"

describe Vagrant::Vagrantfile do
  include_context "unit"

  let(:keys) { [] }
  let(:loader) {
    Vagrant::Config::Loader.new(
      Vagrant::Config::VERSIONS, Vagrant::Config::VERSIONS_ORDER)
  }

  subject { described_class.new(loader, keys) }

  before do
    keys << :test
  end

  def configure(&block)
    loader.set(:test, [["2", block]])
  end

  # A helper to register a provider for use in tests.
  def register_provider(name, config_class=nil, options=nil)
    provider_cls = Class.new(VagrantTests::DummyProvider) do
      if options && options[:unusable]
        def self.usable?(raise_error=false)
          raise Vagrant::Errors::VagrantError if raise_error
          false
        end
      end
    end

    register_plugin("2") do |p|
      p.provider(name, options) { provider_cls }

      if config_class
        p.config(name, :provider) { config_class }
      end
    end

    provider_cls
  end

  describe "#config" do
    it "exposes the global configuration" do
      configure do |config|
        config.vm.box = "what"
      end

      expect(subject.config.vm.box).to eq("what")
    end
  end

  describe "#machine" do
    let(:boxes) { Vagrant::BoxCollection.new(iso_env.boxes_dir) }
    let(:data_path) { Pathname.new(Dir.mktmpdir("vagrant-machine-data-path")) }
    let(:env)   { iso_env.create_vagrant_env }
    let(:iso_env) { isolated_environment }
    let(:vagrantfile) { described_class.new(loader, keys) }

    subject { vagrantfile.machine(:default, :foo, boxes, data_path, env) }

    before do
      @foo_config_cls = Class.new(Vagrant.plugin("2", "config")) do
        attr_accessor :value
      end

      @provider_cls = register_provider("foo", @foo_config_cls)

      configure do |config|
        config.vm.box = "foo"
        config.vm.provider "foo" do |p|
          p.value = "rawr"
        end
      end

      iso_env.box3("foo", "1.0", :foo, vagrantfile: <<-VF)
      Vagrant.configure("2") do |config|
        config.ssh.port = 123
      end
      VF
    end

    after do
      FileUtils.rm_rf(data_path.to_s)
    end

    describe '#data_dir' do
      subject { super().data_dir }
      it { should eq(data_path) }
    end

    describe '#env' do
      subject { super().env }
      it      { should equal(env)    }
    end

    describe '#name' do
      subject { super().name }
      it     { should eq(:default)  }
    end

    describe '#provider' do
      subject { super().provider }
      it { should be_kind_of(@provider_cls) }
    end

    describe '#provider_name' do
      subject { super().provider_name }
      it { should eq(:foo) }
    end

    describe '#vagrantfile' do
      subject { super().vagrantfile }
      it { should equal(vagrantfile) }
    end

    it "has the proper box" do
      expect(subject.box.name).to eq("foo")
    end

    it "has the valid configuration" do
      expect(subject.config.vm.box).to eq("foo")
    end

    it "loads the provider-specific configuration" do
      expect(subject.provider_config).to be_kind_of(@foo_config_cls)
      expect(subject.provider_config.value).to eq("rawr")
    end
  end

  describe "#machine_config" do
    let(:iso_env) { isolated_environment }
    let(:boxes) { Vagrant::BoxCollection.new(iso_env.boxes_dir) }

    it "should return a basic configured machine" do
      provider_cls = register_provider("foo")

      configure do |config|
        config.vm.box = "foo"
      end

      results = subject.machine_config(:default, :foo, boxes)
      box     = results[:box]
      config  = results[:config]
      expect(config.vm.box).to eq("foo")
      expect(box).to be_nil
      expect(results[:provider_cls]).to equal(provider_cls)
    end

    it "configures without a provider or boxes" do
      register_provider("foo")

      configure do |config|
        config.vm.box = "foo"
      end

      results = subject.machine_config(:default, nil, nil)
      box     = results[:box]
      config  = results[:config]
      expect(config.vm.box).to eq("foo")
      expect(box).to be_nil
      expect(results[:provider_cls]).to be_nil
    end

    it "configures with sub-machine config" do
      register_provider("foo")

      configure do |config|
        config.ssh.port = "1"
        config.vm.box = "base"

        config.vm.define "foo" do |f|
          f.ssh.port = 100
        end
      end

      results = subject.machine_config(:foo, :foo, boxes)
      config  = results[:config]
      expect(config.vm.box).to eq("base")
      expect(config.ssh.port).to eq(100)
    end

    it "configures with box configuration if it exists" do
      register_provider("foo")

      configure do |config|
        config.vm.box = "base"
      end

      iso_env.box3("base", "1.0", :foo, vagrantfile: <<-VF)
      Vagrant.configure("2") do |config|
        config.ssh.port = 123
      end
      VF

      results = subject.machine_config(:default, :foo, boxes)
      box     = results[:box]
      config  = results[:config]
      expect(config.vm.box).to eq("base")
      expect(config.ssh.port).to eq(123)
      expect(box).to_not be_nil
      expect(box.name).to eq("base")
    end

    it "does not configure box configuration if set to ignore" do
      register_provider("foo")

      configure do |config|
        config.vm.box = "base"
        config.vm.ignore_box_vagrantfile = true
      end

      iso_env.box3("base", "1.0", :foo, vagrantfile: <<-VF)
      Vagrant.configure("2") do |config|
        config.ssh.port = 123
        config.vm.hostname = "hello"
      end
      VF

      results = subject.machine_config(:default, :foo, boxes)
      box     = results[:box]
      config  = results[:config]
      expect(config.vm.box).to eq("base")
      expect(config.ssh.port).to eq(nil)
      expect(config.vm.hostname).to eq(nil)
      expect(box).to_not be_nil
      expect(box.name).to eq("base")
    end

    it "configures with the proper box version" do
      register_provider("foo")

      configure do |config|
        config.vm.box = "base"
        config.vm.box_version = "~> 1.2"
      end

      iso_env.box3("base", "1.0", :foo, vagrantfile: <<-VF)
      Vagrant.configure("2") do |config|
        config.ssh.port = 123
      end
      VF

      iso_env.box3("base", "1.3", :foo, vagrantfile: <<-VF)
      Vagrant.configure("2") do |config|
        config.ssh.port = 245
      end
      VF

      results = subject.machine_config(:default, :foo, boxes)
      box     = results[:box]
      config  = results[:config]
      expect(config.vm.box).to eq("base")
      expect(config.ssh.port).to eq(245)
      expect(box).to_not be_nil
      expect(box.name).to eq("base")
      expect(box.version).to eq("1.3")
    end

    it "configures with the custom box architecture" do
      register_provider("foo")
      configure do |config|
        config.vm.box = "base"
        config.vm.box_architecture = "custom-arch"
      end

      results = subject.machine_config(:default, :foo, boxes)
      config = results[:config]
      expect(config.vm.box).to eq("base")
      expect(config.vm.box_architecture).to eq("custom-arch")
    end

    it "configures with the default box architecture" do
      register_provider("foo")
      configure do |config|
        config.vm.box = "base"
      end

      results = subject.machine_config(:default, :foo, boxes)
      config = results[:config]
      expect(config.vm.box).to eq("base")
      expect(config.vm.box_architecture).to eq(:auto)
    end

    it "configures box architecture to nil" do
      register_provider("foo")
      configure do |config|
        config.vm.box = "base"
        config.vm.box_architecture = nil
      end

      results = subject.machine_config(:default, :foo, boxes)
      config = results[:config]
      expect(config.vm.box).to eq("base")
      expect(config.vm.box_architecture).to be_nil
    end

    it "configures with box config of other supported formats" do
      register_provider("foo", nil, box_format: "bar")

      configure do |config|
        config.vm.box = "base"
      end

      iso_env.box3("base", "1.0", :bar, vagrantfile: <<-VF)
      Vagrant.configure("2") do |config|
        config.ssh.port = 123
      end
      VF

      results = subject.machine_config(:default, :foo, boxes)
      config  = results[:config]
      expect(config.vm.box).to eq("base")
      expect(config.ssh.port).to eq(123)
    end

    it "loads provider overrides if set" do
      register_provider("foo")
      register_provider("bar")

      configure do |config|
        config.ssh.port = 1
        config.vm.box = "base"

        config.vm.provider "foo" do |_, c|
          c.ssh.port = 100
        end
      end

      # Test with the override
      results = subject.machine_config(:default, :foo, boxes)
      config  = results[:config]
      expect(config.vm.box).to eq("base")
      expect(config.ssh.port).to eq(100)

      # Test without the override
      results = subject.machine_config(:default, :bar, boxes)
      config  = results[:config]
      expect(config.vm.box).to eq("base")
      expect(config.ssh.port).to eq(1)
    end

    it "loads the proper box if in a provider override" do
      register_provider("foo")

      configure do |config|
        config.vm.box = "base"
        config.vm.box_version = "1.0"

        config.vm.provider "foo" do |_, c|
          c.vm.box = "foobox"
          c.vm.box_version = "2.0"
        end
      end

      iso_env.box3("base", "1.0", :foo, vagrantfile: <<-VF)
      Vagrant.configure("2") do |config|
        config.ssh.port = 123
      end
      VF

      iso_env.box3("foobox", "2.0", :foo, vagrantfile: <<-VF)
      Vagrant.configure("2") do |config|
        config.ssh.port = 234
      end
      VF

      results = subject.machine_config(:default, :foo, boxes)
      config  = results[:config]
      box     = results[:box]
      expect(config.vm.box).to eq("foobox")
      expect(config.ssh.port).to eq(234)
      expect(config.vm.box_version).to eq("2.0")
      expect(box).to_not be_nil
      expect(box.name).to eq("foobox")
      expect(box.version).to eq("2.0")
    end

    it "raises an error if the machine is not found" do
      expect { subject.machine_config(:foo, :foo, boxes) }.
        to raise_error(Vagrant::Errors::MachineNotFound)
    end

    it "raises an error if the provider is not found" do
      expect { subject.machine_config(:default, :foo, boxes) }.
        to raise_error(Vagrant::Errors::ProviderNotFound)
    end

    it "raises an error if the provider is not found but gives suggestion" do
      register_provider("foo")

      expect { subject.machine_config(:default, :Foo, boxes) }.
        to raise_error(Vagrant::Errors::ProviderNotFoundSuggestion)
    end

    it "raises an error if the provider is not usable" do
      register_provider("foo", nil, unusable: true)

      expect { subject.machine_config(:default, :foo, boxes) }.
        to raise_error(Vagrant::Errors::ProviderNotUsable)
    end

    it "does not try to load the box if the box is empty" do
      provider_cls = register_provider("foo")

      configure do |config|
        config.vm.box = ""
      end

      expect(boxes).not_to receive(:find)
      results = subject.machine_config(:default, :foo, boxes)
    end

    context "when provider validation is ignored" do
      before do
        configure do |config|
          config.vm.box = "base"
          config.vm.box_version = "1.0"
          config.vm.define :guest1
          config.vm.define :guest2

          config.vm.provider "custom" do |_, c|
            c.ssh.port = 123
          end
        end

        iso_env.box3("base", "1.0", :custom, vagrantfile: <<-VF)
        Vagrant.configure("2") do |config|
          config.vagrant.plugins = "vagrant-custom"
        end
        VF
      end

      it "should not raise an error if provider is not found" do
        expect { subject.machine_config(:guest1, :custom, boxes, nil, false) }.
          not_to raise_error
      end

      it "should return configuration from box Vagrantfile" do
        config = subject.machine_config(:guest1, :custom, boxes, nil, false)[:config]
        expect(config.vagrant.plugins).to be_a(Hash)
        expect(config.vagrant.plugins.keys).to include("vagrant-custom")
      end
    end

    context "local box metadata file" do
      let(:data_path) { double(:data_path) }
      let(:meta_file) { double(:meta_file) }
      let(:box_version) { "2.0" }

      before do
        register_provider("foo")
        iso_env.box3("base", "1.0", :foo)
        allow(data_path).to receive(:join).with("box_meta").
          and_return(meta_file)
        allow(meta_file).to receive(:file?).and_return(false)
        configure do |config|
          config.vm.box = "base"
          config.vm.box_version = box_version
        end
      end

      it "checks for local box metadata file" do
        expect(meta_file).to receive(:file?).and_return(false)
        subject.machine_config(:default, :foo, boxes, data_path)
      end

      context "file exists" do
        let(:meta_file_content) { '{"name":"base","version":"1.0"}' }

        before do
          allow(meta_file).to receive(:file?).and_return(true)
          allow(meta_file).to receive(:read).and_return(meta_file_content)
        end

        it "reads the local box metadata file" do
          expect(meta_file).to receive(:read).and_return(meta_file_content)
          subject.machine_config(:default, :foo, boxes, data_path)
        end

        it "properly loads the box defined in metadata" do
          result = subject.machine_config(:default, :foo, boxes, data_path)
          expect(result[:box]).not_to be_nil
        end

        context "with invalid box version" do
          let(:box_version) { "1.0" }
          let(:meta_file_content) { '{"name":"base","version":"2.0"}' }

          it "loads box base on Vagrantfile information" do
            result = subject.machine_config(:default, :foo, boxes, data_path)
            expect(result[:box]).not_to be_nil
          end
        end
      end
    end
  end

  describe "#machine_names" do
    it "returns the default name when single-VM" do
      configure { |config| }

      expect(subject.machine_names).to eq([:default])
    end

    it "returns all of the names in a multi-VM" do
      configure do |config|
        config.vm.define "foo"
        config.vm.define "bar"
      end

      expect(subject.machine_names).to eq(
        [:foo, :bar])
    end
  end

  describe "#machine_names_and_options" do
    it "returns the default name" do
      configure { |config| }

      expect(subject.machine_names_and_options).to eq({
        default: { config_version: "2" },
      })
    end

    it "returns all the machines" do
      configure do |config|
        config.vm.define "foo"
        config.vm.define "bar", autostart: false
        config.vm.define "baz", autostart: true
      end

      expect(subject.machine_names_and_options).to eq({
        foo: { config_version: "2" },
        bar: { config_version: "2", autostart: false },
        baz: { config_version: "2", autostart: true },
      })
    end
  end

  describe "#primary_machine_name" do
    it "returns the default name when single-VM" do
      configure { |config| }

      expect(subject.primary_machine_name).to eq(:default)
    end

    it "returns the designated machine in multi-VM" do
      configure do |config|
        config.vm.define "foo"
        config.vm.define "bar", primary: true
        config.vm.define "baz"
      end

      expect(subject.primary_machine_name).to eq(:bar)
    end

    it "returns nil if no designation in multi-VM" do
      configure do |config|
        config.vm.define "foo"
        config.vm.define "baz"
      end

      expect(subject.primary_machine_name).to be_nil
    end
  end
end
