# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Spree::OrderShipping do
  include ActiveSupport::Testing::TimeHelpers

  let(:order) { create(:order_ready_to_ship, line_items_count: 1) }

  def emails
    ActionMailer::Base.deliveries
  end

  shared_examples 'shipment shipping' do
    it "marks the inventory units as shipped" do
      expect { subject }.to change { order.inventory_units.reload.map(&:state) }.from(['on_hand']).to(['shipped'])
    end

    it "creates a carton with the shipment's inventory units" do
      expect { subject }.to change { order.cartons.count }.by(1)
      expect(subject.inventory_units).to match_array(shipment.inventory_units)
    end

    describe "shipment email" do
      it "should send a shipment email" do
        expect {
          perform_enqueued_jobs {
            subject
          }
        }.to change { emails.size }.by(1)
        expect(emails.last.subject).to eq("#{order.store.name} Shipment Notification ##{order.number}")
      end
    end

    it "updates the order shipment state" do
      expect { subject }.to change { order.reload.shipment_state }.from('ready').to('shipped')
    end

    it "updates shipment.shipped_at" do
      now = Time.current
      travel_to(now) do
        expect { subject }.to change { shipment.shipped_at }.from(nil).to be_within(1.second).of(now)
      end
    end

    it "updates order.updated_at" do
      future = 1.minute.from_now
      expect do
        travel_to(future) do
          subject
        end
      end.to change { order.updated_at }.to be_within(1.second).of(future)
    end
  end

  describe "#ship" do
    subject do
      order.shipping.ship(
        inventory_units:,
        stock_location:,
        address:,
        shipping_method:
      )
    end

    let(:shipment) { order.shipments.to_a.first }
    let(:inventory_units) { shipment.inventory_units }
    let(:stock_location) { shipment.stock_location }
    let(:address) { order.ship_address }
    let(:shipping_method) { shipment.shipping_method }

    it_behaves_like 'shipment shipping'

    context "with an external_number" do
      subject do
        order.shipping.ship(
          inventory_units:,
          stock_location:,
          address:,
          shipping_method:,
          external_number: 'some-external-number'
        )
      end

      it "sets the external_number" do
        expect(subject.external_number).to eq 'some-external-number'
      end
    end

    context "with a tracking number" do
      subject do
        order.shipping.ship(
          inventory_units:,
          stock_location:,
          address:,
          shipping_method:,
          tracking_number: 'tracking-number'
        )
      end

      it "sets the tracking-number" do
        expect(subject.tracking).to eq 'tracking-number'
      end
    end

    context "when told to suppress the mailer" do
      subject do
        order.shipping.ship(
          inventory_units:,
          stock_location:,
          address:,
          shipping_method:,
          suppress_mailer: true
        )
      end

      it "does not send a shipment email" do
        expect { subject }.to_not change { emails.size }
      end
    end
  end

  describe "#ship_shipment" do
    subject { order.shipping.ship_shipment(shipment) }

    let(:shipment) { order.shipments.to_a.first }

    it_behaves_like 'shipment shipping'

    context "when not all units are shippable" do
      let(:order) { create(:order_ready_to_ship, line_items_count: 2) }
      let(:shippable_line_item) { order.line_items.first }
      let(:unshippable_line_item) { order.line_items.last }

      before do
        unshippable_line_item.inventory_units.each(&:cancel!)
      end

      it "only ships the shippable ones" do
        expect(subject.inventory_units).to match_array(shippable_line_item.inventory_units)
      end
    end

    context "when all units are canceled or shipped" do
      let(:order) { create(:order_ready_to_ship, line_items_count: 2) }

      before { Spree::OrderCancellations.new(order).short_ship([order.inventory_units.first]) }

      it "updates the order shipment state" do
        expect { subject }.to change { order.reload.shipment_state }.from('ready').to('shipped')
      end
    end

    context "with an external_number" do
      subject do
        order.shipping.ship_shipment(
          shipment,
          external_number: 'some-external-number'
        )
      end

      it "sets the external_number" do
        expect(subject.external_number).to eq 'some-external-number'
      end
    end

    context "with a tracking number" do
      subject do
        order.shipping.ship_shipment(
          shipment,
          tracking_number: 'tracking-number'
        )
      end

      it "sets the tracking-number" do
        expect(subject.tracking).to eq 'tracking-number'
      end
    end

    # TODO: We can remove this once Shipment#ship! is called by
    # OrderShipping#ship rather than vice versa
    context "when the tracking number is already on the shipment" do
      before do
        shipment.update!(tracking: 'tracking-number')
      end

      it "sets the tracking-number" do
        expect(subject.tracking).to eq 'tracking-number'
      end
    end

    context "when the shipment has been partially shipped previously" do
      let(:order) { create(:order_ready_to_ship, line_items_count: 2) }
      let(:inventory_units) { shipment.inventory_units.to_a }
      let(:shipped_inventory) { [inventory_units.first] }
      let(:unshipped_inventory) { [inventory_units.last] }

      before do
        order.shipping.ship(
          inventory_units: shipped_inventory,
          stock_location: shipment.stock_location,
          address: order.ship_address,
          shipping_method: shipment.shipping_method
        )
      end

      it "marks the inventory units as shipped" do
        expect { subject }.to change { unshipped_inventory.map(&:reload).map(&:state) }.from(['on_hand']).to(['shipped'])
      end

      it "creates a carton with the shipment's inventory units" do
        expect { subject }.to change { order.cartons.count }.by(1)
        expect(subject.inventory_units).to match_array(unshipped_inventory)
      end
    end

    context "when a second shipment is shipped" do
      let(:order) { create(:order_ready_to_ship) }

      it "sets the order to shipped state" do
        order.shipping.ship_shipment(order.shipments.first)

        unshipped_shipment = create(:shipment, order:, state: "ready")

        order.reload
        order.recalculate

        expect { order.shipping.ship_shipment(unshipped_shipment) }.to(change { order.shipment_state }.from("partial").to("shipped"))
      end
    end

    context "when told to suppress the mailer" do
      subject do
        order.shipping.ship_shipment(
          shipment,
          suppress_mailer: true
        )
      end

      it "does not send a shipment email" do
        expect { subject }.to_not change { emails.size }
      end
    end

    context "with stale inventory units (regression test)" do
      let(:order) { FactoryBot.create(:order_ready_to_ship, line_items_count: 1) }
      let(:shipment) do
        FactoryBot.create(
          :shipment,
          order:
        )
      end

      before { shipment.ready! }

      it "updates the state to shipped" do
        order.shipping.ship_shipment(shipment)
        expect(shipment.reload.state).to eq "shipped"
      end
    end
  end
end
