require 'spec_helper'

describe Grape::Validations::ValuesValidator do
  module ValidationsSpec
    class ValuesModel
      DEFAULT_VALUES = ['valid-type1', 'valid-type2', 'valid-type3'].freeze
      DEFAULT_EXCEPTS = ['invalid-type1', 'invalid-type2', 'invalid-type3'].freeze
      class << self
        def values
          @values ||= []
          [DEFAULT_VALUES + @values].flatten.uniq
        end

        def add_value(value)
          @values ||= []
          @values << value
        end

        def excepts
          @excepts ||= []
          [DEFAULT_EXCEPTS + @excepts].flatten.uniq
        end

        def add_except(except)
          @excepts ||= []
          @excepts << except
        end
      end
    end

    module ValuesValidatorSpec
      class API < Grape::API
        default_format :json

        resources :custom_message do
          params do
            requires :type, values: { value: ValuesModel.values, message: 'value does not include in values' }
          end
          get '/' do
            { type: params[:type] }
          end

          params do
            optional :type, values: { value: -> { ValuesModel.values }, message: 'value does not include in values' }, default: 'valid-type2'
          end
          get '/lambda' do
            { type: params[:type] }
          end

          params do
            requires :type, values: { except: ValuesModel.excepts, except_message: 'value is on exclusions list', message: 'default exclude message' }
          end
          get '/exclude/exclude_message'

          params do
            requires :type, values: { except: -> { ValuesModel.excepts }, except_message: 'value is on exclusions list' }
          end
          get '/exclude/lambda/exclude_message'

          params do
            requires :type, values: { except: ValuesModel.excepts, message: 'default exclude message' }
          end
          get '/exclude/fallback_message'
        end

        params do
          requires :type, values: ValuesModel.values
        end
        get '/' do
          { type: params[:type] }
        end

        params do
          requires :type, values: []
        end
        get '/empty'

        params do
          optional :type, values: { value: ValuesModel.values }, default: 'valid-type2'
        end
        get '/default/hash/valid' do
          { type: params[:type] }
        end

        params do
          optional :type, values: ValuesModel.values, default: 'valid-type2'
        end
        get '/default/valid' do
          { type: params[:type] }
        end

        params do
          optional :type, values: { except: ValuesModel.excepts }, default: 'valid-type2'
        end
        get '/default/except' do
          { type: params[:type] }
        end

        params do
          optional :type, values: -> { ValuesModel.values }, default: 'valid-type2'
        end
        get '/lambda' do
          { type: params[:type] }
        end

        params do
          requires :type, values: ->(v) { ValuesModel.values.include? v }
        end
        get '/lambda_val' do
          { type: params[:type] }
        end

        params do
          requires :number, type: Integer, values: ->(v) { v > 0 }
        end
        get '/lambda_int_val' do
          { number: params[:number] }
        end

        params do
          requires :type, values: -> { [] }
        end
        get '/empty_lambda'

        params do
          optional :type, values: ValuesModel.values, default: -> { ValuesModel.values.sample }
        end
        get '/default_lambda' do
          { type: params[:type] }
        end

        params do
          optional :type, values: -> { ValuesModel.values }, default: -> { ValuesModel.values.sample }
        end
        get '/default_and_values_lambda' do
          { type: params[:type] }
        end

        params do
          optional :type, type: Boolean, desc: 'A boolean', values: [true]
        end
        get '/values/optional_boolean' do
          { type: params[:type] }
        end

        params do
          requires :type, type: Integer, desc: 'An integer', values: [10, 11], default: 10
        end
        get '/values/coercion' do
          { type: params[:type] }
        end

        params do
          requires :type, type: Array[Integer], desc: 'An integer', values: [10, 11], default: 10
        end
        get '/values/array_coercion' do
          { type: params[:type] }
        end

        params do
          optional :optional, type: Array do
            requires :type, values: %w[a b]
          end
        end
        get '/optional_with_required_values'

        params do
          requires :type, values: { except: ValuesModel.excepts }
        end
        get '/except/exclusive' do
          { type: params[:type] }
        end

        params do
          requires :type, type: String, values: { except: ValuesModel.excepts }
        end
        get '/except/exclusive/type' do
          { type: params[:type] }
        end

        params do
          requires :type, values: { except: -> { ValuesModel.excepts } }
        end
        get '/except/exclusive/lambda' do
          { type: params[:type] }
        end

        params do
          requires :type, type: String, values: { except: -> { ValuesModel.excepts } }
        end
        get '/except/exclusive/lambda/type' do
          { type: params[:type] }
        end

        params do
          requires :type, type: Integer, values: { except: -> { [3, 4, 5] } }
        end
        get '/except/exclusive/lambda/coercion' do
          { type: params[:type] }
        end

        params do
          requires :type, type: Integer, values: { value: 1..5, except: [3] }
        end
        get '/mixed/value/except' do
          { type: params[:type] }
        end

        params do
          optional :optional, type: Array[String], values: %w[a b c]
        end
        put '/optional_with_array_of_string_values'

        params do
          requires :type, values: { proc: ->(v) { ValuesModel.values.include? v } }
        end
        get '/proc' do
          { type: params[:type] }
        end

        params do
          requires :type, values: { proc: ->(v) { ValuesModel.values.include? v }, message: 'failed check' }
        end
        get '/proc/message'
      end
    end
  end

  def app
    ValidationsSpec::ValuesValidatorSpec::API
  end

  context 'with a custom validation message' do
    it 'allows a valid value for a parameter' do
      get('/custom_message', type: 'valid-type1')
      expect(last_response.status).to eq 200
      expect(last_response.body).to eq({ type: 'valid-type1' }.to_json)
    end

    it 'does not allow an invalid value for a parameter' do
      get('/custom_message', type: 'invalid-type')
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type value does not include in values' }.to_json)
    end

    it 'validates against values in a proc' do
      ValidationsSpec::ValuesModel.add_value('valid-type4')

      get('/custom_message/lambda', type: 'valid-type4')
      expect(last_response.status).to eq 200
      expect(last_response.body).to eq({ type: 'valid-type4' }.to_json)
    end

    it 'does not allow an invalid value for a parameter using lambda' do
      get('/custom_message/lambda', type: 'invalid-type')
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type value does not include in values' }.to_json)
    end
  end

  context 'with a custom exclude validation message' do
    it 'does not allow an invalid value for a parameter' do
      get('/custom_message/exclude/exclude_message', type: 'invalid-type1')
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type value is on exclusions list' }.to_json)
    end
  end

  context 'with a custom exclude validation message' do
    it 'does not allow an invalid value for a parameter' do
      get('/custom_message/exclude/lambda/exclude_message', type: 'invalid-type1')
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type value is on exclusions list' }.to_json)
    end
  end

  context 'exclude with a standard custom validation message' do
    it 'does not allow an invalid value for a parameter' do
      get('/custom_message/exclude/fallback_message', type: 'invalid-type1')
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type default exclude message' }.to_json)
    end
  end

  it 'allows a valid value for a parameter' do
    get('/', type: 'valid-type1')
    expect(last_response.status).to eq 200
    expect(last_response.body).to eq({ type: 'valid-type1' }.to_json)
  end

  it 'does not allow an invalid value for a parameter' do
    get('/', type: 'invalid-type')
    expect(last_response.status).to eq 400
    expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
  end

  it 'rejects all values if values is an empty array' do
    get('/empty', type: 'invalid-type')
    expect(last_response.status).to eq 400
    expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
  end

  context 'nil value for a parameter' do
    it 'does not allow for root params scope' do
      get('/', type: nil)
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
    end

    it 'allows for a required param in child scope' do
      get('/optional_with_required_values')
      expect(last_response.status).to eq 200
    end

    it 'allows for an optional param with a list of values' do
      put('/optional_with_array_of_string_values', optional: nil)
      expect(last_response.status).to eq 200
    end
  end

  it 'allows a valid default value' do
    get('/default/valid')
    expect(last_response.status).to eq 200
    expect(last_response.body).to eq({ type: 'valid-type2' }.to_json)
  end

  it 'allows a default value with except' do
    get('/default/except')
    expect(last_response.status).to eq 200
    expect(last_response.body).to eq({ type: 'valid-type2' }.to_json)
  end

  it 'allows a valid default value' do
    get('/default/hash/valid')
    expect(last_response.status).to eq 200
    expect(last_response.body).to eq({ type: 'valid-type2' }.to_json)
  end

  it 'allows a proc for values' do
    get('/lambda', type: 'valid-type1')
    expect(last_response.status).to eq 200
    expect(last_response.body).to eq({ type: 'valid-type1' }.to_json)
  end

  it 'does not validate updated values without proc' do
    ValidationsSpec::ValuesModel.add_value('valid-type4')

    get('/', type: 'valid-type4')
    expect(last_response.status).to eq 400
    expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
  end

  it 'validates against values in a proc' do
    ValidationsSpec::ValuesModel.add_value('valid-type4')

    get('/lambda', type: 'valid-type4')
    expect(last_response.status).to eq 200
    expect(last_response.body).to eq({ type: 'valid-type4' }.to_json)
  end

  it 'does not allow an invalid value for a parameter using lambda' do
    get('/lambda', type: 'invalid-type')
    expect(last_response.status).to eq 400
    expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
  end

  it 'does not allow non-numeric string value for int value using lambda' do
    get('/lambda_int_val', number: 'foo')
    expect(last_response.status).to eq 400
    expect(last_response.body).to eq({ error: 'number is invalid, number does not have a valid value' }.to_json)
  end

  it 'does not allow nil for int value using lambda' do
    get('/lambda_int_val', number: nil)
    expect(last_response.status).to eq 400
    expect(last_response.body).to eq({ error: 'number does not have a valid value' }.to_json)
  end

  it 'allows numeric string for int value using lambda' do
    get('/lambda_int_val', number: '3')
    expect(last_response.status).to eq 200
    expect(last_response.body).to eq({ number: 3 }.to_json)
  end

  it 'allows value using lambda' do
    get('/lambda_val', type: 'valid-type1')
    expect(last_response.status).to eq 200
    expect(last_response.body).to eq({ type: 'valid-type1' }.to_json)
  end

  it 'does not allow invalid value using lambda' do
    get('/lambda_val', type: 'invalid-type')
    expect(last_response.status).to eq 400
    expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
  end

  it 'validates against an empty array in a proc' do
    get('/empty_lambda', type: 'any')
    expect(last_response.status).to eq 400
    expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
  end

  it 'validates default value from proc' do
    get('/default_lambda')
    expect(last_response.status).to eq 200
  end

  it 'validates default value from proc against values in a proc' do
    get('/default_and_values_lambda')
    expect(last_response.status).to eq 200
  end

  it 'raises IncompatibleOptionValues on an invalid default value from proc' do
    subject = Class.new(Grape::API)
    expect do
      subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: ValidationsSpec::ValuesModel.values.sample + '_invalid' }
    end.to raise_error Grape::Exceptions::IncompatibleOptionValues
  end

  it 'raises IncompatibleOptionValues on an invalid default value' do
    subject = Class.new(Grape::API)
    expect do
      subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: 'invalid-type' }
    end.to raise_error Grape::Exceptions::IncompatibleOptionValues
  end

  it 'raises IncompatibleOptionValues when type is incompatible with values array' do
    subject = Class.new(Grape::API)
    expect do
      subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], type: Symbol }
    end.to raise_error Grape::Exceptions::IncompatibleOptionValues
  end

  it 'allows values to be true or false when setting the type to boolean' do
    get('/values/optional_boolean', type: true)
    expect(last_response.status).to eq 200
    expect(last_response.body).to eq({ type: true }.to_json)
  end
  it 'allows values to be a kind of the coerced type not just an instance of it' do
    get('/values/coercion', type: 10)
    expect(last_response.status).to eq 200
    expect(last_response.body).to eq({ type: 10 }.to_json)
  end

  it 'allows values to be a kind of the coerced type in an array' do
    get('/values/array_coercion', type: [10])
    expect(last_response.status).to eq 200
    expect(last_response.body).to eq({ type: [10] }.to_json)
  end

  it 'raises IncompatibleOptionValues when values contains a value that is not a kind of the type' do
    subject = Class.new(Grape::API)
    expect do
      subject.params { requires :type, values: [10.5, 11], type: Integer }
    end.to raise_error Grape::Exceptions::IncompatibleOptionValues
  end

  it 'raises IncompatibleOptionValues when except contains a value that is not a kind of the type' do
    subject = Class.new(Grape::API)
    expect do
      subject.params { requires :type, values: { except: [10.5, 11] }, type: Integer }
    end.to raise_error Grape::Exceptions::IncompatibleOptionValues
  end

  context 'with a lambda values' do
    subject do
      Class.new(Grape::API) do
        params do
          optional :type, type: String, values: -> { [SecureRandom.uuid] }, default: -> { SecureRandom.uuid }
        end
        get '/random_values'
      end
    end

    def app
      subject
    end

    before do
      expect(SecureRandom).to receive(:uuid).and_return('foo').once
    end

    it 'only evaluates values dynamically with each request' do
      get '/random_values', type: 'foo'
      expect(last_response.status).to eq 200
    end

    it 'chooses default' do
      get '/random_values'
      expect(last_response.status).to eq 200
    end
  end

  context 'with a range of values' do
    subject(:app) do
      Class.new(Grape::API) do
        params do
          optional :value, type: Float, values: 0.0..10.0
        end
        get '/value' do
          { value: params[:value] }.to_json
        end

        params do
          optional :values, type: Array[Float], values: 0.0..10.0
        end
        get '/values' do
          { values: params[:values] }.to_json
        end
      end
    end

    it 'allows a single value inside of the range' do
      get('/value', value: 5.2)
      expect(last_response.status).to eq 200
      expect(last_response.body).to eq({ value: 5.2 }.to_json)
    end

    it 'allows an array of values inside of the range' do
      get('/values', values: [8.6, 7.5, 3, 0.9])
      expect(last_response.status).to eq 200
      expect(last_response.body).to eq({ values: [8.6, 7.5, 3.0, 0.9] }.to_json)
    end

    it 'rejects a single value outside the range' do
      get('/value', value: 'a')
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq('value is invalid, value does not have a valid value')
    end

    it 'rejects an array of values if any of them are outside the range' do
      get('/values', values: [8.6, 75, 3, 0.9])
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq('values does not have a valid value')
    end
  end

  context 'exclusive excepts' do
    it 'allows any other value outside excepts' do
      get '/except/exclusive', type: 'value'
      expect(last_response.status).to eq 200
      expect(last_response.body).to eq({ type: 'value' }.to_json)
    end

    it 'allows any other value outside excepts when type is included' do
      get '/except/exclusive/type', type: 'value'
      expect(last_response.status).to eq 200
      expect(last_response.body).to eq({ type: 'value' }.to_json)
    end

    it 'rejects values that matches except' do
      get '/except/exclusive', type: 'invalid-type1'
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json)
    end

    it 'rejects an array of values if any of them matches except' do
      get '/except/exclusive', type: %w[valid1 valid2 invalid-type1 valid4]
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json)
    end
  end

  context 'exclusive excepts with lambda' do
    it 'allows any other value outside excepts when type is included' do
      get '/except/exclusive/lambda/type', type: 'value'
      expect(last_response.status).to eq 200
      expect(last_response.body).to eq({ type: 'value' }.to_json)
    end

    it 'allows any other value outside excepts' do
      get '/except/exclusive/lambda', type: 'value'
      expect(last_response.status).to eq 200
      expect(last_response.body).to eq({ type: 'value' }.to_json)
    end

    it 'rejects values that matches except' do
      get '/except/exclusive/lambda', type: 'invalid-type1'
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json)
    end
  end

  context 'exclusive excepts with lambda and coercion' do
    it 'allows any other value outside excepts' do
      get '/except/exclusive/lambda/coercion', type: '10010000'
      expect(last_response.status).to eq 200
      expect(last_response.body).to eq({ type: 10_010_000 }.to_json)
    end

    it 'rejects values that matches except' do
      get '/except/exclusive/lambda/coercion', type: '3'
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json)
    end
  end

  context 'with mixed values and excepts' do
    it 'allows value, but not in except' do
      get '/mixed/value/except', type: 2
      expect(last_response.status).to eq 200
      expect(last_response.body).to eq({ type: 2 }.to_json)
    end

    it 'rejects except' do
      get '/mixed/value/except', type: 3
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json)
    end

    it 'rejects outside except and outside value' do
      get '/mixed/value/except', type: 10
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
    end
  end

  context 'custom validation using proc' do
    it 'accepts a single valid value' do
      get '/proc', type: 'valid-type1'
      expect(last_response.status).to eq 200
      expect(last_response.body).to eq({ type: 'valid-type1' }.to_json)
    end

    it 'accepts multiple valid values' do
      get '/proc', type: ['valid-type1', 'valid-type3']
      expect(last_response.status).to eq 200
      expect(last_response.body).to eq({ type: ['valid-type1', 'valid-type3'] }.to_json)
    end

    it 'rejects a single invalid value' do
      get '/proc', type: 'invalid-type1'
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
    end

    it 'rejects an invalid value among valid ones' do
      get '/proc', type: ['valid-type1', 'invalid-type1', 'valid-type3']
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
    end

    it 'uses supplied message' do
      get '/proc/message', type: 'invalid-type1'
      expect(last_response.status).to eq 400
      expect(last_response.body).to eq({ error: 'type failed check' }.to_json)
    end
  end
end
