require_relative "../spec_helper"
require 'tempfile'

describe "typecast_params plugin" do 
  def tp(arg='a=1&b[]=2&b[]=3&c[d]=4&c[e]=5&f=&g[]=&h[i]=')
    @tp.call(arg)
  end

  def error
    yield
  rescue @tp_error => e
    e
  end

  before do
    res = nil
    app(:typecast_params) do |r|
      if r.env['rack.params']
        r.define_singleton_method(:params){r.env['rack.params']}
      end
      res = typecast_params
      nil
    end

    @tp = lambda do |params|
      if params.is_a?(Hash)
        req('rack.params'=>params)
      else
        req('QUERY_STRING'=>params, 'rack.input'=>StringIO.new)
      end
      res
    end

    @tp_error = Roda::RodaPlugins::TypecastParams::Error
  end

  it ".new should raise error if params is not a hash" do
    lambda{Roda::RodaPlugins::TypecastParams::Params.new('a')}.must_raise @tp_error
  end

  it ".new should raise for non String/Array args passed to conversion method" do
    lambda{tp.any({})}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError
    lambda{tp.any(Object.new)}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError
    lambda{tp.any(:a)}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError
  end

  it "#present should return whether the key is in the obj if given String" do
    tp.present?('a').must_equal true
    tp.present?('b').must_equal true
    tp.present?('c').must_equal true
    tp.present?('d').must_equal false
  end

  it "#present should return whether all keys are in the obj if given an Array" do
    tp.present?(%w'a b c').must_equal true
    tp.present?(%w'a b c d').must_equal false
  end

  it "#present should raise if given an unexpected object" do
    lambda{tp.present?(:a)}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError
    lambda{tp.present?([:a])}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError
    lambda{tp.present?([['a']])}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError
  end

  it "conversion methods should only support one level deep array of keys" do
    lambda{tp.any([['a']])}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError
  end

  it "#any should not do any conversion" do
    tp.any('a').must_equal '1'
    tp.any('b').must_equal ["2", "3"]
    tp.any('c').must_equal('d'=>'4', 'e'=>'5')
    tp.any('d').must_be_nil
    tp.any(%w'g h').must_equal([[''], {'i'=>''}])

    tp.any!('a').must_equal '1'
    tp.any!(%w'a').must_equal %w'1'
    lambda{tp.any!('d')}.must_raise @tp_error
    lambda{tp.any!(%w'd j')}.must_raise @tp_error

    lambda{tp.array(:any, 'a')}.must_raise @tp_error
    tp.array(:any, 'b').must_equal ["2", "3"]
    lambda{tp.array(:any, 'c')}.must_raise @tp_error
    tp.array(:any, 'd').must_be_nil
    tp.array(:any, %w'g').must_equal([['']])
    lambda{tp.array(:any, 'h')}.must_raise @tp_error

    tp.array!(:any, 'b').must_equal ["2", "3"]
    lambda{tp.array!(:any, 'd')}.must_raise @tp_error
    tp.array!(:any, %w'g').must_equal([['']])
  end

  it "#str should require strings" do
    tp.str('a').must_equal '1'
    lambda{tp.str('b')}.must_raise @tp_error
    lambda{tp.str('c')}.must_raise @tp_error
    lambda{tp.str(%w'b c')}.must_raise @tp_error
    tp.str('d').must_be_nil
    tp.str('f').must_equal ''
    lambda{tp.str('g')}.must_raise @tp_error
    lambda{tp.str('h')}.must_raise @tp_error

    tp.str!('a').must_equal '1'
    lambda{tp.str!('d')}.must_raise @tp_error
    tp.str!('f').must_equal ''

    lambda{tp.array(:str, 'a')}.must_raise @tp_error
    tp.array(:str, 'b').must_equal ["2", "3"]
    lambda{tp.array(:str, 'c')}.must_raise @tp_error
    tp.array(:str, 'd').must_be_nil
    tp.array(:str, 'g').must_equal [""]
    lambda{tp.array(:str, 'h')}.must_raise @tp_error

    tp.array!(:str, 'b').must_equal ["2", "3"]
    lambda{tp.array!(:str, 'd')}.must_raise @tp_error
    tp.array!(:str, 'g').must_equal [""]
  end

  it "#nonempty_str should require nonempty strings" do
    tp.nonempty_str('a').must_equal '1'
    tp('a=%201').nonempty_str('a').must_equal ' 1'
    tp('a=1%20').nonempty_str('a').must_equal '1 '
    tp('a=%201%20').nonempty_str('a').must_equal ' 1 '
    tp('a=%20').nonempty_str('a').must_be_nil
    lambda{tp.nonempty_str('b')}.must_raise @tp_error
    lambda{tp.nonempty_str('c')}.must_raise @tp_error
    tp.nonempty_str('d').must_be_nil
    tp.nonempty_str('f').must_be_nil
    lambda{tp.nonempty_str('g')}.must_raise @tp_error
    lambda{tp.nonempty_str('h')}.must_raise @tp_error

    tp.nonempty_str!('a').must_equal '1'
    lambda{tp.nonempty_str!('d')}.must_raise @tp_error
    lambda{tp.nonempty_str!('f')}.must_raise @tp_error

    lambda{tp.array(:nonempty_str, 'a')}.must_raise @tp_error
    tp.array(:nonempty_str, 'b').must_equal ["2", "3"]
    lambda{tp.array(:nonempty_str, 'c')}.must_raise @tp_error
    tp.array(:nonempty_str, 'd').must_be_nil
    tp.array(:nonempty_str, 'g').must_equal [nil]
    lambda{tp.array(:nonempty_str, 'h')}.must_raise @tp_error

    tp.array!(:nonempty_str, 'b').must_equal ["2", "3"]
    lambda{tp.array!(:nonempty_str, 'd')}.must_raise @tp_error
    lambda{tp.array!(:nonempty_str, 'g')}.must_raise @tp_error
  end

  it "#bool should convert to boolean" do
    tp('a=0').bool('a').must_equal false
    tp('a=f').bool('a').must_equal false
    tp('a=false').bool('a').must_equal false
    tp('a=FALSE').bool('a').must_equal false
    tp('a=F').bool('a').must_equal false
    tp('a=n').bool('a').must_equal false
    tp('a=no').bool('a').must_equal false
    tp('a=N').bool('a').must_equal false
    tp('a=NO').bool('a').must_equal false
    tp('a=off').bool('a').must_equal false
    tp('a=OFF').bool('a').must_equal false

    tp('a=1').bool('a').must_equal true
    tp('a=t').bool('a').must_equal true
    tp('a=true').bool('a').must_equal true
    tp('a=TRUE').bool('a').must_equal true
    tp('a=T').bool('a').must_equal true
    tp('a=y').bool('a').must_equal true
    tp('a=yes').bool('a').must_equal true
    tp('a=Y').bool('a').must_equal true
    tp('a=YES').bool('a').must_equal true
    tp('a=on').bool('a').must_equal true
    tp('a=ON').bool('a').must_equal true

    tp.bool('a').must_equal true
    lambda{tp.bool('b')}.must_raise @tp_error
    lambda{tp.bool('c')}.must_raise @tp_error
    tp.bool('d').must_be_nil
    tp.bool('f').must_be_nil

    tp.bool!('a').must_equal true
    lambda{tp.bool!('d')}.must_raise @tp_error
    lambda{tp.bool!('f')}.must_raise @tp_error

    lambda{tp.array(:bool, 'a')}.must_raise @tp_error
    tp('b[]=1&b[]=0').array(:bool, 'b').must_equal [true, false]
    lambda{tp('b[]=1&b[]=a').array(:bool, 'b')}.must_raise @tp_error
    lambda{tp.array(:bool, 'c')}.must_raise @tp_error
    tp.array(:bool, 'd').must_be_nil
    tp.array(:bool, 'g').must_equal [nil]
    lambda{tp.array(:bool, 'h')}.must_raise @tp_error

    tp('b[]=1&b[]=0').array!(:bool, 'b').must_equal [true, false]
    lambda{tp.array!(:bool, 'd')}.must_raise @tp_error
    lambda{tp.array!(:bool, 'g')}.must_raise @tp_error
  end

  it "#int should convert to integer" do
    tp('a=-1').int('a').must_equal(-1)
    tp('a=0').int('a').must_equal 0
    tp('a=a').int('a').must_equal 0
    tp.int('a').must_equal 1
    tp.int('a').must_be_kind_of Integer
    lambda{tp.int('b')}.must_raise @tp_error
    lambda{tp.int('c')}.must_raise @tp_error
    tp.int('d').must_be_nil
    tp.int('f').must_be_nil
    lambda{tp.int('g')}.must_raise @tp_error
    lambda{tp.int('h')}.must_raise @tp_error

    tp.int!('a').must_equal 1
    lambda{tp.int!('d')}.must_raise @tp_error
    lambda{tp.int!('f')}.must_raise @tp_error

    lambda{tp.array(:int, 'a')}.must_raise @tp_error
    tp.array(:int, 'b').must_equal [2, 3]
    lambda{tp.array(:int, 'c')}.must_raise @tp_error
    tp.array(:int, 'd').must_be_nil
    tp.array(:int, 'g').must_equal [nil]
    lambda{tp.array(:int, 'h')}.must_raise @tp_error

    tp.array!(:int, 'b').must_equal [2, 3]
    lambda{tp.array!(:int, 'd')}.must_raise @tp_error
    lambda{tp.array!(:int, 'g')}.must_raise @tp_error
  end

  it "#pos_int should convert to positive integer" do
    tp('a=-1').pos_int('a').must_be_nil
    tp('a=0').pos_int('a').must_be_nil
    tp('a=a').pos_int('a').must_be_nil
    tp.pos_int('a').must_equal 1
    tp.pos_int('a').must_be_kind_of Integer
    lambda{tp.pos_int('b')}.must_raise @tp_error
    lambda{tp.pos_int('c')}.must_raise @tp_error
    tp.pos_int('d').must_be_nil
    tp.pos_int('f').must_be_nil
    lambda{tp.pos_int('g')}.must_raise @tp_error
    lambda{tp.pos_int('h')}.must_raise @tp_error

    lambda{tp('a=-1').pos_int!('a')}.must_raise @tp_error
    lambda{tp('a=0').pos_int!('a')}.must_raise @tp_error
    lambda{tp('a=a').pos_int!('a')}.must_raise @tp_error
    tp.pos_int!('a').must_equal 1
    lambda{tp.pos_int!('d')}.must_raise @tp_error
    lambda{tp.pos_int!('f')}.must_raise @tp_error

    lambda{tp.array(:pos_int, 'a')}.must_raise @tp_error
    tp.array(:pos_int, 'b').must_equal [2, 3]
    lambda{tp.array(:pos_int, 'c')}.must_raise @tp_error
    tp.array(:pos_int, 'd').must_be_nil
    tp.array(:pos_int, 'g').must_equal [nil]
    lambda{tp.array(:pos_int, 'h')}.must_raise @tp_error

    tp.array!(:pos_int, 'b').must_equal [2, 3]
    lambda{tp.array!(:pos_int, 'd')}.must_raise @tp_error
    lambda{tp.array!(:pos_int, 'g')}.must_raise @tp_error
  end

  it "#Integer should convert to integer strictly" do
    tp('a=-1').Integer('a').must_equal(-1)
    tp('a=0').Integer('a').must_equal 0
    lambda{tp('a=a').Integer('a')}.must_raise @tp_error
    tp.Integer('a').must_equal 1
    tp.Integer('a').must_be_kind_of Integer
    lambda{tp.Integer('b')}.must_raise @tp_error
    lambda{tp.Integer('c')}.must_raise @tp_error
    tp.Integer('d').must_be_nil
    tp.Integer('f').must_be_nil
    lambda{tp.Integer('g')}.must_raise @tp_error
    lambda{tp.Integer('h')}.must_raise @tp_error

    tp.Integer!('a').must_equal 1
    lambda{tp.Integer!('d')}.must_raise @tp_error
    lambda{tp.Integer!('f')}.must_raise @tp_error

    lambda{tp.array(:Integer, 'a')}.must_raise @tp_error
    tp.array(:Integer, 'b').must_equal [2, 3]
    lambda{tp.array(:Integer, 'c')}.must_raise @tp_error
    tp.array(:Integer, 'd').must_be_nil
    tp.array(:Integer, 'g').must_equal [nil]
    lambda{tp.array(:Integer, 'h')}.must_raise @tp_error

    tp.array!(:Integer, 'b').must_equal [2, 3]
    lambda{tp.array!(:Integer, 'd')}.must_raise @tp_error
    lambda{tp.array!(:Integer, 'g')}.must_raise @tp_error

    a = 1
    @app.plugin :hooks
    @app.before do
      request.define_singleton_method(:params){{'a'=>a}}
    end
    tp.Integer('a').must_equal 1
    a = 1.0
    tp.Integer('a').must_equal 1
    a = 1.1
    lambda{tp.Integer('a')}.must_raise @tp_error
  end

  it "#float should convert to float" do
    tp('a=-1').float('a').must_equal(-1)
    tp('a=0').float('a').must_equal 0
    tp('a=a').float('a').must_equal 0
    tp.float('a').must_equal 1
    tp.float('a').must_be_kind_of Float
    lambda{tp.float('b')}.must_raise @tp_error
    lambda{tp.float('c')}.must_raise @tp_error
    tp.float('d').must_be_nil
    tp.float('f').must_be_nil
    lambda{tp.float('g')}.must_raise @tp_error
    lambda{tp.float('h')}.must_raise @tp_error

    tp.float!('a').must_equal 1
    lambda{tp.float!('d')}.must_raise @tp_error
    lambda{tp.float!('f')}.must_raise @tp_error

    lambda{tp.array(:float, 'a')}.must_raise @tp_error
    tp.array(:float, 'b').must_equal [2, 3]
    lambda{tp.array(:float, 'c')}.must_raise @tp_error
    tp.array(:float, 'd').must_be_nil
    tp.array(:float, 'g').must_equal [nil]
    lambda{tp.array(:float, 'h')}.must_raise @tp_error

    tp.array!(:float, 'b').must_equal [2, 3]
    lambda{tp.array!(:float, 'd')}.must_raise @tp_error
    lambda{tp.array!(:float, 'g')}.must_raise @tp_error
  end

  it "#Float should convert to float strictly" do
    tp('a=-1').Float('a').must_equal(-1)
    tp('a=0').Float('a').must_equal 0
    lambda{tp('a=a').Float('a')}.must_raise @tp_error
    tp.Float('a').must_equal 1
    tp.Float('a').must_be_kind_of Float
    lambda{tp.Float('b')}.must_raise @tp_error
    lambda{tp.Float('c')}.must_raise @tp_error
    tp.Float('d').must_be_nil
    tp.Float('f').must_be_nil
    lambda{tp.Float('g')}.must_raise @tp_error
    lambda{tp.Float('h')}.must_raise @tp_error

    tp.Float!('a').must_equal 1
    lambda{tp.Float!('d')}.must_raise @tp_error
    lambda{tp.Float!('f')}.must_raise @tp_error

    lambda{tp.array(:Float, 'a')}.must_raise @tp_error
    tp.array(:Float, 'b').must_equal [2, 3]
    lambda{tp.array(:Float, 'c')}.must_raise @tp_error
    tp.array(:Float, 'd').must_be_nil
    tp.array(:Float, 'g').must_equal [nil]
    lambda{tp.array(:Float, 'h')}.must_raise @tp_error

    tp.array!(:Float, 'b').must_equal [2, 3]
    lambda{tp.array!(:Float, 'd')}.must_raise @tp_error
    lambda{tp.array!(:Float, 'g')}.must_raise @tp_error
  end

  it "#Hash should require hashes" do
    lambda{tp.Hash('a')}.must_raise @tp_error
    lambda{tp.Hash('b')}.must_raise @tp_error
    tp.Hash('c').must_equal('d'=>'4', 'e'=>'5')
    tp.Hash('d').must_be_nil
    lambda{tp.Hash('f')}.must_raise @tp_error
    lambda{tp.Hash('g')}.must_raise @tp_error
    tp.Hash('h').must_equal('i'=>'')

    tp.Hash!('c').must_equal('d'=>'4', 'e'=>'5')
    lambda{tp.Hash!('d')}.must_raise @tp_error
    tp.Hash!('h').must_equal('i'=>'')

    lambda{tp.array(:Hash, 'c')}.must_raise @tp_error
    lambda{tp('a[][b]=2&a[]=3').array(:Hash, 'a')}.must_raise @tp_error
    tp('a[][b]=2&a[][b]=3').array(:Hash, 'a').must_equal [{'b'=>'2'}, {'b'=>'3'}]
    tp.array(:Hash, 'd').must_be_nil

    tp('a[][b]=2&a[][b]=3').array!(:Hash, 'a').must_equal [{'b'=>'2'}, {'b'=>'3'}]
    lambda{tp.array!(:Hash, 'd')}.must_raise @tp_error
  end

  it "#Date should parse strings into Date instances" do
    tp('a=').date('a').must_be_nil
    tp('a=2017-10-11').date('a').must_equal Date.new(2017, 10, 11)
    tp('a=17/10/11').date('a').must_equal Date.new(2017, 10, 11)
    lambda{tp.date('b')}.must_raise @tp_error
    lambda{tp('a=a').date('a')}.must_raise @tp_error

    lambda{tp('a=').date!('a')}.must_raise @tp_error
    tp('a=2017-10-11').date!('a').must_equal Date.new(2017, 10, 11)

    tp('a[]=2017-10-11&a[]=2017-10-12').array(:date, 'a').must_equal [Date.new(2017, 10, 11), Date.new(2017, 10, 12)]
    tp('a[]=2017-10-11&a[]=2017-10-12').array(:date, 'b').must_be_nil

    tp('a[]=2017-10-11&a[]=2017-10-12').array!(:date, 'a').must_equal [Date.new(2017, 10, 11), Date.new(2017, 10, 12)]
    lambda{tp('a[]=2017-10-11&a[]=a').array!(:date, 'a')}.must_raise @tp_error
    lambda{tp('a[]=2017-10-11&a[]=2017-10-12').array!(:date, 'b')}.must_raise @tp_error
  end

  it "#Time should parse strings into Time instances" do
    tp('a=').time('a').must_be_nil
    tp('a=2017-10-11%2012:13:14').time('a').must_equal Time.local(2017, 10, 11, 12, 13, 14)
    tp('a=17/10/11%2012:13:14').time('a').must_equal Time.local(2017, 10, 11, 12, 13, 14)
    lambda{tp.time('b')}.must_raise @tp_error
    lambda{tp('a=a').time('a')}.must_raise @tp_error

    lambda{tp('a=').time!('a')}.must_raise @tp_error
    tp('a=2017-10-11%2012:13:14').time!('a').must_equal Time.new(2017, 10, 11, 12, 13, 14)

    tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array(:time, 'a').must_equal [Time.local(2017, 10, 11, 12, 13, 14), Time.local(2017, 10, 12, 12, 13, 14)]
    tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array(:time, 'b').must_be_nil

    tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array!(:time, 'a').must_equal [Time.local(2017, 10, 11, 12, 13, 14), Time.local(2017, 10, 12, 12, 13, 14)]
    lambda{tp('a[]=2017-10-11%2012:13:14&a[]=a').array!(:time, 'a')}.must_raise @tp_error
    lambda{tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array!(:time, 'b')}.must_raise @tp_error
  end

  it "#DateTime should parse strings into DateTime instances" do
    tp('a=').datetime('a').must_be_nil
    tp('a=2017-10-11%2012:13:14').datetime('a').must_equal DateTime.new(2017, 10, 11, 12, 13, 14)
    tp('a=17/10/11%2012:13:14').datetime('a').must_equal DateTime.new(2017, 10, 11, 12, 13, 14)
    lambda{tp.datetime('b')}.must_raise @tp_error
    lambda{tp('a=a').datetime('a')}.must_raise @tp_error

    lambda{tp('a=').datetime!('a')}.must_raise @tp_error
    tp('a=2017-10-11%2012:13:14').datetime!('a').must_equal DateTime.new(2017, 10, 11, 12, 13, 14)

    tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array(:datetime, 'a').must_equal [DateTime.new(2017, 10, 11, 12, 13, 14), DateTime.new(2017, 10, 12, 12, 13, 14)]
    tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array(:datetime, 'b').must_be_nil

    tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array!(:datetime, 'a').must_equal [DateTime.new(2017, 10, 11, 12, 13, 14), DateTime.new(2017, 10, 12, 12, 13, 14)]
    lambda{tp('a[]=2017-10-11%2012:13:14&a[]=a').array!(:datetime, 'a')}.must_raise @tp_error
    lambda{tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array!(:datetime, 'b')}.must_raise @tp_error
  end

  it "#array should handle defaults" do
    tp = tp('b[]=1&c[]=')
    tp.array(:int, 'b', [2]).must_equal [1]
    tp.array(:int, 'c', [2]).must_equal [nil]
    tp.array(:int, 'd', []).must_equal []
    tp.array(:int, 'e', [1]).must_equal [1]

    tp('b[]=1&c[]=').array(:int, %w'b c', [2]).must_equal [[1], [nil]]
    tp('b[]=1&c[]=').array(:int, %w'b d', [2]).must_equal [[1], [2]]
  end

  it "#array! should handle defaults" do
    tp = tp('b[]=1&c[]=')
    tp.array!(:int, 'b', [2]).must_equal [1]
    lambda{tp.array!(:int, 'c', [2])}.must_raise @tp_error
    tp.array!(:int, 'd', []).must_equal []
    tp.array!(:int, 'e', [1]).must_equal [1]

    lambda{tp('b[]=1&c[]=').array!(:int, %w'b c', [2])}.must_raise @tp_error
    tp('b[]=1&c[]=').array!(:int, %w'b d', [2]).must_equal [[1], [2]]
  end

  it "#array should handle key arrays" do
    tp('b[]=1&c[]=2').array(:int, %w'b c').must_equal [[1], [2]]
    tp('b[]=1&c[]=').array(:int, %w'b c').must_equal [[1], [nil]]
  end

  it "#array! should handle key arrays" do
    tp('b[]=1&c[]=2').array!(:int, %w'b c').must_equal [[1], [2]]
    lambda{tp('b[]=1&c[]=').array!(:int, %w'b c')}.must_raise @tp_error
  end

  it "#array should raise ProgrammerError for invalid types" do
    proc{tp('b[]=1').array(:int2, 'b')}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError
  end

  it "#[] should access nested values" do
    tp['c'].must_be_kind_of tp.class
    tp['c'].int('d').must_equal 4
    tp['c'].int('e').must_equal 5
    tp['c'].int(%w'd e').must_equal [4, 5]
  end

  it "#[] should handle deeply nested structures" do
    tp('a[b][c][d][e]=1')['a']['b']['c']['d'].int('e').must_equal 1
    tp('a[][b][][e]=1')['a'][0]['b'][0].int('e').must_equal 1
  end

  it "#[] should raise error for non-Array/Hash parameters" do
    lambda{tp['a']}.must_raise @tp_error
  end

  it "#[] should raise error for accessing hash with integer value (thinking it is an array)" do
    lambda{tp[1]}.must_raise @tp_error
  end

  it "#[] should raise error for accessing array with non-integer value non-Array/Hash parameters" do
    lambda{tp['b']['a']}.must_raise @tp_error
  end

  it "#convert! should return a hash of converted parameters" do
    tp = tp()
    tp.convert! do |ptp|
      ptp.int!('a')
      ptp.array!(:int, 'b')
      ptp['c'].convert! do |stp|
        stp.int!(%w'd e')
      end
    end.must_equal("a"=>1, "b"=>[2, 3], "c"=>{"d"=>4, "e"=>5})
  end

  it "#convert! hash should only include changes made inside block" do
    tp = tp()
    tp.convert! do |ptp|
      ptp.int!('a')
      ptp.array!(:int, 'b')
    end.must_equal("a"=>1, "b"=>[2, 3])

    tp.convert! do |ptp|
      ptp['c'].convert! do |stp|
        stp.int!(%w'd e')
      end
    end.must_equal("c"=>{"d"=>4, "e"=>5})
  end

  it "#convert! should handle deeply nested structures" do
    tp = tp('a[b][c][d][e]=1')
    tp.convert! do |tp0|
      tp0['a'].convert! do |tp1|
        tp1['b'].convert! do |tp2|
          tp2['c'].convert! do |tp3|
            tp3['d'].convert! do |tp4|
              tp4.int('e')
            end
          end
        end
      end
    end.must_equal('a'=>{'b'=>{'c'=>{'d'=>{'e'=>1}}}})

    tp = tp('a[][b][][e]=1')
    tp.convert! do |tp0|
      tp0['a'].convert! do |tp1|
        tp1[0].convert! do |tp2|
          tp2['b'].convert! do |tp3|
            tp3[0].convert! do |tp4|
              tp4.int('e')
            end
          end
        end
      end
    end.must_equal('a'=>[{'b'=>[{'e'=>1}]}])
  end

  it "#convert! should not raise errors for missing keys if :raise option is false" do
    tp = tp('a[b]=1')
    tp.convert! do |tp0|
      tp0.convert!('a') do |tp1|
        tp1.convert!('c', :raise=>false) do |tp2|
          tp2.convert!('d') do |tp2|
          end
        end
      end
    end.must_equal('a'=>{})

    tp.convert!('b', :raise=>false){}.must_equal({})

    tp.convert!('b', :raise=>false) do |tp0|
        tp1.convert!('c'){}
    end.must_equal({})
  end

  it "#convert! should not raise errors for explicit nil values if :raise option is false" do
    tp = tp("id"=>1)
    tp.convert!("price", :raise=>false) do |tp0|
      tp0.convert!('d') do |tp2|
      end
    end.must_equal({})

    tp = tp("id"=>1, "price"=>nil)
    tp.convert!("price", :raise=>false) do |tp0|
      tp0.convert!('d') do |tp2|
      end
    end.must_equal({})
  end

  it "#convert! should handle #[] without #convert! at each level" do
    tp = tp('a[b][c][d][e]=1')
    tp.convert! do |tp0|
      tp0['a'].convert! do |tp1|
        tp1['b']['c']['d'].convert! do |tp4|
          tp4.int('e')
        end
      end
    end.must_equal('a'=>{'b'=>{'c'=>{'d'=>{'e'=>1}}}})
  end

  it "#convert! should handle #[] without #convert! below" do
    tp = tp('a[b][c][d][e]=1')
    tp.convert! do |tp0|
      tp0['a']['b']['c']['d'].int('e')
    end.must_equal('a'=>{'b'=>{'c'=>{'d'=>{'e'=>1}}}})
  end

  it "#convert! should handle multiple calls to #[] and #convert! below" do
    tp = tp('a[b]=2&a[c]=3&a[d]=4&a[e]=5&a[f]=6')
    tp.convert! do |tp0|
      tp0['a'].int('b')
      tp0['a'].convert! do |tp1|
        tp1.int('c')
      end
      tp0['a'].int('d')
      tp0['a'].convert! do |tp1|
        tp1.int('e')
      end
      tp0['a'].int('f')
    end.must_equal('a'=>{'b'=>2, 'c'=>3, 'd'=>4, 'e'=>5, 'f'=>6})
  end

  it "#convert! should handle defaults" do
    tp.convert! do |tp0|
      tp0.int('d', 12)
    end.must_equal('d'=>12)

    tp.convert! do |tp0|
      tp0.int(%w'a d', 12)
    end.must_equal('a'=>1, 'd'=>12)

    tp.convert! do |tp0|
      tp0.array(:int, 'g', [])
    end.must_equal('g'=>[nil])

    tp.convert! do |tp0|
      tp0.array(:int, 'j', [])
    end.must_equal('j'=>[])

    tp('a[]=1&g[]=').convert! do |tp0|
      tp0.array(:int, %w'a d g', [2])
    end.must_equal('a'=>[1], 'd'=>[2], 'g'=>[nil])
  end

  it "#convert! should handle multiple convert! calls inside" do
    tp = tp('a[b]=1&c[d]=2')
    tp.convert! do |tp0|
      tp0.convert!('a'){|d| d.int('b')}
      tp0.convert!('c'){|d| d.int('d')}
    end.must_equal('a'=>{'b'=>1}, 'c'=>{'d'=>2})

    tp.convert!(:symbolize=>true) do |tp0|
      tp0.convert!('a'){|d| d.int('b')}
      tp0.convert!('c'){|d| d.int('d')}
    end.must_equal(:a=>{:b=>1}, :c=>{:d=>2})
  end

  it "#convert! should include missing values as nil" do
    tp = tp()
    tp.convert! do |ptp|
      ptp.int('x')
      ptp.array(:int, 'y')
      ptp['c'].convert! do |stp|
        stp.int(%w'x z')
      end
    end.must_equal("x"=>nil, "y"=>nil, "c"=>{"x"=>nil, "z"=>nil})
  end

  it "#convert! with :missing=>:skip should not include missing values" do
    tp = tp()
    tp.convert!(:skip_missing=>true) do |ptp|
      ptp.int('x')
      ptp.array(:int, 'y')
      ptp['c'].convert! do |stp|
        stp.int(%w'x z')
      end
    end.must_equal("c"=>{})

    tp.convert! do |ptp|
      ptp.int('x')
      ptp.array(:int, 'y')
      ptp['c'].convert!(:skip_missing=>true) do |stp|
        stp.int(%w'x z')
      end
    end.must_equal("x"=>nil, "y"=>nil, "c"=>{})

    tp.convert!(:skip_missing=>true) do |ptp|
      ptp.int('x')
      ptp.array(:int, 'y')
      ptp['c'].convert!(:skip_missing=>false) do |stp|
        stp.int(%w'x z')
      end
    end.must_equal("c"=>{"x"=>nil, "z"=>nil})
  end

  it "#convert_each! should convert each entry in an array" do
    tp = tp('a[][b]=1&a[][c]=2&a[][b]=3&a[][c]=4')
    tp['a'].convert_each! do |tp0|
      tp0.int(%w'b c')
    end.must_equal [{'b'=>1, 'c'=>2}, {'b'=>3, 'c'=>4}]
  end

  it "#convert_each! without :keys option should convert each named entry in a hash when keys are '0'..'N'" do
    tp = tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4')
    tp['a'].convert_each! do |tp0|
      tp0.int(%w'b c')
    end.must_equal [{'b'=>1, 'c'=>2}, {'b'=>3, 'c'=>4}]
  end

  it "#convert_each! with :keys option should convert each named entry in a hash when keys are '0'..'N'" do
    tp = tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4')
    tp['a'].convert_each!(:keys=>%w'0 1') do |tp0|
      tp0.int(%w'b c')
    end.must_equal [{'b'=>1, 'c'=>2}, {'b'=>3, 'c'=>4}]
  end

  it "#convert_each! with :keys option should convert each named entry in a hash" do
    tp = tp('a[d][b]=1&a[d][c]=2&a[e][b]=3&a[e][c]=4')
    tp['a'].convert_each!(:keys=>%w'd e') do |tp0|
      tp0.int(%w'b c')
    end.must_equal [{'b'=>1, 'c'=>2}, {'b'=>3, 'c'=>4}]
  end

  it "#convert_each! with :keys option should store entries when called inside convert" do
    tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4').convert! do |tp|
      tp['a'].convert_each!(:keys=>%w'0 1') do |tp0|
        tp0.int(%w'b c')
      end
    end.must_equal("a"=>{"0"=>{'b'=>1, 'c'=>2}, "1"=>{'b'=>3, 'c'=>4}})
  end

  it "#convert_each! :keys option should accept a Proc" do
    tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4').convert! do |tp|
      tp['a'].convert_each!(:keys=>proc{|obj| obj.keys}) do |tp0|
        tp0.int(%w'b c')
      end
    end.must_equal("a"=>{"0"=>{'b'=>1, 'c'=>2}, "1"=>{'b'=>3, 'c'=>4}})
  end

  it "#convert_each! should raise if :keys option is given and not an Array/Proc/Method" do
    tp = tp('a[0][b]=1&a[0][c]=2&a[2][b]=3&a[2][c]=4')
    lambda{tp['a'].convert_each!(:keys=>Object.new){}}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError
  end

  it "#convert_each! should raise if obj is a hash without '0' keys" do
    lambda{tp.convert_each!{}}.must_raise @tp_error
  end

  it "#convert_each! should raise if obj is not a hash with '0' but not '0'..'N' keys" do
    tp = tp('a[0][b]=1&a[0][c]=2&a[2][b]=3&a[2][c]=4')
    lambda{tp['b'].convert_each!{}}.must_raise @tp_error
  end

  it "#convert_each! should raise if obj is a scalar" do
    tp = tp('a[d][b]=1&a[d][c]=2&a[e][b]=3&a[e][c]=4')
    lambda{tp['d']['b'].convert_each!{}}.must_raise @tp_error
  end

  it "#convert_each! should raise if obj is a array of non-hashes" do
    lambda{tp['b'].convert_each!{}}.must_raise @tp_error
  end

  it "#convert_each! should not include duplicate errors if there is an internal rescue" do
    tp = tp('a=')
    begin
      tp.convert! do |tp0|
        tp0['a'].convert_each!{} rescue nil; tp0['a'].convert_each!{}
      end
    rescue @tp_error => e
    end
    e.all_errors.length.must_equal 1
  end

  it "#convert_each! should not raise errors for missing keys if :raise option is false" do
    tp = tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4')
    tp['a'].convert_each!(:keys=>%w'0 2', :raise=>false) do |tp0|
      tp0.int(%w'b c')
    end.must_equal [{'b'=>1, 'c'=>2}, nil]
  end

  it "#convert! with :symbolize option should return a hash of converted parameters" do
    tp = tp()
    tp.convert!(:symbolize=>true) do |ptp|
      ptp.int!('a')
      ptp.array!(:int, 'b')
      ptp['c'].convert! do |stp|
        stp.int!(%w'd e')
      end
    end.must_equal(:a=>1, :b=>[2, 3], :c=>{:d=>4, :e=>5})
  end

  it "#convert! with :symbolize option should not persist" do
    tp = tp()
    tp.convert!(:symbolize=>true) do |ptp|
      ptp.int!('a')
      ptp.array!(:int, 'b')
      ptp['c'].convert! do |stp|
        stp.int!(%w'd e')
      end
    end.must_equal(:a=>1, :b=>[2, 3], :c=>{:d=>4, :e=>5})

    tp.convert! do |ptp|
      ptp.int!('a')
      ptp.array!(:int, 'b')
      ptp['c'].convert! do |stp|
        stp.int!(%w'd e')
      end
    end.must_equal("a"=>1, "b"=>[2, 3], "c"=>{"d"=>4, "e"=>5})
  end

  it "#convert! with :symbolize option hash should only include changes made inside block" do
    tp = tp()
    tp.convert!(:symbolize=>true) do |ptp|
      ptp.int!('a')
      ptp.array!(:int, 'b')
    end.must_equal(:a=>1, :b=>[2, 3])

    tp.convert!(:symbolize=>true) do |ptp|
      ptp['c'].convert! do |stp|
        stp.int!(%w'd e')
      end
    end.must_equal(:c=>{:d=>4, :e=>5})
  end

  it "#convert! with :symbolize option should handle deeply nested structures" do
    tp = tp('a[b][c][d][e]=1')
    tp.convert!(:symbolize=>true) do |tp0|
      tp0['a'].convert! do |tp1|
        tp1['b'].convert! do |tp2|
          tp2['c'].convert! do |tp3|
            tp3['d'].convert! do |tp4|
              tp4.int('e')
            end
          end
        end
      end
    end.must_equal(:a=>{:b=>{:c=>{:d=>{:e=>1}}}})

    tp = tp('a[][b][][e]=1')
    tp.convert!(:symbolize=>true) do |tp0|
      tp0['a'].convert! do |tp1|
        tp1[0].convert! do |tp2|
          tp2['b'].convert! do |tp3|
            tp3[0].convert! do |tp4|
              tp4.int('e')
            end
          end
        end
      end
    end.must_equal(:a=>[{:b=>[{:e=>1}]}])
  end

  it "#convert! with :symbolize option should handle #[] without #convert! at each level" do
    tp = tp('a[b][c][d][e]=1')
    tp.convert!(:symbolize=>true) do |tp0|
      tp0['a'].convert! do |tp1|
        tp1['b']['c']['d'].convert! do |tp4|
          tp4.int('e')
        end
      end
    end.must_equal(:a=>{:b=>{:c=>{:d=>{:e=>1}}}})
  end

  it "#convert! with :symbolize option should handle #[] without #convert! below" do
    tp = tp('a[b][c][d][e]=1')
    tp.convert!(:symbolize=>true) do |tp0|
      tp0['a']['b']['c']['d'].int('e')
    end.must_equal(:a=>{:b=>{:c=>{:d=>{:e=>1}}}})
  end

  it "#convert! with :symbolize option should handle multiple calls to #[] and #convert! below" do
    tp = tp('a[b]=2&a[c]=3&a[d]=4&a[e]=5&a[f]=6')
    tp.convert!(:symbolize=>true) do |tp0|
      tp0['a'].int('b')
      tp0['a'].convert! do |tp1|
        tp1.int('c')
      end
      tp0['a'].int('d')
      tp0['a'].convert! do |tp1|
        tp1.int('e')
      end
      tp0['a'].int('f')
    end.must_equal(:a=>{:b=>2, :c=>3, :d=>4, :e=>5, :f=>6})
  end

  it "#convert! with :symbolize option should handle defaults" do
    tp.convert!(:symbolize=>true) do |tp0|
      tp0.int('d', 12)
    end.must_equal(:d=>12)

    tp.convert!(:symbolize=>true) do |tp0|
      tp0.int(%w'a d', 12)
    end.must_equal(:a=>1, :d=>12)

    tp.convert!(:symbolize=>true) do |tp0|
      tp0.array(:int, 'g', [])
    end.must_equal(:g=>[nil])

    tp.convert!(:symbolize=>true) do |tp0|
      tp0.array(:int, 'j', [])
    end.must_equal(:j=>[])

    tp('a[]=1&g[]=').convert!(:symbolize=>true) do |tp0|
      tp0.array(:int, %w'a d g', [2])
    end.must_equal(:a=>[1], :d=>[2], :g=>[nil])
  end

  it "#convert_each! with :symbolize option should convert each entry in an array" do
    tp = tp('a[][b]=1&a[][c]=2&a[][b]=3&a[][c]=4')
    tp['a'].convert_each!(:symbolize=>true) do |tp0|
      tp0.int(%w'b c')
    end.must_equal [{:b=>1, :c=>2}, {:b=>3, :c=>4}]
  end

  it "#convert_each! with :symbolize and :keys options should convert each named entry in a hash" do
    tp = tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4')
    tp['a'].convert_each!(:keys=>%w'0 1', :symbolize=>true) do |tp0|
      tp0.int(%w'b c')
    end.must_equal [{:b=>1, :c=>2}, {:b=>3, :c=>4}]
  end

  it "#convert_each! with :symbolize and :keys options should store entries when called inside convert" do
    tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4').convert!(:symbolize=>true) do |tp|
      tp['a'].convert_each!(:keys=>%w'0 1') do |tp0|
        tp0.int(%w'b c')
      end
    end.must_equal(:a=>{:'0'=>{:b=>1, :c=>2}, :'1'=>{:b=>3, :c=>4}})
  end

  it "#convert_each! with :symbolize option should not persist" do
    tp = tp('a[][b]=1&a[][c]=2&a[][b]=3&a[][c]=4')
    tp['a'].convert_each!(:symbolize=>true) do |tp0|
      tp0.int(%w'b c')
    end.must_equal [{:b=>1, :c=>2}, {:b=>3, :c=>4}]

    tp = tp('a[][b]=1&a[][c]=2&a[][b]=3&a[][c]=4')
    tp['a'].convert_each! do |tp0|
      tp0.int(%w'b c')
    end.must_equal [{'b'=>1, 'c'=>2}, {'b'=>3, 'c'=>4}]
  end

  it "#convert! with :symbolize options specified at different levels should work" do
    tp = tp('a[b][c][d][e]=1')
    tp.convert!(:symbolize=>true) do |tp0|
      tp0['a'].convert!(:symbolize=>false) do |tp1|
        tp1['b'].convert!(:symbolize=>true) do |tp2|
          tp2['c'].convert!(:symbolize=>false) do |tp3|
            tp3['d'].convert!(:symbolize=>true) do |tp4|
              tp4.int('e')
            end
          end
        end
      end
    end.must_equal(:a=>{'b'=>{:c=>{'d'=>{:e=>1}}}})

    tp = tp('a[][b][][e]=1')
    tp.convert!(:symbolize=>true) do |tp0|
      tp0['a'].convert! do |tp1|
        tp1[0].convert!(:symbolize=>false) do |tp2|
          tp2['b'].convert! do |tp3|
            tp3[0].convert!(:symbolize=>true) do |tp4|
              tp4.int('e')
            end
          end
        end
      end
    end.must_equal(:a=>[{'b'=>[{:e=>1}]}])
  end

  it "#convert! should add Error if raised and not also the last capture" do
    tp = tp()
    begin
      tp.convert! do |ptp|
        raise @tp_error.create('a', 'b', ArgumentError.new('c'))
      end
    rescue @tp_error => e
    end
    e.must_be_instance_of  @tp_error
    e.all_errors.length.must_equal 1
    e.all_errors.first.message.must_equal 'ArgumentError: c'
  end

  it "#dig should return nested values or nil if there is no value" do
    tp = tp('a[b][c][d][e]=1&b=2')
    tp.dig(:int, 'a', 'b', 'c', 'd', 'e').must_equal 1
    tp.dig(:int, 'b').must_equal 2
    tp.dig(:int, 'a', 0, 'c', 'd', 'e').must_be_nil
    tp.dig(:int, 'a', 'd', 'c', 'd', 'e').must_be_nil
    tp.dig(:int, 'a', 'b', 'c', 'd', 'f').must_be_nil
    tp.dig(:int, 'c').must_be_nil
    tp.dig(:int, 'c', 'd').must_be_nil

    tp = tp('a[][c][][e]=1')
    tp.dig(:int, 'a', 0, 'c', 0, 'e').must_equal 1
    tp.dig(:int, 'a', 1, 'c', 0, 'e').must_be_nil
    tp.dig(:int, 'a', 'b', 'c', 0, 'e').must_be_nil
    tp.dig(:int, 'a', 0, 'c', 0, 'f').must_be_nil
  end

  it "#dig should raise when accessing past the end of the expected structure" do
    tp = tp('a[b][c][d][e]=1&b=2')
    lambda{tp.dig(:int, 'a', 'b', 'c', 'd', 'e', 'f')}.must_raise @tp_error
    lambda{tp.dig(:int, 'b', 'c')}.must_raise @tp_error

    tp = tp('a[][c][][e]=1')
    lambda{tp.dig(:int, 'a', 0, 'c', 0, 'e', 'f')}.must_raise @tp_error
  end

  it "#dig and #dig! should handle array keys" do
    tp('a[b][c][d][e]=1&a[b][c][d][f]=2').dig(:int, 'a', 'b', 'c', 'd', %w'e f').must_equal [1, 2]
    tp('a[b][c][d][e]=1&a[b][c][d][f]=').dig(:int, 'a', 'b', 'c', 'd', %w'e f').must_equal [1, nil]

    tp('a[b][c][d][e]=1&a[b][c][d][f]=2').dig!(:int, 'a', 'b', 'c', 'd', %w'e f').must_equal [1, 2]
    lambda{tp('a[b][c][d][e]=1&a[b][c][d][f]=').dig!(:int, 'a', 'b', 'c', 'd', %w'e f')}.must_raise @tp_error
  end

  it "#dig and #dig! should be able to handle arrays using an array for the type" do
    tp('a[b][c][d][]=1&a[b][c][d][]=2').dig(:array, :int, 'a', 'b', 'c', 'd').must_equal [1, 2]
    tp('a[b][c][d][]=1&a[b][c][d][]=').dig(:array, :int, 'a', 'b', 'c', 'd').must_equal [1, nil]

    tp('a[b][c][d][]=1&a[b][c][d][]=2').dig!(:array!, :int, 'a', 'b', 'c', 'd').must_equal [1, 2]
    lambda{tp('a[b][c][d][]=1&a[b][c][d][]=').dig!(:array!, :int, 'a', 'b', 'c', 'd')}.must_raise @tp_error
  end

  it "#dig should raise for unsupported types" do
    lambda{tp.dig(:foo, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError
  end

  it "#dig should raise for array without subtype" do
    lambda{tp.dig(:array, 'foo', 'a')}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError
  end


  it "#dig should raise for unsupported nest values" do
    lambda{tp.dig(:int, :foo, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError
    lambda{tp.dig(:array, :int, :foo, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError
  end

  it "#dig! should return nested values or raise Error if thers is no value" do
    tp = tp('a[b][c][d][e]=1&b=2')
    tp.dig!(:int, 'a', 'b', 'c', 'd', 'e').must_equal 1
    tp.dig!(:int, 'b').must_equal 2
    lambda{tp.dig!(:int, 'a', 'd', 'c', 'd', 'e')}.must_raise @tp_error
    lambda{tp.dig!(:int, 'a', 'b', 'c', 'd', 'f')}.must_raise @tp_error
    lambda{tp.dig!(:int, 'a', 0, 'c', 'd', 'f')}.must_raise @tp_error
    lambda{tp.dig!(:int, 'c')}.must_raise @tp_error
    lambda{tp.dig!(:int, 'b', 'c')}.must_raise @tp_error
    lambda{tp.dig!(:int, 'c', 'd')}.must_raise @tp_error
    error{tp.dig!(:int, 'a', 'd', 'c', 'd', 'e')}.keys.must_equal %w'a d'
    error{tp.dig!(:int, 'a', 'b', 'c', 'e', 'e')}.keys.must_equal %w'a b c e'

    tp = tp('a[][c][][e]=1')
    tp.dig!(:int, 'a', 0, 'c', 0, 'e').must_equal 1
    lambda{tp.dig!(:int, 'a', 1, 'c', 0, 'e')}.must_raise @tp_error
    lambda{tp.dig!(:int, 'a', 'b', 'c', 0, 'e')}.must_raise @tp_error
    lambda{tp.dig!(:int, 'a', 0, 'c', 0, 'f')}.must_raise @tp_error
  end

  it "#convert! should work with dig" do
    tp('a[b][c][d][e]=1').convert! do |tp|
      tp.dig(:int, 'a', 'b', 'c', 'd', 'e')
    end.must_equal('a'=>{'b'=>{'c'=>{'d'=>{'e'=>1}}}})
  end

  it "#convert! with :symbolize option should work with dig" do
    tp('a[b][c][d][e]=1').convert!(:symbolize=>true) do |tp|
      tp.dig(:int, 'a', 'b', 'c', 'd', 'e')
    end.must_equal(:a=>{:b=>{:c=>{:d=>{:e=>1}}}})
  end

  it "#fetch should be the same as #[] if the key is present" do
    tp.fetch('c').int('d').must_equal 4
  end

  it "#fetch should return nil if the key is not present and no block is given" do
    tp.fetch('d').must_be_nil
  end

  it "#fetch should call the block if the key is not present and a block is given" do
    tp.fetch('d'){1}.must_equal 1
  end

  it "Error#keys should be a path to the error" do
    error{tp.int!('b')}.keys.must_equal ['b']
    error{tp.int!(%w'b f')}.keys.must_equal ['b']
    error{tp['c'].int!('f')}.keys.must_equal ['c', 'f']
    error{tp('a[b][c][d][e]=1')['a']['b']['c']['d'].date('e')}.keys.must_equal %w'a b c d e'
  end

  it "Error#param_name should be the name of the parameter" do
    error{tp.int!('b')}.param_name.must_equal 'b'
    error{tp.int!(%w'b f')}.param_name.must_equal 'b'
    error{tp['c'].int!('f')}.param_name.must_equal 'c[f]'
    error{tp('a[b][c][d][e]=1')['a']['b']['c']['d'].date('e')}.param_name.must_equal 'a[b][c][d][e]'
    error{tp('a[][c][][e]=1')['a'][0]['c'][0].date('e')}.param_name.must_equal 'a[][c][][e]'
    error{tp('a[][c][][e]=1').dig(:date, 'a', 0, 'c', 0, 'e')}.param_name.must_equal 'a[][c][][e]'
  end

  it "Error#param_names and #reason should be correct for errors" do
    e = error{tp.int!('b')}
    e.param_names.must_equal ['b']
    e.reason.must_equal :int

    e = error{tp.int!(%w'b f')}
    e.param_names.must_equal ['b']
    e.reason.must_equal :int

    e = error{tp['c'].int!('f')}
    e.param_names.must_equal ['c[f]']
    e.reason.must_equal :missing

    e = error{tp('a[b][c][d][e]=1')['a']['b']['c']['d'].date('e')}
    e.param_names.must_equal ['a[b][c][d][e]']
    e.reason.must_equal :date

    e = error{tp('a[][c][][e]=1')['a'][0]['c'][0].date('e')}
    e.param_names.must_equal ['a[][c][][e]']
    e.reason.must_equal :date

    e = error{tp('a[][c][][e]=1').dig(:date, 'a', 0, 'c', 0, 'e')}
    e.param_names.must_equal ['a[][c][][e]']
    e.reason.must_equal :date

    e = error{tp('a[][c][][e]=1').dig!(:date, 'a', 1, 'c', 0, 'e')}
    e.param_names.must_equal ['a[]']
    e.reason.must_equal :missing

    e = error{tp('a[][c][][e]=1').dig!(:date, 'a', 'b', 'c', 0, 'e')}
    e.param_names.must_equal ['a[b]']
    e.reason.must_equal :invalid_type
  end

  it "Error#param_names and #all_errors should handle array submission" do
    tp = tp('a[][b]=0')
    e = error do 
      tp.convert!('a') do |tp0|
        tp0.int(%w'a b c')
        tp0.array(:int, %w'a b c')
      end
    end
    e.param_names.must_equal %w'a'
    e.all_errors.map(&:reason).must_equal [:invalid_type]
  end

  it "Error#param_names and #all_errors should include all errors raised in convert! blocks" do
    tp = tp('a[][b][][e]=0')
    e = error do 
      tp.convert! do |tp0|
        tp0['a'].convert! do |tp1|
          tp1[0].convert! do |tp2|
            tp2['b'].convert! do |tp3|
              tp3[0].convert! do |tp4|
                tp4.pos_int!('e')
              end
            end
          end
        end
        tp0.dig!(:pos_int, 'a', 0, 'b', 0, %w'f g')
        tp0.dig!(:pos_int, 'a', 0, 'b')
        tp0.int!('c')
        tp0.array!(:int, %w'd e')
        tp0['b']
      end
    end
    e.param_names.must_equal %w'a[][b][][e] a[][b][][f] a[][b][][g] a[][b] c d e b'
    e.all_errors.map(&:reason).must_equal [:missing, :missing, :missing, :pos_int, :missing, :missing, :missing, :missing]
  end

  it "Error#param_names and #all_errors should handle #[] failures by skipping the rest of the block" do
    tp = tp('a[][b][][e]=0')
    e = error do 
      tp.convert! do |tp0|
        tp0['b']
        tp0.int!('c')
      end
    end
    e.param_names.must_equal %w'b'
    e.all_errors.map(&:reason).must_equal [:missing]

    e = error do 
      tp.convert! do |tp0|
        tp0['a'][0].convert! do |tp1|
          tp1['c']
          tp1.int!('d')
        end
        tp0.int!('c')
      end
    end
    e.param_names.must_equal %w'a[][c] c'
    e.all_errors.map(&:reason).must_equal [:missing, :missing]
  end

  it "Error#param_names and #all_errorsshould handle array! with array of keys where one of the keys is not present" do
    e = error do
      tp('e[]=0').convert! do |tp0|
        tp0.array!(:pos_int, %w'd e')
      end
    end
    e.param_names.must_equal %w'd e'
    e.all_errors.map(&:reason).must_equal [:missing, :invalid_type]
  end

  it "Error#param_names and #all_errors should handle keys given to convert" do
    tp = tp('e[][b][][e]=0')
    e = error do 
      tp.convert! do |tp0|
        tp0.convert!(['a', 0, 'b', 0]) do |tp1|
          tp1.pos_int!('e')
        end
        tp0.convert!('f') do |tp1|
          tp1.dig!(:pos_int, 0, 'b', 0, %w'f g')
        end
        tp0.dig!(:pos_int, 'e', 0, 'b')
        tp0.int!('c')
        tp0.array!(:int, 'd')
      end
    end
    e.param_names.must_equal %w'a f e[][b] c d'
    e.all_errors.map(&:reason).must_equal [:missing, :missing, :pos_int, :missing, :missing]
  end

  it "Error#param_names and #all_errors should include all errors raised in convert_each! blocks" do
    e = error do 
      tp('a[][b]=0&a[][b]=1')['a'].convert_each! do |tp0|
        tp0.dig!(:pos_int, 'b', 0, 'e')
        tp0.dig!(:int, 'b', 0, %w'f g')
        tp0.int!(%w'd e')
        tp0.pos_int!('b')
        tp0['c']
      end
    end
    e.param_names.must_equal %w'a[][b] a[][b] a[][d] a[][e] a[][b] a[][c] a[][b] a[][b] a[][d] a[][e] a[][c]'
    e.all_errors.map(&:reason).must_equal [:invalid_type, :invalid_type, :missing, :missing, :missing, :missing, :invalid_type, :invalid_type, :missing, :missing, :missing]
  end

  it "Error#param_names and #all_errors should include all errors for invalid keys used in convert_each!" do
    tp = tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4')
    e = error do
      tp['a'].convert_each!(:keys=>%w'0 2 3') do |tp0|
        tp0.int(%w'b c')
      end
    end
    e.param_names.must_equal %w'a[2] a[3]'
    e.all_errors.map(&:reason).must_equal [:missing, :missing]
  end

  it "Error.create should handle existing errors with a backtrace" do
    e = ArgumentError.new('a')
    e.set_backtrace(nil)
    e = @tp_error.create('a', 'b', e)
    e.must_be_instance_of @tp_error
    e.message.must_equal "ArgumentError: a"
    e.keys.must_equal 'a'
    e.reason.must_equal 'b'
    e.backtrace.must_be_nil
  end

  it "should raise error invalid params format" do
    proc{Roda::RodaPlugins::TypecastParams::Params.new([]).int('a')}.must_raise @tp_error
  end
end

describe "typecast_params plugin with customized params" do 
  def tp(arg='a=1&b[]=2&b[]=3&c[d]=4&c[e]=5&f=&g[]=&h[i]=')
    @tp.call(arg)
  end

  before do
    res = nil
    app(:bare) do
      plugin :typecast_params do
        handle_type(:opp_int) do |v|
          -v.to_i
        end
      end
      plugin :typecast_params do
        handle_type(:double) do |v|
          v*2
        end
      end

      route do |r|
        res = typecast_params
        nil
      end
    end

    @tp = lambda do |params|
      req('QUERY_STRING'=>params, 'rack.input'=>StringIO.new)
      res
    end

    @tp_error = Roda::RodaPlugins::TypecastParams::Error
  end

  it "should not allow typecast params changes after freezing the app" do
    app.freeze
    lambda{app::TypecastParams.handle_type(:foo){|v| v}}.must_raise RuntimeError
  end

  it "should pass through non-ArgumentError exceptions raised by conversion blocks" do
    app::TypecastParams.handle_type(:foo){|v| raise}
    lambda{tp.foo('a')}.must_raise RuntimeError
  end

  it "should respect custom typecasting methods" do
    tp.opp_int('a').must_equal(-1)
    tp.opp_int!('a').must_equal(-1)
    tp.opp_int('d').must_be_nil
    lambda{tp.opp_int!('d')}.must_raise @tp_error

    tp.array(:opp_int, 'b').must_equal [-2, -3]
    tp.array!(:opp_int, 'b').must_equal [-2, -3]

    tp.double('a').must_equal '11'
    tp.double!('a').must_equal '11'
    tp.double('d').must_be_nil
    lambda{tp.double!('d')}.must_raise @tp_error

    tp.array(:double, 'b').must_equal ['22', '33']
    tp.array!(:double, 'b').must_equal ['22', '33']
  end

  it "should respect custom typecasting methods when subclassing" do
    @app = Class.new(@app)
    @app.plugin :typecast_params do
      handle_type :triple do |v|
        v * 3
      end
    end

    tp.opp_int('a').must_equal(-1)
    tp.opp_int!('a').must_equal(-1)
    tp.opp_int('d').must_be_nil
    lambda{tp.opp_int!('d')}.must_raise @tp_error

    tp.array(:opp_int, 'b').must_equal [-2, -3]
    tp.array!(:opp_int, 'b').must_equal [-2, -3]

    tp.double('a').must_equal '11'
    tp.double!('a').must_equal '11'
    tp.double('d').must_be_nil
    lambda{tp.double!('d')}.must_raise @tp_error

    tp.array(:double, 'b').must_equal ['22', '33']
    tp.array!(:double, 'b').must_equal ['22', '33']

    tp.triple('a').must_equal '111'
    tp.triple!('a').must_equal '111'
    tp.triple('d').must_be_nil
    lambda{tp.triple!('d')}.must_raise @tp_error

    tp.array(:triple, 'b').must_equal ['222', '333']
    tp.array!(:triple, 'b').must_equal ['222', '333']
  end
end

describe "typecast_params plugin with files" do 
  def tp
    @tp.call
  end

  before do
    tempfile = @tempfile = Tempfile.new(['roda_typecast_params_spec', '.txt'])
    tempfile.write('tp_spec')
    tempfile.rewind
    res = nil
    app(:typecast_params) do |r|
      res = typecast_params
      nil
    end
    app::RodaRequest.send(:define_method, :params) do
      {'testfile'=>{:tempfile=>tempfile}, 'testfile2'=>{:tempfile=>tempfile},
       'testfile_array'=>[{:tempfile=>tempfile}, {:tempfile=>tempfile}],
       'a'=>{'b'=>'c', 'tempfile'=>'f'},
       'c'=>['']}
    end

    @tp = lambda do
      req
      res
    end

    @tp_error = Roda::RodaPlugins::TypecastParams::Error
  end

  it "#file should require an uploaded file" do
    tp.file('testfile').must_equal(:tempfile=>@tempfile)
    tp.file(%w'testfile testfile2').must_equal [{:tempfile=>@tempfile}, {:tempfile=>@tempfile}]

    lambda{tp.file('a')}.must_raise @tp_error
    tp.file('b').must_be_nil
    lambda{tp.file('c')}.must_raise @tp_error

    tp.file!('testfile').must_equal(:tempfile=>@tempfile)
    tp.file!(%w'testfile testfile2').must_equal [{:tempfile=>@tempfile}, {:tempfile=>@tempfile}]
    lambda{tp.file!('a')}.must_raise @tp_error
    lambda{tp.file!('b')}.must_raise @tp_error
    lambda{tp.file!('c')}.must_raise @tp_error

    tp.array(:file, 'testfile_array').must_equal [{:tempfile=>@tempfile}, {:tempfile=>@tempfile}]
    lambda{tp.array(:file, 'testfile')}.must_raise @tp_error
    lambda{tp.array(:file, 'a')}.must_raise @tp_error
    tp.array(:file, 'b').must_be_nil
    lambda{tp.array(:file, 'c')}.must_raise @tp_error

    tp.array!(:file, 'testfile_array').must_equal [{:tempfile=>@tempfile}, {:tempfile=>@tempfile}]
    lambda{tp.array!(:file, 'testfile')}.must_raise @tp_error
    lambda{tp.array!(:file, 'a')}.must_raise @tp_error
    lambda{tp.array!(:file, 'b')}.must_raise @tp_error
    lambda{tp.array!(:file, 'c')}.must_raise @tp_error
  end
end

describe "typecast_params plugin with strip: :all option" do 
  def tp(arg='a=+1+')
    @tp.call(arg)
  end


  before do
    res = nil
    app(:bare) do
      plugin :typecast_params, strip: :all
      route do |r|
        res = typecast_params
        nil
      end
    end

    @tp = lambda do |params|
      req('QUERY_STRING'=>params, 'rack.input'=>StringIO.new)
      res
    end

    @tp_error = Roda::RodaPlugins::TypecastParams::Error
  end

  it "#should strip String values" do
    tp.str('a').must_equal '1'
    tp.nonempty_str('a').must_equal '1'
    tp.int('a').must_equal 1
    tp.pos_int('a').must_equal 1
    tp.Integer('a').must_equal 1
    tp.float('a').must_equal 1.0
    tp.Float('a').must_equal 1.0
  end

  it "#should not attempt to strip non-String values" do
    @tp.call("a[]=1").any('a').must_equal ["1"]
  end
end
