# frozen_string_literal: true

require "spec_helper"

RSpec.describe Orders::SyncService do
  describe "updating the shipping method" do
    let!(:subscription) { create(:subscription, with_items: true, with_proxy_orders: true) }
    let!(:order) { subscription.proxy_orders.first.initialise_order! }
    let!(:shipping_method) { subscription.shipping_method }
    let!(:new_shipping_method) { create(:shipping_method, distributors: [subscription.shop]) }

    let(:syncer) { Orders::SyncService.new(subscription) }

    context "when the shipping method on an order is the same as the subscription" do
      let(:params) { { shipping_method_id: new_shipping_method.id } }

      before do
        # Create shipping rates for available shipping methods.
        order.shipments.each(&:refresh_rates)
        order.select_shipping_method(shipping_method)
      end

      it "updates the shipping_method on the order and on shipments" do
        expect(order.shipments.first.shipping_method).to eq shipping_method
        subscription.assign_attributes(params)
        expect(syncer.sync!).to be true
        expect(order.reload.shipping_method).to eq new_shipping_method
        expect(order.shipments.first.shipping_method).to eq new_shipping_method
      end
    end

    context "when the shipping method on a shipment is not the same " \
            "as the original shipping method on the subscription" do
      let(:params) { { shipping_method_id: new_shipping_method.id } }

      context "when the shipping method on a shipment is the same as the new shipping method " \
              "on the subscription" do
        before do
          # Create shipping rates for available shipping methods.
          order.shipments.each(&:refresh_rates)

          # Updating the shipping method on a shipment updates the shipping method on the order,
          # and vice-versa via logic in Spree's shipments controller. So updating both here mimics
          # that behaviour.
          order.select_shipping_method(new_shipping_method.id)
          subscription.assign_attributes(params)
          expect(syncer.sync!).to be true
        end

        it "does not update the shipping_method on the subscription or " \
           "on the pre-altered shipment" do
          expect(order.reload.shipping_method).to eq new_shipping_method
          expect(order.reload.shipments.first.shipping_method).to eq new_shipping_method
          expect(syncer.order_update_issues[order.id]).to be nil
        end
      end

      context "when the shipping method on a shipment is not the same as the new shipping method " \
              "on the subscription" do
        let!(:changed_shipping_method) { create(:shipping_method) }

        before do
          # Create shipping rates for available shipping methods.
          order.shipments.each(&:refresh_rates)

          # Updating the shipping method on a shipment updates the shipping method on the order,
          # and vice-versa via logic in Spree's shipments controller. So updating both here mimics
          # that behaviour.
          order.select_shipping_method(changed_shipping_method.id)
          subscription.assign_attributes(params)

          expect(syncer.sync!).to be true
        end

        it "does not update the shipping_method on the subscription or " \
           "on the pre-altered shipment" do
          expect(order.reload.shipping_method).to eq changed_shipping_method
          expect(order.reload.shipments.first.shipping_method).to eq changed_shipping_method
          expect(syncer.order_update_issues[order.id]).to include "Shipping Method"
        end
      end
    end
  end

  describe "changing the payment method" do
    let(:subscription) { create(:subscription, with_items: true, with_proxy_orders: true) }
    let(:order) { subscription.proxy_orders.first.initialise_order! }
    let(:payment_method) { subscription.payment_method }
    let(:new_payment_method) { create(:payment_method, distributors: [subscription.shop]) }
    let(:invalid_payment_method) { create(:payment_method, distributors: [create(:enterprise)]) }
    let(:syncer) { Orders::SyncService.new(subscription) }

    context "when the payment method on an order is the same as the subscription" do
      let(:params) { { payment_method_id: new_payment_method.id } }

      it "voids existing payments and creates a new payment with the relevant payment method" do
        expect(order.payments.reload.first.payment_method).to eq payment_method
        subscription.assign_attributes(params)
        expect(syncer.sync!).to be true
        payments = order.reload.payments
        expect(payments.count).to be 2
        expect(payments.with_state('void').count).to be 1
        expect(payments.with_state('checkout').count).to be 1
        expect(payments.with_state('void').first.payment_method).to eq payment_method
        expect(payments.with_state('checkout').first.payment_method).to eq new_payment_method
      end
    end

    context "when the payment method on a payment is not the same as the subscription" do
      let(:params) { { payment_method_id: new_payment_method.id } }

      context "when the payment method on a payment is the same as the original payment method " \
              "on the subscription" do
        before do
          order.payments.first.update_attribute(:payment_method_id, new_payment_method.id)
          subscription.assign_attributes(params)
          expect(syncer.sync!).to be true
        end

        it "keeps pre-altered payments and doesn't add an issue to order_update_issues" do
          payments = order.reload.payments
          expect(payments.count).to be 1
          expect(payments.first.payment_method).to eq new_payment_method
          expect(syncer.order_update_issues[order.id]).to be nil
        end
      end

      context "when the payment method on a shipment is not the same " \
              "as the original payment method on the subscription" do
        let(:changed_payment_method) { create(:payment_method) }

        before do
          order.payments.first.update_attribute(:payment_method_id, changed_payment_method.id)
          subscription.assign_attributes(params)
          expect(syncer.sync!).to be true
        end

        it "keeps pre-altered payments and adds an issue to order_update_issues" do
          payments = order.reload.payments
          expect(payments.count).to be 1
          expect(payments.first.payment_method).to eq changed_payment_method
          expect(syncer.order_update_issues[order.id]).to include "Payment Method"
        end
      end
    end
  end

  describe "changing the billing address" do
    let!(:distributor_address) { create(:address, :randomized) }
    let!(:distributor) { create(:distributor_enterprise, address: distributor_address) }
    let(:subscription) do
      create(:subscription, shop: distributor, shipping_method:, with_items: true,
                            with_proxy_orders: true)
    end
    let!(:order) { subscription.proxy_orders.first.initialise_order! }
    let!(:bill_address_attrs) { subscription.bill_address.attributes }
    let!(:ship_address_attrs) { subscription.ship_address.attributes }

    let(:params) {
      { bill_address_attributes: { id: bill_address_attrs["id"], firstname: "Bill",
                                   address1: "123 abc st", phone: "1123581321" } }
    }
    let(:syncer) { Orders::SyncService.new(subscription) }

    context "when a ship address is not required" do
      let!(:shipping_method) do
        create(:shipping_method, distributors: [distributor], require_ship_address: false)
      end

      context "when the bill_address on the order matches that on the subscription" do
        it "updates all bill_address attrs and ship_address names + phone" do
          subscription.assign_attributes(params)
          expect(syncer.sync!).to be true
          expect(syncer.order_update_issues.keys).not_to include order.id
          order.reload;
          expect(order.bill_address.firstname).to eq "Bill"
          expect(order.bill_address.lastname).to eq bill_address_attrs["lastname"]
          expect(order.bill_address.address1).to eq "123 abc st"
          expect(order.bill_address.phone).to eq "1123581321"
          expect(order.ship_address.firstname).to eq "Bill"
          expect(order.ship_address.lastname).to eq bill_address_attrs["lastname"]
          expect(order.ship_address.address1).to eq distributor_address.address1
          expect(order.ship_address.phone).to eq "1123581321"
        end
      end

      context "when the bill_address on the order doesn't match that on the subscription" do
        before do
          order.bill_address.update!(firstname: "Jane")
          order.update_order!
        end

        it "does not update bill_address or ship_address on the order" do
          subscription.assign_attributes(params)
          expect(syncer.sync!).to be true
          expect(syncer.order_update_issues.keys).to include order.id
          order.reload;
          expect(order.bill_address.firstname).to eq "Jane"
          expect(order.bill_address.lastname).to eq bill_address_attrs["lastname"]
          expect(order.bill_address.address1).to eq bill_address_attrs["address1"]
          expect(order.bill_address.phone).to eq bill_address_attrs["phone"]
          expect(order.ship_address.firstname).to eq "Jane"
          expect(order.ship_address.lastname).to eq bill_address_attrs["lastname"]
          expect(order.ship_address.address1).to eq distributor_address.address1
          expect(order.ship_address.phone).to eq bill_address_attrs["phone"]
        end
      end
    end

    context "when a ship address is required" do
      let!(:shipping_method) do
        create(:shipping_method, distributors: [distributor], require_ship_address: true)
      end

      context "when the bill_address on the order matches that on the subscription" do
        it "only updates bill_address attrs" do
          subscription.assign_attributes(params)
          expect(syncer.sync!).to be true
          expect(syncer.order_update_issues.keys).not_to include order.id
          order.reload;
          expect(order.bill_address.firstname).to eq "Bill"
          expect(order.bill_address.lastname).to eq bill_address_attrs["lastname"]
          expect(order.bill_address.address1).to eq "123 abc st"
          expect(order.bill_address.phone).to eq "1123581321"
          expect(order.ship_address.firstname).to eq ship_address_attrs["firstname"]
          expect(order.ship_address.lastname).to eq ship_address_attrs["lastname"]
          expect(order.ship_address.address1).to eq ship_address_attrs["address1"]
          expect(order.ship_address.phone).to eq ship_address_attrs["phone"]
        end
      end

      context "when the bill_address on the order doesn't match that on the subscription" do
        before do
          order.bill_address.update!(firstname: "Jane")
          order.update_order!
        end

        it "does not update bill_address or ship_address on the order" do
          subscription.assign_attributes(params)
          expect(syncer.sync!).to be true
          expect(syncer.order_update_issues.keys).to include order.id
          order.reload;
          expect(order.bill_address.firstname).to eq "Jane"
          expect(order.bill_address.lastname).to eq bill_address_attrs["lastname"]
          expect(order.bill_address.address1).to eq bill_address_attrs["address1"]
          expect(order.bill_address.phone).to eq bill_address_attrs["phone"]
          expect(order.ship_address.firstname).to eq ship_address_attrs["firstname"]
          expect(order.ship_address.lastname).to eq ship_address_attrs["lastname"]
          expect(order.ship_address.address1).to eq ship_address_attrs["address1"]
          expect(order.ship_address.phone).to eq ship_address_attrs["phone"]
        end
      end
    end
  end

  describe "changing the ship address" do
    let!(:distributor_address) { create(:address, :randomized) }
    let!(:distributor) { create(:distributor_enterprise, address: distributor_address) }
    let!(:subscription) do
      create(:subscription, shop: distributor, shipping_method:, with_items: true,
                            with_proxy_orders: true)
    end
    let!(:order) { subscription.proxy_orders.first.initialise_order! }
    let!(:bill_address_attrs) { subscription.bill_address.attributes }
    let!(:ship_address_attrs) { subscription.ship_address.attributes }

    let(:params) {
      { ship_address_attributes: { id: ship_address_attrs["id"], firstname: "Ship",
                                   address1: "123 abc st", phone: "1123581321" } }
    }
    let(:syncer) { Orders::SyncService.new(subscription) }

    context "when a ship address is not required" do
      let!(:shipping_method) do
        create(:shipping_method, distributors: [distributor], require_ship_address: false)
      end

      it "does not change the ship address" do
        subscription.assign_attributes(params)
        expect(syncer.sync!).to be true
        expect(syncer.order_update_issues.keys).not_to include order.id
        order.reload;
        expect(order.ship_address.firstname).to eq bill_address_attrs["firstname"]
        expect(order.ship_address.lastname).to eq bill_address_attrs["lastname"]
        expect(order.ship_address.address1).to eq distributor_address.address1
        expect(order.ship_address.phone).to eq bill_address_attrs["phone"]
      end

      context "but the shipping method is being changed to one that requires a ship_address" do
        let(:new_shipping_method) {
          create(:shipping_method, distributors: [distributor], require_ship_address: true)
        }

        before { params.merge!(shipping_method_id: new_shipping_method.id) }

        context "when the original ship address is the bill contact using distributor address" do
          let!(:original_bill_address) { create(:address, :randomized) }
          let!(:original_ship_address) do
            create(:address, firstname: original_bill_address.firstname,
                             lastname: original_bill_address.lastname,
                             address1: distributor_address.address1,
                             phone: original_bill_address.phone,
                             city: distributor_address.city,
                             zipcode: distributor_address.zipcode)
          end
          let(:subscription) do
            create(:subscription, shop: distributor, bill_address: original_bill_address,
                                  ship_address: original_ship_address,
                                  shipping_method:, with_items: true,
                                  with_proxy_orders: true)
          end

          context "when there is no pending shipment using the former shipping method" do
            before do
              order.shipment.destroy
              subscription.assign_attributes(params)
            end

            it "updates ship_address attrs" do
              expect(syncer.sync!).to be true
              expect(syncer.order_update_issues.keys).to include order.id
              order.reload
              expect(order.ship_address.firstname).to eq ship_address_attrs["firstname"]
              expect(order.ship_address.lastname).to eq ship_address_attrs["lastname"]
              expect(order.ship_address.address1).to eq ship_address_attrs["address1"]
              expect(order.ship_address.phone).to eq ship_address_attrs["phone"]
            end
          end

          context "when the order has a pending shipment using the former shipping method" do
            before do
              subscription.assign_attributes(params)
            end

            it "updates ship_address attrs" do
              expect(syncer.sync!).to be true
              expect(syncer.order_update_issues.keys).not_to include order.id
              order.reload
              expect(order.ship_address.firstname).to eq "Ship"
              expect(order.ship_address.lastname).to eq ship_address_attrs["lastname"]
              expect(order.ship_address.address1).to eq "123 abc st"
              expect(order.ship_address.phone).to eq "1123581321"
            end
          end
        end
      end
    end

    context "when a ship address is required" do
      let!(:shipping_method) do
        create(:shipping_method, distributors: [distributor], require_ship_address: true)
      end

      context "when the ship address on the order matches that on the subscription" do
        it "updates ship_address attrs" do
          subscription.assign_attributes(params)
          expect(syncer.sync!).to be true
          expect(syncer.order_update_issues.keys).not_to include order.id
          order.reload;
          expect(order.ship_address.firstname).to eq "Ship"
          expect(order.ship_address.lastname).to eq ship_address_attrs["lastname"]
          expect(order.ship_address.address1).to eq "123 abc st"
          expect(order.ship_address.phone).to eq "1123581321"
        end
      end

      context "when the ship address on the order doesn't match that on the subscription" do
        before do
          order.ship_address.update(firstname: "Jane")
          order.update_order!
        end

        it "does not update ship_address on the order" do
          subscription.assign_attributes(params)
          expect(syncer.sync!).to be true
          expect(syncer.order_update_issues.keys).to include order.id
          order.reload;
          expect(order.ship_address.firstname).to eq "Jane"
          expect(order.ship_address.lastname).to eq ship_address_attrs["lastname"]
          expect(order.ship_address.address1).to eq ship_address_attrs["address1"]
          expect(order.ship_address.phone).to eq ship_address_attrs["phone"]
        end
      end
    end
  end

  describe "changing the quantity of a line item" do
    let(:subscription) { create(:subscription, with_items: true, with_proxy_orders: true) }
    let(:order) { subscription.proxy_orders.first.initialise_order! }
    let(:sli) { subscription.subscription_line_items.first }
    let(:variant) { sli.variant }

    before { variant.update_attribute(:on_hand, 2) }

    context "when quantity is within available stock" do
      let(:params) { { subscription_line_items_attributes: [{ id: sli.id, quantity: 2 }] } }
      let(:syncer) { Orders::SyncService.new(subscription) }

      it "updates the line_item quantities and totals on all orders" do
        expect(order.reload.total.to_f).to eq 59.97
        subscription.assign_attributes(params)
        expect(syncer.sync!).to be true
        line_items = Spree::LineItem.where(order_id: subscription.orders,
                                           variant_id: sli.variant_id)
        expect(line_items.map(&:quantity)).to eq [2]
        expect(order.reload.total.to_f).to eq 79.96
      end
    end

    context "when quantity is greater than available stock" do
      let(:params) { { subscription_line_items_attributes: [{ id: sli.id, quantity: 3 }] } }
      let(:syncer) { Orders::SyncService.new(subscription) }

      before do
        expect(order.reload.total.to_f).to eq 59.97
        subscription.assign_attributes(params)
      end

      context "when order is not complete" do
        it "updates the line_item quantities and totals on all orders" do
          expect(syncer.sync!).to be true

          line_items = Spree::LineItem.where(order_id: subscription.orders,
                                             variant_id: sli.variant_id)
          expect(line_items.map(&:quantity)).to eq [3]
          expect(order.reload.total.to_f).to eq 99.95
        end
      end

      context "when order is complete" do
        it "does not update the line_item quantities and adds the order " \
           "to order_update_issues with insufficient stock" do
          Orders::WorkflowService.new(order).complete

          expect(syncer.sync!).to be true

          line_items = Spree::LineItem.where(order_id: subscription.orders,
                                             variant_id: sli.variant_id)
          expect(line_items.map(&:quantity)).to eq [1]
          expect(order.reload.total.to_f).to eq 59.97
          line_item = order.line_items.find_by(variant_id: sli.variant_id)
          expect(syncer.order_update_issues[order.id])
            .to include "#{line_item.product.name} - #{line_item.variant.full_name} - " \
                        "Insufficient stock available"
        end

        it "does not update the line_item quantities and adds the order " \
           "to order_update_issues with out of stock" do
          # this single item available is used when the order is completed below,
          # making the item out of stock
          variant.update_attribute(:on_hand, 1)
          Orders::WorkflowService.new(order).complete

          expect(syncer.sync!).to be true

          line_item = order.line_items.find_by(variant_id: sli.variant_id)
          expect(syncer.order_update_issues[order.id])
            .to include "#{line_item.product.name} - #{line_item.variant.full_name} - Out of Stock"
        end
      end
    end

    context "where the quantity of the item on an initialised order has already been changed" do
      let(:params) { { subscription_line_items_attributes: [{ id: sli.id, quantity: 3 }] } }
      let(:syncer) { Orders::SyncService.new(subscription) }
      let(:changed_line_item) { order.line_items.find_by(variant_id: sli.variant_id) }

      before { variant.update_attribute(:on_hand, 3) }

      context "when the changed line_item quantity matches the new quantity " \
              "on the subscription line item" do
        before do
          changed_line_item.update(quantity: 3)
          order.update_order!
        end

        it "does not change the quantity, and doesn't add the order to order_update_issues" do
          expect(order.reload.total.to_f).to eq 99.95
          subscription.assign_attributes(params)
          expect(syncer.sync!).to be true
          expect(changed_line_item.reload.quantity).to eq 3
          expect(order.reload.total.to_f).to eq 99.95
          expect(syncer.order_update_issues[order.id]).to be nil
        end
      end

      context "when the changed line_item quantity doesn't match the new quantity " \
              "on the subscription line item" do
        before do
          changed_line_item.update(quantity: 2)
          order.update_order!
        end

        it "does not change the quantity, and adds the order to order_update_issues" do
          expect(order.reload.total.to_f).to eq 79.96

          subscription.assign_attributes(params)
          expect(syncer.sync!).to be true

          expect(changed_line_item.reload.quantity).to eq 2
          expect(order.reload.total.to_f).to eq 79.96
          expect(syncer.order_update_issues[order.id])
            .to include "#{changed_line_item.product.name} - #{changed_line_item.variant.full_name}"
        end
      end
    end
  end

  describe "adding a new line item" do
    let(:subscription) { create(:subscription, with_items: true, with_proxy_orders: true) }
    let(:order) { subscription.proxy_orders.first.initialise_order! }
    let(:variant) { create(:variant) }
    let(:syncer) { Orders::SyncService.new(subscription) }

    before do
      expect(order.reload.total.to_f).to eq 59.97
      subscription.assign_attributes(params)
    end

    context "when quantity is within available stock" do
      let(:params) {
        { subscription_line_items_attributes: [{ id: nil, variant_id: variant.id, quantity: 1 }] }
      }

      it "adds the line item and updates the total on all orders" do
        expect(syncer.sync!).to be true

        line_items = Spree::LineItem.where(order_id: subscription.orders, variant_id: variant.id)
        expect(line_items.map(&:quantity)).to eq [1]
        expect(order.reload.total.to_f).to eq 79.96
      end
    end

    context "when quantity is greater than available stock" do
      let(:params) {
        { subscription_line_items_attributes: [{ id: nil, variant_id: variant.id, quantity: 7 }] }
      }

      context "when order is not complete" do
        it "adds the line_item and updates totals on all orders" do
          expect(syncer.sync!).to be true

          line_items = Spree::LineItem.where(order_id: subscription.orders, variant_id: variant.id)
          expect(line_items.map(&:quantity)).to eq [7]
          expect(order.reload.total.to_f).to eq 199.9
        end
      end

      context "when order is complete" do
        before { Orders::WorkflowService.new(order).complete }

        it "does not add line_item and adds the order to order_update_issues" do
          expect(syncer.sync!).to be true

          line_items = Spree::LineItem.where(order_id: subscription.orders, variant_id: variant.id)
          expect(line_items.map(&:quantity)).to eq []
          expect(order.reload.total.to_f).to eq 59.97
          expect(syncer.order_update_issues[order.id])
            .to include "#{variant.product.name} - #{variant.full_name} " \
                        "- Insufficient stock available"
        end

        context "and then updating the quantity of that subscription line item " \
                "that was not added to the completed order" do
          it "does nothing to the order and adds the order to order_update_issues" do
            expect(syncer.sync!).to be true

            line_items = Spree::LineItem.where(order_id: subscription.orders,
                                               variant_id: variant.id)
            expect(line_items.map(&:quantity)).to eq []

            subscription.save # this is necessary to get an id on the subscription_line_items
            params = { subscription_line_items_attributes: [{
              id: subscription.subscription_line_items.last.id, quantity: 2
            }] }
            subscription.assign_attributes(params)
            expect(syncer.sync!).to be true

            line_items = Spree::LineItem.where(order_id: subscription.orders,
                                               variant_id: variant.id)
            expect(line_items.map(&:quantity)).to eq []
            expect(syncer.order_update_issues[order.id])
              .to include "#{variant.product.name} - #{variant.full_name}"
          end
        end
      end
    end
  end

  describe "removing an existing line item" do
    let(:subscription) { create(:subscription, with_items: true, with_proxy_orders: true) }
    let(:order) { subscription.proxy_orders.first.initialise_order! }
    let(:sli) { subscription.subscription_line_items.first }
    let(:variant) { sli.variant }
    let(:params) { { subscription_line_items_attributes: [{ id: sli.id, _destroy: true }] } }
    let(:syncer) { Orders::SyncService.new(subscription) }

    it "removes the line item and updates totals on all orders" do
      expect(order.reload.total.to_f).to eq 59.97
      subscription.assign_attributes(params)
      expect(syncer.sync!).to be true
      line_items = Spree::LineItem.where(order_id: subscription.orders, variant_id: variant.id)
      expect(line_items.count).to be 0
      expect(order.reload.total.to_f).to eq 39.98
    end
  end
end
