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

require File.expand_path("../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/kernel_v2/config/trigger")

describe Vagrant::Plugin::V2::Trigger do
  include_context "unit"

  let(:iso_env) do
    # We have to create a Vagrantfile so there is a root path
    isolated_environment.tap do |env|
      env.vagrantfile("")
    end
  end
  let(:iso_vagrant_env) { iso_env.create_vagrant_env }
  let(:state) { double("state", id: :running) }
  let(:machine) do
    iso_vagrant_env.machine(iso_vagrant_env.machine_names[0], :dummy).tap do |m|
      allow(m).to receive(:state).and_return(state)
    end
  end
  let(:ui) { Vagrant::UI::Silent.new }
  let(:env) { {
    machine: machine,
    ui: ui,
  } }

  let(:triggers) {
    @triggers ||= VagrantPlugins::Kernel_V2::TriggerConfig.new.tap do |triggers|
      triggers.before(:up, hash_block)
      triggers.before(:destroy, hash_block)
      triggers.before(:halt, hash_block_two)
      triggers.after(:up, hash_block)
      triggers.after(:destroy, hash_block)
      triggers.finalize!
    end
  }
  let(:hash_block) { {info: "hi", run: {inline: "echo 'hi'"}} }
  let(:hash_block_two) { {warn: "WARNING!!", run_remote: {inline: "echo 'hi'"}} }

  let(:subject) { described_class.new(env, triggers, machine, ui) }

  describe "#fire" do
    it "raises an error if an improper stage is given" do
      expect{ subject.fire(:up, :not_real, "guest", :action) }.
       to raise_error(Vagrant::Errors::TriggersNoStageGiven)
    end

    it "does not fire triggers if community plugin is detected" do
      allow(subject).to receive(:community_plugin_detected?).and_return(true)

      expect(subject).not_to receive(:execute)
      subject.fire(:up, :before, "guest", :action)
    end

    it "does fire triggers if community plugin is not detected" do
      allow(subject).to receive(:community_plugin_detected?).and_return(false)

      expect(subject).to receive(:execute)
      subject.fire(:up, :before, "guest", :action)
    end
  end

  describe "#find" do
    it "raises an error if an improper stage is given" do
      expect { subject.find(:up, :not_real, "guest", :action) }.
        to raise_error(Vagrant::Errors::TriggersNoStageGiven)
    end

    it "returns empty array when no triggers are found" do
      expect(subject.find(:halt, :after, "guest", :action)).to be_empty
    end

    it "returns items in array when triggers are found" do
      expect(subject.find(:halt, :before, "guest", :action)).not_to be_empty
    end

    it "returns the execpted number of items in the array when triggers are found" do
      expect(subject.find(:halt, :before, "guest", :action).count).to eq(1)
    end

    it "filters all found triggers" do
      expect(subject).to receive(:filter_triggers)
      subject.find(:halt, :before, "guest", :action)
    end

    it "should not attempt to match hook name with non-hook type" do
      expect(subject).not_to receive(:matched_hook?)
      subject.find(:halt, :before, "guest", :action)
    end

    context "with :all special value" do
      let(:triggers) { VagrantPlugins::Kernel_V2::TriggerConfig.new }
      let(:ignores) { [] }

      before do
        triggers.before(:all, hash_block.merge(ignore: ignores))
        triggers.after(:all, hash_block.merge(ignore: ignores))
        triggers.finalize!
      end

      [:destroy, :halt, :provision, :reload, :resume, :suspend, :up].each do |supported_action|
        it "should locate trigger when before #{supported_action} action is requested" do
          expect(subject.find(supported_action, :before, "guest", :action, all: true)).not_to be_empty
        end

        it "should locate trigger when after #{supported_action} action is requested" do
          expect(subject.find(supported_action, :after, "guest", :action, all: true)).not_to be_empty
        end
      end

      context "with ignores" do
        let(:ignores) { [:halt, :up] }

        it "should not locate trigger when before command is ignored" do
          expect(subject.find(:up, :before, "guest", :action, all: true)).to be_empty
        end

        it "should not locate trigger when after command is ignored" do
          expect(subject.find(:halt, :after, "guest", :action, all: true)).to be_empty
        end

        it "should locate trigger when before command is not ignored" do
          expect(subject.find(:provision, :before, "guest", :action, all: true)).not_to be_empty
        end

        it "should locate trigger when after command is not ignored" do
          expect(subject.find(:provision, :after, "guest", :action, all: true)).not_to be_empty
        end
      end
    end

    context "with hook type" do
      before do
        triggers.before(:environment_load, hash_block.merge(type: :hook))
        triggers.before(Vagrant::Action::Builtin::SyncedFolders, hash_block.merge(type: :hook))
        triggers.finalize!
      end

      it "returns empty array when no triggers are found" do
        expect(subject.find(:environment_unload, :before, "guest", :hook)).to be_empty
      end

      it "returns items in array when triggers are found" do
        expect(subject.find(:environment_load, :before, "guest", :hook).size).to eq(1)
      end

      it "should locate hook trigger using class constant" do
        expect(subject.find(Vagrant::Action::Builtin::SyncedFolders, :before, "guest", :hook)).
          not_to be_empty
      end

      it "should locate hook trigger using string" do
        expect(subject.find("environment_load", :before, "guest", :hook)).not_to be_empty
      end

      it "should locate hook trigger using full converted name" do
        expect(subject.find(:vagrant_action_builtin_synced_folders, :before, "guest", :hook)).
          not_to be_empty
      end

      it "should locate hook trigger using partial suffix converted name" do
        expect(subject.find(:builtin_synced_folders, :before, "guest", :hook)).
          not_to be_empty
      end

      it "should not locate hook trigger using partial prefix converted name" do
        expect(subject.find(:vagrant_action, :before, "guest", :hook)).
          to be_empty
      end
    end
  end

  describe "#filter_triggers" do
    it "returns all triggers if no constraints" do
      before_triggers = triggers.before_triggers
      filtered_triggers = subject.send(:filter_triggers, before_triggers, "guest", :action)
      expect(filtered_triggers).to eq(before_triggers)
    end

    it "filters a trigger if it doesn't match guest_name" do
      trigger_config = {info: "no", only_on: "notrealguest"}
      triggers.after(:up, trigger_config)
      triggers.finalize!

      after_triggers = triggers.after_triggers
      expect(after_triggers.size).to eq(3)
      subject.send(:filter_triggers, after_triggers, :ubuntu, :action)
      expect(after_triggers.size).to eq(2)
    end

    it "keeps a trigger that has a restraint that matches guest name" do
      trigger_config = {info: "no", only_on: /guest/}
      triggers.after(:up, trigger_config)
      triggers.finalize!

      after_triggers = triggers.after_triggers
      expect(after_triggers.size).to eq(3)
      subject.send(:filter_triggers, after_triggers, "ubuntu-guest", :action)
      expect(after_triggers.size).to eq(3)
    end

    it "keeps a trigger that has multiple restraints that matches guest name" do
      trigger_config = {info: "no", only_on: ["debian", /guest/]}
      triggers.after(:up, trigger_config)
      triggers.finalize!

      after_triggers = triggers.after_triggers
      expect(after_triggers.size).to eq(3)
      subject.send(:filter_triggers, after_triggers, "ubuntu-guest", :action)
      expect(after_triggers.size).to eq(3)
    end
  end

  describe "#execute" do
    it "calls the corresponding trigger methods if options set" do
      expect(subject).to receive(:info).twice
      expect(subject).to receive(:warn).once
      expect(subject).to receive(:run).twice
      expect(subject).to receive(:run_remote).once
      subject.send(:execute, triggers.before_triggers)
    end
  end

  describe "#info" do
    let(:message) { "Printing some info" }

    it "prints messages at INFO" do
      expect(ui).to receive(:info).with(message).and_call_original

      subject.send(:info, message)
    end
  end

  describe "#warn" do
    let(:message) { "Printing some warnings" }

    it "prints messages at WARN" do
      expect(ui).to receive(:warn).with(message).and_call_original

      subject.send(:warn, message)
    end
  end

  describe "#run" do
    let(:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new }
    let(:shell_block) { {info: "hi", run: {inline: "echo 'hi'", env: {"KEY"=>"VALUE"}}} }
    let(:shell_block_exit_codes) {
      {info: "hi", run: {inline: "echo 'hi'", env: {"KEY"=>"VALUE"}},
       exit_codes: [0,50]} }
    let(:path_block) { {warn: "bye",
                         run: {path: "path/to the/script.sh", args: "HELLO", env: {"KEY"=>"VALUE"}},
                         on_error: :continue} }

    let(:path_block_ps1) { {warn: "bye",
                         run: {path: "script.ps1", args: ["HELLO", "THERE"], env: {"KEY"=>"VALUE"}},
                         on_error: :continue} }

    let(:exit_code) { 0 }
    let(:options) { {:notify=>[:stdout, :stderr]} }

    let(:subprocess_result) do
      double("subprocess_result").tap do |result|
        allow(result).to receive(:exit_code).and_return(exit_code)
        allow(result).to receive(:stderr).and_return("")
      end
    end

    let(:subprocess_result_failure) do
      double("subprocess_result_failure").tap do |result|
        allow(result).to receive(:exit_code).and_return(1)
        allow(result).to receive(:stderr).and_return("")
      end
    end

    let(:subprocess_result_custom) do
      double("subprocess_result_custom").tap do |result|
        allow(result).to receive(:exit_code).and_return(50)
        allow(result).to receive(:stderr).and_return("")
      end
    end

    before do
      trigger_run.after(:up, shell_block)
      trigger_run.after(:up, shell_block_exit_codes)
      trigger_run.before(:destroy, path_block)
      trigger_run.before(:destroy, path_block_ps1)
      trigger_run.finalize!
    end

    it "executes an inline script with powershell if windows" do
      allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true)
      allow(Vagrant::Util::PowerShell).to receive(:execute_inline).
        and_yield(:stderr, "some output").
        and_return(subprocess_result)

      trigger = trigger_run.after_triggers.first
      shell_config = trigger.run
      on_error = trigger.on_error
      exit_codes = trigger.exit_codes

      expect(Vagrant::Util::PowerShell).to receive(:execute_inline).
        with("echo 'hi'", options)
      subject.send(:run, shell_config, on_error, exit_codes)
    end

    it "executes a path script with powershell if windows" do
      allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true)
      allow(Vagrant::Util::PowerShell).to receive(:execute).
        and_yield(:stdout, "more output").
        and_return(subprocess_result)
      allow(env).to receive(:root_path).and_return("/vagrant/home")

      trigger = trigger_run.before_triggers[1]
      shell_config = trigger.run
      on_error = trigger.on_error
      exit_codes = trigger.exit_codes

      expect(Vagrant::Util::PowerShell).to receive(:execute).
        with("/vagrant/home/script.ps1", "HELLO", "THERE", options)
      subject.send(:run, shell_config, on_error, exit_codes)
    end

    it "executes an inline script" do
      allow(Vagrant::Util::Subprocess).to receive(:execute).
        and_return(subprocess_result)

      trigger = trigger_run.after_triggers.first
      shell_config = trigger.run
      on_error = trigger.on_error
      exit_codes = trigger.exit_codes

      expect(Vagrant::Util::Subprocess).to receive(:execute).
        with("echo", "hi", options)
      subject.send(:run, shell_config, on_error, exit_codes)
    end

    it "executes a path script" do
      allow(Vagrant::Util::Subprocess).to receive(:execute).
        and_return(subprocess_result)
      allow(env).to receive(:root_path).and_return("/vagrant/home")
      allow(FileUtils).to receive(:chmod).and_return(true)

      trigger = trigger_run.before_triggers.first
      shell_config = trigger.run
      on_error = trigger.on_error
      exit_codes = trigger.exit_codes

      expect(Vagrant::Util::Subprocess).to receive(:execute).
        with("/vagrant/home/path/to the/script.sh", "HELLO", options)
      subject.send(:run, shell_config, on_error, exit_codes)
    end

    it "continues on error" do
      allow(Vagrant::Util::Subprocess).to receive(:execute).
        and_raise("Fail!")
      allow(env).to receive(:root_path).and_return("/vagrant/home")
      allow(FileUtils).to receive(:chmod).and_return(true)

      trigger = trigger_run.before_triggers.first
      shell_config = trigger.run
      on_error = trigger.on_error
      exit_codes = trigger.exit_codes

      expect(Vagrant::Util::Subprocess).to receive(:execute).
        with("/vagrant/home/path/to the/script.sh", "HELLO", options)
      subject.send(:run, shell_config, on_error, exit_codes)
    end

    it "halts on error" do
      allow(Vagrant::Util::Subprocess).to receive(:execute).
        and_raise("Fail!")

      trigger = trigger_run.after_triggers.first
      shell_config = trigger.run
      on_error = trigger.on_error
      exit_codes = trigger.exit_codes

      expect(Vagrant::Util::Subprocess).to receive(:execute).
        with("echo", "hi", options)
      expect { subject.send(:run, shell_config, on_error, exit_codes) }.to raise_error("Fail!")
    end

    it "allows for acceptable exit codes" do
      allow(Vagrant::Util::Subprocess).to receive(:execute).
        and_return(subprocess_result_custom)

      trigger = trigger_run.after_triggers[1]
      shell_config = trigger.run
      on_error = trigger.on_error
      exit_codes = trigger.exit_codes

      expect(Vagrant::Util::Subprocess).to receive(:execute).
        with("echo", "hi", options)
      subject.send(:run, shell_config, on_error, exit_codes)
    end

    it "exits if given a bad exit code" do
      allow(Vagrant::Util::Subprocess).to receive(:execute).
        and_return(subprocess_result_custom)

      trigger = trigger_run.after_triggers.first
      shell_config = trigger.run
      on_error = trigger.on_error
      exit_codes = trigger.exit_codes

      expect(Vagrant::Util::Subprocess).to receive(:execute).
        with("echo", "hi", options)
      expect { subject.send(:run, shell_config, on_error, exit_codes) }.to raise_error(Vagrant::Errors::TriggersBadExitCodes)
    end
  end

  describe "#run_remote" do
    let (:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new }
    let (:shell_block) { {info: "hi", run_remote: {inline: "echo 'hi'", env: {"KEY"=>"VALUE"}}} }
    let (:path_block) { {warn: "bye",
                         run_remote: {path: "script.sh", env: {"KEY"=>"VALUE"}},
                         on_error: :continue} }
    let(:provision) { double("provision") }

    before do
      trigger_run.after(:up, shell_block)
      trigger_run.before(:destroy, path_block)
      trigger_run.finalize!
    end

    context "with no machine existing" do
      let(:machine) { nil }

      it "raises an error and halts if guest does not exist" do
        trigger = trigger_run.after_triggers.first
        shell_config = trigger.run_remote
        on_error = trigger.on_error
        exit_codes = trigger.exit_codes

        expect { subject.send(:run_remote, shell_config, on_error, exit_codes) }.
          to raise_error(Vagrant::Errors::TriggersGuestNotExist)
      end

      it "continues on if guest does not exist but is configured to continue on error" do
        trigger = trigger_run.before_triggers.first
        shell_config = trigger.run_remote
        on_error = trigger.on_error
        exit_codes = trigger.exit_codes

        subject.send(:run_remote, shell_config, on_error, exit_codes)
      end
    end

    it "raises an error and halts if guest is not running" do
      allow(machine.state).to receive(:id).and_return(:not_running)

      trigger = trigger_run.after_triggers.first
      shell_config = trigger.run_remote
      on_error = trigger.on_error
      exit_codes = trigger.exit_codes

      expect { subject.send(:run_remote, shell_config, on_error, exit_codes) }.
        to raise_error(Vagrant::Errors::TriggersGuestNotRunning)
    end

    it "continues on if guest is not running but is configured to continue on error" do
      allow(machine.state).to receive(:id).and_return(:not_running)

      allow(env).to receive(:root_path).and_return("/vagrant/home")
      allow(FileUtils).to receive(:chmod).and_return(true)

      trigger = trigger_run.before_triggers.first
      shell_config = trigger.run_remote
      on_error = trigger.on_error
      exit_codes = trigger.exit_codes

      subject.send(:run_remote, shell_config, on_error, exit_codes)
    end

    it "calls the provision function on the shell provisioner" do
      allow(machine.state).to receive(:id).and_return(:running)
      allow(provision).to receive(:provision).and_return("Provision!")
      allow(VagrantPlugins::Shell::Provisioner).to receive(:new).
        and_return(provision)

      trigger = trigger_run.after_triggers.first
      shell_config = trigger.run_remote
      on_error = trigger.on_error
      exit_codes = trigger.exit_codes

      subject.send(:run_remote, shell_config, on_error, exit_codes)
    end

    it "continues on if provision fails" do
      allow(machine.state).to receive(:id).and_return(:running)
      allow(provision).to receive(:provision).and_raise("Nope!")
      allow(VagrantPlugins::Shell::Provisioner).to receive(:new).
        and_return(provision)

      trigger = trigger_run.before_triggers.first
      shell_config = trigger.run_remote
      on_error = trigger.on_error
      exit_codes = trigger.exit_codes

      subject.send(:run_remote, shell_config, on_error, exit_codes)
    end

    it "fails if it encounters an error" do
      allow(machine.state).to receive(:id).and_return(:running)
      allow(provision).to receive(:provision).and_raise("Nope!")
      allow(VagrantPlugins::Shell::Provisioner).to receive(:new).
        and_return(provision)

      trigger = trigger_run.after_triggers.first
      shell_config = trigger.run_remote
      on_error = trigger.on_error
      exit_codes = trigger.exit_codes

      expect { subject.send(:run_remote, shell_config, on_error, exit_codes) }.
        to raise_error("Nope!")
    end
  end

  describe "#trigger_abort" do
    it "system exits when called" do
      allow(Process).to receive(:exit!).and_return(true)

      expect(Process).to receive(:exit!).with(3)
      subject.send(:trigger_abort, 3)
    end

    context "when running in parallel" do
      let(:thread) {
        @t ||= Thread.new do
          Thread.current[:batch_parallel_action] = true
          Thread.stop
          subject.send(:trigger_abort, exit_code)
        end
      }
      let(:exit_code) { 22 }

      before do
        expect(Process).not_to receive(:exit!)
        sleep(0.1) until thread.stop?
      end

      after { @t = nil }

      it "should terminate the thread" do
        expect(thread).to receive(:terminate).and_call_original
        thread.wakeup
        thread.join(1) while thread.alive?
      end

      it "should set the exit code into the thread data" do
        expect(thread).to receive(:terminate).and_call_original
        thread.wakeup
        thread.join(1) while thread.alive?
        expect(thread[:exit_code]).to eq(exit_code)
      end
    end
  end

  describe "#ruby" do
    let(:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new }
    let(:block) { proc{var = 1+1} }
    let(:ruby_trigger) { {info: "hi", ruby: block} }

    before do
      trigger_run.after(:up, ruby_trigger)
      trigger_run.finalize!
    end

    it "executes a ruby block" do
      expect(block).to receive(:call)
      subject.send(:execute_ruby, block)
    end
  end

  describe "#nameify" do
    it "should return empty string when object" do
      expect(subject.send(:nameify, "")).to eq("")
    end

    it "should return name of class" do
      expect(subject.send(:nameify, String)).to eq("String")
    end

    it "should return empty string when class has no name" do
      expect(subject.send(:nameify, Class.new)).to eq("")
    end
  end
end
