# frozen_string_literal: true
require_relative "spec_helper"

describe "Sequel::Plugins::ValidationHelpers" do
  before do
    @c = Class.new(Sequel::Model) do
      def self.set_validations(&block)
        define_method(:validate, &block)
        alias_method(:validate, :validate)
      end
      columns :value
    end
    @c.plugin :validation_helpers
    @m = @c.new
  end

  it "should take an :allow_blank option" do
    @c.set_validations{validates_format(/.+_.+/, :value, :allow_blank=>true)}
    @m.value = 'abc_'
    @m.wont_be :valid?
    @m.value = '1_1'
    @m.must_be :valid?
    o = String.new
    o.singleton_class.send(:undef_method, :blank?)
    @m.value = o
    @m.must_be :valid?
    o = Object.new
    @m.value = o
    @m.wont_be :valid?
    def o.blank?
      true
    end
    @m.must_be :valid?
  end

  it "should take an :allow_missing option" do
    @c.set_validations{validates_format(/.+_.+/, :value, :allow_missing=>true)}
    @m.values.clear
    @m.must_be :valid?
    @m.value = nil
    @m.wont_be :valid?
    @m.value = '1_1'
    @m.must_be :valid?
  end

  it "should take an :allow_nil option" do
    @c.set_validations{validates_format(/.+_.+/, :value, :allow_nil=>true)}
    @m.value = 'abc_'
    @m.wont_be :valid?
    @m.value = '1_1'
    @m.must_be :valid?
    @m.value = nil
    @m.must_be :valid?
  end

  it "should take a :message option" do
    @c.set_validations{validates_format(/.+_.+/, :value, :message=>"is so blah")}
    @m.value = 'abc_'
    @m.wont_be :valid?
    @m.errors.full_messages.must_equal ['value is so blah']
    @m.value = '1_1'
    @m.must_be :valid?
  end
  
  it "should take a :from=>:values option to lookup in values hash" do
    @c.set_validations{validates_max_length(50, :value, :from=>:values)}
    @c.send(:define_method, :value){super() * 2}
    @m.value = ' ' * 26
    @m.must_be :valid?
  end
  
  it "should take an :skip_invalid option to not validate a column that already has an error" do
    @c.set_validations{validates_not_null(:value); validates_not_null(:value, :skip_invalid=>true)}
    @m.wont_be :valid?
    @m.errors.on(:value).must_equal ['is not present']
  end

  it "should add validation errors even if columns that already have an error" do
    @c.set_validations{validates_not_null(:value); validates_not_null(:value)}
    @m.wont_be :valid?
    @m.errors.on(:value).must_equal ['is not present', 'is not present']
  end

  it "should allow a proc for the :message option" do
    @c.set_validations{validates_format(/.+_.+/, :value, :message=>proc{|f| "doesn't match #{f.inspect}"})}
    @m.value = 'abc_'
    @m.wont_be :valid?
    @m.errors.must_equal(:value=>["doesn't match /.+_.+/"])
  end

  it "should take multiple attributes in the same call" do
    @c.columns :value, :value2
    @c.set_validations{validates_presence([:value, :value2])}
    @m.wont_be :valid?
    @m.value = 1
    @m.wont_be :valid?
    @m.value2 = 1
    @m.must_be :valid?
  end
  
  it "should support modifying default validation options for a particular model" do
    @c.set_validations{validates_presence(:value)}
    @m.wont_be :valid?
    @m.errors.must_equal(:value=>['is not present'])
    @c.class_eval do
      private
      def default_validation_helpers_options(type)
        {:allow_missing=>true, :message=>proc{'was not entered'}}
      end
    end
    @m.value = nil
    @m.wont_be :valid?
    @m.errors.must_equal(:value=>["was not entered"])
    @m.value = 1
    @m.must_be :valid?
    
    @m.values.clear
    @m.must_be :valid?
    
    c = Class.new(Sequel::Model)
    c.class_eval do
      plugin :validation_helpers
      attr_accessor :value
      def validate
        validates_presence(:value)
      end
    end
    m = c.new
    m.wont_be :valid?
    m.errors.must_equal(:value=>['is not present'])
  end

  it "should support validates_exact_length" do
    @c.set_validations{validates_exact_length(3, :value)}
    @m.wont_be :valid?
    @m.value = '123'
    @m.must_be :valid?
    @m.value = '12'
    @m.wont_be :valid?
    @m.value = '1234'
    @m.wont_be :valid?
  end
  
  it "should support validate_format" do
    @c.set_validations{validates_format(/.+_.+/, :value)}
    @m.value = 'abc_'
    @m.wont_be :valid?
    @m.value = 'abc_def'
    @m.must_be :valid?
  end
  
  it "should support validates_includes with an array" do
    @c.set_validations{validates_includes([1,2], :value)}
    @m.wont_be :valid?
    @m.value = 1
    @m.must_be :valid?
    @m.value = 1.5
    @m.wont_be :valid?
    @m.value = 2
    @m.must_be :valid?    
    @m.value = 3
    @m.wont_be :valid? 
  end
  
  it "should support validates_includes with a range" do
    @c.set_validations{validates_includes(1..4, :value)}
    @m.wont_be :valid?
    @m.value = 1
    @m.must_be :valid?
    @m.value = 1.5
    @m.must_be :valid?
    @m.value = 0
    @m.wont_be :valid?
    @m.value = 5
    @m.wont_be :valid?    
  end
  
  it "should supports validates_integer" do
    @c.set_validations{validates_integer(:value)}
    @m.value = 'blah'
    @m.wont_be :valid?
    @m.value = '123'
    @m.must_be :valid?
    @m.value = '123.1231'
    @m.wont_be :valid?
  end
  
  it "should support validates_length_range" do
    @c.set_validations{validates_length_range(2..5, :value)}
    @m.wont_be :valid?
    @m.value = '12345'
    @m.must_be :valid?
    @m.value = '1'
    @m.wont_be :valid?
    @m.value = '123456'
    @m.wont_be :valid?
  end

  it "should support validates_max_length" do
    @c.set_validations{validates_max_length(5, :value)}
    @m.wont_be :valid?
    @m.value = '12345'
    @m.must_be :valid?
    @m.value = '123456'
    @m.wont_be :valid?
    @m.errors[:value].must_equal ['is longer than 5 characters']
    @m.value = nil
    @m.wont_be :valid?
    @m.errors[:value].must_equal ['is not present']
  end

  it "should support validates_max_length with nil value" do
    @c.set_validations{validates_max_length(5, :value, :message=>'tl', :nil_message=>'np')}
    @m.value = '123456'
    @m.wont_be :valid?
    @m.errors[:value].must_equal ['tl']
    @m.value = nil
    @m.wont_be :valid?
    @m.errors[:value].must_equal ['np']
  end

  it "should support validates_min_length" do
    @c.set_validations{validates_min_length(5, :value)}
    @m.wont_be :valid?
    @m.value = '12345'
    @m.must_be :valid?
    @m.value = '1234'
    @m.wont_be :valid?
  end

  it "should support validates_max_value" do
    @c.set_validations{validates_max_value(5, :value)}
    @m.must_be :valid?

    @m.value = 6
    @m.wont_be :valid?
    @m.errors[:value].must_equal ['is greater than maximum allowed value']

    @m.value = 5
    @m.must_be :valid?
  end

  it "should support validates_min_value" do
    @c.set_validations{validates_min_value(7, :value)}
    @m.must_be :valid?

    @m.value = 6
    @m.wont_be :valid?
    @m.errors[:value].must_equal ['is less than minimum allowed value']

    @m.value = 7
    @m.must_be :valid?
  end

  it "should support validates_schema_types" do
    @c.set_validations{validates_schema_types}
    @m.value = 123
    @m.must_be :valid?
    @m.value = '123'
    @m.must_be :valid?
    def @m.db_schema; {:value=>{:type=>:integer}} end
    @m.singleton_class.send(:alias_method, :db_schema, :db_schema)
    @m.wont_be :valid?
    @m.errors.full_messages.must_equal ['value is not a valid integer']

    @c.set_validations{validates_schema_types(:value)}
    def @m.db_schema; {:value=>{:type=>:integer}} end
    @m.singleton_class.send(:alias_method, :db_schema, :db_schema)
    @m.wont_be :valid?
    @m.errors.full_messages.must_equal ['value is not a valid integer']

    @c.set_validations{validates_schema_types(:value, :message=>'is bad')}
    def @m.db_schema; {:value=>{:type=>:integer}} end
    @m.wont_be :valid?
    @m.errors.full_messages.must_equal ['value is bad']
  end

  it "should support validates_numeric" do
    @c.set_validations{validates_numeric(:value)}
    @m.value = 'blah'
    @m.wont_be :valid?
    @m.value = '123'
    @m.must_be :valid?
    @m.value = '123.1231'
    @m.must_be :valid?
    @m.value = '+1'
    @m.must_be :valid?
    @m.value = '-1'
    @m.must_be :valid?
    @m.value = '+1.123'
    @m.must_be :valid?
    @m.value = '-0.123'
    @m.must_be :valid?
    @m.value = '-0.123E10'
    @m.must_be :valid?
    @m.value = '32.123e10'
    @m.must_be :valid?
    @m.value = '+32.123E10'
    @m.must_be :valid?
    @m.must_be :valid?
    @m.value = '.0123'
  end
  
  it "should support validates_type" do
    @c.set_validations{validates_type(Integer, :value)}
    @m.value = 123
    @m.must_be :valid?
    @m.value = '123'
    @m.wont_be :valid?
    @m.errors.full_messages.must_equal ['value is not a valid integer']
    
    @c.set_validations{validates_type(:String, :value)}
    @m.value = '123'
    @m.must_be :valid?
    @m.value = 123
    @m.wont_be :valid?
    @m.errors.full_messages.must_equal ['value is not a valid string']
    
    @c.set_validations{validates_type('Integer', :value)}
    @m.value = 123
    @m.must_be :valid?
    @m.value = 123.05
    @m.wont_be :valid?
    @m.errors.full_messages.must_equal ['value is not a valid integer']
    
    @c.set_validations{validates_type(Integer, :value)}
    @m.value = 1
    @m.must_be :valid?
    @m.value = false
    @m.wont_be :valid?
    
    @c.set_validations{validates_type([Integer, Float], :value)}
    @m.value = 1
    @m.must_be :valid?
    @m.value = 1.0
    @m.must_be :valid?
    @m.value = BigDecimal('1.0')
    @m.wont_be :valid?
    @m.errors.full_messages.must_equal ['value is not a valid integer or float']
  end

  it "should support validates_not_null" do
    @c.set_validations{validates_not_null(:value)}
    @m.wont_be :valid?
    @m.value = ''
    @m.must_be :valid?
    @m.value = 1234
    @m.must_be :valid?
    @m.value = nil
    @m.wont_be :valid?
    @m.value = true
    @m.must_be :valid?
    @m.value = false
    @m.must_be :valid?
    @m.value = Time.now
    @m.must_be :valid?
  end
  
  it "should support validates_no_null_byte" do
    @c.set_validations{validates_no_null_byte(:value)}
    @m.must_be :valid?
    @m.value = ''
    @m.must_be :valid?
    @m.value = 1234
    @m.must_be :valid?
    @m.value = "asdfasl\0asdf"
    @m.wont_be :valid?
    @m.value = true
    @m.must_be :valid?
    @m.value = false
    @m.must_be :valid?
    @m.value = Time.now
    @m.must_be :valid?
  end
  
  it "should support validates_presence" do
    @c.set_validations{validates_presence(:value)}
    @m.wont_be :valid?
    @m.value = ''
    @m.wont_be :valid?
    @m.value = 1234
    @m.must_be :valid?
    @m.value = nil
    @m.wont_be :valid?
    @m.value = true
    @m.must_be :valid?
    @m.value = false
    @m.must_be :valid?
    @m.value = Time.now
    @m.must_be :valid?
  end
  
  it "should support validates_unique with a single attribute" do
    @c.columns(:id, :username, :password)
    @c.set_dataset DB[:items]
    @c.set_validations{validates_unique(:username, :only_if_modified=>false)}
    @c.dataset = @c.dataset.with_fetch(proc do |sql|
      case sql
      when /1 AS one.*username = '0records'/
        {:v => nil}
      when /1 AS one.*username = '1record'/
        {:v => 1}
      end
    end)
    
    @user = @c.new(:username => "0records", :password => "anothertest")
    @user.must_be :valid?
    @user = @c.load(:id=>3, :username => "0records", :password => "anothertest")
    @user.must_be :valid?

    DB.sqls
    @user = @c.new(:password => "anothertest")
    @user.must_be :valid?
    DB.sqls.must_equal []

    @user = @c.new(:username => "1record", :password => "anothertest")
    @user.wont_be :valid?
    @user.errors.full_messages.must_equal ['username is already taken']
    @user = @c.load(:id=>4, :username => "1record", :password => "anothertest")
    @user.wont_be :valid?
    @user.errors.full_messages.must_equal ['username is already taken']

    @user = @c.load(:id=>1, :username => "0records", :password => "anothertest")
    @user.must_be :valid?
    DB.sqls.last.must_equal "SELECT 1 AS one FROM items WHERE ((username = '0records') AND (id != 1)) LIMIT 1"
    @user = @c.new(:username => "0records", :password => "anothertest")
    @user.must_be :valid?
    DB.sqls.last.must_equal "SELECT 1 AS one FROM items WHERE (username = '0records') LIMIT 1"
  end
  
  it "should support validates_unique with multiple attributes" do
    @c.columns(:id, :username, :password)
    @c.set_dataset DB[:items]
    @c.set_validations{validates_unique([:username, :password], :only_if_modified=>false)}
    @c.dataset = @c.dataset.with_fetch(proc do |sql|
      case sql
      when /1 AS one.*username = '0records'/
        {:v => nil}
      when /1 AS one.*username = '1record'/
        {:v => 1}
      end
    end)
    
    @user = @c.new(:username => "0records", :password => "anothertest")
    @user.must_be :valid?
    @user = @c.load(:id=>3, :username => "0records", :password => "anothertest")
    @user.must_be :valid?

    DB.sqls
    @user = @c.new(:password => "anothertest")
    @user.must_be :valid?
    @user.errors.full_messages.must_equal []
    @user = @c.new(:username => "0records")
    @user.must_be :valid?
    @user.errors.full_messages.must_equal []
    @user = @c.new
    @user.must_be :valid?
    @user.errors.full_messages.must_equal []
    DB.sqls.must_equal []

    @user = @c.new(:username => "1record", :password => "anothertest")
    @user.wont_be :valid?
    @user.errors.full_messages.must_equal ['username and password is already taken']
    @user = @c.load(:id=>4, :username => "1record", :password => "anothertest")
    @user.wont_be :valid?
    @user.errors.full_messages.must_equal ['username and password is already taken']

    @user = @c.load(:id=>1, :username => "0records", :password => "anothertest")
    @user.must_be :valid?
    DB.sqls.last.must_equal "SELECT 1 AS one FROM items WHERE ((username = '0records') AND (password = 'anothertest') AND (id != 1)) LIMIT 1"
    @user = @c.new(:username => "0records", :password => "anothertest")
    @user.must_be :valid?
    DB.sqls.last.must_equal "SELECT 1 AS one FROM items WHERE ((username = '0records') AND (password = 'anothertest')) LIMIT 1"
  end

  it "should support validates_unique with a block" do
    @c.columns(:id, :username, :password)
    @c.set_dataset DB[:items]
    @c.set_validations{validates_unique(:username, :only_if_modified=>false){|ds| ds.filter(:active)}}
    @c.dataset = @c.dataset.with_fetch(:v=>nil)
    
    DB.reset
    @c.new(:username => "0records", :password => "anothertest").must_be :valid?
    @c.load(:id=>3, :username => "0records", :password => "anothertest").must_be :valid?
    DB.sqls.must_equal ["SELECT 1 AS one FROM items WHERE ((username = '0records') AND active) LIMIT 1",
                    "SELECT 1 AS one FROM items WHERE ((username = '0records') AND active AND (id != 3)) LIMIT 1"]
  end

  it "should support validates_unique with :where option" do
    @c.columns(:id, :username, :password)
    @c.set_dataset DB[:items]
    @c.set_validations{validates_unique(:username, :only_if_modified=>false, :where=>proc{|ds, obj, cols| ds.where(cols.map{|c| [Sequel.function(:lower, c), obj.send(c).downcase]})})}
    @c.dataset = @c.dataset.with_fetch(:v=>nil)
      
    DB.reset
    @c.new(:username => "0RECORDS", :password => "anothertest").must_be :valid?
    @c.load(:id=>3, :username => "0RECORDS", :password => "anothertest").must_be :valid?
    DB.sqls.must_equal ["SELECT 1 AS one FROM items WHERE (lower(username) = '0records') LIMIT 1",
                    "SELECT 1 AS one FROM items WHERE ((lower(username) = '0records') AND (id != 3)) LIMIT 1"]
  end

  it "should support validates_unique with :dataset option" do
    @c.columns(:id, :username, :password)
    @c.set_dataset DB[:items]
    c = @c
    @c.set_validations{validates_unique(:username, :only_if_modified=>false, :dataset=>c.where(:a=>[1,2,3]))}
    @c.dataset = @c.dataset.with_fetch(:v=>nil)
    
    DB.reset
    @c.new(:username => "0records", :password => "anothertest").must_be :valid?
    @c.load(:id=>3, :username => "0records", :password => "anothertest").must_be :valid?
    DB.sqls.must_equal ["SELECT 1 AS one FROM items WHERE ((a IN (1, 2, 3)) AND (username = '0records')) LIMIT 1",
                    "SELECT 1 AS one FROM items WHERE ((a IN (1, 2, 3)) AND (username = '0records') AND (id != 3)) LIMIT 1"]
  end

  it "should use qualified primary keys for validates_unique when the dataset is joined" do
    @c.columns(:id, :username, :password)
    @c.set_dataset DB[:items]
    c = @c
    @c.set_validations{validates_unique(:username, :only_if_modified=>false, :dataset=>c.cross_join(:a))}
    @c.dataset = @c.dataset.with_fetch(:v=>nil)
    
    DB.reset
    @c.new(:username => "0records", :password => "anothertest").must_be :valid?
    @c.load(:id=>3, :username => "0records", :password => "anothertest").must_be :valid?
    DB.sqls.must_equal ["SELECT 1 AS one FROM items CROSS JOIN a WHERE (username = '0records') LIMIT 1",
                    "SELECT 1 AS one FROM items CROSS JOIN a WHERE ((username = '0records') AND (items.id != 3)) LIMIT 1"]
  end

  it "should not have validates_unique check uniqueness for existing records if values haven't changed" do
    @c.columns(:id, :username, :password)
    @c.set_dataset DB[:items]
    @c.set_validations{validates_unique([:username, :password])}
    @c.dataset = @c.dataset.with_fetch(:v=>nil)
    
    DB.reset
    @c.new(:username => "0records", :password => "anothertest").must_be :valid?
    DB.sqls.must_equal ["SELECT 1 AS one FROM items WHERE ((username = '0records') AND (password = 'anothertest')) LIMIT 1"]
    DB.reset
    m = @c.load(:id=>3, :username => "0records", :password => "anothertest")
    m.must_be :valid?
    DB.sqls.must_equal []

    m.username = '1'
    m.must_be :valid?
    DB.sqls.must_equal ["SELECT 1 AS one FROM items WHERE ((username = '1') AND (password = 'anothertest') AND (id != 3)) LIMIT 1"]

    m = @c.load(:id=>3, :username => "0records", :password => "anothertest")
    DB.reset
    m.password = '1'
    m.must_be :valid?
    DB.sqls.must_equal ["SELECT 1 AS one FROM items WHERE ((username = '0records') AND (password = '1') AND (id != 3)) LIMIT 1"]
    DB.reset
    m.username = '2'
    m.must_be :valid?
    DB.sqls.must_equal ["SELECT 1 AS one FROM items WHERE ((username = '2') AND (password = '1') AND (id != 3)) LIMIT 1"]
  end

  it "should not attempt a database query if the underlying columns have validation errors" do
    @c.columns(:id, :username, :password)
    @c.set_dataset DB[:items]
    @c.set_validations{errors.add(:username, 'foo'); validates_unique([:username, :password])}
    @c.dataset = @c.dataset.with_fetch(:v=>0)
    
    DB.reset
    m = @c.new(:username => "1", :password => "anothertest")
    m.wont_be :valid?
    DB.sqls.must_equal []
  end

  it "should support validates_operator" do
    @c.set_validations{validates_operator(:>, 3, :value)}
    @m.value = 1
    @m.wont_be :valid?
    @m.errors.full_messages.must_equal ['value is not > 3']
    @m.value = 3
    @m.wont_be :valid?
    @m.value = nil
    @m.wont_be :valid?
    @m.value = 4
    @m.must_be :valid?
  end
end 
