require "abstract_unit"
require "active_support/inflector"
require "active_support/time"
require "active_support/json"
require "time_zone_test_helpers"

class DurationTest < ActiveSupport::TestCase
  include TimeZoneTestHelpers

  def test_is_a
    d = 1.day
    assert d.is_a?(ActiveSupport::Duration)
    assert_kind_of ActiveSupport::Duration, d
    assert_kind_of Numeric, d
    assert_kind_of Integer, d
    assert !d.is_a?(Hash)

    k = Class.new
    class << k; undef_method :== end
    assert !d.is_a?(k)
  end

  def test_instance_of
    assert 1.minute.instance_of?(Fixnum)
    assert 2.days.instance_of?(ActiveSupport::Duration)
    assert !3.second.instance_of?(Numeric)
  end

  def test_threequals
    assert ActiveSupport::Duration === 1.day
    assert !(ActiveSupport::Duration === 1.day.to_i)
    assert !(ActiveSupport::Duration === "foo")
  end

  def test_equals
    assert 1.day == 1.day
    assert 1.day == 1.day.to_i
    assert 1.day.to_i == 1.day
    assert !(1.day == "foo")
  end

  def test_to_s
    assert_equal "1", 1.second.to_s
  end

  def test_eql
    rubinius_skip "Rubinius' #eql? definition relies on #instance_of? " \
                  "which behaves oddly for the sake of backward-compatibility."

    assert 1.minute.eql?(1.minute)
    assert 1.minute.eql?(60.seconds)
    assert 2.days.eql?(48.hours)
    assert !1.second.eql?(1)
    assert !1.eql?(1.second)
    assert 1.minute.eql?(180.seconds - 2.minutes)
    assert !1.minute.eql?(60)
    assert !1.minute.eql?("foo")
  end

  def test_inspect
    assert_equal "0 seconds",                       0.seconds.inspect
    assert_equal "1 month",                         1.month.inspect
    assert_equal "1 month and 1 day",               (1.month + 1.day).inspect
    assert_equal "6 months and -2 days",            (6.months - 2.days).inspect
    assert_equal "10 seconds",                      10.seconds.inspect
    assert_equal "10 years, 2 months, and 1 day",   (10.years + 2.months + 1.day).inspect
    assert_equal "10 years, 2 months, and 1 day",   (10.years + 1.month  + 1.day + 1.month).inspect
    assert_equal "10 years, 2 months, and 1 day",   (1.day + 10.years + 2.months).inspect
    assert_equal "7 days",                          7.days.inspect
    assert_equal "1 week",                          1.week.inspect
    assert_equal "2 weeks",                         1.fortnight.inspect
  end

  def test_inspect_locale
    current_locale = I18n.default_locale
    I18n.default_locale = :de
    I18n.backend.store_translations(:de, support: { array: { last_word_connector: " und " } })
    assert_equal "10 years, 1 month und 1 day", (10.years + 1.month + 1.day).inspect
  ensure
    I18n.default_locale = current_locale
  end

  def test_minus_with_duration_does_not_break_subtraction_of_date_from_date
    assert_nothing_raised { Date.today - Date.today }
  end

  def test_plus_with_time
    assert_equal 1 + 1.second, 1.second + 1, "Duration + Numeric should == Numeric + Duration"
  end

  def test_time_plus_duration_returns_same_time_datatype
    twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Moscow"] , Time.utc(2016, 4, 28, 00, 45))
    now = Time.now.utc
    %w( second minute hour day week month year ).each do |unit|
      assert_equal((now + 1.send(unit)).class, Time, "Time + 1.#{unit} must be Time")
      assert_equal((twz + 1.send(unit)).class, ActiveSupport::TimeWithZone, "TimeWithZone + 1.#{unit} must be TimeWithZone")
    end
  end

  def test_argument_error
    e = assert_raise ArgumentError do
      1.second.ago("")
    end
    assert_equal 'expected a time or date, got ""', e.message, "ensure ArgumentError is not being raised by dependencies.rb"
  end

  def test_fractional_weeks
    assert_equal((86400 * 7) * 1.5, 1.5.weeks)
    assert_equal((86400 * 7) * 1.7, 1.7.weeks)
  end

  def test_fractional_days
    assert_equal 86400 * 1.5, 1.5.days
    assert_equal 86400 * 1.7, 1.7.days
  end

  def test_since_and_ago
    t = Time.local(2000)
    assert_equal t + 1, 1.second.since(t)
    assert_equal t - 1, 1.second.ago(t)
  end

  def test_since_and_ago_without_argument
    now = Time.now
    assert 1.second.since >= now + 1
    now = Time.now
    assert 1.second.ago >= now - 1
  end

  def test_since_and_ago_with_fractional_days
    t = Time.local(2000)
    # since
    assert_equal 36.hours.since(t), 1.5.days.since(t)
    assert_in_delta((24 * 1.7).hours.since(t), 1.7.days.since(t), 1)
    # ago
    assert_equal 36.hours.ago(t), 1.5.days.ago(t)
    assert_in_delta((24 * 1.7).hours.ago(t), 1.7.days.ago(t), 1)
  end

  def test_since_and_ago_with_fractional_weeks
    t = Time.local(2000)
    # since
    assert_equal((7 * 36).hours.since(t), 1.5.weeks.since(t))
    assert_in_delta((7 * 24 * 1.7).hours.since(t), 1.7.weeks.since(t), 1)
    # ago
    assert_equal((7 * 36).hours.ago(t), 1.5.weeks.ago(t))
    assert_in_delta((7 * 24 * 1.7).hours.ago(t), 1.7.weeks.ago(t), 1)
  end

  def test_since_and_ago_anchored_to_time_now_when_time_zone_is_not_set
    Time.zone = nil
    with_env_tz "US/Eastern" do
      Time.stub(:now, Time.local(2000)) do
        # since
        assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.since
        assert_equal Time.local(2000, 1, 1, 0, 0, 5), 5.seconds.since
        # ago
        assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago
        assert_equal Time.local(1999, 12, 31, 23, 59, 55), 5.seconds.ago
      end
    end
  end

  def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_is_set
    Time.zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
    with_env_tz "US/Eastern" do
      Time.stub(:now, Time.local(2000)) do
        # since
        assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.since
        assert_equal Time.utc(2000, 1, 1, 0, 0, 5), 5.seconds.since.time
        assert_equal "Eastern Time (US & Canada)", 5.seconds.since.time_zone.name
        # ago
        assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago
        assert_equal Time.utc(1999, 12, 31, 23, 59, 55), 5.seconds.ago.time
        assert_equal "Eastern Time (US & Canada)", 5.seconds.ago.time_zone.name
      end
    end
  ensure
    Time.zone = nil
  end

  def test_adding_hours_across_dst_boundary
    with_env_tz "CET" do
      assert_equal Time.local(2009, 3, 29, 0, 0, 0) + 24.hours, Time.local(2009, 3, 30, 1, 0, 0)
    end
  end

  def test_adding_day_across_dst_boundary
    with_env_tz "CET" do
      assert_equal Time.local(2009, 3, 29, 0, 0, 0) + 1.day, Time.local(2009, 3, 30, 0, 0, 0)
    end
  end

  def test_delegation_with_block_works
    counter = 0
    assert_nothing_raised do
      1.minute.times { counter += 1 }
    end
    assert_equal counter, 60
  end

  def test_as_json
    assert_equal 172800, 2.days.as_json
  end

  def test_to_json
    assert_equal "172800", 2.days.to_json
  end

  def test_case_when
    cased = \
      case 1.day
      when 1.day
        "ok"
      end
    assert_equal cased, "ok"
  end

  def test_respond_to
    assert_respond_to 1.day, :since
    assert_respond_to 1.day, :zero?
  end

  def test_hash
    assert_equal 1.minute.hash, 60.seconds.hash
  end

  def test_comparable
    assert_equal(-1, (0.seconds <=> 1.second))
    assert_equal(-1, (1.second <=> 1.minute))
    assert_equal(-1, (1 <=> 1.minute))
    assert_equal(0, (0.seconds <=> 0.seconds))
    assert_equal(0, (0.seconds <=> 0.minutes))
    assert_equal(0, (1.second <=> 1.second))
    assert_equal(1, (1.second <=> 0.second))
    assert_equal(1, (1.minute <=> 1.second))
    assert_equal(1, (61 <=> 1.minute))
  end

  # ISO8601 string examples are taken from ISO8601 gem at https://github.com/arnau/ISO8601/blob/b93d466840/spec/iso8601/duration_spec.rb
  # published under the conditions of MIT license at https://github.com/arnau/ISO8601/blob/b93d466840/LICENSE
  #
  # Copyright (c) 2012-2014 Arnau Siches
  #
  # MIT License
  #
  # 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.

  def test_iso8601_parsing_wrong_patterns_with_raise
    invalid_patterns = ["", "P", "PT", "P1YT", "T", "PW", "P1Y1W", "~P1Y", ".P1Y", "P1.5Y0.5M", "P1.5Y1M", "P1.5MT10.5S"]
    invalid_patterns.each do |pattern|
      assert_raise ActiveSupport::Duration::ISO8601Parser::ParsingError, pattern.inspect do
        ActiveSupport::Duration.parse(pattern)
      end
    end
  end

  def test_iso8601_output
    expectations = [
      ["P1Y",           1.year                           ],
      ["P1W",           1.week                           ],
      ["P1Y1M",         1.year + 1.month                 ],
      ["P1Y1M1D",       1.year + 1.month + 1.day         ],
      ["-P1Y1D",        -1.year - 1.day                  ],
      ["P1Y-1DT-1S",    1.year - 1.day - 1.second        ], # Parts with different signs are exists in PostgreSQL interval datatype.
      ["PT1S",          1.second                         ],
      ["PT1.4S",        (1.4).seconds                    ],
      ["P1Y1M1DT1H",    1.year + 1.month + 1.day + 1.hour],
      ["PT0S",          0.minutes                        ],
    ]
    expectations.each do |expected_output, duration|
      assert_equal expected_output, duration.iso8601, expected_output.inspect
    end
  end

  def test_iso8601_output_precision
    expectations = [
        [nil, "P1Y1MT8.55S",  1.year + 1.month + (8.55).seconds ],
        [0,   "P1Y1MT9S",     1.year + 1.month + (8.55).seconds ],
        [1,   "P1Y1MT8.6S",   1.year + 1.month + (8.55).seconds ],
        [2,   "P1Y1MT8.55S",  1.year + 1.month + (8.55).seconds ],
        [3,   "P1Y1MT8.550S", 1.year + 1.month + (8.55).seconds ],
        [nil, "PT1S",         1.second                          ],
        [2,   "PT1.00S",      1.second                          ],
        [nil, "PT1.4S",       (1.4).seconds                     ],
        [0,   "PT1S",         (1.4).seconds                     ],
        [1,   "PT1.4S",       (1.4).seconds                     ],
        [5,   "PT1.40000S",   (1.4).seconds                     ],
    ]
    expectations.each do |precision, expected_output, duration|
      assert_equal expected_output, duration.iso8601(precision: precision), expected_output.inspect
    end
  end

  def test_iso8601_output_and_reparsing
    patterns = %w[
      P1Y P0.5Y P0,5Y P1Y1M P1Y0.5M P1Y0,5M P1Y1M1D P1Y1M0.5D P1Y1M0,5D P1Y1M1DT1H P1Y1M1DT0.5H P1Y1M1DT0,5H P1W +P1Y -P1Y
      P1Y1M1DT1H1M P1Y1M1DT1H0.5M P1Y1M1DT1H0,5M P1Y1M1DT1H1M1S P1Y1M1DT1H1M1.0S P1Y1M1DT1H1M1,0S P-1Y-2M3DT-4H-5M-6S
    ]
    # That could be weird, but if we parse P1Y1M0.5D and output it to ISO 8601, we'll get P1Y1MT12.0H.
    # So we check that initially parsed and reparsed duration added to time will result in the same time.
    time = Time.current
    patterns.each do |pattern|
      duration = ActiveSupport::Duration.parse(pattern)
      assert_equal time + duration, time + ActiveSupport::Duration.parse(duration.iso8601), pattern.inspect
    end
  end

  def test_iso8601_parsing_across_spring_dst_boundary
    with_env_tz eastern_time_zone do
      with_tz_default "Eastern Time (US & Canada)" do
        travel_to Time.utc(2016, 3, 11) do
          assert_equal 604800, ActiveSupport::Duration.parse("P7D").to_i
          assert_equal 604800, ActiveSupport::Duration.parse("P1W").to_i
        end
      end
    end
  end

  def test_iso8601_parsing_across_autumn_dst_boundary
    with_env_tz eastern_time_zone do
      with_tz_default "Eastern Time (US & Canada)" do
        travel_to Time.utc(2016, 11, 4) do
          assert_equal 604800, ActiveSupport::Duration.parse("P7D").to_i
          assert_equal 604800, ActiveSupport::Duration.parse("P1W").to_i
        end
      end
    end
  end

  private
    def eastern_time_zone
      if Gem.win_platform?
        "EST5EDT"
      else
        "America/New_York"
      end
    end
end
