require 'spec_helper'

module Vcloud
  module Core
    describe ConfigValidator do

      context "sanitize type" do

        it "should be ok with type as bare String" do
          data = "hello world"
          schema = { type: String }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

        it "should be ok with type as string 'String'" do
          data = "hello world"
          schema = { type: 'String' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

        it "should be ok with type as string 'string'" do
          data = "hello world"
          schema = { type: 'string' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

      end

      context "string validations" do

        it "should validate a basic string" do
          data = "hello world"
          schema = { type: 'string' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

        it "should not validate a number as a basic string" do
          data = 42
          schema = { type: 'string' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_false
        end

        it "should log error with number as a basic string" do
          data = 42
          schema = { type: 'string' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq([ 'base: 42 is not a string'] )
        end

        it "should return error with empty string (by default)" do
          data = ""
          schema = { type: 'string' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq([ 'base: cannot be empty string'] )
        end

        it "should return error with empty string with allowed_empty: false)" do
          data = ""
          schema = { type: 'string', allowed_empty: false }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq([ 'base: cannot be empty string'] )
        end

        it "should validate ok with empty string with allowed_empty: true)" do
          data = ""
          schema = { type: 'string', allowed_empty: true }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

        it "should return error for nil value with allowed_empty: true)" do
          data = nil
          schema = { type: 'string', allowed_empty: true }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq([ 'base:  is not a string'] )
        end

        it "should validate ok with a :matcher regex specified" do
          data = "name-1234"
          schema = { type: 'string', matcher: /^name-\d+$/ }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

        it "should return errror with a :matcher regex not matching" do
          data = "name-123a"
          schema = { type: 'string', matcher: /^name-\d+$/ }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq(['base: name-123a does not match'])
        end

      end

      context "hash validations" do

        it "should validate a basic hash" do
          data = { name: "santa", address: "north pole" }
          schema = {
            type: "Hash",
            internals: {
              name: { type: 'string' },
              address: { type: 'string' },
            }
          }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

        it "should not validate a bogus hash" do
          data = { name: 42, address: 42 }
          schema = {
            type: "Hash",
            internals: {
              name: { type: "string" },
              address: { type: "string" },
            }
          }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_false
        end

        it "should return correct errors validating a bogus hash" do
          data = { name: 42, address: 42 }
          schema = {
            type: "Hash",
            internals: {
              name: { type: "string" },
              address: { type: "string" },
            }
          }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq([
            "name: 42 is not a string",
            "address: 42 is not a string",
          ])
        end

        it "should return error with empty hash (by default)" do
          data = {}
          schema = { type: 'hash' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq([ 'base: cannot be empty hash'] )
        end

        it "should return error with empty hash with allowed_empty: false)" do
          data = {}
          schema = { type: 'hash', allowed_empty: false }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq([ 'base: cannot be empty hash'] )
        end

        it "should validate ok with empty hash with allowed_empty: true)" do
          data = {}
          schema = { type: 'hash', allowed_empty: true }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

        it "should validate ok with a missing parameter, when marked :required => false" do
          data = {
            name: 'hello'
          }
          schema = {
            type: 'hash',
            internals: {
              name: { type: 'string' },
              optional_param: { type: 'string', required: false },
            }
          }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

        it "should return error with a missing :required => true param" do
          data = {
            name: 'hello'
          }
          schema = {
            type: 'hash',
            internals: {
              name: { type: 'string' },
              not_optional_param: { type: 'string', required: true },
            }
          }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq(["base: missing 'not_optional_param' parameter"])
        end

        it "should return error if a bogus parameter is specified" do
          data = {
            name: 'hello',
            bogus_parameter: [ 'wibble' ],
            bogus_parameter2: 'hello',
          }
          schema = {
            type: 'hash',
            internals: {
              name: { type: 'string' },
            },
          }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq([
            "base: parameter 'bogus_parameter' is invalid",
            "base: parameter 'bogus_parameter2' is invalid",
          ])
        end

        it "should validate ok if a bogus parameter is specified, when :permit_unknown_parameters is true" do
          data = {
            name: 'hello',
            bogus_parameter: [ 'wibble' ],
            bogus_parameter2: 'hello',
          }
          schema = {
            type: 'hash',
            permit_unknown_parameters: true,
            internals: {
              name: { type: 'string' },
            },
          }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

      end

      context "array validations" do

        it "should validate a basic array" do
          data = [ "santa", "north pole" ]
          schema = {
            type: "Array",
            each_element_is: { type: "string" }
          }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

        it "should validate a bogus array" do
          data = [ 42, 43 ]
          schema = {
            type: "Array",
            each_element_is: { type: "string" }
          }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_false
        end

        it "should return correct errors validating a bogus array" do
          data = [ 42, 43 ]
          schema = {
            type: "Array",
            each_element_is: { type: "string" }
          }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq([
            "base: 42 is not a string",
            "base: 43 is not a string",
          ])
        end

        it "should return error with empty array (by default)" do
          data = []
          schema = { type: 'array' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq([ 'base: cannot be empty array'] )
        end

        it "should return error with empty array with allowed_empty: false)" do
          data = []
          schema = { type: 'array', allowed_empty: false }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq([ 'base: cannot be empty array'] )
        end

        it "should validate ok with empty array with allowed_empty: true)" do
          data = []
          schema = { type: 'array', allowed_empty: true }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

      end

      context "array of hashes validations" do

        it "should validate an array of hashes" do
          data = [
            { name: "santa", address: "north pole" },
            { name: "mole",  address: "1 hole street" },
          ]
          schema = {
            type: "array",
            each_element_is: {
              type: "hash",
              internals: {
                name: { type: 'string' },
                address: { type: 'string' },
              }
            }
          }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

        it "should correctly error on an invalid an array of hashes" do
          data = [
            { name: "santa", address: [] },
            { name: 43,  address: "1 hole street" },
          ]
          schema = {
            type: "array",
            each_element_is: {
              type: "hash",
              internals: {
                name: { type: 'string' },
                address: { type: 'string' },
              }
            }
          }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq([
            "address: [] is not a string",
            "name: 43 is not a string",
          ])
        end

      end

      context "hash of arrays validations" do

        it "should validate a hash of arrays" do
          data = {
            boys_names: [ 'bob', 'andrew', 'charlie', 'dave' ],
            girls_names: [ 'alice', 'beth', 'carol', 'davina' ],
          }
          schema = {
            type: "Hash",
            internals: {
              boys_names: {
                type: "Array",
                each_element_is: { type: 'String' }
              },
              girls_names: {
                type: "Array",
                each_element_is: { type: 'String' }
              }
            }
          }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

        it "should correctly error on an invalid hash of arrays" do
          data = {
            boys_names: [ 'bob', 'andrew', 'charlie', 'dave' ],
            girls_names: [ 'alice', 'beth', 'carol', 'davina' ],
          }
          schema = {
            type: "Hash",
            internals: {
              boys_names: {
                type: "Array",
                each_element_is: { type: 'String' },
              },
              girls_names: {
                type: "Array",
                each_element_is: { type: 'String' },
              }
            },
          }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

      end

      context "string_or_number validations" do

        it "should correctly validate an Integer" do
          data = 2
          schema = { type: 'string_or_number' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

        it "should correctly validate a String" do
          data = '2'
          schema = { type: 'string_or_number' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

        it "should correctly error if not a string or numeric" do
          data = []
          schema = { type: 'string_or_number' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq(["base: [] is not a string_or_number"])
        end

      end

      context "ip_address validations" do

        it "should correctly validate an IP address" do
          data = '192.168.100.100'
          schema = { type: 'ip_address' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end

        it "should correctly error on an invalid IP address" do
          data = '256.168.100.100'
          schema = { type: 'ip_address' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq(['base: 256.168.100.100 is not a valid ip_address'])
        end

        it "should error if ip address have wrong octets" do
          data = '192.168.100.100/33/33/33'
          schema = { type: 'ip_address' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq(['base: 192.168.100.100/33/33/33 is not a valid ip_address'])
        end

      end

      context "ip_address_range validations" do
        context "validate CIDR" do
          it "should validate OK if CIDR is correct" do
            data = '192.168.100.100/24'
            schema = { type: 'ip_address_range' }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_true
          end

          it "should return error if network bit value is greater than 32" do
            data = '192.168.100.100/33'
            schema = { type: 'ip_address_range' }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_false
            expect(v.errors).to eq(["base: 192.168.100.100/33 is not a valid IP address range. Valid values can be IP address, CIDR, IP range, 'Any','internal' and 'external'."])
          end

          it "should return error if network bit value is less than 0" do
            data = '192.168.100.100/33'
            schema = { type: 'ip_address_range' }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_false
            expect(v.errors).to eq(["base: 192.168.100.100/33 is not a valid IP address range. Valid values can be IP address, CIDR, IP range, 'Any','internal' and 'external'."])
          end

          it "should return error if network IP address is incorrect" do
            data = '192.168.100./33'
            schema = { type: 'ip_address_range' }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_false
            expect(v.errors).to eq(["base: 192.168.100./33 is not a valid IP address range. Valid values can be IP address, CIDR, IP range, 'Any','internal' and 'external'."])
          end
        end

        context "validate alphabetical values for IP range" do
          %w(Any internal external).each do |data|
            it "should validate OK if IP range is '#{data}'" do
              schema = { type: 'ip_address_range' }
              v = ConfigValidator.validate(:base, data, schema)
              expect(v.valid?).to be_true
              expect(v.errors).to be_empty
            end
          end

          it "should error if IP range is a string but not a valid alphabetical value" do
            data = 'invalid_ip_range_string'
            schema = { type: 'ip_address_range' }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_false
            expect(v.errors).to eq(["base: invalid_ip_range_string is not a valid IP address range. Valid values can be IP address, CIDR, IP range, 'Any','internal' and 'external'."])
          end
        end

        context "validate ranges specified using start and end addresses" do
          it "should validate ok if the combination of start IP and end IP is correct" do
            data = '192.168.100.100-192.168.100.110'
            schema = { type: 'ip_address_range' }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_true
            expect(v.errors).to be_empty
          end

          it "should error if start IP address is incorrect" do
            data = '192.168.100-192.168.100.110'
            schema = { type: 'ip_address_range' }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_false
            expect(v.errors).to eq(["base: 192.168.100-192.168.100.110 is not a valid IP address range. Valid values can be IP address, CIDR, IP range, 'Any','internal' and 'external'."])
          end

          it "should error if end IP address is incorrect" do
            data = '192.168.100.110-192.168.100'
            schema = { type: 'ip_address_range' }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_false
            expect(v.errors).to eq(["base: 192.168.100.110-192.168.100 is not a valid IP address range. Valid values can be IP address, CIDR, IP range, 'Any','internal' and 'external'."])
          end

          it "should error if the combination of start IP and end IP is incorrect" do
            data = '200.168.100.99-192.168.100'
            schema = { type: 'ip_address_range' }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_false
            expect(v.errors).to eq(["base: 200.168.100.99-192.168.100 is not a valid IP address range. Valid values can be IP address, CIDR, IP range, 'Any','internal' and 'external'."])
          end

          it "should error if the start and end IPS are not separated by -" do
            data = '190.168.100.99:192.168.100'
            schema = { type: 'ip_address_range' }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_false
            expect(v.errors).to eq(["base: 190.168.100.99:192.168.100 is not a valid IP address range. Valid values can be IP address, CIDR, IP range, 'Any','internal' and 'external'."])
          end
        end

        it "should accept single ip address as range" do
          data = '190.168.100.99'
          schema = { type: 'ip_address_range' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
          expect(v.errors).to eq([])
        end
      end

      context "enum validations" do
        it "should error if enum value is not present in list" do
          data = 'blah'
          schema = { type: 'enum', required: false, acceptable_values: ['allow', 'decline', 'none']}
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq(["base: blah is not a valid value. Acceptable values are 'allow', 'decline', 'none'."])
        end

        it "should raise error if enum schema does not contain acceptable_values" do
          data = 'blah'
          schema = { type: 'enum', required: false}
          expect{ ConfigValidator.validate(:base, data, schema) }.to raise_error("Must set :acceptable_values for type 'enum'")
        end

        it "should validate ok if enum is acceptable" do
          data = 'allow'
          schema = { type: 'enum', required: false, acceptable_values: ['allow', 'decline', 'none']}
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.valid?).to be_true
        end
      end

      context "boolean validations" do
        it "should error if boolean value is not valid" do
          data = 'blah'
          schema = { type: 'boolean' }
          v = ConfigValidator.validate(:base, data, schema)
          expect(v.errors).to eq(["base: blah is not a valid boolean value."])
        end

        [true, false].each do |boolean_value|
          it "should validate ok if value is #{boolean_value}" do
            schema = { type: 'boolean' }
            v = ConfigValidator.validate(:base, boolean_value, schema)
            expect(v.valid?).to be_true
          end
        end
      end

      context "deprecated_by" do
        # For clarification:
        #
        # - deprecatee: is the old param with a `deprecated_by` field.
        # - deprecator: is the new param listed in the `deprecated_by` field.
        #
        context "deprecatee is provided and deprecator is not" do
          let(:data) {{ name: "santa" }}

          it "should validate and warn when deprecator is required" do
            schema = {
              type: "Hash",
              internals: {
                name: { type: 'string', deprecated_by: 'full_name' },
                full_name: { type: 'string', required: true },
              }
            }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_true
            expect(v.warnings).to eq(["name: is deprecated by 'full_name'"])
          end

          it "should validate and warn when deprecator appears before deprecatee in schema" do
            schema = {
              type: "Hash",
              internals: {
                full_name: { type: 'string', required: true },
                name: { type: 'string', deprecated_by: 'full_name' },
              }
            }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_true
            expect(v.warnings).to eq(["name: is deprecated by 'full_name'"])
          end

          it "should warn about deprecatee even when both are not required" do
            schema = {
              type: "Hash",
              internals: {
                name: { type: 'string', required: false, deprecated_by: 'full_name' },
                full_name: { type: 'string', required: false },
              }
            }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_true
            expect(v.warnings).to eq(["name: is deprecated by 'full_name'"])
          end
        end

        context "neither deprecatee or deprecator are provided" do
          let(:data) {{ bogus: "blah" }}

          it "should return error for deprecator but not deprecatee if neither are set" do
            schema = {
              type: "Hash",
              internals: {
                name: { type: 'string', deprecated_by: 'full_name' },
                full_name: { type: 'string', required: true },
                bogus: { type: 'string' },
              }
            }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_false
            expect(v.errors).to eq(["base: missing 'full_name' parameter"])
          end

          it "should raise exception if deprecator does not exist in schema" do
            schema = {
              type: "Hash",
              internals: {
                name: { type: 'string', deprecated_by: 'does_not_exist' },
                bogus: { type: 'string' },
              }
            }
            expect {
              ConfigValidator.validate(:base, data, schema)
            }.to raise_error("name: deprecated_by target 'does_not_exist' not found in schema")
          end
        end

        context "deprecatee and deprecator are provided and of the wrong type" do
          let(:data) {{ name: 123, full_name: 123 }}

          it "should return an error for each and a warning for deprecatee" do
            schema = {
              type: "Hash",
              internals: {
                name: { type: 'string', deprecated_by: 'full_name' },
                full_name: { type: 'string', required: true },
              }
            }
            v = ConfigValidator.validate(:base, data, schema)
            expect(v.valid?).to be_false
            expect(v.errors).to eq([
              "name: 123 is not a string",
              "full_name: 123 is not a string",
            ])
            expect(v.warnings).to eq(["name: is deprecated by 'full_name'"])
          end
        end

        it "should not honour deprecation across nested structures" do
          data = { bogus: "blah" }
          schema = {
            type: "Hash",
            internals: {
              name: { type: 'string', deprecated_by: 'full_name' },
              nested: { type: 'hash', internals: {
                full_name: { type: 'string', required: true },
              }},
              bogus: { type: 'string' },
            }
          }
          expect {
            ConfigValidator.validate(:base, data, schema)
          }.to raise_error("name: deprecated_by target 'full_name' not found in schema")
        end
      end

    end
  end
end
