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

require "json"
require "pathname"
require "tempfile"

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

require "vagrant/machine_index"

describe Vagrant::MachineIndex do
  include_context "unit"

  let(:data_dir) { Pathname.new(Dir.mktmpdir("vagrant-test-machine-index-data-dir")) }
  let(:entry_klass) { Vagrant::MachineIndex::Entry }

  let(:new_entry) do
    entry_klass.new.tap do |e|
      e.name = "foo"
      e.vagrantfile_path = "/bar"
    end
  end

  subject { described_class.new(data_dir) }

  after do
    FileUtils.rm_rf(data_dir)
  end

  it "raises an exception if the data file is corrupt" do
    data_dir.join("index").open("w") do |f|
      f.write(JSON.dump({}))
    end

    expect { subject }.
      to raise_error(Vagrant::Errors::CorruptMachineIndex)
  end

  it "raises an exception if the JSON is invalid" do
    data_dir.join("index").open("w") do |f|
      f.write("foo")
    end

    expect { subject }.
      to raise_error(Vagrant::Errors::CorruptMachineIndex)
  end

  describe "#each" do
    before do
      5.times do |i|
        e = entry_klass.new
        e.name = "entry-#{i}"
        e.vagrantfile_path = "/foo"
        subject.release(subject.set(e))
      end
    end

    it "should iterate over all the elements" do
      items = []

      subject = described_class.new(data_dir)
      subject.each do |entry|
        items << entry.name
      end

      items.sort!
      expect(items).to eq([
        "entry-0",
        "entry-1",
        "entry-2",
        "entry-3",
        "entry-4",
      ])
    end
  end

  describe "#get and #release" do
    before do
      data = {
        "version" => 1,
        "machines" => {
          "bar" => {
            "name" => "default",
            "provider" => "vmware",
            "local_data_path" => "/foo",
            "vagrantfile_path" => "/foo/bar/baz",
            "state" => "running",
            "updated_at" => "foo",
          },
          "baz" => {
            "name" => "default",
            "provider" => "vmware",
            "vagrantfile_path" => "/foo/bar/baz",
            "state" => "running",
            "updated_at" => "foo",
            "extra_data" => {
              "foo" => "bar",
            },
          },
        }
      }

      data_dir.join("index").open("w") do |f|
        f.write(JSON.dump(data))
      end
    end

    it "returns nil if the machine doesn't exist" do
      expect(subject.get("foo")).to be_nil
      expect(subject.get(nil)).to be_nil
    end

    it "returns nil if the machine doesn't exist (is an empty string)" do
      expect(subject.get("")).to be_nil
      expect(subject.get(nil)).to be_nil
    end

    it "returns a valid entry if the machine exists" do
      result = subject.get("bar")

      expect(result.id).to eq("bar")
      expect(result.name).to eq("default")
      expect(result.provider).to eq("vmware")
      expect(result.local_data_path).to eq(Pathname.new("/foo"))
      expect(result.vagrantfile_path).to eq(Pathname.new("/foo/bar/baz"))
      expect(result.state).to eq("running")
      expect(result.updated_at).to eq("foo")
      expect(result.extra_data).to eq({})
    end

    it "returns a valid entry with extra data" do
      result = subject.get("baz")
      expect(result.id).to eq("baz")
      expect(result.extra_data).to eq({
        "foo" => "bar",
      })
    end

    it "returns a valid entry by unique prefix" do
      result = subject.get("b")

      expect(result).to_not be_nil
      expect(result.id).to eq("bar")
    end

    it "should include? by prefix" do
      expect(subject.include?("b")).to be(true)
    end

    it "should return false if given nil input" do
      expect(subject.include?(nil)).to be(false)
    end

    it "locks the entry so subsequent gets fail" do
      result = subject.get("bar")
      expect(result).to_not be_nil

      expect { subject.get("bar") }.
        to raise_error(Vagrant::Errors::MachineLocked)
    end

    it "can unlock a machine" do
      result = subject.get("bar")
      expect(result).to_not be_nil
      subject.release(result)

      result = subject.get("bar")
      expect(result).to_not be_nil
    end
  end

  describe "#include" do
    it "should not include non-existent things" do
      expect(subject.include?("foo")).to be(false)
    end

    it "should include created entries" do
      result = subject.set(new_entry)
      expect(result.id).to_not be_empty
      subject.release(result)

      subject = described_class.new(data_dir)
      expect(subject.include?(result.id)).to be(true)
    end
  end

  describe "#set and #get and #delete" do
    it "adds a new entry" do
      result = subject.set(new_entry)
      expect(result.id).to_not be_empty

      # It should be locked
      expect { subject.get(result.id) }.
        to raise_error(Vagrant::Errors::MachineLocked)

      # Get it froma new class and check the results
      subject.release(result)
      subject = described_class.new(data_dir)
      entry   = subject.get(result.id)
      expect(entry).to_not be_nil
      expect(entry.name).to eq("foo")

      # TODO: test that updated_at is set
    end

    it "can delete an entry" do
      result = subject.set(new_entry)
      expect(result.id).to_not be_empty
      subject.delete(result)

      # Get it from a new class and check the results
      subject = described_class.new(data_dir)
      entry   = subject.get(result.id)
      expect(entry).to be_nil
    end

    it "can delete an entry that doesn't exist" do
      e = entry_klass.new
      expect(subject.delete(e)).to be(true)
    end

    it "updates an existing entry" do
      entry = entry_klass.new
      entry.name = "foo"
      entry.vagrantfile_path = "/bar"

      result = subject.set(entry)
      expect(result.id).to_not be_empty

      result.name = "bar"
      result.extra_data["foo"] = "bar"

      nextresult = subject.set(result)
      expect(nextresult.id).to eq(result.id)

      # Release it so we can test the contents
      subject.release(nextresult)

      # Get it froma new class and check the results
      subject = described_class.new(data_dir)
      entry   = subject.get(result.id)
      expect(entry).to_not be_nil
      expect(entry.name).to eq("bar")
      expect(entry.extra_data).to eq({
        "foo" => "bar",
      })
    end

    it "updates an existing directory if the name, provider, and path are the same" do
      entry = entry_klass.new
      entry.name = "foo"
      entry.provider = "bar"
      entry.vagrantfile_path = "/bar"
      entry.state = "foo"

      result = subject.set(entry)
      expect(result.id).to_not be_empty

      # Release it so we can modify it
      subject.release(result)

      entry2 = entry_klass.new
      entry2.name = entry.name
      entry2.provider = entry.provider
      entry2.vagrantfile_path = entry.vagrantfile_path
      entry2.state = "bar"
      expect(entry2.id).to be_nil

      nextresult = subject.set(entry2)
      expect(nextresult.id).to eq(result.id)

      # Release it so we can test the contents
      subject.release(nextresult)

      # Get it from a new class and check the results
      subject = described_class.new(data_dir)
      entry   = subject.get(result.id)
      expect(entry).to_not be_nil
      expect(entry.name).to eq(entry2.name)
      expect(entry.state).to eq(entry2.state)
    end
  end

  describe "#recover" do
    it "recovers an entry if not in the index" do
      result = subject.recover(new_entry)
      expect(result.id).to_not be_empty
      expect { subject.get(result.id) }.to raise_error(Vagrant::Errors::MachineLocked)
    end

    it "returns an entry if in the index" do
      test_entry = entry_klass.new()
      entry = subject.set(test_entry)
      subject.release(entry)

      new_test_entry = entry_klass.new(id=entry.id, {})
      result = subject.recover(new_test_entry)

      expect(result.id).to eq(entry.id)
    end
  end
end

describe Vagrant::MachineIndex::Entry do
  include_context "unit"

  let(:env) {
    iso_env = isolated_environment
    iso_env.vagrantfile(vagrantfile)
    iso_env.create_vagrant_env
  }

  let(:vagrantfile) { "" }

  describe "#valid?" do
    let(:machine) { env.machine(:default, :dummy) }

    subject do
      described_class.new.tap do |e|
        e.name = "default"
        e.provider = "dummy"
        e.vagrantfile_path = env.root_path
      end
    end

    it "should be valid with a valid entry" do
      machine.id = "foo"
      expect(subject).to be_valid(env.home_path)
    end

    it "should be invalid if no Vagrantfile path is set" do
      subject.vagrantfile_path = nil
      expect(subject).to_not be_valid(env.home_path)
    end

    it "should be invalid if the Vagrantfile path does not exist" do
      subject.vagrantfile_path = Pathname.new("/i/should/not/exist")
      expect(subject).to_not be_valid(env.home_path)
    end

    it "should be invalid if the machine is inactive" do
      machine.id = nil
      expect(subject).to_not be_valid(env.home_path)
    end

    it "should be invalid if machine is not created" do
      machine.id = "foo"
      machine.provider.state = Vagrant::MachineState::NOT_CREATED_ID
      expect(subject).to_not be_valid(env.home_path)
    end

    context "with another active machine" do
      let(:vagrantfile) do
        <<-VF
        Vagrant.configure("2") do |config|
          config.vm.define "web"
          config.vm.define "db"
        end
        VF
      end

      it "should be invalid if the wrong machine is active only" do
        m = env.machine(:web, :dummy)
        m.id = "foo"

        subject.name = "db"
        expect(subject).to_not be_valid(env.home_path)
      end

      it "should be valid if the correct machine is active" do
        env.machine(:web, :dummy).id = "foo"
        env.machine(:db, :dummy).id = "foo"

        subject.name = "db"
        expect(subject).to be_valid(env.home_path)
      end
    end
  end
end
