require 'test_helper'

class FedExTest < Minitest::Test
  include ActiveShipping::Test::Fixtures

  def setup
    @carrier           = FedEx.new(:key => '1111', :password => '2222', :account => '3333', :login => '4444')
    @tracking_response = xml_fixture('fedex/tracking_response')
  end

  def test_initialize_options_requirements
    assert_raises(ArgumentError) { FedEx.new }
    assert_raises(ArgumentError) { FedEx.new(:login => '999999999') }
    assert_raises(ArgumentError) { FedEx.new(:password => '7777777') }
    FedEx.new(:key => '999999999', :password => '7777777', :account => '123', :login => '123')
  end

  def test_business_days
    today = DateTime.civil(2013, 3, 12, 0, 0, 0, "-4")

    Timecop.freeze(today) do
      assert_equal DateTime.civil(2013, 3, 13, 0, 0, 0, "-4"), @carrier.send(:business_days_from, today, 1)
      assert_equal DateTime.civil(2013, 3, 15, 0, 0, 0, "-4"), @carrier.send(:business_days_from, today, 3)
      assert_equal DateTime.civil(2013, 3, 19, 0, 0, 0, "-4"), @carrier.send(:business_days_from, today, 5)
    end
  end

  def test_turn_around_time_default
    mock_response = xml_fixture('fedex/ottawa_to_beverly_hills_rate_response').gsub('<v6:DeliveryTimestamp>2011-07-29</v6:DeliveryTimestamp>', '')

    today = DateTime.civil(2013, 3, 11, 0, 0, 0, "-4")

    Timecop.freeze(today) do
      delivery_date = Date.today + 7.days # FIVE_DAYS in fixture response, plus weekend
      timestamp = Time.now.iso8601
      @carrier.expects(:commit).with do |request, _options|
        parsed_response = Hash.from_xml(request)
        parsed_response['RateRequest']['RequestedShipment']['ShipTimestamp'] == timestamp
      end.returns(mock_response)

      destination = ActiveShipping::Location.from(location_fixtures[:beverly_hills].to_hash, :address_type => :commercial)
      response = @carrier.find_rates location_fixtures[:ottawa], destination, package_fixtures[:book], :test => true
      assert_equal [delivery_date, delivery_date], response.rates.first.delivery_range
    end
  end

  def test_turn_around_time
    mock_response = xml_fixture('fedex/ottawa_to_beverly_hills_rate_response').gsub('<v6:DeliveryTimestamp>2011-07-29</v6:DeliveryTimestamp>', '')
    Timecop.freeze(DateTime.new(2013, 3, 11)) do
      delivery_date = Date.today + 8.days # FIVE_DAYS in fixture response, plus turn_around_time, plus weekend
      timestamp = (Time.now + 1.day).iso8601
      @carrier.expects(:commit).with do |request, _options|
        parsed_response = Hash.from_xml(request)
        parsed_response['RateRequest']['RequestedShipment']['ShipTimestamp'] == timestamp
      end.returns(mock_response)

      destination = ActiveShipping::Location.from(location_fixtures[:beverly_hills].to_hash, :address_type => :commercial)
      response = @carrier.find_rates location_fixtures[:ottawa], destination, package_fixtures[:book], :turn_around_time => 24, :test => true

      assert_equal [delivery_date, delivery_date], response.rates.first.delivery_range
    end
  end

  def test_transaction_id_sent_as_customer_transaction_id
    transaction_id = '9999-test'
    @carrier = FedEx.new(:key => '1111', :password => '2222', :account => '3333', :login => '4444', :transaction_id => transaction_id)
    @carrier.expects(:commit).with do |request, _options|
      parsed_request = Hash.from_xml(request)
      parsed_request['RateRequest']['TransactionDetail']['CustomerTransactionId'] == transaction_id
    end.returns(xml_fixture('fedex/ottawa_to_beverly_hills_rate_response'))

    destination = ActiveShipping::Location.from(location_fixtures[:beverly_hills].to_hash, :address_type => :commercial)
    @carrier.find_rates location_fixtures[:ottawa], destination, package_fixtures[:book], :test => true
  end

  def test_find_tracking_info_should_return_a_tracking_response
    @carrier.expects(:commit).returns(@tracking_response)
    assert_instance_of ActiveShipping::TrackingResponse, @carrier.find_tracking_info('077973360403984', :test => true)
  end

  def test_find_tracking_info_should_mark_shipment_as_delivered
    @carrier.expects(:commit).returns(@tracking_response)
    assert_equal true, @carrier.find_tracking_info('077973360403984').delivered?
  end

  def test_find_tracking_info_should_return_correct_carrier
    @carrier.expects(:commit).returns(@tracking_response)
    assert_equal :fedex, @carrier.find_tracking_info('077973360403984').carrier
  end

  def test_find_tracking_info_should_return_correct_carrier_name
    @carrier.expects(:commit).returns(@tracking_response)
    assert_equal 'FedEx', @carrier.find_tracking_info('077973360403984').carrier_name
  end

  def test_find_tracking_info_should_return_correct_status
    @carrier.expects(:commit).returns(@tracking_response)
    assert_equal :delivered, @carrier.find_tracking_info('077973360403984').status
  end

  def test_find_tracking_info_should_return_correct_status_code
    @carrier.expects(:commit).returns(@tracking_response)
    assert_equal 'dl', @carrier.find_tracking_info('077973360403984').status_code.downcase
  end

  def test_find_tracking_info_should_return_correct_status_description
    @carrier.expects(:commit).returns(@tracking_response)
    assert_equal 'delivered', @carrier.find_tracking_info('1Z5FX0076803466397').status_description.downcase
  end

  def test_find_tracking_info_should_return_delivery_signature
    @carrier.expects(:commit).returns(@tracking_response)
    assert_equal 'KKING', @carrier.find_tracking_info('077973360403984').delivery_signature
  end

  def test_find_tracking_info_should_return_destination_address
    @carrier.expects(:commit).returns(@tracking_response)
    result = @carrier.find_tracking_info('077973360403984')
    assert_equal 'sacramento', result.destination.city.downcase
    assert_equal 'CA', result.destination.state
  end

  def test_find_tracking_info_should_gracefully_handle_missing_destination_information
    @carrier.expects(:commit).returns(xml_fixture('fedex/tracking_response_no_destination'))
    result = @carrier.find_tracking_info('077973360403984')
    assert_equal 'unknown', result.destination.city.downcase
    assert_equal 'unknown', result.destination.state
    assert_equal 'ZZ', result.destination.country.code(:alpha2).to_s
  end

  def test_find_tracking_info_should_gracefully_handle_empty_destination_information
    @carrier.expects(:commit).returns(xml_fixture('fedex/tracking_response_empty_destination'))
    result = @carrier.find_tracking_info('077973360403984')
    assert_equal 'unknown', result.destination.city.downcase
    assert_equal 'unknown', result.destination.state
    assert_equal 'ZZ', result.destination.country.code(:alpha2).to_s
  end

  def test_find_tracking_info_should_return_correct_shipper_address
    @carrier.expects(:commit).returns(xml_fixture('fedex/tracking_response_with_shipper_address'))
    response = @carrier.find_tracking_info('927489999894450502838')
    assert_equal 'wallingford', response.shipper_address.city.downcase
    assert_equal 'CT', response.shipper_address.state
  end

  def test_find_tracking_info_should_gracefully_handle_missing_shipper_address
    @carrier.expects(:commit).returns(@tracking_response)
    response = @carrier.find_tracking_info('077973360403984')
    assert_equal nil, response.shipper_address
  end

  def test_find_tracking_info_should_return_correct_ship_time
    @carrier.expects(:commit).returns(@tracking_response)
    response = @carrier.find_tracking_info('927489999894450502838')
    assert_equal Time.parse("2008-12-03T00:00:00").utc, response.ship_time
  end

  def test_find_tracking_info_should_gracefully_handle_missing_ship_time
    @carrier.expects(:commit).returns(xml_fixture('fedex/tracking_response_no_ship_time'))
    response = @carrier.find_tracking_info('927489999894450502838')
    assert_equal nil, response.ship_time
  end

  def test_find_tracking_info_should_return_correct_actual_delivery_date
    @carrier.expects(:commit).returns(@tracking_response)
    response = @carrier.find_tracking_info('077973360403984')
    assert_equal Time.parse('2008-12-08T07:43:37-08:00').utc, response.actual_delivery_date
  end

  def test_find_tracking_info_should_gracefully_handle_missing_actual_delivery_date
    # This particular fixture doesn't contain an actual delivery date
    # (in addition to having a shipper address)
    @carrier.expects(:commit).returns(xml_fixture('fedex/tracking_response_with_shipper_address'))
    response = @carrier.find_tracking_info('9274899998944505028386')
    assert_equal nil, response.actual_delivery_date
  end

  def test_find_tracking_info_should_return_correct_scheduled_delivery_date
    @carrier.expects(:commit).returns(xml_fixture('fedex/tracking_response_with_estimated_delivery_date'))
    response = @carrier.find_tracking_info('1234567890111')
    assert_equal Time.parse('2013-10-15T00:00:00').utc, response.scheduled_delivery_date
  end

  def test_find_tracking_info_should_gracefully_handle_missing_scheduled_delivery_date
    @carrier.expects(:commit).returns(@tracking_response)
    response = @carrier.find_tracking_info('077973360403984')
    assert_equal nil, response.scheduled_delivery_date
  end

  def test_find_tracking_info_should_return_origin_address
    @carrier.expects(:commit).returns(@tracking_response)
    result = @carrier.find_tracking_info('077973360403984')
    assert_equal 'nashville', result.origin.city.downcase
    assert_equal 'TN', result.origin.state
  end

  def test_find_tracking_info_should_parse_response_into_correct_number_of_shipment_events
    @carrier.expects(:commit).returns(@tracking_response)
    response = @carrier.find_tracking_info('077973360403984', :test => true)
    assert_equal 6, response.shipment_events.size
  end

  def test_find_tracking_info_should_return_shipment_events_in_ascending_chronological_order
    @carrier.expects(:commit).returns(@tracking_response)
    response = @carrier.find_tracking_info('077973360403984', :test => true)
    assert_equal response.shipment_events.map(&:time).sort, response.shipment_events.map(&:time)
  end

  def test_find_tracking_info_should_not_include_events_without_an_address
    @carrier.expects(:commit).returns(@tracking_response)
    response = @carrier.find_tracking_info('077973360403984', :test => true)
    assert_nil response.shipment_events.find { |event| event.name == 'Shipment information sent to FedEx' }
  end

  def test_building_request_with_address_type_commercial_should_not_include_residential
    mock_response = xml_fixture('fedex/ottawa_to_beverly_hills_rate_response')
    expected_request = xml_fixture('fedex/ottawa_to_beverly_hills_commercial_rate_request')
    Time.any_instance.expects(:to_xml_value).returns("2009-07-20T12:01:55-04:00")

    @carrier.expects(:commit).with { |request, test_mode| Hash.from_xml(request) == Hash.from_xml(expected_request) && test_mode }.returns(mock_response)
    destination = ActiveShipping::Location.from(location_fixtures[:beverly_hills].to_hash, :address_type => :commercial)
    @carrier.find_rates( location_fixtures[:ottawa],
                         destination,
                         package_fixtures.values_at(:book, :wii), :test => true)
  end

  def test_building_freight_request_and_parsing_response
    expected_request = xml_fixture('fedex/freight_rate_request')
    mock_response = xml_fixture('fedex/freight_rate_response')
    Time.any_instance.expects(:to_xml_value).returns("2013-11-01T14:04:01-07:00")

    @carrier.expects(:commit).with { |request, test_mode| Hash.from_xml(request) == Hash.from_xml(expected_request) && test_mode }.returns(mock_response)

    # shipping and billing addresses below are provided by fedex test credentials

    shipping_location = Location.new( address1: '1202 Chalet Ln',
                                      address2: 'Do Not Delete - Test Account',
                                      city: 'Harrison',
                                      state: 'AR',
                                      postal_code: '72601',
                                      country: 'US')

    billing_location = Location.new(  address1: '2000 Freight LTL Testing',
                                      address2: 'Do Not Delete - Test Account',
                                      city: 'Harrison',
                                      state: 'AR',
                                      postal_code: '72601',
                                      country: 'US')

    freight_options = {
      account: '5555',
      billing_location: billing_location,
      payment_type: 'SENDER',
      freight_class: 'CLASS_050',
      packaging: 'PALLET',
      role: 'SHIPPER'
    }

    response = @carrier.find_rates( shipping_location,
                                    location_fixtures[:ottawa],
                                    [package_fixtures[:wii]],  :freight => freight_options, :test => true )

    assert_equal ["FedEx Freight Economy", "FedEx Freight Priority"], response.rates.map(&:service_name)
    assert_equal [66263, 68513], response.rates.map(&:price)

    assert response.success?, response.message
    assert_instance_of Hash, response.params
    assert_instance_of String, response.xml
    assert_instance_of Array, response.rates
    assert response.rates.length > 0, "There should've been more than 0 rates returned"

    rate = response.rates.first
    assert_equal 'FedEx', rate.carrier
    assert_equal 'USD', rate.currency
    assert_instance_of Fixnum, rate.total_price
    assert_instance_of Fixnum, rate.price
    assert_instance_of String, rate.service_name
    assert_instance_of String, rate.service_code
    assert_instance_of Array, rate.package_rates
    assert_equal [package_fixtures[:wii]], rate.packages

    package_rate = rate.package_rates.first
    assert_instance_of Hash, package_rate
    assert_instance_of Package, package_rate[:package]
    assert_nil package_rate[:rate]
  end

  def test_building_request_and_parsing_response
    expected_request = xml_fixture('fedex/ottawa_to_beverly_hills_rate_request')
    mock_response = xml_fixture('fedex/ottawa_to_beverly_hills_rate_response')
    Time.any_instance.expects(:to_xml_value).returns("2009-07-20T12:01:55-04:00")

    @carrier.expects(:commit).with { |request, test_mode| Hash.from_xml(request) == Hash.from_xml(expected_request) && test_mode }.returns(mock_response)
    response = @carrier.find_rates( location_fixtures[:ottawa],
                                    location_fixtures[:beverly_hills],
                                    package_fixtures.values_at(:book, :wii), :test => true)
    assert_equal ["FedEx Ground"], response.rates.map(&:service_name)
    assert_equal [3836], response.rates.map(&:price)

    assert response.success?, response.message
    assert_instance_of Hash, response.params
    assert_instance_of String, response.xml
    assert_instance_of Array, response.rates
    assert response.rates.length > 0, "There should've been more than 0 rates returned"

    rate = response.rates.first
    assert_equal 'FedEx', rate.carrier
    assert_equal 'CAD', rate.currency
    assert_instance_of Fixnum, rate.total_price
    assert_instance_of Fixnum, rate.price
    assert_instance_of String, rate.service_name
    assert_instance_of String, rate.service_code
    assert_instance_of Array, rate.package_rates
    assert_equal package_fixtures.values_at(:book, :wii), rate.packages

    package_rate = rate.package_rates.first
    assert_instance_of Hash, package_rate
    assert_instance_of Package, package_rate[:package]
    assert_nil package_rate[:rate]
  end

  def test_parsing_response_with_no_rate_reply
    expected_request = xml_fixture('fedex/ottawa_to_beverly_hills_rate_request')
    mock_response = xml_fixture('fedex/unknown_fedex_document_reply')
    Time.any_instance.expects(:to_xml_value).returns("2009-07-20T12:01:55-04:00")

    @carrier.expects(:commit).with { |request, test_mode| Hash.from_xml(request) == Hash.from_xml(expected_request) && test_mode }.returns(mock_response)
    exception = assert_raises ActiveShipping::ResponseContentError do
      @carrier.find_rates( location_fixtures[:ottawa],
                           location_fixtures[:beverly_hills],
                           package_fixtures.values_at(:book, :wii), :test => true)
    end
    message = "Invalid document \n\n<?xml version='1.0' encoding='UTF-8'?>\n<RandomElement xmlns:v6='http://fedex.com/ws/rate/v6' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>\n</RandomElement>\n"
    assert_equal message, exception.message
  end

  def test_service_name_for_code
    FedEx::SERVICE_TYPES.each do |capitalized_name, readable_name|
      assert_equal readable_name, FedEx.service_name_for_code(capitalized_name)
    end
  end

  def test_service_name_for_code_handles_yet_unknown_codes
    assert_equal "FedEx Express Saver Saturday Delivery", FedEx.service_name_for_code('FEDEX_EXPRESS_SAVER_SATURDAY_DELIVERY')
    assert_equal "FedEx Some Weird Rate", FedEx.service_name_for_code('SOME_WEIRD_RATE')
  end

  def test_delivery_range_based_on_delivery_date
    mock_response = xml_fixture('fedex/ottawa_to_beverly_hills_rate_response').gsub('CAD', 'UKL')

    @carrier.expects(:commit).returns(mock_response)
    rate_estimates = @carrier.find_rates( location_fixtures[:ottawa],
                                          location_fixtures[:beverly_hills],
                                          package_fixtures.values_at(:book, :wii), :test => true)

    delivery_date = Date.new(2011, 7, 29)
    assert_equal delivery_date, rate_estimates.rates[0].delivery_date
    assert_equal [delivery_date] * 2, rate_estimates.rates[0].delivery_range
  end

  def test_delivery_date_from_transit_time
    mock_response = xml_fixture('fedex/raterequest_reply').gsub('CAD', 'UKL')

    @carrier.expects(:commit).returns(mock_response)

    today = DateTime.civil(2013, 3, 15, 0, 0, 0, "-4")

    Timecop.freeze(today) do
      rate_estimates = @carrier.find_rates( location_fixtures[:ottawa],
                                            location_fixtures[:beverly_hills],
                                            package_fixtures.values_at(:book, :wii), :test => true)

      # the above fixture will specify a transit time of 5 days, with 2 weekend days accounted for
      delivery_date = Date.today + 7
      assert_equal delivery_date, rate_estimates.rates[0].delivery_date
    end
  end

  def test_failure_to_parse_invalid_xml_results_in_a_useful_error
    mock_response = xml_fixture('fedex/invalid_fedex_reply')

    @carrier.expects(:commit).returns(mock_response)

    assert_raises ActiveShipping::ResponseContentError do
    @carrier.find_rates(
        location_fixtures[:ottawa],
        location_fixtures[:beverly_hills],
        package_fixtures.values_at(:book, :wii),
        :test => true
      )
    end
  end

  def test_response_without_notifications_raises_useful_error
    mock_response = xml_fixture('fedex/reply_without_notifications')

    @carrier.expects(:commit).returns(mock_response)

    assert_raises ActiveShipping::ResponseError do
    @carrier.find_rates(
        location_fixtures[:ottawa],
        location_fixtures[:beverly_hills],
        package_fixtures.values_at(:book, :wii),
        :test => true
      )
    end
  end
end