# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Api::V0::ShipmentsController do
  render_views

  let!(:shipment) { create(:shipment) }
  let(:attributes) { %w[id tracking number cost shipped_at order_id] }
  let(:current_api_user) { build(:user) }

  before do
    allow(controller).to receive(:spree_current_user) { current_api_user }
  end

  context "as a non-admin" do
    it "cannot make a shipment ready" do
      api_put :ready, order_id: shipment.order.to_param, id: shipment.to_param
      assert_unauthorized!
    end

    it "cannot make a shipment shipped" do
      api_put :ship, order_id: shipment.order.to_param, id: shipment.to_param
      assert_unauthorized!
    end
  end

  context "as an admin" do
    let(:current_api_user) { build(:admin_user) }
    let!(:order) { shipment.order }
    let(:order_ship_address) { create(:address) }
    let!(:variant) { create(:variant) }
    let(:params) do
      { quantity: 2,
        variant_id: variant.to_param,
        order_id: order.number,
        format: :json }
    end
    let(:error_message) { "broken shipments creation" }

    before do
      order.update_attribute :ship_address_id, order_ship_address.id
      order.update_attribute :distributor, variant.supplier
      shipment.shipping_method.distributors << variant.supplier
    end

    context '#create' do
      it 'creates a shipment if order does not have a shipment' do
        order.shipment.destroy
        order.reload

        spree_post :create, params

        expect_valid_response
        expect(order.shipment.reload.inventory_units.size).to eq 2
        expect(order.reload.line_items.first.variant.price).to eq(variant.price)
      end

      it 'updates and returns exiting shipment, if order already has a shipment' do
        original_shipment_id = order.shipment.id

        spree_post :create, params

        expect(json_response["id"]).to eq(original_shipment_id)
        expect_valid_response
        expect(order.shipment.reload.inventory_units.size).to eq 2
        expect(order.reload.line_items.first.variant.price).to eq(variant.price)
      end

      it 'updates existing shipment with variant override if an VO is sent', feature: :inventory do
        hub = create(:distributor_enterprise)
        order.update_attribute(:distributor, hub)
        shipment.shipping_method.distributors << hub
        variant_override = create(:variant_override, hub:, variant:)

        spree_post :create, params

        expect_valid_response
        expect(order.shipment.reload.inventory_units.size).to eq 2
        expect(order.reload.line_items.first.price).to eq(variant_override.price)
      end

      it 'returns error code when adding to order contents fails' do
        make_order_contents_fail

        spree_post :create, params

        expect_error_response
      end

      it "applies any enterprise fees that are present" do
        order_cycle = create(:simple_order_cycle,
                             coordinator: order.distributor,
                             coordinator_fees: [create(:enterprise_fee, amount: 20)],
                             distributors: [order.distributor],
                             variants: [variant])
        order.update!(order_cycle_id: order_cycle.id)
        spree_post :create, params

        expect(order.line_item_adjustments.where(originator_type: "EnterpriseFee")).to be_present
      end
    end

    it "can make a shipment ready" do
      allow_any_instance_of(Spree::Order).to receive_messages(paid?: true, complete?: true)
      api_put :ready, order_id: shipment.order.to_param, id: shipment.to_param

      expect(json_response.keys).to include(*attributes)
      expect(json_response["state"]).to eq("ready")
      expect(shipment.reload.state).to eq("ready")
    end

    it "checks if shipment is ready already" do
      allow_any_instance_of(Spree::Order).to receive_messages(paid?: true, complete?: true)
      shipment.ready!

      api_put :ready, order_id: shipment.order.to_param, id: shipment.to_param

      expect(json_response.keys).to include(*attributes)
      expect(json_response["state"]).to eq("ready")
      expect(shipment.reload.state).to eq("ready")
    end

    it "cannot make a shipment ready if the order is unpaid" do
      allow_any_instance_of(Spree::Order).to receive_messages(paid?: false)
      api_put :ready, order_id: shipment.order.to_param, id: shipment.to_param

      expect(json_response["error"]).to eq("Cannot ready shipment.")
      expect(response).to have_http_status(:unprocessable_entity)
    end

    describe "#add and #remove" do
      let(:order) { create :completed_order_with_totals }
      let(:line_item) { order.line_items.first }
      let(:existing_variant) { line_item.variant }
      let(:new_variant) { create(:variant) }
      let(:params) {
        {
          quantity: 2,
          order_id: order.to_param,
          id: order.shipments.first.to_param
        }
      }

      before do
        line_item.update!(quantity: 3)
      end

      context 'for completed shipments' do
        it 'adds a variant to a shipment' do
          expect {
            api_put :add, params.merge(variant_id: new_variant.to_param)
            expect(response).to have_http_status(:ok)
          }.to change { inventory_units_for(new_variant).size }.by(2)
        end

        it 'adjusts stock when adding a variant' do
          expect {
            api_put :add, params.merge(variant_id: new_variant.to_param)
          }.to change { new_variant.reload.on_hand }.by(-2)
        end

        it 'removes a variant from a shipment' do
          expect {
            api_put :remove, params.merge(variant_id: existing_variant.to_param)
            expect(response).to have_http_status(:ok)
          }.to change { inventory_units_for(existing_variant).size }.by(-2)
        end

        it 'adjusts stock when removing a variant' do
          expect {
            api_put :remove, params.merge(variant_id: existing_variant.to_param)
          }.to change { existing_variant.reload.on_hand }.by(2)
        end

        it 'does not adjust stock when removing a variant' do
          expect {
            api_put :remove, params.merge(variant_id: existing_variant.to_param,
                                          restock_item: 'false')
          }.to change { existing_variant.reload.on_hand }.by(0)
        end
      end

      context "for canceled orders" do
        before do
          expect(order.cancel).to eq true
        end

        it "doesn't adjust stock when adding a variant" do
          expect {
            api_put :add, params.merge(variant_id: existing_variant.to_param)
            expect(response).to have_http_status(:unprocessable_entity)
          }.not_to change { existing_variant.reload.on_hand }
        end

        it "doesn't adjust stock when removing a variant" do
          expect {
            api_put :remove, params.merge(variant_id: existing_variant.to_param)
            expect(response).to have_http_status(:unprocessable_entity)
          }.not_to change { existing_variant.reload.on_hand }
        end
      end

      context "with shipping fees" do
        let!(:distributor) { create(:distributor_enterprise) }
        let(:fee_amount) { 10 }
        let!(:shipping_method_with_fee) {
          create(:shipping_method_with, :shipping_fee, distributors: [distributor],
                                                       shipping_fee: fee_amount)
        }
        let!(:order_cycle) { create(:order_cycle, distributors: [distributor]) }
        let!(:order) {
          create(:completed_order_with_totals, order_cycle:, distributor:)
        }
        let(:shipping_fee) { order.reload.shipment.adjustments.first }

        before do
          order.shipments.first.shipping_methods = [shipping_method_with_fee]
          order.select_shipping_method(shipping_method_with_fee.id)
          order.update_order!
        end

        context "adding item to a shipment" do
          it "updates the shipping fee" do
            expect {
              api_put :add, params.merge(variant_id: new_variant.to_param)
            }.to change { order.reload.shipment.adjustments.first.amount }.by(20)
          end
        end

        context "removing item from a shipment" do
          it "updates the shipping fee" do
            expect {
              api_put :remove, params.merge(variant_id: existing_variant.to_param)
            }.to change { order.reload.shipment.adjustments.first.amount }.by(-20)
          end
        end
      end
    end

    describe "#update" do
      let!(:distributor) { create(:distributor_enterprise) }
      let!(:shipping_method1) {
        create(:shipping_method_with, :flat_rate, distributors: [distributor], amount: 10)
      }
      let!(:shipping_method2) {
        create(:shipping_method_with, :flat_rate, distributors: [distributor], amount: 20)
      }
      let!(:order_cycle) { create(:order_cycle, distributors: [distributor]) }
      let!(:order) {
        create(:completed_order_with_totals, order_cycle:, distributor:)
      }
      let(:new_shipping_rate) {
        order.shipment.shipping_rates.select{ |sr| sr.shipping_method == shipping_method2 }.first
      }
      let(:params) {
        {
          id: order.shipment.number,
          order_id: order.number,
          shipment: {
            selected_shipping_rate_id: new_shipping_rate.id
          }
        }
      }

      before do
        order.shipments.first.shipping_methods = [shipping_method1, shipping_method2]
        order.shipments.each(&:refresh_rates)
        order.select_shipping_method(shipping_method1.id)
        order.update_order!
        order.update_columns(
          payment_total: 60,
          payment_state: "paid"
        )
      end

      context "when an order has multiple shipping methods available which could be chosen" do
        context "changing the selected shipping method" do
          it "updates the order's totals and states" do
            expect(order.shipment.shipping_method).to eq shipping_method1
            expect(order.shipment.cost).to eq 10
            expect(order.total).to eq 60 # item total is 50, shipping cost is 10
            expect(order.payment_state).to eq "paid" # order is fully paid for

            api_put :update, params
            expect(response).to have_http_status :ok

            order.reload

            expect(order.shipment.shipping_method).to eq shipping_method2
            expect(order.shipment.cost).to eq 20
            expect(order.total).to eq 70 # item total is 50, shipping cost is 20
            expect(order.payment_state).to eq "balance_due" # total changed, payment is due
          end

          it "updates closed adjustments" do
            expect {
              api_put :update, params
              expect(response).to have_http_status :ok
            }.to change { order.reload.shipment.fee_adjustment.amount }
          end
        end
      end
    end

    context "#ship" do
      before do
        allow_any_instance_of(Spree::Order).to receive_messages(paid?: true, complete?: true)
        # For the shipment notification email
        Spree::Config[:mails_from] = "ofn@example.com"

        shipment.update!(shipment.order)
        expect(shipment.state).to eq("ready")
        allow_any_instance_of(Spree::ShippingRate).to receive_messages(cost: 5)
      end

      it "can transition a shipment from ready to ship" do
        shipment.reload
        api_put :ship, order_id: shipment.order.to_param,
                       id: shipment.to_param,
                       shipment: { tracking: "123123" }

        expect(json_response.keys).to include(*attributes)
        expect(json_response["state"]).to eq("shipped")
      end
    end

    context 'for a completed order with shipment' do
      let(:order) { create :completed_order_with_totals }

      before { params[:id] = order.shipments.first.to_param }

      context '#add' do
        it 'adds a variant to the shipment' do
          spree_put :add, params

          expect_valid_response
          expect(inventory_units_for(variant).size).to eq 2
        end

        it 'returns error code when adding to order contents fails' do
          make_order_contents_fail

          spree_put :add, params

          expect_error_response
        end

        it 'adds a variant override to the shipment', feature: :inventory do
          hub = create(:distributor_enterprise)
          order.update_attribute(:distributor, hub)
          variant_override = create(:variant_override, hub:, variant:)

          spree_put :add, params

          expect_valid_response
          expect(inventory_units_for(variant).size).to eq 2
          expect(order.reload.line_items.last.price).to eq(variant_override.price)
        end

        context "when line items have fees" do
          let(:fee_order) {
            instance_double(Spree::Order, number: "123", distributor: variant.supplier)
          }
          let(:contents) { instance_double(Spree::OrderContents) }
          let(:fee_order_shipment) {
            instance_double(Spree::Shipment)
          }

          before do
            allow(Spree::Order).to receive(:find_by!) { fee_order }
            allow(controller).to receive(:refuse_changing_cancelled_orders)
            allow(fee_order).to receive(:contents) { contents }
            allow(contents).to receive_messages(add: {}, remove: {})
            allow(fee_order).to receive_message_chain(:shipments, :find_by!) { fee_order_shipment }
            allow(fee_order_shipment).to receive_messages(update: nil, reload: nil, persisted?: nil)
            allow(fee_order).to receive(:recreate_all_fees!)
          end

          it "recalculates fees for the line item" do
            params[:order_id] = fee_order.number
            spree_put :add, params
            expect(fee_order).to have_received(:recreate_all_fees!)
          end

          it "recalculates fees for the line item when qty is decreased" do
            params[:order_id] = fee_order.number
            spree_put :remove, params
            expect(fee_order).to have_received(:recreate_all_fees!)
          end
        end
      end

      context '#remove' do
        before do
          params[:variant_id] = order.line_items.first.variant.to_param
          params[:quantity] = 1
        end

        it 'removes a variant from the shipment' do
          spree_put :remove, params

          expect_valid_response
          expect(inventory_units_for(variant).size).to eq 0
        end

        it 'returns error code when removing from order contents fails' do
          make_order_contents_fail

          spree_put :remove, params

          expect_error_response
        end
      end
    end

    def inventory_units_for(variant)
      order.shipment.reload.inventory_units.select { |unit| unit['variant_id'] == variant.id }
    end

    def expect_valid_response
      expect(response).to have_http_status :ok
      expect(json_response.keys).to include(*attributes)
    end

    def make_order_contents_fail
      expect(Spree::Order).to receive(:find_by!).with({ number: order.number }) { order }
      expect(order).to receive(:contents) { raise error_message }
    end

    def expect_error_response
      expect(response).to have_http_status :unprocessable_entity
      expect(json_response["exception"]).to eq error_message
    end
  end
end
