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

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

require "pathname"
require "stringio"
require "tempfile"

require "vagrant/box_metadata"

describe Vagrant::Box, :skip_windows do
  include_context "unit"

  let(:environment)   { isolated_environment }

  let(:box_collection) { Vagrant::BoxCollection.new(environment.boxes_dir) }

  let(:name)          { "foo" }
  let(:provider)      { :virtualbox }
  let(:version)       { "1.0" }
  let(:architecture)  { "test-architecture" }
  let(:directory)     { environment.box3("foo", "1.0", :virtualbox) }
  subject             { described_class.new(name, provider, version, directory) }

  describe '#metadata_url' do
    subject { super().metadata_url }
    it { should be_nil }
  end

  it "provides the name" do
    expect(subject.name).to eq(name)
  end

  it "provides the provider" do
    expect(subject.provider).to eq(provider)
  end

  it "provides the directory" do
    expect(subject.directory).to eq(directory)
  end

  it "provides the metadata associated with a box" do
    data = { "foo" => "bar" }

    # Write the metadata
    directory.join("metadata.json").open("w") do |f|
      f.write(JSON.generate(data))
    end

    # Verify the metadata
    expect { subject.metadata }.
        to raise_error(Vagrant::Errors::BoxMetadataMissingRequiredFields)
  end

  it "provides the metadata associated with a box" do
    data = { "provider" => "bar" }

    # Write the metadata
    directory.join("metadata.json").open("w") do |f|
      f.write(JSON.generate(data))
    end

    # Verify the metadata
    expect(subject.metadata).to eq(data)
  end

  context "with a metadata URL" do
    subject do
      described_class.new(
        name, provider, version, directory,
        metadata_url: "foo")
    end

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

  context "with a corrupt metadata file" do
    before do
      directory.join("metadata.json").open("w") do |f|
        f.write("")
      end
    end

    it "should raise an exception" do
      expect { subject }.
        to raise_error(Vagrant::Errors::BoxMetadataCorrupted)
    end
  end

  context "without a metadata file" do
    before :each do
      directory.join("metadata.json").delete
    end

    it "should raise an exception" do
      expect { subject }.
        to raise_error(Vagrant::Errors::BoxMetadataFileNotFound)
    end
  end

  context "#has_update?" do
    subject do
      described_class.new(
        name, provider, version, directory,
        metadata_url: "foo")
    end

    it "raises an exception if no metadata_url is set" do
      subject = described_class.new(
        name, provider, version, directory)

      expect { subject.has_update?("> 0") }.
        to raise_error(Vagrant::Errors::BoxUpdateNoMetadata)
    end

    it "returns nil if there is no update" do
      metadata = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW))
      {
        "name": "foo",
        "versions": [
          { "version": "1.0" }
        ]
      }
      RAW

      allow(subject).to receive(:load_metadata).and_return(metadata)

      expect(subject.has_update?).to be_nil
    end

    it "returns the updated box info if there is an update available" do
      metadata = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW))
      {
        "name": "foo",
        "versions": [
          {
            "version": "1.0"
          },
          {
            "version": "1.1",
            "providers": [
              {
                "name": "virtualbox",
                "url": "bar"
              }
            ]
          }
        ]
      }
      RAW

      allow(subject).to receive(:load_metadata).and_return(metadata)

      result = subject.has_update?
      expect(result).to_not be_nil

      expect(result[0]).to be_kind_of(Vagrant::BoxMetadata)
      expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version)
      expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider)

      expect(result[0].name).to eq("foo")
      expect(result[1].version).to eq("1.1")
      expect(result[2].url).to eq("bar")
    end

    it "returns the updated box info within constraints" do
      metadata = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW))
      {
        "name": "foo",
        "versions": [
          {
            "version": "1.0"
          },
          {
            "version": "1.1",
            "providers": [
              {
                "name": "virtualbox",
                "url": "bar"
              }
            ]
          },
          {
            "version": "1.4",
            "providers": [
              {
                "name": "virtualbox",
                "url": "bar"
              }
            ]
          }
        ]
      }
      RAW

      allow(subject).to receive(:load_metadata).and_return(metadata)

      result = subject.has_update?(">= 1.1, < 1.4")
      expect(result).to_not be_nil

      expect(result[0]).to be_kind_of(Vagrant::BoxMetadata)
      expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version)
      expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider)

      expect(result[0].name).to eq("foo")
      expect(result[1].version).to eq("1.1")
      expect(result[2].url).to eq("bar")
    end

    context "with architecture" do
      subject do
        described_class.new(
          name, provider, version, directory,
          architecture: architecture,
          metadata_url: "foo"
        )
      end

      it "raises an exception if no metadata_url is set" do
        subject = described_class.new(
          name, provider, version, directory,
          architecture: architecture,
        )

        expect { subject.has_update?("> 0") }.
          to raise_error(Vagrant::Errors::BoxUpdateNoMetadata)
      end

      it "returns nil if there is no update" do
        metadata = Vagrant::BoxMetadata.new(
          {
            name: "foo",
            versions: [ { version: "1.0" } ]
          }.to_json
        )
        allow(subject).to receive(:load_metadata).and_return(metadata)

        expect(subject.has_update?).to be_nil
      end

      it "returns the updated box info if there is an update available" do
        metadata = Vagrant::BoxMetadata.new(
          {
            name: "foo",
            versions: [
              {version: "1.0"},
              {
                version: "1.1",
                providers: [
                  {
                    name: "virtualbox",
                    url: "bar",
                    architecture: architecture,
                  }
                ]
              }
            ]
          }.to_json
        )

        allow(subject).to receive(:load_metadata).and_return(metadata)

        result = subject.has_update?
        expect(result).to_not be_nil

        expect(result[0]).to be_kind_of(Vagrant::BoxMetadata)
        expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version)
        expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider)

        expect(result[0].name).to eq("foo")
        expect(result[1].version).to eq("1.1")
        expect(result[2].url).to eq("bar")
      end

      it "returns nil if update does not support architecture" do
        metadata = Vagrant::BoxMetadata.new(
          {
            name: "foo",
            versions: [
              {version: "1.0"},
              {
                version: "1.1",
                providers: [
                  {
                    name: "virtualbox",
                    url: "bar",
                    architecture: "other-architecture",
                  }
                ]
              }
            ]
          }.to_json
        )

        allow(subject).to receive(:load_metadata).and_return(metadata)

        result = subject.has_update?
        expect(result).to be_nil
      end

      it "returns the updated box info within constraints" do
        metadata = Vagrant::BoxMetadata.new(
          {
            name: "foo",
            versions: [
              {
                version: "1.0",
              },
              {
                version: "1.1",
                providers: [
                  {
                    name: "virtualbox",
                    url: "bar",
                    architecture: architecture
                  },
                ]
              },
              {
                version: "1.2",
                providers: [
                  {
                    name: "virtualbox",
                    url: "bar",
                    architecture: "other-architecture",
                  },
                ]
              },
              {
                version: "1.4",
                providers: [
                  {
                    name: "virtualbox",
                    url: "bar",
                    architecture: architecture
                  }
                ]
              }
            ]
          }.to_json
        )

        allow(subject).to receive(:load_metadata).and_return(metadata)

        result = subject.has_update?(">= 1.1, < 1.4")
        expect(result).to_not be_nil

        expect(result[0]).to be_kind_of(Vagrant::BoxMetadata)
        expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version)
        expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider)

        expect(result[0].name).to eq("foo")
        expect(result[1].version).to eq("1.1")
        expect(result[2].url).to eq("bar")
      end
    end
  end

  context "#automatic_update_check_allowed?" do
    it "should return true on intial check" do
      expect(subject.automatic_update_check_allowed?).to be_truthy
    end

    it "should return false on second check" do
      expect(subject.automatic_update_check_allowed?).to be_truthy
      expect(subject.automatic_update_check_allowed?).to be_falsey
    end

    it "should use a file to mark last check time" do
      expect(FileUtils).to receive(:touch)
      subject.automatic_update_check_allowed?
    end

    it "should return true when time since last check is greater than check interval" do
      subject.automatic_update_check_allowed?
      stub_const("Vagrant::Box::BOX_UPDATE_CHECK_INTERVAL", -1)
      expect(subject.automatic_update_check_allowed?).to be_truthy
    end
  end

  context "#in_use?" do
    let(:index) { [] }

    def new_entry(name, provider, version, architecture=nil)
      Vagrant::MachineIndex::Entry.new.tap do |entry|
        entry.extra_data["box"] = {
          "name" => name,
          "provider" => provider,
          "version" => version,
          "architecture" => architecture,
        }
      end
    end

    it "returns nil if the index has no matching entries" do
      index << new_entry("foo", "bar", "1.0")
      index << new_entry("foo", "baz", "1.2")

      expect(subject).to_not be_in_use(index)
    end

    it "returns matching entries if they exist" do
      matching = new_entry(name, provider.to_s, version)
      index << new_entry("foo", "bar", "1.0")
      index << matching
      index << new_entry("foo", "baz", "1.2")

      expect(subject.in_use?(index)).to eq([matching])
    end

    context "with architecture information" do
      subject { described_class.new(name, provider, version, directory, architecture: architecture) }

      it "returns nil if the index has no matching entries" do
        index << new_entry("foo", "bar", "1.0", "amd64")
        index << new_entry("foo", "baz", "1.2", "arm64")
        index << new_entry(name, provider.to_s, version, "random-arch")

        expect(subject).to_not be_in_use(index)
      end

      it "returns matching entries if they exist" do
        matching = new_entry(name, provider.to_s, version, architecture)
        index << new_entry("foo", "bar", "1.0", "amd64")
        index << matching
        index << new_entry("foo", "baz", "1.2")

        expect(subject.in_use?(index)).to eq([matching])
      end
    end
  end

  context "#load_metadata" do
    let(:metadata_url) do
      Tempfile.new("vagrant-test-box-test").tap do |f|
        f.write(<<-RAW)
        {
          "name": "foo",
          "description": "bar"
        }
        RAW
        f.close
      end
    end

    subject do
      described_class.new(
        name, provider, version, directory,
        metadata_url: metadata_url.path)
    end

    after do
      metadata_url.unlink
    end

    it "loads the url and returns the data" do
      result = subject.load_metadata
      expect(result.name).to eq("foo")
      expect(result.description).to eq("bar")
    end

    it "raises an error if the download failed" do
      dl = double("downloader")
      allow(Vagrant::Util::Downloader).to receive(:new).and_return(dl)
      expect(dl).to receive(:download!).and_raise(
        Vagrant::Errors::DownloaderError.new(message: "foo"))

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

    context "box has a hook for adding authentication" do

      let(:hook){ double("hook") }

      subject do
        described_class.new(
          name, provider, version, directory,
          metadata_url: metadata_url.path, hook: hook)
      end

      it "add authentication headers to the url" do
        expect(hook).to receive(:call).with(:authenticate_box_downloader, any_args)
        result = subject.load_metadata
        expect(result.name).to eq("foo")
        expect(result.description).to eq("bar")
      end
    end
  end

  describe "destroying" do
    it "should destroy an existing box" do
      # Verify that our "box" exists
      expect(directory.exist?).to be

      # Destroy it
      expect(subject.destroy!).to be

      # Verify that it is "destroyed"
      expect(directory.exist?).not_to be
    end

    it "should not error destroying a non-existent box" do
      # Get the subject so that it is instantiated
      box = subject

      # Delete the directory
      directory.rmtree

      # Destroy it
      expect(box.destroy!).to be
    end
  end

  describe "repackaging" do
    let(:scratch) { Dir.mktmpdir("vagrant-test-box-repackaging") }

    let(:box_output_path) { File.join(scratch, "package.box") }

    after do
      FileUtils.rm_rf(scratch)
    end

    it "should repackage the box", :bsdtar do
      test_file_contents = "hello, world!"

      # Put a file in the box directory to verify it is packaged properly
      # later.
      directory.join("test_file").open("w") do |f|
        f.write(test_file_contents)
      end

      # Repackage our box to some temporary directory
      expect(subject.repackage(box_output_path)).to be(true)

      # Let's now add this box again under a different name, and then
      # verify that we get the proper result back.
      new_box = box_collection.add(box_output_path, "foo2", "1.0")
      expect(new_box.directory.join("test_file").read).to eq(test_file_contents)
    end
  end

  describe "comparison and ordering" do
    it "should be equal if the name, provider, version match" do
      a = described_class.new("a", :foo, "1.0", directory)
      b = described_class.new("a", :foo, "1.0", directory)

      expect(a).to eq(b)
    end

    it "should not be equal if name doesn't match" do
      a = described_class.new("a", :foo, "1.0", directory)
      b = described_class.new("b", :foo, "1.0", directory)

      expect(a).to_not eq(b)
    end

    it "should not be equal if provider doesn't match" do
      a = described_class.new("a", :foo, "1.0", directory)
      b = described_class.new("a", :bar, "1.0", directory)

      expect(a).to_not eq(b)
    end

    it "should not be equal if version doesn't match" do
      a = described_class.new("a", :foo, "1.0", directory)
      b = described_class.new("a", :foo, "1.1", directory)

      expect(a).to_not eq(b)
    end

    it "should sort them in order of name, version, provider" do
      a = described_class.new("a", :foo, "1.0", directory)
      b = described_class.new("a", :foo2, "1.0", directory)
      c = described_class.new("a", :foo2, "1.1", directory)
      d = described_class.new("b", :foo2, "1.0", directory)

      expect([d, c, a, b].sort).to eq([a, b, c, d])
    end
  end
end
