require "minitest/autorun"
require "tmpdir"
require "helpers/mock_loader"
require_relative "../../../../lib/inspec/plugin/v2"

# This file relies on setting environment variables for some
# of its tests - it is NOT thread-safe.

describe "Inspec::Plugin::V2::ConfigFile" do
  orig_home = ENV["HOME"]

  let(:repo_path) { MockLoader.home }
  let(:config_fixtures_path) { "#{repo_path}/test/fixtures/config_dirs" }
  let(:config_file_obj) { Inspec::Plugin::V2::ConfigFile.new(constructor_arg) }
  let(:constructor_arg) { "#{config_fixtures_path}/plugin_config_files/#{fixture_name}.json" }

  after do
    ENV["HOME"] = orig_home
    ENV["INSPEC_CONFIG_DIR"] = nil
  end

  #----------------------------------------------------------#
  #                     Path Handling
  #----------------------------------------------------------#
  describe "locating the file" do
    describe "when no env var is set" do
      let(:constructor_arg) { nil }
      it "defaults to the home directory" do
        ENV["HOME"] = File.join(config_fixtures_path, "fakehome")
        expected_path = File.join(ENV["HOME"], ".inspec", "plugins.json")

        _(config_file_obj.path).must_equal expected_path
      end
    end

    describe "when an env var is set" do
      let(:constructor_arg) { nil }
      it "looks to the dir specified by the env var" do
        ENV["INSPEC_CONFIG_DIR"] = File.join(config_fixtures_path, "meaning-by-path")
        expected_path = File.join(ENV["INSPEC_CONFIG_DIR"], "plugins.json")

        _(config_file_obj.path).must_equal expected_path
      end
    end

    describe "when a path is provided to the constructor" do
      let(:fixture_name) { "no_plugins" }
      it "uses the provided path" do
        _(config_file_obj.path).must_equal constructor_arg
      end
    end
  end

  #----------------------------------------------------------#
  #                    Reading a File
  #----------------------------------------------------------#

  describe "reading the file" do

    describe "when the file is missing" do
      let(:fixture_name) { "nonesuch" }
      it "creates a empty datastructure" do
        Dir.mktmpdir do |tmp_dir|
          _(config_file_obj.count).must_equal 0
        end
      end
    end

    describe "when the file is corrupt" do
      let(:fixture_name) { "corrupt" }
      it "throws an exception" do
        ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
        _(ex.message).must_include("Failed to load")
        _(ex.message).must_include("JSON")
      end
    end

    describe "when the file is valid" do
      let(:fixture_name) { "basic" }
      it "can count plugins" do
        _(config_file_obj.count).must_equal 3
      end
      it "can look up plugins by name with a String" do
        _(config_file_obj.plugin_by_name("inspec-test-fixture-01")).wont_be_nil
        _(config_file_obj.plugin_by_name("inspec-test-fixture-99")).must_be_nil
      end
      it "can look up plugins by name with a Symbol" do
        _(config_file_obj.plugin_by_name(:'inspec-test-fixture-01')).wont_be_nil
        _(config_file_obj.plugin_by_name(:'inspec-test-fixture-99')).must_be_nil
      end
      it "symbolizes the keys of the entries" do
        config_file_obj.each do |entry|
          entry.keys.each do |key|
            _(key).must_be_kind_of(Symbol)
          end
        end
      end
      it "implements Enumerable" do
        _(config_file_obj.select { |entry| entry[:name].to_s.start_with?("inspec-test-fixture") }.count).must_equal 3
      end
    end

    #----------------------------------------------------------#
    #                     Validation
    #----------------------------------------------------------#
    describe "when the file is invalid" do

      describe "because the file version is wrong" do
        let(:fixture_name) { "bad_plugin_conf_version" }
        it "throws an exception" do
          ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
          _(ex.message).must_include("Unsupported")
          _(ex.message).must_include("version")
          _(ex.message).must_include("99.99.9")
          _(ex.message).must_include("1.0.0")
        end
      end

      describe "because the file version is missing" do
        let(:fixture_name) { "missing_plugin_conf_version" }
        it "throws an exception" do
          ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
          _(ex.message).must_include("Missing")
          _(ex.message).must_include("version")
          _(ex.message).must_include("1.0.0")
        end
      end

      describe "because the plugins field is missing" do
        let(:fixture_name) { "missing_plugins_key" }
        it "throws an exception" do
          ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
          _(ex.message).must_include("missing")
          _(ex.message).must_include("'plugins'")
          _(ex.message).must_include("array")
        end
      end

      describe "because the plugins field is not an array" do
        let(:fixture_name) { "hash_plugins_key" }
        it "throws an exception" do
          ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
          _(ex.message).must_include("Malformed")
          _(ex.message).must_include("'plugins'")
          _(ex.message).must_include("array")
        end
      end

      describe "because a plugin entry is not a hash" do
        let(:fixture_name) { "entry_not_hash" }
        it "throws an exception" do
          ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
          _(ex.message).must_include("Malformed")
          _(ex.message).must_include("Hash")
          _(ex.message).must_include("at index 2")
        end
      end

      describe "because it contains duplicate plugin entries" do
        let(:fixture_name) { "entry_duplicate" }
        it "throws an exception" do
          ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
          _(ex.message).must_include("Malformed")
          _(ex.message).must_include("duplicate")
          _(ex.message).must_include("inspec-test-fixture-01")
          _(ex.message).must_include("at index 1 and 3")
        end
      end

      describe "because a plugin entry does not have a name" do
        let(:fixture_name) { "entry_no_name" }
        it "throws an exception" do
          ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
          _(ex.message).must_include("Malformed")
          _(ex.message).must_include("missing 'name'")
          _(ex.message).must_include("at index 1")
        end
      end

      describe "because a plugin entry has an unrecognized installation type" do
        let(:fixture_name) { "entry_bad_installation_type" }
        it "throws an exception" do
          ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
          _(ex.message).must_include("Malformed")
          _(ex.message).must_include("unrecognized installation_type")
          _(ex.message).must_include("one of 'gem' or 'path'")
          _(ex.message).must_include("at index 1")
        end
      end

      describe "because a path plugin entry does not have a path" do
        let(:fixture_name) { "entry_no_path_for_path_type" }
        it "throws an exception" do
          ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
          _(ex.message).must_include("Malformed")
          _(ex.message).must_include("missing installation path")
          _(ex.message).must_include("at index 2")
        end
      end
    end
  end

  describe "modifying the conf file" do
    #----------------------------------------------------------#
    #                     Adding Entries
    #----------------------------------------------------------#
    describe "adding an entry" do
      let(:fixture_name) { "no_plugins" }

      describe "when the conf is empty" do
        it "should add one valid entry" do
          _(config_file_obj.count).must_equal 0
          config_file_obj.add_entry(name: "inspec-test-fixture")
          _(config_file_obj.count).must_equal 1
          _(config_file_obj.plugin_by_name(:'inspec-test-fixture')).wont_be_nil
        end
      end

      describe "when the conf has entries" do
        let(:fixture_name) { "basic" }
        it "should append one valid entry" do
          _(config_file_obj.count).must_equal 3
          config_file_obj.add_entry(name: "inspec-test-fixture-03")
          _(config_file_obj.count).must_equal 4
          _(config_file_obj.plugin_by_name(:'inspec-test-fixture-03')).wont_be_nil
        end
      end

      describe "when adding a gem entry" do
        it "should add a gem entry" do
          config_file_obj.add_entry(
            name: "inspec-test-fixture-03",
            installation_type: :gem
          )
          entry = config_file_obj.plugin_by_name(:'inspec-test-fixture-03')
          _(entry).wont_be_nil
          _(entry[:installation_type]).must_equal :gem
        end
      end

      describe "when adding a path entry" do
        it "should add a path entry" do
          config_file_obj.add_entry(
            name: "inspec-test-fixture-03",
            installation_type: :path,
            installation_path: "/my/path.rb"
          )
          entry = config_file_obj.plugin_by_name(:'inspec-test-fixture-03')
          _(entry).wont_be_nil
          _(entry[:installation_type]).must_equal :path
          _(entry[:installation_path]).must_equal "/my/path.rb"
        end
      end

      describe "when adding a duplicate plugin name" do
        let(:fixture_name) { "basic" }
        it "should throw an exception" do
          assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj.add_entry(name: "inspec-test-fixture-02") }
        end
      end

      describe "when adding an invalid entry" do
        it "should throw an exception" do
          [
            { name: "inspec-test-fixture", installation_type: :path },
            { installation_type: :gem },
            { name: "inspec-test-fixture", installation_type: :invalid },
            { "name" => "inspec-test-fixture" },
          ].each do |proposed_entry|
            assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj.add_entry(proposed_entry) }
          end
        end
      end
    end

    #----------------------------------------------------------#
    #                     Removing Entries
    #----------------------------------------------------------#
    describe "removing an entry" do
      let(:fixture_name) { "basic" }

      describe "when the entry exists" do
        it "should remove the entry by symbol name" do
          _(config_file_obj.count).must_equal 3
          _(config_file_obj.plugin_by_name(:'inspec-test-fixture-01')).wont_be_nil
          config_file_obj.remove_entry(:'inspec-test-fixture-01')
          _(config_file_obj.count).must_equal 2
          _(config_file_obj.plugin_by_name(:'inspec-test-fixture-01')).must_be_nil
        end
        it "should remove the entry by String name" do
          _(config_file_obj.count).must_equal 3
          _(config_file_obj.plugin_by_name("inspec-test-fixture-01")).wont_be_nil
          config_file_obj.remove_entry("inspec-test-fixture-01")
          _(config_file_obj.count).must_equal 2
          _(config_file_obj.plugin_by_name("inspec-test-fixture-01")).must_be_nil
        end
      end

      describe "when the entry does not exist" do
        let(:fixture_name) { "basic" }
        it "should throw an exception" do
          _(config_file_obj.count).must_equal 3
          _(config_file_obj.plugin_by_name(:'inspec-test-fixture-99')).must_be_nil
          ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj.remove_entry(:'inspec-test-fixture-99') }
          _(ex.message).must_include "No such entry"
          _(ex.message).must_include "inspec-test-fixture-99"
          _(config_file_obj.count).must_equal 3
        end
      end
    end

    describe "writing the file" do
      let(:fixture_name) { "unused" }

      describe "when the file does not exist" do
        it "is created" do
          Dir.mktmpdir do |tmp_dir|
            path = File.join(tmp_dir, "plugins.json")
            _(File.exist?(path)).must_equal false
            cfo_writer = Inspec::Plugin::V2::ConfigFile.new(path)
            cfo_writer.add_entry(name: :'inspec-resource-lister')
            cfo_writer.save

            _(File.exist?(path)).must_equal true
            cfo_reader = Inspec::Plugin::V2::ConfigFile.new(path)
            _(cfo_reader.existing_entry?(:'inspec-resource-lister')).must_equal true
          end
        end
      end

      describe "when the directory does not exist" do
        it "is created" do
          Dir.mktmpdir do |tmp_dir|
            path = File.join(tmp_dir, "subdir", "plugins.json")
            _(File.exist?(path)).must_equal false
            cfo_writer = Inspec::Plugin::V2::ConfigFile.new(path)
            cfo_writer.add_entry(name: :'inspec-resource-lister')
            cfo_writer.save

            _(File.exist?(path)).must_equal true
            cfo_reader = Inspec::Plugin::V2::ConfigFile.new(path)
            _(cfo_reader.existing_entry?(:'inspec-resource-lister')).must_equal true
          end
        end
      end

      describe "when the file does exist" do
        it "is overwritten" do
          Dir.mktmpdir do |tmp_dir|
            path = File.join(tmp_dir, "plugins.json")
            cfo_writer = Inspec::Plugin::V2::ConfigFile.new(path)
            cfo_writer.add_entry(name: :'inspec-resource-lister')
            cfo_writer.save

            _(File.exist?(path)).must_equal true

            cfo_modifier = Inspec::Plugin::V2::ConfigFile.new(path)
            cfo_modifier.remove_entry(:'inspec-resource-lister')
            cfo_modifier.add_entry(name: :'inspec-test-fixture')
            cfo_modifier.save

            cfo_reader = Inspec::Plugin::V2::ConfigFile.new(path)
            _(cfo_reader.existing_entry?(:'inspec-resource-lister')).must_equal false
            _(cfo_reader.existing_entry?(:'inspec-test-fixture')).must_equal true
          end
        end
      end
    end
  end
end
