# frozen_string_literal: true

require 'spec_helper'
require 'benchmark'

RSpec.describe Spree::Shipment do
  let(:order) { build(:order) }
  let(:shipping_method) { build(:shipping_method, name: "UPS") }
  let(:shipment) do
    shipment = Spree::Shipment.new(order:)
    allow(shipment).to receive_messages(shipping_method:)
    shipment.state = 'pending'
    shipment
  end

  let(:charge) { build(:adjustment) }
  let(:variant) { build(:variant) }

  it 'is backordered if one of its inventory_units is backordered' do
    unit1 = create(:inventory_unit)
    unit2 = create(:inventory_unit)
    allow(unit1).to receive(:backordered?) { false }
    allow(unit2).to receive(:backordered?) { true }
    allow(shipment).to receive_messages(inventory_units: [unit1, unit2])
    expect(shipment).to be_backordered
  end

  context "display_cost" do
    it "retuns a Spree::Money" do
      allow(shipment).to receive(:cost) { 21.22 }
      expect(shipment.display_cost).to eq Spree::Money.new(21.22)
    end
  end

  context "display_item_cost" do
    it "retuns a Spree::Money" do
      allow(shipment).to receive(:item_cost) { 21.22 }
      expect(shipment.display_item_cost).to eq Spree::Money.new(21.22)
    end
  end

  it "#item_cost" do
    shipment = Spree::Shipment.new(
      order: build_stubbed(:order_with_totals, line_items: [build_stubbed(:line_item)])
    )
    expect(shipment.item_cost).to eql(10.0)
  end

  context "manifest" do
    let(:order) { Spree::Order.create }
    let!(:variant) { create(:variant) }
    let!(:line_item) { order.contents.add variant }
    let!(:shipment) { order.create_proposed_shipments.first }

    it "returns variant expected" do
      expect(shipment.manifest.first.variant).to eq variant
    end

    context "variant was removed" do
      before { variant.product.destroy }

      it "still returns variant expected" do
        expect(shipment.manifest.first.variant).to eq variant
      end
    end

    describe "with soft-deleted products or variants" do
      let(:variant) { create(:variant) }
      let(:order) { create(:order, distributor: variant.supplier) }

      context "when the variant is soft-deleted" do
        it "can still access the variant" do
          order.line_items.first.variant.delete

          variants = shipment.reload.manifest.map(&:variant).uniq
          expect(variants).to eq [order.line_items.first.variant]
        end
      end

      context "when the product is soft-deleted" do
        it "can still access the variant" do
          order.line_items.first.product.delete

          variants = shipment.reload.manifest.map(&:variant)
          expect(variants).to eq [order.line_items.first.variant]
        end
      end
    end

    context "with variant override", feature: :inventory do
      let(:order) { create(:order, distributor: variant.supplier) }

      it "returns the scoped variant" do
        create(:variant_override, hub: variant.supplier, variant:, price: 25.00)

        expect(shipment.manifest.first.variant.price).to eq 25.00
      end
    end
  end

  context 'shipping_rates' do
    let(:shipment) { create(:shipment) }
    let(:shipping_method1) { create(:shipping_method) }
    let(:shipping_method2) { create(:shipping_method) }
    let(:shipping_rates) {
      [
        Spree::ShippingRate.new(shipping_method: shipping_method1, cost: 10.00, selected: true),
        Spree::ShippingRate.new(shipping_method: shipping_method2, cost: 20.00)
      ]
    }

    it 'returns shipping_method from selected shipping_rate' do
      shipment.shipping_rates.delete_all
      shipment.shipping_rates.create shipping_method: shipping_method1, cost: 10.00, selected: true
      expect(shipment.shipping_method).to eq shipping_method1
    end

    context 'refresh_rates' do
      let(:mock_estimator) { double('estimator', shipping_rates:) }

      it 'should request new rates, and maintain shipping_method selection' do
        expect(OrderManagement::Stock::Estimator).
          to receive(:new).with(shipment.order).and_return(mock_estimator)
        # The first call is for the original shippping method,
        #   the second call is for the shippping method after the Estimator was executed
        allow(shipment).to receive(:shipping_method).and_return(shipping_method2, shipping_method1)

        expect(shipment.refresh_rates).to eq shipping_rates
        expect(shipment.reload.selected_shipping_rate.shipping_method_id).to eq shipping_method2.id
      end

      it 'should handle no shipping_method selection' do
        expect(OrderManagement::Stock::Estimator).
          to receive(:new).with(shipment.order).and_return(mock_estimator)
        allow(shipment).to receive_messages(shipping_method: nil)
        expect(shipment.refresh_rates).to eq shipping_rates
        expect(shipment.reload.selected_shipping_rate).not_to be_nil
      end

      it 'should not refresh if shipment is shipped' do
        expect(OrderManagement::Stock::Estimator).not_to receive(:new)
        shipment.shipping_rates.delete_all
        allow(shipment).to receive_messages(shipped?: true)
        expect(shipment.refresh_rates).to eq []
      end

      context 'to_package' do
        it 'should use symbols for states when adding contents to package' do
          shipment = Spree::Shipment.new(order: build_stubbed(:order))
          allow(shipment).
            to receive_message_chain(
              :inventory_units,
              includes: [
                build_stubbed(
                  :inventory_unit,
                  shipment:,
                  variant: build_stubbed(:variant),
                  state: 'on_hand'
                ),
                build_stubbed(
                  :inventory_unit,
                  shipment:,
                  variant: build_stubbed(:variant),
                  state: 'backordered'
                )
              ]
            )
          package = shipment.to_package
          expect(package.on_hand.count).to eq 1
          expect(package.backordered.count).to eq 1
        end
      end
    end
  end

  context "#update!" do
    shared_examples_for "immutable once shipped" do
      it "should remain in shipped state once shipped" do
        shipment.state = 'shipped'
        expect(shipment).to receive(:update_columns).
          with(state: 'shipped', updated_at: kind_of(Time))
        shipment.update!(order)
      end
    end

    shared_examples_for "pending if backordered" do
      it "should have a state of pending if backordered" do
        unit = create(:inventory_unit)
        allow(unit).to receive(:backordered?) { true }
        allow(shipment).to receive_messages(inventory_units: [unit])
        expect(shipment).to receive(:update_columns).
          with(state: 'pending', updated_at: kind_of(Time))
        shipment.update!(order)
      end
    end

    context "when order is canceled" do
      it "should result in a 'pending' state" do
        allow(order).to receive(:canceled?) { true }

        expect(shipment).to receive(:update_columns).
          with(state: 'canceled', updated_at: kind_of(Time))
        shipment.update!(order)
      end
    end

    context "when order cannot ship" do
      it "should result in a 'pending' state" do
        allow(order).to receive(:can_ship?) { false }

        expect(shipment).to receive(:update_columns).
          with(state: 'pending', updated_at: kind_of(Time))
        shipment.update!(order)
      end
    end

    context "when order can ship" do
      before { allow(order).to receive(:can_ship?) { true } }

      context "when order is paid" do
        before { allow(order).to receive(:paid?) { true } }

        it "should result in a 'ready' state" do
          expect(shipment).to receive(:update_columns).
            with(state: 'ready', updated_at: kind_of(Time))
          shipment.update!(order)
        end

        it_should_behave_like 'immutable once shipped'

        it_should_behave_like 'pending if backordered'

        context "when order has a credit owed" do
          before { allow(order).to receive(:payment_state) { 'credit_owed' } }

          it "should result in a 'ready' state" do
            shipment.state = 'pending'
            expect(shipment).to receive(:update_columns).
              with(state: 'ready', updated_at: kind_of(Time))
            shipment.update!(order)
          end

          it_should_behave_like 'immutable once shipped'

          it_should_behave_like 'pending if backordered'
        end
      end

      context "when order has balance due" do
        before { allow(order).to receive(:paid?) { false } }

        it "should result in a 'pending' state" do
          shipment.state = 'ready'
          expect(shipment).to receive(:update_columns).
            with(state: 'pending', updated_at: kind_of(Time))
          shipment.update!(order)
        end

        it_should_behave_like 'immutable once shipped'

        it_should_behave_like 'pending if backordered'
      end
    end

    context "when shipment state changes to shipped" do
      it "should call after_ship" do
        shipment.state = 'pending'
        expect(shipment).to receive :after_ship
        allow(shipment).to receive_messages determine_state: 'shipped'
        expect(shipment).to receive(:update_columns).
          with(state: 'shipped', updated_at: kind_of(Time))
        shipment.update!(order)
      end
    end
  end

  describe "#finalize!" do
    subject(:shipment) { order.shipments.first }
    let(:variant) { order.variants.first }
    let(:order) { create(:order_ready_for_confirmation) }

    it "reduces stock" do
      variant.on_hand = 5

      expect { shipment.finalize! }
        .to change { variant.on_hand }.from(5).to(4)
    end

    it "reduces stock of a variant override", feature: :inventory do
      variant.on_hand = 5
      variant_override = VariantOverride.create!(
        variant:,
        hub: order.distributor,
        count_on_hand: 7,
        on_demand: false,
      )

      expect {
        shipment.finalize!
        variant.reload
        variant_override.reload
      }
        .to change { variant_override.count_on_hand }.from(7).to(6)
        .and change { variant.on_hand }.by(0)
    end
  end

  context "when order is completed" do
    before do
      allow(order).to receive_messages completed?: true
      allow(order).to receive_messages canceled?: false
    end

    it "should validate with inventory" do
      shipment.inventory_units = [create(:inventory_unit)]
      expect(shipment.valid?).to be_truthy
    end
  end

  context "#cancel" do
    it 'cancels the shipment' do
      allow(shipment).to receive(:ensure_correct_adjustment)
      allow(shipment.order).to receive(:update_order!)

      shipment.state = 'pending'
      expect(shipment).to receive(:after_cancel)
      shipment.cancel!
      expect(shipment.state).to eq 'canceled'
    end

    it 'restocks the items' do
      unit = double(:inventory_unit, variant:)
      allow(unit).to receive(:quantity) { 1 }
      allow(shipment).to receive_message_chain(:inventory_units,
                                               :group_by,
                                               map: [unit])
      expect(variant).to receive(:move).with(1)
      shipment.after_cancel
    end
  end

  context "#resume" do
    it 'will determine new state based on order' do
      allow(shipment).to receive(:ensure_correct_adjustment)
      allow(shipment.order).to receive(:update_order!)

      shipment.state = 'canceled'
      expect(shipment).to receive(:determine_state).and_return(:ready)
      expect(shipment).to receive(:after_resume)
      shipment.resume!
      expect(shipment.state).to eq 'ready'
    end

    it 'unstocks the items' do
      unit = create(:inventory_unit, variant:)
      allow(unit).to receive(:quantity) { 1 }
      allow(shipment).to receive_message_chain(:inventory_units,
                                               :group_by,
                                               map: [unit])
      expect(variant).to receive(:move).with(-1)
      shipment.after_resume
    end

    it 'will determine new state based on order' do
      allow(shipment).to receive(:ensure_correct_adjustment)
      allow(shipment.order).to receive(:update_order!)

      shipment.state = 'canceled'
      expect(shipment).to receive(:determine_state).twice.and_return('ready')
      expect(shipment).to receive(:after_resume)
      shipment.resume!
      # Shipment is pending because order is already paid
      expect(shipment.state).to eq 'pending'
    end
  end

  context "#ship" do
    before do
      allow(order).to receive(:update_order!)
      allow(shipment).to receive_messages(update_order: true, state: 'ready')
      allow(shipment).to receive_messages(fee_adjustment: charge)
      allow(shipping_method).to receive(:create_adjustment)
      allow(shipment).to receive(:ensure_correct_adjustment)
    end

    it "should update shipped_at timestamp" do
      allow(shipment).to receive(:send_shipped_email)
      shipment.ship!
      expect(shipment.shipped_at).not_to be_nil
      # Ensure value is persisted
      shipment.reload
      expect(shipment.shipped_at).not_to be_nil
    end

    it "should send a shipment email if order.send_shipment_email is true" do
      shipment.order.send_shipment_email = true
      mail_message = double 'Mail::Message'
      shipment_id = nil
      expect(Spree::ShipmentMailer).to receive(:shipped_email) { |*args|
        shipment_id = args[0]
        mail_message
      }
      expect(mail_message).to receive :deliver_later
      shipment.ship!
      expect(shipment_id).to eq shipment.id
    end

    it "should finalize the shipment's adjustment" do
      allow(shipment).to receive(:send_shipped_email)
      shipment.ship!
      expect(shipment.fee_adjustment.state).to eq 'finalized'
      expect(shipment.fee_adjustment).to be_immutable
    end
  end

  context "#ready" do
    # Regression test for #2040
    it "cannot ready a shipment for an order if the order is unpaid" do
      allow(order).to receive_messages(paid?: false)
      assert !shipment.can_ready?
    end
  end

  context "ensure_correct_adjustment" do
    before do
      shipment.save
      allow(shipment).to receive(:reload)
    end

    it "should create adjustment when not present" do
      allow(shipment).to receive_messages(fee_adjustment: nil)
      allow(shipment).to receive_messages(selected_shipping_rate_id: 1)
      expect(shipping_method).to receive(:create_adjustment).with(shipment.adjustment_label,
                                                                  shipment, true, "open")
      shipment.__send__(:ensure_correct_adjustment)
    end

    it "should update originator when adjustment is present" do
      allow(shipment).
        to receive_messages(selected_shipping_rate: Spree::ShippingRate.new(cost: 10.00))
      adjustment = build(:adjustment)
      allow(shipment).to receive_messages(fee_adjustment: adjustment, update_columns: true)
      allow(adjustment).to receive(:open?) { true }
      expect(shipment.fee_adjustment).to receive(:originator=).with(shipping_method)
      expect(shipment.fee_adjustment).to receive(:label=).with(shipment.adjustment_label)
      expect(shipment.fee_adjustment).to receive(:amount=).with(10.00)
      allow(shipment.fee_adjustment).to receive(:save!)
      expect(shipment.fee_adjustment).to receive(:reload)
      shipment.__send__(:ensure_correct_adjustment)
    end

    it 'should not update amount if adjustment is not open?' do
      allow(shipment).
        to receive_messages(selected_shipping_rate: Spree::ShippingRate.new(cost: 10.00))
      adjustment = build(:adjustment)
      allow(shipment).to receive_messages(fee_adjustment: adjustment, update_columns: true)
      allow(adjustment).to receive(:open?) { false }
      expect(shipment.fee_adjustment).to receive(:originator=).with(shipping_method)
      expect(shipment.fee_adjustment).to receive(:label=).with(shipment.adjustment_label)
      expect(shipment.fee_adjustment).not_to receive(:amount=).with(10.00)
      allow(shipment.fee_adjustment).to receive(:save!)
      expect(shipment.fee_adjustment).to receive(:reload)
      shipment.__send__(:ensure_correct_adjustment)
    end
  end

  describe "#update_amounts" do
    it "persists the shipping cost from the shipping fee adjustment" do
      allow(shipment).to receive(:fee_adjustment) { double(:adjustment, amount: 10) }
      expect(shipment).to receive(:update_columns).with(cost: 10, updated_at: kind_of(Time))

      shipment.update_amounts
    end
  end

  context "after_save" do
    it "should run correct callbacks" do
      expect(shipment).to receive(:ensure_correct_adjustment)
      expect(shipment).to receive(:update_adjustments)
      shipment.run_callbacks(:save)
    end
  end

  context "currency" do
    it "returns the order currency" do
      expect(shipment.currency).to eq order.currency
    end
  end

  context "#tracking_url" do
    it "uses shipping method to determine url" do
      expect(shipping_method).to receive(:build_tracking_url).with('1Z12345').and_return(:some_url)
      shipment.tracking = '1Z12345'

      expect(shipment.tracking_url).to eq :some_url
    end
  end

  context "set up new inventory units" do
    let(:variant) { double("Variant", id: 9) }
    let(:inventory_units) { double }
    let(:params) do
      { variant_id: variant.id, state: 'on_hand', order_id: order.id }
    end

    before { allow(shipment).to receive_messages(inventory_units:) }

    it "associates variant and order" do
      expect(inventory_units).to receive(:create).with(params)
      unit = shipment.set_up_inventory('on_hand', variant, order)
    end
  end

  # Regression test for #3349
  context "#destroy" do
    it "destroys linked shipping_rates" do
      reflection = Spree::Shipment.reflect_on_association(:shipping_rates)
      reflection.options[:dependent] = :destroy
    end
  end
end
