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

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

require Vagrant.source_root.join("plugins/commands/cloud/client/client")

describe VagrantPlugins::CloudCommand::Client do
  include_context "unit"

  let(:env) { isolated_environment.create_vagrant_env }
  let(:token) { nil }
  let(:vc_client) { double("vagrantcloud-client", access_token: token) }

  subject(:client) { described_class.new(env) }

  before(:all) do
    I18n.load_path << Vagrant.source_root.join("plugins/commands/cloud/locales/en.yml")
    I18n.reload!
  end

  before do
    stub_env("ATLAS_TOKEN" => nil)
    stub_env("VAGRANT_CLOUD_TOKEN" => nil)
    allow(VagrantCloud::Client).to receive(:new).and_return(vc_client)
    allow(Vagrant::Util::CredentialScrubber).to receive(:sensitive)
  end

  after do
    described_class.reset!
    Vagrant::Util::CredentialScrubber.reset!
  end

  describe "#logged_in?" do
    before { allow(subject).to receive(:token).and_return(token) }

    context "when token is not set" do
      it "should return false" do
        expect(subject.logged_in?).to be_falsey
      end
    end

    context "when token is set" do
      let(:token) { double("token") }

      before do
        allow(vc_client).to receive(:authentication_token_validate)
      end

      it "should return true when token is valid" do
        expect(subject.logged_in?).to be_truthy
      end

      it "should validate the set token" do
        expect(vc_client).to receive(:authentication_token_validate)
        subject.logged_in?
      end

      it "should return false when token does not validate" do
        expect(vc_client).to receive(:authentication_token_validate).
          and_raise(Excon::Error::Unauthorized.new(StandardError.new))
        expect(subject.logged_in?).to be_falsey
      end

      it "should add token to scrubber" do
        expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with(token)
        subject.logged_in?
      end
    end
  end

  describe "#login" do
    let(:new_token) { double("new-token") }
    let(:result) { {token: new_token} }
    let(:password) { double("password") }
    let(:username) { double("username") }

    before do
      subject.username_or_email = username
      subject.password = password
      allow(vc_client).to receive(:authentication_token_create).
        and_return(result)
    end

    it "should add password to scrubber" do
      expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with(password)
      subject.login
    end

    it "should create an authentication token" do
      expect(vc_client).to receive(:authentication_token_create).
        and_return(result)
      subject.login
    end

    it "should wrap remote request to handle errors" do
      expect(subject).to receive(:with_error_handling)
      subject.login
    end

    it "should add new token to scrubber" do
      expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with(new_token)
      subject.login
    end

    it "should create a new internal client" do
      expect(VagrantCloud::Client).to receive(:new).with(access_token: new_token, url_base: anything)
      subject.login
    end

    it "should create authentication token using username and password" do
      expect(vc_client).to receive(:authentication_token_create).
        with(hash_including(username: username, password: password)).and_return(result)
      subject.login
    end

    it "should return the new token" do
      expect(subject.login).to eq(new_token)
    end

    context "with description and code" do
      let(:description) { double("description") }
      let(:code) { double("code") }

      it "should create authentication token using description and code" do
        expect(vc_client).to receive(:authentication_token_create).with(
          hash_including(username: username, password: password,
            description: description, code: code))
        subject.login(description: description, code: code)
      end
    end
  end

  describe "#request_code" do
    let(:password) { double("password") }
    let(:username) { double("username") }
    let(:delivery_method) { double("delivery-method", upcase: nil) }
    let(:result) { {two_factor: two_factor} }
    let(:two_factor) { {obfuscated_destination: obfuscated_destination} }
    let(:obfuscated_destination) { double("obfuscated-destination", to_s: "2FA_DESTINATION") }

    before do
      subject.password = password
      subject.username_or_email = username
      allow(vc_client).to receive(:authentication_request_2fa_code).and_return(result)
    end

    it "should add password to scrubber" do
      expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with(password)
      subject.request_code(delivery_method)
    end

    it "should request the code" do
      expect(vc_client).to receive(:authentication_request_2fa_code).with(
        hash_including(username: username, password: password, delivery_method: delivery_method))
      subject.request_code(delivery_method)
    end

    it "should print the destination" do
      expect(env.ui).to receive(:success).with(/2FA_DESTINATION/)
      subject.request_code(delivery_method)
    end
  end

  describe "#store_token" do
    let(:token_path) { double("token-path") }
    let(:new_token) { double("new-token") }

    before do
      allow(subject).to receive(:token_path).and_return(token_path)
      allow(token_path).to receive(:open)
    end

    it "should add token to scrubber" do
      expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with(new_token)
      subject.store_token(new_token)
    end

    it "should create a new internal client with token" do
      expect(VagrantCloud::Client).to receive(:new).with(access_token: new_token, url_base: anything)
      subject.store_token(new_token)
    end

    it "should open the token path and write the new token" do
      f = double("file")
      expect(token_path).to receive(:open).with("w").and_yield(f)
      expect(f).to receive(:write).with(new_token)
      subject.store_token(new_token)
    end
  end

  describe "#token" do
    let(:env_token) { "ENV_TOKEN" }
    let(:file_token) { "FILE_TOKEN" }
    let(:token_path) { double("token-path", read: file_token) }
    let(:path_exists) { false }

    before do
      allow(subject).to receive(:token).and_call_original
      allow(subject).to receive(:token_path).and_return(token_path)
      allow(token_path).to receive(:exist?).and_return(path_exists)
    end

    context "when VAGRANT_CLOUD_TOKEN env var is set" do
      before { stub_env("VAGRANT_CLOUD_TOKEN" => env_token) }

      it "should return the env token" do
        expect(subject.token).to eq(env_token)
      end

      context "when token path exists" do
        let(:path_exists) { true }

        it "should return the env token" do
          expect(subject.token).to eq(env_token)
        end

        it "should print warning of two tokens" do
          expect(env.ui).to receive(:warn)
          subject.token
        end

        it "should only print warning of two tokens once" do
          expect(env.ui).to receive(:warn).with(/detected/).once
          3.times { subject.token }
        end
      end
    end

    context "when token path exists" do
      let(:path_exists) { true }

      it "should return the stored token" do
        expect(subject.token).to eq(file_token)
      end

      context "when VAGRANT_CLOUD_TOKEN env var is set" do
        before { stub_env("VAGRANT_CLOUD_TOKEN" => env_token) }

        it "should return the env token" do
          expect(subject.token).to eq(env_token)
        end
      end
    end

    context "when ATLAS_TOKEN env var is set" do
      before { stub_env("ATLAS_TOKEN" => env_token) }

      it "should return the env token" do
        expect(subject.token).to eq(env_token)
      end

      context "when VAGRANT_CLOUD_TOKEN is set" do
        let(:vc_token) { "VC_TOKEN" }

        before { stub_env("VAGRANT_CLOUD_TOKEN" => vc_token) }

        it "should return the VAGRANT_CLOUD_TOKEN value" do
          expect(subject.token).to eq(vc_token)
        end
      end

      context "when file exists" do
        let(:path_exists) { true }

        it "should return the file token" do
          expect(subject.token).to eq(file_token)
        end
      end
    end
  end
end
