# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Spree::Adjustment do
  let(:order) { build(:order) }
  let(:adjustment) { described_class.create(label: "Adjustment", amount: 5) }

  describe "associations" do
    it { is_expected.to have_one(:metadata) }
    it { is_expected.to have_many(:adjustments) }

    it { is_expected.to belong_to(:adjustable).optional }
    it { is_expected.to belong_to(:originator).optional }
    it { is_expected.to belong_to(:order).optional }
    it { is_expected.to belong_to(:tax_category).optional }
  end

  describe "scopes" do
    let!(:arbitrary_adjustment) { create(:adjustment, label: "Arbitrary") }
    let!(:return_authorization_adjustment) {
      create(:adjustment, originator: create(:return_authorization))
    }

    it "returns return_authorization adjustments" do
      expect(described_class.return_authorization.to_a).to eq [return_authorization_adjustment]
    end
  end

  context "#update_adjustment!" do
    context "when originator present" do
      let(:originator) { instance_double(EnterpriseFee, compute_amount: 10.0) }
      let(:adjustable) { instance_double(Spree::LineItem) }

      before do
        allow(adjustment).to receive_messages originator:, label: 'adjustment',
                                              adjustable:, amount: 0
      end

      it "should do nothing when closed" do
        adjustment.close
        expect(originator).not_to receive(:compute_amount)
        adjustment.update_adjustment!
      end

      it "should do nothing when finalized" do
        adjustment.finalize
        expect(originator).not_to receive(:compute_amount)
        adjustment.update_adjustment!
      end

      it "should ask the originator to recalculate the amount" do
        expect(originator).to receive(:compute_amount)
        adjustment.update_adjustment!
      end

      context "using the :force argument" do
        it "should update adjustments without changing their state" do
          expect(originator).to receive(:compute_amount)
          adjustment.update_adjustment!(force: true)
          expect(adjustment.state).to eq "open"
        end

        it "should update closed adjustments" do
          adjustment.close
          expect(originator).to receive(:compute_amount)
          adjustment.update_adjustment!(force: true)
        end
      end
    end

    it "should do nothing when originator is nil" do
      allow(adjustment).to receive_messages originator: nil
      expect(adjustment).not_to receive(:update_columns)
      adjustment.update_adjustment!
    end

    context "where the adjustable has been deleted" do
      let(:line_item) { create(:line_item, price: 10) }
      let!(:adjustment) { create(:adjustment, adjustable: line_item) }

      it "returns zero" do
        line_item.delete
        expect(adjustment.reload.update_adjustment!).to eq 0.0
      end

      it "removes orphaned adjustments" do
        expect {
          line_item.delete
          adjustment.reload.update_adjustment!
        }.to change{ described_class.count }.by(-1)
      end
    end
  end

  context "adjustment state" do
    let(:adjustment) { create(:adjustment, state: 'open') }

    context "#immutable?" do
      it "is true when adjustment state isn't open" do
        adjustment.state = "closed"
        expect(adjustment).to be_immutable
        adjustment.state = "finalized"
        expect(adjustment).to be_immutable
      end

      it "is false when adjustment state is open" do
        adjustment.state = "open"
        expect(adjustment).not_to be_immutable
      end
    end

    context "#finalized?" do
      it "is true when adjustment state is finalized" do
        adjustment.state = "finalized"
        expect(adjustment).to be_finalized
      end

      it "is false when adjustment state isn't finalized" do
        adjustment.state = "closed"
        expect(adjustment).not_to be_finalized
        adjustment.state = "open"
        expect(adjustment).not_to be_finalized
      end
    end
  end

  context "#display_amount" do
    before { adjustment.amount = 10.55 }

    context "with display_currency set to true" do
      before { Spree::Config[:display_currency] = true }

      it "shows the currency" do
        expect(adjustment.display_amount.to_s).to eq "$10.55 AUD"
      end
    end

    context "with display_currency set to false" do
      before { Spree::Config[:display_currency] = false }

      it "does not include the currency" do
        expect(adjustment.display_amount.to_s).to eq "$10.55"
      end
    end

    context "with currency set to JPY" do
      context "when adjustable is set to an order" do
        before do
          allow(order).to receive(:currency) { 'JPY' }
          adjustment.adjustable = order
        end

        it "displays in JPY" do
          expect(adjustment.display_amount.to_s).to eq "¥11"
        end
      end

      context "when adjustable is nil" do
        it "displays in the default currency" do
          expect(adjustment.display_amount.to_s).to eq "$10.55"
        end
      end
    end
  end

  context '#currency' do
    it 'returns the globally configured currency' do
      expect(adjustment.currency).to eq "AUD"
    end
  end

  it "has metadata" do
    adjustment = create(:adjustment, metadata: create(:adjustment_metadata))
    expect(adjustment.metadata).to be
  end

  describe "recording included tax" do
    describe "TaxRate adjustments" do
      let!(:zone)        { create(:zone_with_member) }
      let!(:order)       { create(:order, bill_address: create(:address)) }
      let!(:line_item)   { create(:line_item, order:) }
      let(:tax_category) { create(:tax_category, tax_rates: [tax_rate]) }
      let(:tax_rate)     { create(:tax_rate, included_in_price: true, amount: 0.10) }
      let(:adjustment)   { line_item.adjustments.reload.first }

      before do
        order.reload
        tax_rate.adjust(order, line_item)
      end

      context "when the tax rate is inclusive" do
        it "has 10% inclusive tax correctly recorded" do
          amount = line_item.amount - (line_item.amount / (1 + tax_rate.amount))
          rounded_amount = tax_rate.calculator.__send__(:round_to_two_places, amount)
          expect(adjustment.amount).to eq rounded_amount
          expect(adjustment.amount).to eq 0.91
          expect(adjustment.included).to be true
        end

        it "does not crash when order data has been updated previously" do
          order.line_item_adjustments.first.destroy
          tax_rate.adjust(order, line_item)
        end
      end

      context "when the tax rate is additional" do
        let(:tax_rate) { create(:tax_rate, included_in_price: false, amount: 0.10) }

        it "has 10% added tax correctly recorded" do
          expect(adjustment.amount).to eq line_item.amount * tax_rate.amount
          expect(adjustment.amount).to eq 1.0
          expect(adjustment.included).to be false
        end
      end
    end

    describe "Shipment adjustments" do
      let(:zone) { create(:zone_with_member) }
      let(:inclusive_tax) { true }
      let(:tax_rate) {
        create(:tax_rate, included_in_price: inclusive_tax, zone:, amount: 0.25)
      }
      let(:tax_category)    { create(:tax_category, name: "Shipping", tax_rates: [tax_rate] ) }
      let(:hub)             { create(:distributor_enterprise, charges_sales_tax: true) }
      let(:order)           { create(:order, distributor: hub, state: 'payment') }
      let(:line_item)       { create(:line_item, order:) }

      let(:shipping_method) {
        create(:shipping_method_with, :flat_rate, tax_category:)
      }
      let(:shipment) {
        create(:shipment_with, :shipping_method, shipping_method:, order:)
      }

      describe "the shipping charge" do
        it "is the adjustment amount" do
          order.shipments = [shipment]
          expect(order.shipment_adjustments.first.amount).to eq(50)
          expect(shipment.cost).to eq(50)
        end
      end

      context "with tax" do
        before do
          allow(order).to receive(:tax_zone) { zone }
        end

        context "when the shipment has an inclusive tax rate" do
          it "calculates the shipment tax from the tax rate" do
            order.shipments = [shipment]
            order.state = "payment"
            order.create_tax_charge!
            order.update_totals

            # Finding the tax included in an amount that's already inclusive of tax:
            # total - ( total / (1 + rate) )
            # 50    - ( 50    / (1 + 0.25) )
            # = 10
            expect(order.shipment_adjustments.tax.first.amount).to eq(10)
            expect(order.shipment_adjustments.tax.first.included).to eq true

            expect(shipment.reload.cost).to eq(50)
            expect(shipment.included_tax_total).to eq(10)
            expect(shipment.additional_tax_total).to eq(0)

            expect(order.included_tax_total).to eq(10)
            expect(order.additional_tax_total).to eq(0)
          end
        end

        context "when the shipment has an added tax rate" do
          let(:inclusive_tax) { false }

          it "records the tax on the shipment's adjustments" do
            order.shipments = [shipment]
            order.state = "payment"
            order.create_tax_charge!
            order.update_totals

            # Finding the added tax for an amount:
            # total * rate
            # 50    * 0.25
            # = 12.5
            expect(order.shipment_adjustments.tax.first.amount).to eq(12.50)
            expect(order.shipment_adjustments.tax.first.included).to eq false

            expect(shipment.reload.cost).to eq(50)
            expect(shipment.included_tax_total).to eq(0)
            expect(shipment.additional_tax_total).to eq(12.50)

            expect(order.included_tax_total).to eq(0)
            expect(order.additional_tax_total).to eq(12.50)
          end
        end

        context "when the distributor does not charge sales tax" do
          it "records 0% tax on shipments" do
            order.distributor.update! charges_sales_tax: false
            order.shipments = [shipment]
            order.create_tax_charge!
            order.update_totals

            expect(order.shipment_adjustments.tax.count).to be_zero

            expect(shipment.reload.cost).to eq(50)
            expect(shipment.included_tax_total).to eq(0)
            expect(shipment.additional_tax_total).to eq(0)

            expect(order.included_tax_total).to eq(0)
            expect(order.additional_tax_total).to eq(0)
          end
        end

        context "when the shipment has no applicable tax rate" do
          it "records 0% tax on shipments" do
            allow(shipment).to receive(:tax_category) { nil }
            order.shipments = [shipment]
            order.create_tax_charge!
            order.update_totals

            expect(order.shipment_adjustments.tax.count).to be_zero

            expect(shipment.reload.cost).to eq(50)
            expect(shipment.included_tax_total).to eq(0)
            expect(shipment.additional_tax_total).to eq(0)

            expect(order.included_tax_total).to eq(0)
            expect(order.additional_tax_total).to eq(0)
          end
        end
      end
    end

    describe "EnterpriseFee adjustments" do
      let(:zone)             { create(:zone_with_member) }
      let(:fee_tax_rate)     {
        create(:tax_rate, included_in_price: true, calculator: Calculator::DefaultTax.new,
                          zone:, amount: 0.1)
      }
      let(:fee_tax_category) { create(:tax_category, tax_rates: [fee_tax_rate]) }

      let(:coordinator) { create(:distributor_enterprise, charges_sales_tax: true) }
      let(:variant)     { create(:variant, product: create(:product, tax_category: nil)) }
      let(:order_cycle) {
        create(:simple_order_cycle, coordinator:, coordinator_fees: [enterprise_fee],
                                    distributors: [coordinator], variants: [variant])
      }
      let(:line_item)   { create(:line_item, variant:) }
      let(:order)       {
        create(:order, line_items: [line_item], order_cycle:,
                       distributor: coordinator, state: 'payment')
      }
      let(:fee)         { order.all_adjustments.reload.enterprise_fee.first }
      let(:fee_tax)     { fee.adjustments.tax.first }

      context "when enterprise fees have a fixed tax_category" do
        before do
          order.update(state: "payment")
          order.recreate_all_fees!
        end

        context "when enterprise fees are taxed per-order" do
          let(:enterprise_fee) {
            create(:enterprise_fee, enterprise: coordinator, tax_category: fee_tax_category,
                                    calculator: Calculator::FlatRate.new(
                                      preferred_amount: 50.0
                                    ))
          }

          describe "when the tax rate includes the tax in the price" do
            it "records the correct amount in a tax adjustment" do
              # The fee is $50, tax is 10%, and the fee is inclusive of tax
              # Therefore, the included tax should be 0.1/1.1 * 50 = $4.55

              expect(fee_tax.amount).to eq(4.55)
            end
          end

          describe "when the tax rate does not include the tax in the price" do
            before do
              fee_tax_rate.update_attribute :included_in_price, false
              order.recreate_all_fees!
            end

            it "records the correct amount in a tax adjustment" do
              expect(fee_tax.amount).to eq(5.0)
            end
          end

          describe "when enterprise fees have no tax" do
            before do
              enterprise_fee.tax_category = nil
              enterprise_fee.save!
              order.recreate_all_fees!
            end

            it "records no tax as charged" do
              expect(fee_tax).to be_nil
            end
          end
        end

        context "when enterprise fees are taxed per-item" do
          let(:enterprise_fee) {
            create(:enterprise_fee, enterprise: coordinator, tax_category: fee_tax_category,
                                    calculator: Calculator::PerItem.new(preferred_amount: 50.0))
          }

          describe "when the tax rate includes the tax in the price" do
            it "records the correct amount in a tax adjustment" do
              expect(fee_tax.amount).to eq(4.55)
            end
          end

          describe "when the tax rate does not include the tax in the price" do
            before do
              fee_tax_rate.update_attribute :included_in_price, false
              order.recreate_all_fees!
            end

            it "records the correct amount in a tax adjustment" do
              expect(fee_tax.amount).to eq(5.0)
            end
          end
        end
      end

      context "when enterprise fees inherit tax_category from the product they are applied to" do
        let(:product_tax_rate) {
          create(:tax_rate, included_in_price: true, calculator: Calculator::DefaultTax.new,
                            zone:, amount: 0.2)
        }
        let(:product_tax_category) { create(:tax_category, tax_rates: [product_tax_rate]) }

        before do
          variant.update_attribute(:tax_category_id, product_tax_category.id)
          order.recreate_all_fees!
        end

        context "when enterprise fees are taxed per-order" do
          let(:enterprise_fee) {
            create(:enterprise_fee, enterprise: coordinator, inherits_tax_category: false,
                                    calculator: Calculator::FlatRate.new(
                                      preferred_amount: 50.0
                                    ))
          }

          before do
            order.update(state: "payment")
            order.create_tax_charge!
          end

          describe "when the tax rate includes the tax in the price" do
            it "records no tax on the enterprise fee adjustments" do
              # EnterpriseFee tax category is nil and inheritance only applies to per item fees
              # so tax on the enterprise_fee adjustment will be 0
              # Tax on line item is: 0.2/1.2 x $10 = $1.67
              expect(fee_tax).to be_nil
              expect(line_item.adjustments.tax.first.amount).to eq(1.67)
            end
          end

          describe "when the tax rate does not include the tax in the price" do
            before do
              product_tax_rate.update_attribute :included_in_price, false
              order.reload.recreate_all_fees!
            end

            it "records the no tax on TaxRate adjustment on the order" do
              # EnterpriseFee tax category is nil and inheritance only applies to per item fees
              # so total tax on the order is only that which applies to the line_item itself
              # ie. $10 x 0.2 = $2.0
              expect(fee_tax).to be_nil
            end
          end
        end

        context "when enterprise fees are taxed per-item" do
          let(:enterprise_fee) {
            create(:enterprise_fee, enterprise: coordinator, inherits_tax_category: true,
                                    calculator: Calculator::PerItem.new(preferred_amount: 50.0))
          }

          before do
            order.update(state: "payment")
            order.create_tax_charge!
          end

          describe "when the tax rate includes the tax in the price" do
            it "records the correct amount in a tax adjustment" do
              # Applying product tax rate of 0.2 to enterprise fee of $50
              # gives tax on fee of 0.2/1.2 x $50 = $8.33
              # Tax on line item is: 0.2/1.2 x $10 = $1.67
              expect(fee_tax.amount).to eq(8.33)
              expect(line_item.adjustments.tax.first.amount).to eq(1.67)
            end
          end

          describe "when the tax rate does not include the tax in the price" do
            before do
              product_tax_rate.update_attribute :included_in_price, false
              order.recreate_all_fees!
            end

            it "records the correct amount in a tax adjustment" do
              # EnterpriseFee inherits tax_category from product so total tax on
              # the order is that which applies to the line item itself, plus the
              # same rate applied to the fee of $50. ie. ($10 + $50) x 0.2 = $12.0
              expect(fee_tax.amount).to eq(10.0)
              expect(order.all_adjustments.tax.sum(:amount)).to eq(12.0)
            end
          end
        end
      end
    end
  end

  context "extends LocalizedNumber" do
    it_behaves_like "a model using the LocalizedNumber module", [:amount]
  end

  describe "inclusive and additional taxes" do
    let!(:zone) { create(:zone_with_member) }
    let!(:tax_category) { create(:tax_category, name: "Tax Test") }
    let(:distributor) { create(:distributor_enterprise, charges_sales_tax: true) }
    let(:order) { create(:order, distributor:, state: "payment") }
    let(:included_in_price) { true }
    let(:tax_rate) {
      create(:tax_rate, included_in_price:, zone:,
                        calculator: Calculator::FlatRate.new(preferred_amount: 0.1))
    }
    let(:variant) { create(:variant, tax_category:) }
    let(:product) { variant.product }

    describe "tax adjustment creation" do
      before do
        tax_category.tax_rates << tax_rate
        allow(order).to receive(:tax_zone) { zone }
        order.line_items << create(:line_item, variant:, quantity: 5)
        order.update(state: "payment")
        order.create_tax_charge!
      end

      context "with included taxes" do
        it "records the tax as included" do
          expect(order.all_adjustments.tax.count).to eq 1
          expect(order.all_adjustments.tax.first.included).to be true
        end
      end

      context "with additional taxes" do
        let(:included_in_price) { false }

        it "records the tax as additional" do
          expect(order.all_adjustments.tax.count).to eq 1
          expect(order.all_adjustments.tax.first.included).to be false
        end
      end
    end

    describe "inclusive and additional scopes" do
      let(:included) { true }
      let(:adjustment) {
        create(:adjustment, adjustable: order, originator: tax_rate, included:)
      }

      context "when tax is included in price" do
        it "is returned by the #included scope" do
          expect(described_class.inclusive).to eq [adjustment]
        end
      end

      context "when tax is additional to the price" do
        let(:included) { false }

        it "is returned by the #additional scope" do
          expect(described_class.additional).to eq [adjustment]
        end
      end
    end
  end

  context "return authorization adjustments" do
    let!(:return_authorization) { create(:return_authorization, amount: 123) }
    let(:order) { return_authorization.order }
    let!(:return_adjustment) {
      create(:adjustment, originator: return_authorization, order:,
                          adjustable: order, amount: 456)
    }

    describe "#update_adjustment!" do
      it "sets a negative value equal to the return authorization amount" do
        expect { return_adjustment.update_adjustment! }.
          to change { return_adjustment.reload.amount }.to(-123)
      end
    end
  end
end
