# Copyright (c) 2008-2012 Phusion
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

require 'bundler/setup'
require 'minitest/autorun'
require 'minitest/around/unit'
require 'active_record'
require 'action_pack'

if ActiveSupport::VERSION::MAJOR == 3
  require 'active_support/core_ext/logger'
end

if ActionPack::VERSION::MAJOR > 3
  require 'action_controller'
end

# Handle an edge-case when using Arel 5 (i.e. Rails <= 4.1) with Ruby >= 2.4:
# See: https://github.com/rails/arel/commit/dc85a6e9c74942945ad696f5da4d82490a85b865
# See: https://stackoverflow.com/a/51481088
rails_match_data = RUBY_VERSION.match(/\A(?<major>\d+).(?<minor>\d+)/)
rails_2_4_or_newer = rails_match_data[:major].to_i > 2 || (rails_match_data[:major].to_i == 2 && rails_match_data[:minor].to_i >= 4)
arel_match_data = Arel::VERSION.match(/\A(?<major>\d+).(?<minor>\d+)/)
arel_older_than_7_1 = arel_match_data[:major].to_i < 7 || (arel_match_data[:major].to_i == 7 && arel_match_data[:minor].to_i < 1)

if rails_2_4_or_newer && arel_older_than_7_1
  module Arel
    module Visitors
      class DepthFirst < Arel::Visitors::Visitor
        alias :visit_Integer :terminal
      end

      class Dot < Arel::Visitors::Visitor
        alias :visit_Integer :visit_String
      end

      # The super class for ToSql changed with Arel 6
      # See: https://github.com/rails/arel/commit/a6a7c75ff486657909e20e2f48764136caa5e87e#diff-3538aead5b80677372eea0e903ff728eR7
      class ToSql < (Arel::VERSION[/\A\d+/].to_i >= 6 ? Arel::Visitors::Reduce : Arel::Visitors::Visitor)
        alias :visit_Integer :literal
      end
    end
  end
end

require 'default_value_for'

puts "\nTesting with ruby version #{RUBY_VERSION}"
puts "\nTesting with Active Record version #{ActiveRecord::VERSION::STRING}"
puts "\nTesting with Action Pack version #{ActionPack::VERSION::STRING}\n\n"

if ActiveRecord.respond_to?(:default_timezone)
  ActiveRecord.default_timezone = :local
else
  # Deprecated in rails 7.0, removed in 7.1
  ActiveRecord::Base.default_timezone = :local
end

ActiveRecord::Base.logger           = Logger.new(STDERR)
ActiveRecord::Base.logger.level     = Logger::WARN

ActiveRecord::Base.establish_connection(
  :adapter  => RUBY_PLATFORM == 'java' ? 'jdbcsqlite3' : 'sqlite3',
  :database => ':memory:'
)

ActiveRecord::Base.connection.create_table(:users, :force => true) do |t|
  t.string :username
  t.integer :default_number
  t.text :settings
end

ActiveRecord::Base.connection.create_table(:books, :force => true) do |t|
  t.string :type
  t.integer :number
  t.integer :count, :null => false, :default => 1
  t.integer :user_id
  t.timestamp :timestamp
  t.text :stuff
  t.boolean :flag
end

if defined?(Rails::Railtie)
  DefaultValueFor.initialize_railtie
  DefaultValueFor.initialize_active_record_extensions
end

class DefaultValuePluginTest < Minitest::Test
  def around
    Object.const_set(:User, Class.new(ActiveRecord::Base))
    Object.const_set(:Book, Class.new(ActiveRecord::Base))
    Object.const_set(:Novel, Class.new(Book))
    User.has_many :books
    Book.belongs_to :user

    ActiveRecord::Base.transaction do
      yield
      raise ActiveRecord::Rollback
    end
  ensure
    Object.send(:remove_const, :User)
    Object.send(:remove_const, :Book)
    Object.send(:remove_const, :Novel)
    ActiveSupport::Dependencies.clear unless ActiveSupport::VERSION::MAJOR > 6
  end

  def test_default_value_on_attribute_methods
    Book.class_eval do
      serialize :stuff
      default_value_for :color, :green
      def color; (self.stuff || {})[:color]; end
      def color=(val)
        self.stuff ||= {}
        self.stuff[:color] = val
      end
    end
    assert_equal :green, Book.create.color
  end

  def test_default_value_can_be_passed_as_argument
    Book.default_value_for(:number, 1234)
    assert_equal 1234, Book.new.number
  end

  def test_default_value_can_be_passed_as_block
    Book.default_value_for(:number) { 1234 }
    assert_equal 1234, Book.new.number
  end

  def test_works_with_create
    Book.default_value_for :number, 1234

    object = Book.create
    refute_nil Book.find_by_number(1234)

    # allows nil for existing records
    object.update_attribute(:number, nil)
    assert_nil Book.find_by_number(1234)
    assert_nil Book.find(object.id).number
  end

  def test_does_not_allow_nil_sets_default_value_on_existing_nils
    Book.default_value_for(:number, :allows_nil => false) { 1234 }
    object = Book.create
    object.update_attribute(:number, nil)
    assert_nil Book.find_by_number(1234)
    assert_equal 1234, Book.find(object.id).number
  end

  def test_overwrites_db_default
    Book.default_value_for :count, 1234
    assert_equal 1234, Book.new.count
  end

  def test_doesnt_overwrite_values_provided_by_mass_assignment
    Book.default_value_for :number, 1234
    assert_equal 1, Book.new(:number => 1, :count => 2).number
  end

  def test_doesnt_overwrite_values_provided_by_multiparameter_assignment
    Book.default_value_for :timestamp, Time.mktime(2000, 1, 1)
    timestamp = Time.mktime(2009, 1, 1)
    object = Book.new('timestamp(1i)' => '2009', 'timestamp(2i)' => '1', 'timestamp(3i)' => '1')
    assert_equal timestamp, object.timestamp
  end

  def test_doesnt_overwrite_values_provided_by_constructor_block
    Book.default_value_for :number, 1234
    object = Book.new do |x|
      x.number = 1
      x.count  = 2
    end
    assert_equal 1, object.number
  end

  def test_doesnt_overwrite_explicitly_provided_nil_values_in_mass_assignment
    Book.default_value_for :number, 1234
    assert_nil Book.new(:number => nil).number
  end

  def test_overwrites_explicitly_provided_nil_values_in_mass_assignment
    Book.default_value_for :number, :value => 1234, :allows_nil => false
    assert_equal 1234, Book.new(:number => nil).number
  end

  def test_default_values_are_inherited
    Book.default_value_for :number, 1234
    assert_equal 1234, Novel.new.number
  end

  def test_default_values_in_superclass_are_saved_in_subclass
    Book.default_value_for :number, 1234
    Novel.default_value_for :flag, true
    object = Novel.create!
    assert_equal object.id, Novel.find_by_number(1234).id
    assert_equal object.id, Novel.find_by_flag(true).id
  end

  def test_default_values_in_subclass
    Novel.default_value_for :number, 5678
    assert_equal 5678, Novel.new.number
    assert_nil Book.new.number
  end

  def test_multiple_default_values_in_subclass_with_default_values_in_parent_class
    Book.class_eval do
      default_value_for :other_number, nil
      attr_accessor :other_number
    end
    Novel.default_value_for :number, 5678

    # Ensure second call in this class doesn't reset _default_attribute_values,
    # and also doesn't consider the parent class' _default_attribute_values when
    # making that check.
    Novel.default_value_for :user_id, 9999

    object = Novel.new
    assert_nil object.other_number
    assert_equal 5678, object.number
    assert_equal 9999, object.user_id
  end

  def test_override_default_values_in_subclass
    Book.default_value_for :number, 1234
    Novel.default_value_for :number, 5678
    assert_equal 5678, Novel.new.number
    assert_equal 1234, Book.new.number
  end

  def test_default_values_in_subclass_do_not_affect_parent_class
    Book.default_value_for :number, 1234
    Novel.class_eval do
      default_value_for :hello, "hi"
      attr_accessor :hello
    end

    assert Book.new
    assert !Book._default_attribute_values.include?(:hello)
  end

  def test_doesnt_set_default_on_saved_records
    Book.create(:number => 9876)
    Book.default_value_for :number, 1234
    assert_equal 9876, Book.first.number
  end

  def test_also_works_on_attributes_that_arent_database_columns
    Book.class_eval do
      default_value_for :hello, "hi"
      attr_accessor :hello
    end
    assert_equal 'hi', Book.new.hello
  end

  def test_works_on_attributes_that_only_have_writers
    Book.class_eval do
      default_value_for :hello, "hi"
      attr_writer :hello
    end
    assert_equal 'hi', Book.new.instance_variable_get('@hello')
  end

  def test_doesnt_conflict_with_overrided_initialize_method_in_model_class
    Book.class_eval do
      def initialize(attrs = {})
        @initialized = true
        super(:count => 5678)
      end

      default_value_for :number, 1234
    end
    object = Book.new
    assert_equal 1234, object.number
    assert_equal 5678, object.count
    assert object.instance_variable_get('@initialized')
  end

  def test_doesnt_conflict_with_prependeed_initialize_method_in_model_class
    m = Object.const_set("BookOverride", Module.new)
    m.define_method(:initialize) do |*args|
      @initialized = true
      super(:count => 5678)
    end

    Book.class_eval do
      prepend BookOverride

      default_value_for :number, 1234
    end

    object = Book.new
    assert_equal 1234, object.number
    assert_equal 5678, object.count
    assert object.instance_variable_get('@initialized')
  end

  def test_model_instance_is_passed_to_the_given_block
    instance = nil
    Book.default_value_for :number do |n|
      instance = n
    end
    object = Book.new
    assert_same object.object_id, instance.object_id
  end

  def test_can_specify_default_value_via_association
    user = User.create(:username => 'Kanako', :default_number => 123)
    Book.default_value_for :number do |n|
      n.user.default_number
    end
    assert_equal 123, user.books.create!.number
  end

  def test_default_values
    Book.default_values({
      :type      => "normal",
      :number    => lambda { 10 + 5 },
      :timestamp => lambda {|_| Time.now }
    })

    object = Book.new
    assert_equal("normal", object.type)
    assert_equal(15, object.number)
  end

  def test_default_value_order
    Book.default_value_for :count, 5
    Book.default_value_for :number do |this|
      this.count * 2
    end
    object = Book.new
    assert_equal(5, object.count)
    assert_equal(10, object.number)
  end

  def test_attributes_with_default_values_are_not_marked_as_changed
    Book.default_value_for :count, 5
    Book.default_value_for :number, 2

    object = Book.new
    assert(!object.changed?)
    assert_equal([], object.changed)

    object.type = "foo"
    assert(object.changed?)
    assert_equal(["type"], object.changed)
  end

  def test_default_values_are_duplicated
    User.default_value_for :username, "hello"
    user1 = User.new
    user1.username << " world"
    user2 = User.new
    assert_equal("hello", user2.username)
  end

  def test_default_values_are_shallow_copied
    User.class_eval do
      attr_accessor :hash
      default_value_for :hash, { 1 => [] }
    end
    user1 = User.new
    user1.hash[1] << 1
    user2 = User.new
    assert_equal([1], user2.hash[1])
  end

  def test_constructor_does_not_affect_the_hash_passed_to_it
    Book.default_value_for :count, 5
    options = { :count => 5, :user_id => 1 }
    options_dup = options.dup
    Book.new(options)
    assert_equal(options_dup, options)
  end

  def test_subclass_find
    Book.default_value_for :number, 5678
    n = Novel.create
    assert Novel.find(n.id)
  end

  def test_does_not_see_false_as_blank_at_boolean_columns_for_existing_records
    Book.default_value_for(:flag, :allows_nil => false) { true }

    object = Book.create

    # allows nil for existing records
    object.update_attribute(:flag, false)
    assert_equal false, Book.find(object.id).flag
  end

  def test_works_with_nested_attributes
    User.accepts_nested_attributes_for :books
    User.default_value_for :books do
      [Book.create!(:number => 0)]
    end

    user = User.create! :books_attributes => [{:number => 1}]
    assert_equal 1, Book.all.first.number
  end

  def test_works_with_stored_attribute_accessors_when_initializing_value_that_does_not_allow_nil
    User.store :settings, :accessors => :bio
    User.default_value_for :bio, :value => 'None given', :allows_nil => false

    user = User.create!(:bio => 'This is a bio')
    assert_equal 'This is a bio', user.bio
  end

  if ActionPack::VERSION::MAJOR > 3
    def test_doesnt_overwrite_explicitly_provided_nil_values_in_mass_assignment_with_action_controller_parameters
      Book.default_value_for :number, 1234

      assert_nil Book.new(ActionController::Parameters.new(:number => nil).permit!).number
    end

    def test_overwrites_explicitly_provided_nil_values_in_mass_assignment_with_action_controller_parameters
      Book.default_value_for :number, :value => 1234, :allows_nil => false

      assert_equal 1234, Book.new(ActionController::Parameters.new(:number => nil).permit!).number
    end

    def test_works_with_nested_attributes_with_action_controller_parameters
      User.accepts_nested_attributes_for :books
      User.default_value_for :books do
        [Book.create!(:number => 0)]
      end

      user = User.create!(ActionController::Parameters.new(:books_attributes => [{:number => 1}]).permit!)
      assert_equal 1, Book.all.first.number
    end

    def test_works_with_stored_attribute_accessors_when_initializing_value_that_does_not_allow_nil_with_action_controller_parameters
      User.store :settings, :accessors => :bio
      User.default_value_for :bio, :value => 'None given', :allows_nil => false

      user = User.create!(ActionController::Parameters.new(:bio => 'This is a bio').permit!)
      assert_equal 'This is a bio', user.bio
    end
  end

  if ActiveRecord::VERSION::MAJOR == 3
    def test_constructor_ignores_forbidden_mass_assignment_attributes
      Book.class_eval do
        default_value_for :number, 1234
        attr_protected :number
      end
      object = Book.new(:number => 5678, :count => 987)
      assert_equal 1234, object.number
      assert_equal 987, object.count
    end

    def test_constructor_respects_without_protection_option
      Book.class_eval do
        default_value_for :number, 1234
        attr_protected :number
      end

      object = Book.create!({:number => 5678, :count => 987}, :without_protection => true)
      assert_equal 5678, object.number
      assert_equal 987, object.count
    end
  end
end
