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

require "pathname"
require "tmpdir"

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

require Vagrant.source_root.join("plugins/commands/box/command/update")

describe VagrantPlugins::CommandBox::Command::Update do
  include_context "unit"

  let(:argv)     { [] }
  let(:iso_env) do
    # We have to create a Vagrantfile so there is a root path
    test_iso_env.vagrantfile("")
    test_iso_env.create_vagrant_env
  end
  let(:test_iso_env) { isolated_environment }

  let(:action_runner) { double("action_runner") }
  let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) }

  let(:download_options) { ["--insecure",
                            "--cacert", "foo",
                            "--capath", "bar",
                            "--cert", "baz"] }

  subject { described_class.new(argv, iso_env) }

  before do
    allow(iso_env).to receive(:action_runner).and_return(action_runner)
    machine.config.vm.box = "foo"
  end

  describe "execute" do
    context "updating specific box" do
      let(:argv) { ["--box", "foo"] }
      let(:scratch) { Dir.mktmpdir("vagrant-test-command-box-update-execute") }
      let(:metadata_url) { Pathname.new(scratch).join("metadata.json") }
      let(:box_args) { ["foo", "1.0", :virtualbox] }
      let(:box_opts) { {metadata_url: metadata_url.to_s} }
      before do
        metadata_url.open("w") do |f|
          f.write("")
        end

        test_iso_env.box3(*box_args, **box_opts)
      end

      after do
        FileUtils.rm_rf(scratch)
      end

      it "doesn't update if they're up to date" do
        called = false
        allow(action_runner).to receive(:run) do |callable, opts|
          if opts[:box_provider]
            called = true
          end

          opts
        end

        subject.execute

        expect(called).to be(false)
      end

      it "does the correct update if there is an update" do
        metadata_url.open("w") do |f|
          f.write(
           {
             name: "foo",
             versions: [
              {
                version: "1.0"
              },
              {
                version: "1.8",
                providers: [
                 {
                   name: "virtualbox",
                   url: "bar"
                 }
                ]
              },
              {
                version: "1.10",
                providers: [
                 {
                   name: "virtualbox",
                   url: "bar"
                 }
                ]
              },
              {
                version: "1.11",
                providers: [
                 {
                   name: "virtualbox",
                   url: "bar"
                 }
                ]
              }
             ]
           }.to_json
          )
        end

        action_called = false
        allow(action_runner).to receive(:run) do |action, opts|
          if opts[:box_provider]
            action_called = true
            expect(opts[:box_force]).to eq(nil)
            expect(opts[:box_url]).to eq(metadata_url.to_s)
            expect(opts[:box_provider]).to eq("virtualbox")
            expect(opts[:box_version]).to eq("1.11")
            expect(opts[:box_download_ca_path]).to be_nil
            expect(opts[:box_download_ca_cert]).to be_nil
            expect(opts[:box_download_client_cert]).to be_nil
            expect(opts[:box_download_insecure]).to be_nil
          end

          opts
        end

        subject.execute

        expect(action_called).to be(true)
      end

      it "raises an error if there are multiple providers" do
        test_iso_env.box3("foo", "1.0", :vmware)

        expect(action_runner).to receive(:run).never

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

      context "with multiple providers and specifying the provider" do
        let(:argv) { ["--box", "foo", "--provider", "vmware"] }

        it "updates the proper box" do
          metadata_url.open("w") do |f|
            f.write(
             {
               name: "foo",
               versions: [
                {
                  version: "1.0"
                },
                {
                  version: "1.1",
                  providers: [
                   {
                     name: "vmware",
                     url: "bar"
                   }
                  ]
                }
               ]
             }.to_json
            )
          end

          test_iso_env.box3("foo", "1.0", :vmware)

          action_called = false
          allow(action_runner).to receive(:run) do |action, opts|
            if opts[:box_provider]
              action_called = true
              expect(opts[:box_url]).to eq(metadata_url.to_s)
              expect(opts[:box_provider]).to eq("vmware")
              expect(opts[:box_version]).to eq("1.1")
            end

            opts
          end

          subject.execute

          expect(action_called).to be(true)
        end

        it "raises an error if that provider doesn't exist" do
          expect(action_runner).to receive(:run).never

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

      context "download options are specified" do
        let(:argv) { ["--box", "foo" ].concat(download_options) }

        it "passes down download options" do
          metadata_url.open("w") do |f|
            f.write(
             {
               name: "foo",
               versions: [
                {
                  version: "1.0"
                },
                {
                  version: "1.1",
                  providers: [
                   {
                     name: "virtualbox",
                     url: "bar"
                   }
                  ]
                }
               ]
             }.to_json
            )
          end

          action_called = false
          allow(action_runner).to receive(:run) do |action, opts|
            if opts[:box_provider]
              action_called = true
              expect(opts[:box_download_ca_cert]).to eq("foo")
              expect(opts[:box_download_ca_path]).to eq("bar")
              expect(opts[:box_download_client_cert]).to eq("baz")
              expect(opts[:box_download_insecure]).to be(true)
            end

            opts
          end

          subject.execute
          expect(action_called).to be(true)
        end
      end

      context "with a box that doesn't exist" do
        let(:argv) { ["--box", "nope"] }

        it "raises an exception" do
          expect(action_runner).to receive(:run).never

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

      context "with architecture" do
        let(:box_opts) { {metadata_url: metadata_url.to_s, architecture: "test-arch"} }

        it "doesn't update if they're up to date" do
          called = false
          allow(action_runner).to receive(:run) do |callable, opts|
            if opts[:box_provider]
              called = true
            end

            opts
          end

          subject.execute

          expect(called).to be(false)
        end

        it "does the correct update if there is an update" do
          metadata_url.open("w") do |f|
            f.write(
             {
               name: "foo",
               versions: [
                {
                  version: "1.0"
                },
                {
                  version: "1.8",
                  providers: [
                   {
                     name: "virtualbox",
                     url: "bar",
                     architecture: "test-arch",
                     default_architecture: true
                   }
                  ]
                },
                {
                  version: "1.10",
                  providers: [
                   {
                     name: "virtualbox",
                     url: "bar",
                     architecture: "test-arch",
                     default_architecture: true
                   }
                  ]
                },
                {
                  version: "1.11",
                  providers: [
                   {
                     name: "virtualbox",
                     url: "bar",
                     architecture: "test-arch",
                     default_architecture: true
                   }
                  ]
                }
               ]
             }.to_json
            )
          end

          action_called = false
          allow(action_runner).to receive(:run) do |action, opts|
            if opts[:box_provider]
              action_called = true
              expect(opts[:box_force]).to eq(nil)
              expect(opts[:box_url]).to eq(metadata_url.to_s)
              expect(opts[:box_provider]).to eq("virtualbox")
              expect(opts[:box_version]).to eq("1.11")
              expect(opts[:box_architecture]).to eq("test-arch")
              expect(opts[:box_download_ca_path]).to be_nil
              expect(opts[:box_download_ca_cert]).to be_nil
              expect(opts[:box_download_client_cert]).to be_nil
              expect(opts[:box_download_insecure]).to be_nil
            end

            opts
          end

          subject.execute

          expect(action_called).to be(true)
        end

        it "raises an error if there are multiple providers" do
          test_iso_env.box3("foo", "1.0", :vmware)

          expect(action_runner).to receive(:run).never

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

        it "raises an error if there are multiple architectures" do
          test_iso_env.box3("foo", "1.0", :virtualbox, architecture: "other-arch")

          expect(action_runner).to receive(:run).never

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

        context "with multiple architectures and specifying the architecture" do
          let(:argv) { ["--box", "foo", "--architecture", "other-arch"] }

          it "updates the proper box" do
            metadata_url.open("w") do |f|
              f.write(
                {
                  name: "foo",
                  versions: [
                   {
                     version: "1.0"
                   },
                   {
                     version: "1.1",
                     providers: [
                      {
                        name: "virtualbox",
                        url: "bar",
                        architecture: "test-arch",
                      },
                      {
                        name: "virtualbox",
                        url: "bar",
                        architecture: "other-arch",
                      }
                     ]
                   }
                  ]
                }.to_json
               )
            end

            test_iso_env.box3("foo", "1.0", :virtualbox, architecture: "other-arch")

            action_called = false
            allow(action_runner).to receive(:run) do |action, opts|
              if opts[:box_provider]
                action_called = true
                expect(opts[:box_url]).to eq(metadata_url.to_s)
                expect(opts[:box_provider]).to eq("virtualbox")
                expect(opts[:box_version]).to eq("1.1")
                expect(opts[:box_architecture]).to eq("other-arch")
              end

              opts
            end

            subject.execute

            expect(action_called).to be(true)
          end

          it "raises an error if that provider doesn't exist" do
            expect(action_runner).to receive(:run).never

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

      end
    end

    context "updating environment machines" do
      before do
        allow(subject).to receive(:with_target_vms) { |&block| block.call machine }
      end

      let(:box) do
        box_dir = test_iso_env.box3("foo", "1.0", :virtualbox)
        box = Vagrant::Box.new(
          "foo", :virtualbox, "1.0", box_dir, metadata_url: "foo")
        allow(box).to receive(:has_update?).and_return(nil)
        box
      end

      it "ignores machines without boxes" do
        expect(action_runner).to receive(:run).never

        subject.execute
      end

      it "doesn't update boxes if they're up-to-date" do
        allow(machine).to receive(:box).and_return(box)
        expect(box).to receive(:has_update?).
          with(machine.config.vm.box_version,
               {download_options:
                 {ca_cert: nil, ca_path: nil, client_cert: nil,
                  insecure: false}}).
          and_return(nil)

        expect(action_runner).to receive(:run).never

        subject.execute
      end

      context "boxes have an update" do
        let(:md) {
          Vagrant::BoxMetadata.new(
           StringIO.new(
            {
              name: "foo",
              versions: [
               {
                 version: "1.0"
               },
               {
                 version: "1.1",
                 providers: [
                  {
                    name: "virtualbox",
                    url: "bar"
                  }
                 ]
               }
              ]
            }.to_json
           )
          )
        }

        before { allow(machine).to receive(:box).and_return(box) }

        it "updates boxes" do
          expect(box).to receive(:has_update?).
            with(machine.config.vm.box_version,
                 {download_options:
                   {ca_cert: nil, ca_path: nil, client_cert: nil,
                    insecure: false}}).
            and_return([md, md.version("1.1"), md.version("1.1").provider("virtualbox")])

          expect(action_runner).to receive(:run).with(any_args) { |action, opts|
            expect(opts[:box_url]).to eq(box.metadata_url)
            expect(opts[:box_provider]).to eq("virtualbox")
            expect(opts[:box_version]).to eq("1.1")
            expect(opts[:ui]).to equal(machine.ui)
            true
          }

          subject.execute
        end

        context "when box version is updated but previous box exists" do

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

          it "updates the box" do
            # First call gets nil result to for lookup
            expect(machine).to receive(:box).and_return(nil)
            expect(Vagrant::BoxCollection).to receive(:new).and_return(collection)
            expect(collection).to receive(:find).and_return(box)

            expect(box).to receive(:has_update?).
              with(machine.config.vm.box_version,
              {download_options:
                {ca_cert: nil, ca_path: nil, client_cert: nil,
                  insecure: false}}).
              and_return([md, md.version("1.1"), md.version("1.1").provider("virtualbox")])

            expect(action_runner).to receive(:run).with(any_args) { |action, opts|
              expect(opts[:box_url]).to eq(box.metadata_url)
              expect(opts[:box_provider]).to eq("virtualbox")
              expect(opts[:box_version]).to eq("1.1")
              expect(opts[:ui]).to equal(machine.ui)
              true
            }

            subject.execute
          end
        end

        context "machine has download options" do
          before do
            machine.config.vm.box_download_ca_cert = "oof"
            machine.config.vm.box_download_ca_path = "rab"
            machine.config.vm.box_download_client_cert = "zab"
            machine.config.vm.box_download_insecure = false
          end

          it "uses download options from machine" do
            expect(box).to receive(:has_update?).
              with(machine.config.vm.box_version,
                   {download_options:
                     {ca_cert: "oof", ca_path: "rab", client_cert: "zab",
                      insecure: false}}).
              and_return([md, md.version("1.1"), md.version("1.1").provider("virtualbox")])

            expect(action_runner).to receive(:run).with(any_args) { |action, opts|
              expect(opts[:box_download_ca_cert]).to eq("oof")
              expect(opts[:box_download_ca_path]).to eq("rab")
              expect(opts[:box_download_client_cert]).to eq("zab")
              expect(opts[:box_download_insecure]).to be(false)
              true
            }

            subject.execute
          end

          context "download options are specified on the command line" do
            let(:argv) { download_options }

            it "overrides download options from machine with options from CLI" do
              expect(box).to receive(:has_update?).
                with(machine.config.vm.box_version,
                     {download_options:
                       {ca_cert: "foo", ca_path: "bar", client_cert: "baz",
                        insecure: true}}).
                and_return([md, md.version("1.1"),
                            md.version("1.1").provider("virtualbox")])

              expect(action_runner).to receive(:run).with(any_args) { |action, opts|
                expect(opts[:box_download_ca_cert]).to eq("foo")
                expect(opts[:box_download_ca_path]).to eq("bar")
                expect(opts[:box_download_client_cert]).to eq("baz")
                expect(opts[:box_download_insecure]).to be(true)
                true
              }

              subject.execute
            end
          end

          context "ignoring boxes with no metadata" do
            before do
              allow(subject).to receive(:with_target_vms) { |&block| block.call machine }
            end

            let(:box) do
              box_dir = test_iso_env.box3("foo", "1.0", :virtualbox)
              box = Vagrant::Box.new(
                "foo", :virtualbox, "1.0", box_dir, metadata_url: "foo")
              allow(box).to receive(:has_update?).and_raise(Vagrant::Errors::BoxUpdateNoMetadata, name: "foo")
              box
            end

            it "continues to update the rest of the boxes in the environment" do
              subject.execute
            end
          end

          context "force flag is specified on the command line" do
            let(:argv) { ["--force"].concat(download_options) }

            it "passes force through to action_box_add as true" do
              expect(box).to receive(:has_update?).
                with(machine.config.vm.box_version,
                     {download_options:
                       {ca_cert: "foo", ca_path: "bar", client_cert: "baz",
                        insecure: true}}).
                and_return([md, md.version("1.1"),
                            md.version("1.1").provider("virtualbox")])

              expect(action_runner).to receive(:run).with(any_args) { |action, opts|
                expect(opts[:box_force]).to be(true)
                true
              }

              subject.execute
            end
          end
        end
      end
    end
  end
end
