# frozen_string_literal: true
require_relative "spec_helper"

describe "Model#before_create && Model#after_create" do
  before do
    @c = Class.new(Sequel::Model(:items)) do
      columns :x
      set_primary_key :x
      unrestrict_primary_key
      
      def after_create
        DB << "BLAH after"
      end
    end
    DB.reset
  end
  
  it "should be called around new record creation" do
    @c.send(:define_method, :before_create){DB << "BLAH before"}
    @c.create(:x => 2)
    DB.sqls.must_equal ['BLAH before', 'INSERT INTO items (x) VALUES (2)', 'SELECT * FROM items WHERE x = 2', 'BLAH after']
  end

  it ".create should cancel the save and raise an error if before_create calls cancel_action and raise_on_save_failure is true" do
    @c.send(:define_method, :before_create){cancel_action 'not good'}
    err = proc{@c.create(:x => 2)}.must_raise(Sequel::HookFailed)
    err.message.must_equal('not good')
    DB.sqls.must_equal []
    @c.load(:id => 2233).save
  end

  it ".create should cancel the save and return nil if before_create calls cancel_action and raise_on_save_failure is false" do
    @c.send(:define_method, :before_create){cancel_action}
    @c.raise_on_save_failure = false
    @c.create(:x => 2).must_be_nil
    DB.sqls.must_equal []
  end
end

describe "Model#before_update && Model#after_update" do
  before do
    @c = Class.new(Sequel::Model(:items)) do
      columns :id, :x
      def after_update
        DB << "BLAH after"
      end
    end
    DB.reset
  end
  
  it "should be called around record update" do
    @c.send(:define_method, :before_update){DB << "BLAH before"}
    m = @c.load(:id => 2233, :x=>123)
    m.save
    DB.sqls.must_equal ['BLAH before', 'UPDATE items SET x = 123 WHERE (id = 2233)', 'BLAH after']
  end

  it "#save should cancel the save and raise an error if before_update calls cancel_action and raise_on_save_failure is true" do
    @c.send(:define_method, :before_update){cancel_action}
    proc{@c.load(:id => 2233).save}.must_raise(Sequel::HookFailed)
    DB.sqls.must_equal []
  end

  it "#save should cancel the save and return nil if before_update calls cancel_action and raise_on_save_failure is false" do
    @c.send(:define_method, :before_update){cancel_action}
    @c.raise_on_save_failure = false
    @c.load(:id => 2233).save.must_be_nil
    DB.sqls.must_equal []
  end
end

describe "Model#before_save && Model#after_save" do
  before do
    @c = Class.new(Sequel::Model(:items)) do
      columns :x
      def after_save
        DB << "BLAH after"
      end
    end
    DB.reset
  end
  
  it "should be called around record update" do
    @c.send(:define_method, :before_save){DB << "BLAH before"}
    m = @c.load(:id => 2233, :x=>123)
    m.save
    DB.sqls.must_equal ['BLAH before', 'UPDATE items SET x = 123 WHERE (id = 2233)', 'BLAH after']
  end
  
  it "should be called around record creation" do
    @c.send(:define_method, :before_save){DB << "BLAH before"}
    @c.set_primary_key :x
    @c.unrestrict_primary_key
    @c.create(:x => 2)
    DB.sqls.must_equal ['BLAH before', 'INSERT INTO items (x) VALUES (2)', 'SELECT * FROM items WHERE x = 2', 'BLAH after']
  end

  it "#save should cancel the save and raise an error if before_save calls cancel_action and raise_on_failure option is true" do
    @c.send(:define_method, :before_save){cancel_action}
    @c.raise_on_save_failure = false
    proc{@c.load(:id => 2233).save(:raise_on_failure => true)}.must_raise(Sequel::HookFailed)
    DB.sqls.must_equal []
  end

  it "#save should cancel the save and return nil if before_save calls cancel_action and raise_on_save_failure is false" do
    @c.send(:define_method, :before_save){cancel_action}
    @c.raise_on_save_failure = false
    @c.load(:id => 2233).save.must_be_nil
    DB.sqls.must_equal []
  end

  it "#save should have a raised exception reference the model instance" do
    @c.send(:define_method, :before_save){cancel_action}
    e = proc{@c.create(:x => 2233)}.must_raise(Sequel::HookFailed)
    e.model.must_equal @c.load(:x=>2233)
    DB.sqls.must_equal []
  end
end

describe "Model#before_destroy && Model#after_destroy" do
  before do
    @c = Class.new(Sequel::Model(:items)) do
      def after_destroy
        DB << "BLAH after"
      end
    end
    DB.reset
  end
  
  it "should be called around record destruction" do
    @c.send(:define_method, :before_destroy){DB << "BLAH before"}
    m = @c.load(:id => 2233)
    m.destroy
    DB.sqls.must_equal ['BLAH before', 'DELETE FROM items WHERE id = 2233', 'BLAH after']
  end

  it "#destroy should cancel the destroy and raise an error if before_destroy calls cancel_action and raise_on_save_failure is true" do
    @c.send(:define_method, :before_destroy){cancel_action; true}
    proc{@c.load(:id => 2233).destroy}.must_raise(Sequel::HookFailed)
    DB.sqls.must_equal []
  end

  it "#destroy should cancel the destroy and return nil if before_destroy calls cancel_action and raise_on_save_failure is false" do
    @c.send(:define_method, :before_destroy){cancel_action; true}
    @c.raise_on_save_failure = false
    @c.load(:id => 2233).destroy.must_be_nil
    DB.sqls.must_equal []
  end
end

describe "Model#before_validation && Model#after_validation" do
  before do
    @c = Class.new(Sequel::Model(:items)) do
      columns :id
      def after_validation
        DB << "BLAH after"
      end

      def validate
        errors.add(:id, 'not valid') unless id == 2233
      end
    end
    DB.reset
  end
  
  it "should be called around validation" do
    @c.send(:define_method, :before_validation){DB << "BLAH before"}
    m = @c.load(:id => 2233)
    m.must_be :valid?
    DB.sqls.must_equal ['BLAH before', 'BLAH after']

    m = @c.load(:id => 22)
    m.wont_be :valid?
    DB.sqls.must_equal ['BLAH before', 'BLAH after']
  end

  it "should be called when calling save" do
    @c.send(:define_method, :before_validation){DB << "BLAH before"}
    m = @c.load(:id => 2233, :x=>123)
    m.save.must_equal m
    DB.sqls.must_equal ['BLAH before', 'BLAH after', 'UPDATE items SET x = 123 WHERE (id = 2233)']

    m = @c.load(:id => 22)
    m.raise_on_save_failure = false
    m.save.must_be_nil
    DB.sqls.must_equal ['BLAH before', 'BLAH after']
  end

  it "#save should cancel the save and raise an error if before_validation calls cancel_action and raise_on_save_failure is true" do
    @c.send(:define_method, :before_validation){cancel_action}
    proc{@c.load(:id => 2233).save}.must_raise(Sequel::HookFailed)
    DB.sqls.must_equal []
  end

  it "#save should cancel the save and return nil if before_validation calls cancel_action and raise_on_save_failure is false" do
    @c.send(:define_method, :before_validation){cancel_action}
    @c.raise_on_save_failure = false
    @c.load(:id => 2233).save.must_be_nil
    DB.sqls.must_equal []
  end
  
  it "#valid? should return false if before_validation calls cancel_action" do
    @c.send(:define_method, :before_validation){cancel_action}
    @c.load(:id => 2233).valid?.must_equal false
  end
end

describe "Model around filters" do
  before do
    @c = Class.new(Sequel::Model(:items)) do
      columns :id, :x
    end
    DB.reset
  end
  
  it "around_create should be called around new record creation" do
    @c.class_eval do
      def around_create
        DB << 'ac_before'
        super
        DB << 'ac_after'
      end
    end
    @c.create(:x => 2)
    DB.sqls.must_equal ['ac_before', 'INSERT INTO items (x) VALUES (2)', "SELECT * FROM items WHERE id = 10", 'ac_after']
  end

  it "around_delete should be called around record destruction" do
    @c.class_eval do
      def around_destroy
        DB << 'ad_before'
        super
        DB << 'ad_after'
      end
    end
    @c.load(:id=>1, :x => 2).destroy
    DB.sqls.must_equal ['ad_before', 'DELETE FROM items WHERE id = 1', 'ad_after']
  end
  
  it "around_update should be called around updating existing records" do
    @c.class_eval do
      def around_update
        DB << 'au_before'
        super
        DB << 'au_after'
      end
    end
    @c.load(:id=>1, :x => 2).save
    DB.sqls.must_equal ['au_before', 'UPDATE items SET x = 2 WHERE (id = 1)', 'au_after']
  end

  it "around_save should be called around saving both new and existing records, around either after_create and after_update" do
    @c.class_eval do
      def around_update
        DB << 'au_before'
        super
        DB << 'au_after'
      end
      def around_create
        DB << 'ac_before'
        super
        DB << 'ac_after'
      end
      def around_save
        DB << 'as_before'
        super
        DB << 'as_after'
      end
    end
    @c.create(:x => 2)
    DB.sqls.must_equal ['as_before', 'ac_before', 'INSERT INTO items (x) VALUES (2)', "SELECT * FROM items WHERE id = 10", 'ac_after', 'as_after']
    @c.load(:id=>1, :x => 2).save
    DB.sqls.must_equal ['as_before', 'au_before', 'UPDATE items SET x = 2 WHERE (id = 1)', 'au_after', 'as_after']
  end

  it "around_validation should be called around validating records" do
    @c.class_eval do
      def around_validation
        DB << 'av_before'
        super
        DB << 'av_after'
      end
      def validate
        DB << 'validate'
      end
    end
    @c.new(:x => 2).valid?.must_equal true
    DB.sqls.must_equal [ 'av_before', 'validate', 'av_after' ]
  end

  it "around_validation should handle cancel_action" do
    @c.class_eval do
      def around_validation
        DB << 'av_before'
        cancel_action
        super
        DB << 'av_after'
      end
      def validate
        DB << 'validate'
      end
    end
    @c.new(:x => 2).valid?.must_equal false
    DB.sqls.must_equal [ 'av_before' ]
  end

  it "around_validation should be able to catch validation errors and modify them" do
    @c.class_eval do
      def validate
        errors.add(:x, 'foo')
      end
    end
    @c.new(:x => 2).valid?.must_equal false
    @c.class_eval do
      def around_validation
        super
        errors.clear
      end
    end
    @c.new(:x => 2).valid?.must_equal true
  end

  it "around_create that doesn't call super should raise a HookFailed" do
    @c.send(:define_method, :around_create){}
    proc{@c.create(:x => 2)}.must_raise(Sequel::HookFailed)
  end
  
  it "around_update that doesn't call super should raise a HookFailed" do
    @c.send(:define_method, :around_update){}
    proc{@c.load(:x => 2).save}.must_raise(Sequel::HookFailed)
  end
  
  it "around_save that doesn't call super should raise a HookFailed" do
    @c.send(:define_method, :around_save){}
    proc{@c.create(:x => 2)}.must_raise(Sequel::HookFailed)
    proc{@c.load(:x => 2).save}.must_raise(Sequel::HookFailed)
  end
  
  it "around_destroy that doesn't call super should raise a HookFailed" do
    @c.send(:define_method, :around_destroy){}
    proc{@c.load(:x => 2).destroy}.must_raise(Sequel::HookFailed)
  end
  
  it "around_validation that doesn't call super should raise a HookFailed" do
    @c.send(:define_method, :around_validation){}
    proc{@c.new.save}.must_raise(Sequel::HookFailed)
  end

  it "around_validation that doesn't call super should have valid? return false" do
    @c.send(:define_method, :around_validation){}
    @c.new.valid?.must_equal false
  end

  it "around_* that doesn't call super should return nil if raise_on_save_failure is false" do
    @c.raise_on_save_failure = false

    o = @c.load(:id => 1)
    def o.around_save(&_) end
    o.save.must_be_nil

    o = @c.load(:id => 1)
    def o.around_update(&_) end
    o.save.must_be_nil

    o = @c.new
    def o.around_create(&_) end
    o.save.must_be_nil

    o = @c.new
    def o.around_validation(&_) end
    o.save.must_be_nil
  end
end
