require 'rails_helper'

RSpec.describe Spree::LineItem, type: :model do
  let(:order) { create :order_with_line_items, line_items_count: 1 }
  let(:line_item) { order.line_items.first }

  context '#destroy' do
    it "fetches soft-deleted products" do
      line_item.product.discard
      expect(line_item.reload.product).to be_a Spree::Product
    end

    it "fetches soft-deleted variants" do
      line_item.variant.discard
      expect(line_item.reload.variant).to be_a Spree::Variant
    end

    it "returns inventory when a line item is destroyed" do
      expect_any_instance_of(Spree::OrderInventory).to receive(:verify)
      line_item.destroy
    end

    it "deletes inventory units" do
      expect { line_item.destroy }.to change { line_item.inventory_units.count }.from(1).to(0)
    end
  end

  context "#save" do
    context "target_shipment is provided" do
      it "verifies inventory" do
        line_item.target_shipment = Spree::Shipment.new
        expect_any_instance_of(Spree::OrderInventory).to receive(:verify)
        line_item.save
      end
    end
  end

  describe 'line item creation' do
    let(:variant) { create :variant }

    subject(:line_item) { Spree::LineItem.new(variant: variant, order: order) }

    # Tests for https://github.com/spree/spree/issues/3391
    context 'before validation' do
      before { line_item.valid? }

      it 'copies the variants price' do
        expect(line_item.price).to eq(variant.price)
      end

      it 'copies the variants cost_price' do
        expect(line_item.cost_price).to eq(variant.cost_price)
      end

      it "copies the order's currency" do
        expect(line_item.currency).to eq(order.currency)
      end

      # Test for https://github.com/spree/spree/issues/3481
      it 'copies the variants tax category' do
        expect(line_item.tax_category).to eq(line_item.variant.tax_category)
      end
    end

    # Specs for https://github.com/solidusio/solidus/pull/522#issuecomment-170668125
    context "with `#copy_price` defined" do
      before(:context) do
        Spree::LineItem.class_eval do
          def copy_price
            self.cost_price = 10
            self.price = 20
          end
        end
      end

      after(:context) do
        Spree::LineItem.class_eval do
          remove_method :copy_price
        end
      end

      it 'should display a deprecation warning' do
        expect(Spree::Deprecation).to receive(:warn)
        Spree::LineItem.new(variant: variant, order: order)
      end

      it 'should run the user-defined copy_price method' do
        expect_any_instance_of(Spree::LineItem).to receive(:copy_price).and_call_original
        Spree::Deprecation.silence do
          Spree::LineItem.new(variant: variant, order: order)
        end
      end
    end
  end

  # TODO: Remove this spec after the method has been removed.
  describe '#discounted_amount' do
    it "returns the amount minus any discounts" do
      line_item.price = 10
      line_item.quantity = 2
      line_item.promo_total = -5
      expect(Spree::Deprecation.silence { line_item.discounted_amount }).to eq(15)
    end
  end

  # TODO: Remove this spec after the method has been removed.
  describe "#discounted_money" do
    it "should return a money object with the discounted amount" do
      expect(Spree::Deprecation.silence { line_item.discounted_amount }).to eq(10.00)
      expect(Spree::Deprecation.silence { line_item.discounted_money.to_s }).to eq "$10.00"
    end
  end

  describe '#total_before_tax' do
    before do
      line_item.update!(price: 10, quantity: 2)
    end
    let!(:admin_adjustment) { create(:adjustment, adjustable: line_item, order: line_item.order, amount: -1, source: nil) }
    let!(:promo_adjustment) { create(:adjustment, adjustable: line_item, order: line_item.order, amount: -2, source: promo_action) }
    let!(:ineligible_promo_adjustment) { create(:adjustment, eligible: false, adjustable: line_item, order: line_item.order, amount: -4, source: promo_action) }
    let(:promo_action) { promo.actions[0] }
    let(:promo) { create(:promotion, :with_line_item_adjustment) }

    it 'returns the amount minus any adjustments' do
      expect(line_item.total_before_tax).to eq(20 - 1 - 2)
    end
  end

  describe ".money" do
    before do
      line_item.price = 3.50
      line_item.quantity = 2
    end

    it "returns a Spree::Money representing the total for this line item" do
      expect(line_item.money.to_s).to eq("$7.00")
    end
  end

  describe '.single_money' do
    before { line_item.price = 3.50 }
    it "returns a Spree::Money representing the price for one variant" do
      expect(line_item.single_money.to_s).to eq("$3.50")
    end
  end

  context 'setting a line item price' do
    let(:store) { create(:store, default: true) }
    let(:order) { Spree::Order.new(currency: "RUB", store: store) }
    let(:variant) { Spree::Variant.new(product: Spree::Product.new) }
    let(:line_item) { Spree::LineItem.new(order: order, variant: variant) }

    before { expect(variant).to receive(:price_for).at_least(:once).and_return(price) }

    context "when a price exists in order currency" do
      let(:price) { Spree::Money.new(99.00, currency: "RUB") }

      it "is a valid line item" do
        expect(line_item.valid?).to be_truthy
        expect(line_item.errors[:price].size).to eq(0)
      end
    end

    context "when a price does not exist in order currency" do
      let(:price) { nil }

      it "is not a valid line item" do
        expect(line_item.valid?).to be_falsey
        expect(line_item.errors[:price].size).to eq(1)
      end
    end
  end

  describe "#options=" do
    let(:options) { { price: 123, quantity: 5 } }

    it "updates the data provided in the options" do
      line_item.options = options

      expect(line_item.price).to eq 123
      expect(line_item.quantity).to eq 5
    end

    context "when price is not provided" do
      let(:options) { { quantity: 5 } }

      it "sets price anyway, retrieving it from line item options" do
        expect(line_item.variant)
          .to receive(:price_for)
          .and_return(Spree::Money.new(123, currency: "USD"))

        line_item.options = options

        expect(line_item.price).to eq 123
        expect(line_item.quantity).to eq 5
      end
    end
  end

  describe 'money_price=' do
    let(:currency) { "USD" }
    let(:new_price) { Spree::Money.new(99.00, currency: currency) }

    it 'assigns a new price' do
      line_item.money_price = new_price
      expect(line_item.price).to eq(new_price.cents / 100.0)
    end

    context 'when the new price is nil' do
      let(:new_price) { nil }

      it 'makes the line item price empty' do
        line_item.money_price = new_price
        expect(line_item.price).to be_nil
      end
    end

    context 'when the price has a currency different from the order currency' do
      let(:currency) { "RUB" }

      it 'raises an exception' do
        expect {
          line_item.money_price = new_price
        }.to raise_exception Spree::LineItem::CurrencyMismatch
      end
    end
  end

  describe "#pricing_options" do
    subject { line_item.pricing_options }

    it { is_expected.to be_a(Spree::Config.pricing_options_class) }

    it "holds the order currency" do
      expect(subject.currency).to eq("USD")
    end
  end
end
