# frozen_string_literal: true

require "cases/helper"
require "models/dats"

# The logic of the `guard` method is extensively tested indirectly, via the test
# suites of each type of association.
#
# Those tests verify that the `report` method is invoked as expected. Here, we
# unit test the `report` method itself.
module AssociationDeprecationTest
  class TestCase < ActiveRecord::TestCase
    # __FILE__ is relative if passed as an argument to ruby, and it is absolute
    # when running the suite. We'll take the path relative to the component to
    # make backtraces generated by the backtrace cleaner to be more predictable
    # and easy to test.
    THIS_FILE = __FILE__.sub(%r(.*?/activerecord/(?=test/)), "")

    def setup
      @original_mode = ActiveRecord::Associations::Deprecation.mode
      @original_backtrace = ActiveRecord::Associations::Deprecation.backtrace
      @original_backtrace_cleaner = ActiveRecord::LogSubscriber.backtrace_cleaner

      bc = ActiveSupport::BacktraceCleaner.new
      bc.add_silencer { !_1.include?(THIS_FILE) }

      ActiveRecord::LogSubscriber.backtrace_cleaner = bc
    end

    def teardown
      ActiveRecord::Associations::Deprecation.mode = @original_mode
      ActiveRecord::Associations::Deprecation.backtrace = @original_backtrace
      ActiveRecord::LogSubscriber.backtrace_cleaner = @original_backtrace_cleaner
    end

    def assert_message(message, line)
      re = /The association DATS::Car#deprecated_tires is deprecated, the method deprecated_tires was invoked \(#{__FILE__}:#{line}:in/
      assert_match re, message
    end
  end

  class OptionsTest < TestCase
    test "valid options, mode only" do
      ActiveRecord.deprecated_associations_options = { mode: :warn }
      assert_equal :warn, ActiveRecord::Associations::Deprecation.mode
      assert_equal false, ActiveRecord::Associations::Deprecation.backtrace
    end

    test "valid options, backtrace only" do
      ActiveRecord.deprecated_associations_options = { backtrace: true }
      assert_equal :warn, ActiveRecord::Associations::Deprecation.mode
      assert_equal true, ActiveRecord::Associations::Deprecation.backtrace
    end

    test "valid options, both" do
      ActiveRecord.deprecated_associations_options = {
        mode: :notify,
        backtrace: true
      }
      assert_equal :notify, ActiveRecord::Associations::Deprecation.mode
      assert_equal true, ActiveRecord::Associations::Deprecation.backtrace
    end

    test "not a hash" do
      error = assert_raises(ArgumentError) do
        ActiveRecord.deprecated_associations_options = :invalid
      end
      assert_equal "deprecated_associations_options must be a hash", error.message
    end

    test "invalid keys" do
      error = assert_raises(ArgumentError) do
        ActiveRecord.deprecated_associations_options = { invalid: true }
      end
      assert_equal "invalid deprecated_associations_options key :invalid (valid keys are :mode and :backtrace)", error.message
    end

    test "invalid mode" do
      error = assert_raises(ArgumentError) do
        ActiveRecord.deprecated_associations_options = { mode: :invalid }
      end
      assert_equal "invalid deprecated associations mode :invalid (valid modes are :warn, :raise, and :notify)", error.message
    end
  end

  class ModeWriterTest < TestCase
    test "valid values" do
      [:warn, :raise, :notify].each do |mode|
        ActiveRecord::Associations::Deprecation.mode = mode
        assert_equal mode, ActiveRecord::Associations::Deprecation.mode
      end
    end

    test "invalid values" do
      error = assert_raises(ArgumentError) do
        ActiveRecord::Associations::Deprecation.mode = :invalid
      end
      assert_equal "invalid deprecated associations mode :invalid (valid modes are :warn, :raise, and :notify)", error.message
    end

    test "the backtrace flag becomes a true/false singleton" do
      ActiveRecord::Associations::Deprecation.backtrace = 1
      assert_same true, ActiveRecord::Associations::Deprecation.backtrace

      ActiveRecord::Associations::Deprecation.backtrace = nil
      assert_same false, ActiveRecord::Associations::Deprecation.backtrace
    end
  end

  class WarnModeTest < TestCase
    def setup
      super
      ActiveRecord::Associations::Deprecation.mode = :warn

      @original_logger = ActiveRecord::Base.logger
      @io = StringIO.new
      ActiveRecord::Base.logger = Logger.new(@io)
    end

    def teardown
      super
      ActiveRecord::Base.logger = @original_logger
    end

    test "report warns in :warn mode" do
      DATS::Car.new.deprecated_tires
      assert_message @io.string, __LINE__ - 1
    end
  end

  class WarnBacktraceModeTest < TestCase
    def setup
      super
      ActiveRecord::Associations::Deprecation.mode = :warn
      ActiveRecord::Associations::Deprecation.backtrace = true

      @original_logger = ActiveRecord::Base.logger
      @io = StringIO.new
      ActiveRecord::Base.logger = Logger.new(@io)
    end

    def teardown
      super
      ActiveRecord::Base.logger = @original_logger
    end

    test "report warns in :warn mode" do
      line = __LINE__ + 1
      DATS::Car.new.deprecated_tires

      assert_message @io.string, line
      assert_includes @io.string, "\t#{__FILE__}:#{line}:in"
    end
  end

  class WarnModeNoLoggerTest < TestCase
    def setup
      super
      ActiveRecord::Associations::Deprecation.mode = :warn

      @original_logger = ActiveRecord::Base.logger
      ActiveRecord::Base.logger = nil
    end

    def teardown
      super
      ActiveRecord::Base.logger = @original_logger
    end

    test "report does not assume the logger is present" do
      assert_nothing_raised { DATS::Car.new.deprecated_tires }
    end
  end

  class RaiseModeTest < TestCase
    def setup
      super
      ActiveRecord::Associations::Deprecation.mode = :raise
    end

    test "report raises an error in :raise mode" do
      error = assert_raises(ActiveRecord::DeprecatedAssociationError) { DATS::Car.new.deprecated_tires }
      assert_message error.message, __LINE__ - 1
    end
  end

  class RaiseBacktraceModeTest < TestCase
    def setup
      super
      ActiveRecord::Associations::Deprecation.mode = :raise
      ActiveRecord::Associations::Deprecation.backtrace = true
    end

    test "report raises an error in :raise mode" do
      line = __LINE__ + 1
      error = assert_raises(ActiveRecord::DeprecatedAssociationError) { DATS::Car.new.deprecated_tires }

      assert_message error.message, line
      assert_includes error.backtrace.last, "#{__FILE__}:#{line}:in"
    end
  end

  class NotifyModeTest < TestCase
    def setup
      super
      ActiveRecord::Associations::Deprecation.mode = :notify
      ActiveRecord::Associations::Deprecation.backtrace = true
    end

    def teardown
      super
      ActiveRecord::LogSubscriber.backtrace_cleaner = @original_backtrace_cleaner
    end

    def assert_user_facing_reflection(model, association)
      payloads = []
      callback = ->(event) { payloads << event.payload }

      ActiveSupport::Notifications.subscribed(callback, "deprecated_association.active_record") do
        model.new.send(association)
      end

      assert_equal 1, payloads.size
      assert_equal model.reflect_on_association(association), payloads[0][:reflection]
    end

    test "report publishes an Active Support notification in :notify mode" do
      payloads = []
      callback = ->(event) { payloads << event.payload }

      line = __LINE__ + 2
      ActiveSupport::Notifications.subscribed(callback, "deprecated_association.active_record") do
        DATS::Car.new.deprecated_tires
      end

      assert_equal 1, payloads.size
      payload = payloads.first

      assert_equal DATS::Car.reflect_on_association(:deprecated_tires), payload[:reflection]

      assert_message payload[:message], line

      assert_equal __FILE__, payload[:location].path
      assert_equal line, payload[:location].lineno

      assert_equal __FILE__, payload[:backtrace][-2].path
      assert_equal line, payload[:backtrace][-2].lineno
    end

    test "has_many receives the user-facing reflection in the payload" do
      assert_user_facing_reflection(DATS::Author, :deprecated_posts)
    end

    test "has_one receives the user-facing reflection in the payload" do
      assert_user_facing_reflection(DATS::Author, :deprecated_post)
    end

    test "belongs_to receives the user-facing reflection in the payload" do
      assert_user_facing_reflection(DATS::Bulb, :deprecated_car)
    end

    test "has_many :through receives the user-facing reflection in the payload" do
      assert_user_facing_reflection(DATS::Author, :deprecated_has_many_through)
    end

    test "has_one :through receives the user-facing reflection in the payload" do
      assert_user_facing_reflection(DATS::Author, :deprecated_has_one_through)
    end

    test "HABTM receives the user-facing reflection in the payload" do
      assert_user_facing_reflection(DATS::Category, :deprecated_posts)
    end
  end
end
