require 'spec_helper'

describe Grape::Validations::ParamsScope do
  subject do
    Class.new(Grape::API)
  end

  def app
    subject
  end

  context 'setting a default' do
    let(:documentation) { subject.routes.first.params }

    context 'when the default value is truthy' do
      before do
        subject.params do
          optional :int, type: Integer, default: 42
        end
        subject.get
      end

      it 'adds documentation about the default value' do
        expect(documentation).to have_key('int')
        expect(documentation['int']).to have_key(:default)
        expect(documentation['int'][:default]).to eq(42)
      end
    end

    context 'when the default value is false' do
      before do
        subject.params do
          optional :bool, type: Virtus::Attribute::Boolean, default: false
        end
        subject.get
      end

      it 'adds documentation about the default value' do
        expect(documentation).to have_key('bool')
        expect(documentation['bool']).to have_key(:default)
        expect(documentation['bool'][:default]).to eq(false)
      end
    end

    context 'when the default value is nil' do
      before do
        subject.params do
          optional :object, type: Object, default: nil
        end
        subject.get
      end

      it 'adds documentation about the default value' do
        expect(documentation).to have_key('object')
        expect(documentation['object']).to have_key(:default)
        expect(documentation['object'][:default]).to eq(nil)
      end
    end
  end

  context 'without a default' do
    before do
      subject.params do
        optional :object, type: Object
      end
      subject.get
    end

    it 'does not add documentation for the default value' do
      documentation = subject.routes.first.params
      expect(documentation).to have_key('object')
      expect(documentation['object']).not_to have_key(:default)
    end
  end

  context 'setting description' do
    [:desc, :description].each do |description_type|
      it "allows setting #{description_type}" do
        subject.params do
          requires :int, type: Integer, description_type => 'My very nice integer'
        end
        subject.get '/single' do
          'int works'
        end
        get '/single', int: 420
        expect(last_response.status).to eq(200)
        expect(last_response.body).to eq('int works')
      end
    end
  end

  context 'when using custom types' do
    module ParamsScopeSpec
      class CustomType
        attr_reader :value

        def self.parse(value)
          raise if value == 'invalid'
          new(value)
        end

        def initialize(value)
          @value = value
        end
      end
    end

    it 'coerces the parameter via the type\'s parse method' do
      subject.params do
        requires :foo, type: ParamsScopeSpec::CustomType
      end
      subject.get('/types') { params[:foo].value }

      get '/types', foo: 'valid'
      expect(last_response.status).to eq(200)
      expect(last_response.body).to eq('valid')

      get '/types', foo: 'invalid'
      expect(last_response.status).to eq(400)
      expect(last_response.body).to match(/foo is invalid/)
    end
  end

  context 'array without coerce type explicitly given' do
    it 'sets the type based on first element' do
      subject.params do
        requires :periods, type: Array, values: -> { %w(day month) }
      end
      subject.get('/required') { 'required works' }

      get '/required', periods: %w(day month)
      expect(last_response.status).to eq(200)
      expect(last_response.body).to eq('required works')
    end

    it 'fails to call API without Array type' do
      subject.params do
        requires :periods, type: Array, values: -> { %w(day month) }
      end
      subject.get('/required') { 'required works' }

      get '/required', periods: 'day'
      expect(last_response.status).to eq(400)
      expect(last_response.body).to eq('periods is invalid')
    end

    it 'raises exception when values are of different type' do
      expect do
        subject.params { requires :numbers, type: Array, values: [1, 'definitely not a number', 3] }
      end.to raise_error Grape::Exceptions::IncompatibleOptionValues
    end

    it 'raises exception when range values have different endpoint types' do
      expect do
        subject.params { requires :numbers, type: Array, values: 0.0..10 }
      end.to raise_error Grape::Exceptions::IncompatibleOptionValues
    end
  end

  context 'coercing values validation with proc' do
    it 'allows the proc to pass validation without checking' do
      subject.params { requires :numbers, type: Integer, values: -> { [0, 1, 2] } }

      subject.post('/required') { 'coercion with proc works' }
      post '/required', numbers: '1'
      expect(last_response.status).to eq(201)
      expect(last_response.body).to eq('coercion with proc works')
    end

    it 'allows the proc to pass validation without checking in value' do
      subject.params { requires :numbers, type: Integer, values: { value: -> { [0, 1, 2] } } }

      subject.post('/required') { 'coercion with proc works' }
      post '/required', numbers: '1'
      expect(last_response.status).to eq(201)
      expect(last_response.body).to eq('coercion with proc works')
    end

    it 'allows the proc to pass validation without checking in except' do
      subject.params { requires :numbers, type: Integer, values: { except: -> { [0, 1, 2] } } }

      subject.post('/required') { 'coercion with proc works' }
      post '/required', numbers: '10'
      expect(last_response.status).to eq(201)
      expect(last_response.body).to eq('coercion with proc works')
    end
  end

  context 'with range values' do
    context "when left range endpoint isn't #kind_of? the type" do
      it 'raises exception' do
        expect do
          subject.params { requires :latitude, type: Integer, values: -90.0..90 }
        end.to raise_error Grape::Exceptions::IncompatibleOptionValues
      end
    end

    context "when right range endpoint isn't #kind_of? the type" do
      it 'raises exception' do
        expect do
          subject.params { requires :latitude, type: Integer, values: -90..90.0 }
        end.to raise_error Grape::Exceptions::IncompatibleOptionValues
      end
    end

    context 'when the default is an array' do
      context 'and is the entire range of allowed values' do
        it 'does not raise an exception' do
          expect do
            subject.params { optional :numbers, type: Array[Integer], values: 0..2, default: 0..2 }
          end.to_not raise_error
        end
      end

      context 'and is a subset of allowed values' do
        it 'does not raise an exception' do
          expect do
            subject.params { optional :numbers, type: Array[Integer], values: [0, 1, 2], default: [1, 0] }
          end.to_not raise_error
        end
      end
    end

    context 'when both range endpoints are #kind_of? the type' do
      it 'accepts values in the range' do
        subject.params do
          requires :letter, type: String, values: 'a'..'z'
        end
        subject.get('/letter') { params[:letter] }

        get '/letter', letter: 'j'
        expect(last_response.status).to eq(200)
        expect(last_response.body).to eq('j')
      end

      it 'rejects values outside the range' do
        subject.params do
          requires :letter, type: String, values: 'a'..'z'
        end
        subject.get('/letter') { params[:letter] }

        get '/letter', letter: 'J'
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('letter does not have a valid value')
      end
    end
  end

  context 'parameters in group' do
    it 'errors when no type is provided' do
      expect do
        subject.params do
          group :a do
            requires :b
          end
        end
      end.to raise_error Grape::Exceptions::MissingGroupTypeError

      expect do
        subject.params do
          optional :a do
            requires :b
          end
        end
      end.to raise_error Grape::Exceptions::MissingGroupTypeError
    end

    it 'allows Hash as type' do
      subject.params do
        group :a, type: Hash do
          requires :b
        end
      end
      subject.get('/group') { 'group works' }
      get '/group', a: { b: true }
      expect(last_response.status).to eq(200)
      expect(last_response.body).to eq('group works')

      subject.params do
        optional :a, type: Hash do
          requires :b
        end
      end
      get '/optional_type_hash'
    end

    it 'allows Array as type' do
      subject.params do
        group :a, type: Array do
          requires :b
        end
      end
      subject.get('/group') { 'group works' }
      get '/group', a: [{ b: true }]
      expect(last_response.status).to eq(200)
      expect(last_response.body).to eq('group works')

      subject.params do
        optional :a, type: Array do
          requires :b
        end
      end
      get '/optional_type_array'
    end

    it 'errors with an unsupported type' do
      expect do
        subject.params do
          group :a, type: Set do
            requires :b
          end
        end
      end.to raise_error Grape::Exceptions::UnsupportedGroupTypeError

      expect do
        subject.params do
          optional :a, type: Set do
            requires :b
          end
        end
      end.to raise_error Grape::Exceptions::UnsupportedGroupTypeError
    end
  end

  context 'when validations are dependent on a parameter' do
    before do
      subject.params do
        optional :a
        given :a do
          requires :b
        end
      end
      subject.get('/test') { declared(params).to_json }
    end

    it 'applies the validations only if the parameter is present' do
      get '/test'
      expect(last_response.status).to eq(200)

      get '/test', a: true
      expect(last_response.status).to eq(400)
      expect(last_response.body).to eq('b is missing')

      get '/test', a: true, b: true
      expect(last_response.status).to eq(200)
    end

    it 'applies the validations of multiple parameters' do
      subject.params do
        optional :a, :b
        given :a, :b do
          requires :c
        end
      end
      subject.get('/multiple') { declared(params).to_json }

      get '/multiple'
      expect(last_response.status).to eq(200)

      get '/multiple', a: true
      expect(last_response.status).to eq(200)

      get '/multiple', b: true
      expect(last_response.status).to eq(200)

      get '/multiple', a: true, b: true
      expect(last_response.status).to eq(400)
      expect(last_response.body).to eq('c is missing')

      get '/multiple', a: true, b: true, c: true
      expect(last_response.status).to eq(200)
    end

    it 'applies only the appropriate validation' do
      subject.params do
        optional :a
        optional :b
        mutually_exclusive :a, :b
        given :a do
          requires :c, type: String
        end
        given :b do
          requires :c, type: Integer
        end
      end
      subject.get('/multiple') { declared(params).to_json }

      get '/multiple'
      expect(last_response.status).to eq(200)

      get '/multiple', a: true, c: 'test'
      expect(last_response.status).to eq(200)
      expect(JSON.parse(last_response.body).symbolize_keys).to eq a: 'true', b: nil, c: 'test'

      get '/multiple', b: true, c: '3'
      expect(last_response.status).to eq(200)
      expect(JSON.parse(last_response.body).symbolize_keys).to eq a: nil, b: 'true', c: 3

      get '/multiple', a: true
      expect(last_response.status).to eq(400)
      expect(last_response.body).to eq('c is missing')

      get '/multiple', b: true
      expect(last_response.status).to eq(400)
      expect(last_response.body).to eq('c is missing')

      get '/multiple', a: true, b: true, c: 'test'
      expect(last_response.status).to eq(400)
      expect(last_response.body).to eq('a, b are mutually exclusive, c is invalid')
    end

    it 'raises an error if the dependent parameter was never specified' do
      expect do
        subject.params do
          given :c do
          end
        end
      end.to raise_error(Grape::Exceptions::UnknownParameter)
    end

    it 'includes the parameter within #declared(params)' do
      get '/test', a: true, b: true

      expect(JSON.parse(last_response.body)).to eq('a' => 'true', 'b' => 'true')
    end

    it 'returns a sensible error message within a nested context' do
      subject.params do
        requires :bar, type: Hash do
          optional :a
          given :a do
            requires :b
          end
        end
      end
      subject.get('/nested') { 'worked' }

      get '/nested', bar: { a: true }
      expect(last_response.status).to eq(400)
      expect(last_response.body).to eq('bar[b] is missing')
    end

    it 'includes the nested parameter within #declared(params)' do
      subject.params do
        requires :bar, type: Hash do
          optional :a
          given :a do
            requires :b
          end
        end
      end
      subject.get('/nested') { declared(params).to_json }

      get '/nested', bar: { a: true, b: 'yes' }
      expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'true', 'b' => 'yes' })
    end

    it 'includes level 2 nested parameters outside the given within #declared(params)' do
      subject.params do
        requires :bar, type: Hash do
          optional :a
          given :a do
            requires :c, type: Hash do
              requires :b
            end
          end
        end
      end
      subject.get('/nested') { declared(params).to_json }

      get '/nested', bar: { a: true, c: { b: 'yes' } }
      expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'true', 'c' => { 'b' => 'yes' } })
    end
  end

  context 'when validations are dependent on a parameter with specific value' do
    # build test cases from all combinations of declarations and options
    a_decls = %i(optional requires)
    a_options = [{}, { values: %w(x y z) }]
    b_options = [{}, { type: String }, { allow_blank: false }, { type: String, allow_blank: false }]
    combinations = a_decls.product(a_options, b_options)
    combinations.each_with_index do |combination, i|
      a_decl, a_opts, b_opts = combination

      context "(case #{i})" do
        before do
          # puts "a_decl: #{a_decl}, a_opts: #{a_opts}, b_opts: #{b_opts}"
          subject.params do
            send a_decl, :a, **a_opts
            given(a: ->(val) { val == 'x' }) { requires :b, **b_opts }
            given(a: ->(val) { val == 'y' }) { requires :c, **b_opts }
          end
          subject.get('/test') { declared(params).to_json }
        end

        if a_decl == :optional
          it 'skips validation when base param is missing' do
            get '/test'
            expect(last_response.status).to eq(200)
          end
        end

        it 'skips validation when base param does not have a specified value' do
          get '/test', a: 'z'
          expect(last_response.status).to eq(200)

          get '/test', a: 'z', b: ''
          expect(last_response.status).to eq(200)
        end

        it 'applies the validation when base param has the specific value' do
          get '/test', a: 'x'
          expect(last_response.status).to eq(400)
          expect(last_response.body).to include('b is missing')

          get '/test', a: 'x', b: true
          expect(last_response.status).to eq(200)

          get '/test', a: 'x', b: true, c: ''
          expect(last_response.status).to eq(200)
        end

        it 'includes the parameter within #declared(params)' do
          get '/test', a: 'x', b: true
          expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => 'true', 'c' => nil)
        end
      end
    end
  end

  it 'raises an error if the dependent parameter was never specified' do
    expect do
      subject.params do
        given :c do
        end
      end
    end.to raise_error(Grape::Exceptions::UnknownParameter)
  end

  it 'returns a sensible error message within a nested context' do
    subject.params do
      requires :bar, type: Hash do
        optional :a
        given a: ->(val) { val == 'x' } do
          requires :b
        end
      end
    end
    subject.get('/nested') { 'worked' }

    get '/nested', bar: { a: 'x' }
    expect(last_response.status).to eq(400)
    expect(last_response.body).to eq('bar[b] is missing')
  end

  it 'includes the nested parameter within #declared(params)' do
    subject.params do
      requires :bar, type: Hash do
        optional :a
        given a: ->(val) { val == 'x' } do
          requires :b
        end
      end
    end
    subject.get('/nested') { declared(params).to_json }

    get '/nested', bar: { a: 'x', b: 'yes' }
    expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'x', 'b' => 'yes' })
  end

  it 'includes level 2 nested parameters outside the given within #declared(params)' do
    subject.params do
      requires :bar, type: Hash do
        optional :a
        given a: ->(val) { val == 'x' } do
          requires :c, type: Hash do
            requires :b
          end
        end
      end
    end
    subject.get('/nested') { declared(params).to_json }

    get '/nested', bar: { a: 'x', c: { b: 'yes' } }
    expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'x', 'c' => { 'b' => 'yes' } })
  end

  context 'failing fast' do
    context 'when fail_fast is not defined' do
      it 'does not stop validation' do
        subject.params do
          requires :one
          requires :two
          requires :three
        end
        subject.get('/fail-fast') { declared(params).to_json }

        get '/fail-fast'
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('one is missing, two is missing, three is missing')
      end
    end
    context 'when fail_fast is defined it stops the validation' do
      it 'of other params' do
        subject.params do
          requires :one, fail_fast: true
          requires :two
        end
        subject.get('/fail-fast') { declared(params).to_json }

        get '/fail-fast'
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('one is missing')
      end
      it 'for a single param' do
        subject.params do
          requires :one, allow_blank: false, regexp: /[0-9]+/, fail_fast: true
        end
        subject.get('/fail-fast') { declared(params).to_json }

        get '/fail-fast', one: ''
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('one is empty')
      end
    end
  end
end
