# frozen_string_literal: true

require 'spec_helper'

RSpec.describe SolidusSubscriptions::Subscription, type: :model do
  it { is_expected.to validate_presence_of :user }
  it { is_expected.to validate_presence_of :skip_count }
  it { is_expected.to validate_presence_of :successive_skip_count }
  it { is_expected.to validate_numericality_of(:skip_count).is_greater_than_or_equal_to(0) }
  it { is_expected.to validate_numericality_of(:successive_skip_count).is_greater_than_or_equal_to(0) }
  it { is_expected.to accept_nested_attributes_for(:line_items) }

  it 'validates currency correctly' do
    expect(subject).to validate_inclusion_of(:currency).
      in_array(::Money::Currency.all.map(&:iso_code)).
      with_message('is not a valid currency code')
  end

  it 'validates payment_source ownership' do
    subscription = create(:subscription)

    subscription.update(payment_source: create(:credit_card))
    expect(subscription.errors.messages[:payment_source]).to include('does not belong to the user associated with the subscription')

    subscription.update(payment_source: create(:credit_card, user: subscription.user))
    expect(subscription.errors.messages[:payment_source]).not_to include('does not belong to the user associated with the subscription')
  end

  describe 'creating a subscription' do
    it 'tracks the creation' do
      stub_const('Spree::Event', class_spy(Spree::Event))

      subscription = create(:subscription)

      expect(Spree::Event).to have_received(:fire).with(
        'solidus_subscriptions.subscription_created',
        subscription: subscription,
      )
    end

    it 'generates a guest token' do
      subscription = create(:subscription)

      expect(subscription.guest_token).to be_present
    end

    it 'sets default config currency if not given' do
      subscription = create(:subscription, currency: nil)

      expect(subscription.currency).to eq(Spree::Config.currency)
    end
  end

  describe 'updating a subscription' do
    it 'tracks interval changes' do
      stub_const('Spree::Event', class_spy(Spree::Event))
      subscription = create(:subscription)

      subscription.update!(interval_length: subscription.interval_length + 1)

      expect(Spree::Event).to have_received(:fire).with(
        'solidus_subscriptions.subscription_frequency_changed',
        subscription: subscription,
      )
    end

    it 'tracks shipping address changes' do
      stub_const('Spree::Event', class_spy(Spree::Event))
      subscription = create(:subscription)

      subscription.update!(shipping_address: create(:address))

      expect(Spree::Event).to have_received(:fire).with(
        'solidus_subscriptions.subscription_shipping_address_changed',
        subscription: subscription,
      )
    end

    it 'tracks billing address changes' do
      stub_const('Spree::Event', class_spy(Spree::Event))
      subscription = create(:subscription)

      subscription.update!(billing_address: create(:address))

      expect(Spree::Event).to have_received(:fire).with(
        'solidus_subscriptions.subscription_billing_address_changed',
        subscription: subscription,
      )
    end

    it 'tracks payment method changes' do
      stub_const('Spree::Event', class_spy(Spree::Event))

      subscription = create(:subscription)
      subscription.update!(payment_source: create(:credit_card, user: subscription.user))

      expect(Spree::Event).to have_received(:fire).with(
        'solidus_subscriptions.subscription_payment_method_changed',
        subscription: subscription,
      )
    end
  end

  describe '#cancel' do
    subject { subscription.cancel }

    let(:subscription) do
      create :subscription, :with_line_item, actionable_date: actionable_date
    end

    around { |e| Timecop.freeze { e.run } }

    before do
      allow(SolidusSubscriptions.configuration).to receive(:minimum_cancellation_notice) { minimum_cancellation_notice }
    end

    context 'when the subscription can be canceled' do
      let(:actionable_date) { 1.month.from_now }
      let(:minimum_cancellation_notice) { 1.day }

      it 'is canceled' do
        subject
        expect(subscription).to be_canceled
      end

      it 'creates a subscription_canceled event' do
        subject
        expect(subscription.events.last).to have_attributes(event_type: 'subscription_canceled')
      end
    end

    context 'when the subscription cannot be canceled' do
      let(:actionable_date) { Date.current }
      let(:minimum_cancellation_notice) { 1.day }

      it 'is pending cancelation' do
        subject
        expect(subscription).to be_pending_cancellation
      end

      it 'creates a subscription_canceled event' do
        subject
        expect(subscription.events.last).to have_attributes(event_type: 'subscription_canceled')
      end
    end

    context 'when the minimum cancellation date is 0.days' do
      let(:actionable_date) { Date.current }
      let(:minimum_cancellation_notice) { 0.days }

      it 'is canceled' do
        subject
        expect(subscription).to be_canceled
      end
    end
  end

  describe '#skip' do
    subject { subscription.skip&.to_date }

    let(:total_skips) { 0 }
    let(:successive_skips) { 0 }
    let(:expected_date) { 2.months.from_now.to_date }

    let(:subscription) do
      create(
        :subscription,
        :with_line_item,
        skip_count: total_skips,
        successive_skip_count: successive_skips
      )
    end

    before { stub_config(maximum_total_skips: 1) }

    context 'when the successive skips have been exceeded' do
      let(:successive_skips) { 1 }

      it { is_expected.to be_falsy }

      it 'adds errors to the subscription' do
        subject
        expect(subscription.errors[:successive_skip_count]).not_to be_empty
      end

      it 'does not create an event' do
        expect { subject }.not_to change(subscription.events, :count)
      end
    end

    context 'when the total skips have been exceeded' do
      let(:total_skips) { 1 }

      it { is_expected.to be_falsy }

      it 'adds errors to the subscription' do
        subject
        expect(subscription.errors[:skip_count]).not_to be_empty
      end

      it 'does not create an event' do
        expect { subject }.not_to change(subscription.events, :count)
      end
    end

    context 'when the subscription can be skipped' do
      it { is_expected.to eq expected_date }

      it 'creates a subscription_skipped event' do
        subject
        expect(subscription.events.last).to have_attributes(event_type: 'subscription_skipped')
      end
    end
  end

  describe '#deactivate' do
    subject { subscription.deactivate }

    let(:attributes) { {} }
    let(:subscription) do
      create :subscription, :actionable, :with_line_item, attributes do |s|
        s.installments = build_list(:installment, 2)
      end
    end

    context 'when the subscription can be deactivated' do
      let(:attributes) do
        { end_date: Date.current.ago(2.days) }
      end

      it 'is inactive' do
        subject
        expect(subscription).to be_inactive
      end

      it 'creates a subscription_deactivated event' do
        subject
        expect(subscription.events.last).to have_attributes(event_type: 'subscription_ended')
      end
    end

    context 'when the subscription cannot be deactivated' do
      it { is_expected.to be_falsy }

      it 'does not create an event' do
        expect { subject }.not_to change(subscription.events, :count)
      end
    end
  end

  describe '#activate' do
    context 'when the subscription can be activated' do
      it 'activates the subscription' do
        subscription = create(:subscription,
          actionable_date: Time.zone.today,
          end_date: Time.zone.yesterday,)
        subscription.deactivate!

        subscription.activate

        expect(subscription.state).to eq('active')
      end

      it 'creates a subscription_activated event' do
        subscription = create(:subscription,
          actionable_date: Time.zone.today,
          end_date: Time.zone.yesterday,)
        subscription.deactivate!

        subscription.activate

        expect(subscription.events.last).to have_attributes(event_type: 'subscription_activated')
      end
    end

    context 'when the subscription cannot be activated' do
      it 'returns false' do
        subscription = create(:subscription, actionable_date: Time.zone.today)

        expect(subscription.activate).to eq(false)
      end

      it 'does not create an event' do
        subscription = create(:subscription, actionable_date: Time.zone.today)

        expect {
          subscription.activate
        }.not_to change(subscription.events, :count)
      end
    end
  end

  describe '#next_actionable_date' do
    subject { subscription.next_actionable_date }

    context "when the subscription is active" do
      let(:expected_date) { Date.current + subscription.interval }
      let(:subscription) do
        build_stubbed(
          :subscription,
          :with_line_item,
          actionable_date: Date.current
        )
      end

      it { is_expected.to eq expected_date }
    end

    context "when the subscription is not active" do
      let(:subscription) { build_stubbed :subscription, :with_line_item, state: :canceled }

      it { is_expected.to be_nil }
    end
  end

  describe '#advance_actionable_date' do
    subject { subscription.advance_actionable_date }

    let(:expected_date) { Date.current + subscription.interval }
    let(:subscription) do
      build(
        :subscription,
        :with_line_item,
        actionable_date: Date.current
      )
    end

    it { is_expected.to eq expected_date }

    it 'updates the subscription with the new actionable date' do
      subject
      expect(subscription.reload).to have_attributes(
        actionable_date: expected_date
      )
    end
  end

  describe ".actionable" do
    subject { described_class.actionable }

    let!(:past_subscription) { create :subscription, actionable_date: 2.days.ago }
    let!(:future_subscription) { create :subscription, actionable_date: 1.month.from_now }
    let!(:inactive_subscription) { create :subscription, state: "inactive", actionable_date: 7.days.ago }
    let!(:canceled_subscription) { create :subscription, state: "canceled", actionable_date: 4.days.ago }

    it "returns subscriptions that have an actionable date in the past" do
      expect(subject).to include past_subscription
    end

    it "does not include future subscriptions" do
      expect(subject).not_to include future_subscription
    end

    it "does not include inactive subscriptions" do
      expect(subject).not_to include inactive_subscription
    end

    it "does not include canceled subscriptions" do
      expect(subject).not_to include canceled_subscription
    end
  end

  describe '#processing_state' do
    subject { subscription.processing_state }

    context 'when the subscription has never been processed' do
      let(:subscription) { build_stubbed :subscription }

      it { is_expected.to eq 'pending' }
    end

    context 'when the last processing attempt failed' do
      let(:subscription) do
        create(
          :subscription,
          installments: create_list(:installment, 1, :failed)
        )
      end

      it { is_expected.to eq 'failed' }
    end

    context 'when the last processing attempt succeeded' do
      let(:order) { create :completed_order_with_totals }

      let(:subscription) do
        create(
          :subscription,
          installments: create_list(
            :installment,
            1,
            :success,
            details: build_list(:installment_detail, 1, order: order, success: true)
          )
        )
      end

      it { is_expected.to eq 'success' }
    end
  end

  describe '.ransackable_scopes' do
    subject { described_class.ransackable_scopes }

    it { is_expected.to match_array [:in_processing_state] }
  end

  describe '.in_processing_state' do
    subject { described_class.in_processing_state(state) }

    let!(:new_subs) { create_list :subscription, 2 }
    let!(:failed_subs) { create_list(:installment, 2, :failed).map(&:subscription) }
    let!(:success_subs) { create_list(:installment, 2, :success).map(&:subscription) }

    context 'with successfull subscriptions' do
      let(:state) { :success }

      it { is_expected.to match_array success_subs }
    end

    context 'with failed subscriptions' do
      let(:state) { :failed }

      it { is_expected.to match_array failed_subs }
    end

    context 'with new subscriptions' do
      let(:state) { :pending }

      it { is_expected.to match_array new_subs }
    end

    context 'with unknown state' do
      let(:state) { :foo }

      it 'raises an error' do
        expect { subject }.to raise_error ArgumentError, /state must be one of/
      end
    end
  end

  describe '.processing_states' do
    subject { described_class.processing_states }

    it { is_expected.to match_array [:pending, :success, :failed] }
  end

  describe '#payment_source_to_use' do
    context 'when the subscription has a payment method without source' do
      it 'returns nil' do
        payment_method = create(:check_payment_method)

        subscription = create(:subscription, payment_method: payment_method)

        expect(subscription.payment_source_to_use).to eq(nil)
      end
    end

    context 'when the subscription has a payment method with a source' do
      it 'returns the source on the subscription' do
        user = create(:user)
        payment_method = create(:credit_card_payment_method)
        payment_source = create(:credit_card,
          payment_method: payment_method,
          gateway_customer_profile_id: 'BGS-123',
          user: user,)

        subscription = create(:subscription,
          user: user,
          payment_method: payment_method,
          payment_source: payment_source,)

        expect(subscription.payment_source_to_use).to eq(payment_source)
      end
    end

    context 'when the subscription has no payment method' do
      it "returns the default source from the user's wallet_payment_source" do
        user = create(:user)
        payment_source = create(:credit_card, gateway_customer_profile_id: 'BGS-123', user: user)
        wallet_payment_source = user.wallet.add(payment_source)
        user.wallet.default_wallet_payment_source = wallet_payment_source

        subscription = create(:subscription, user: user)

        expect(subscription.payment_source_to_use).to eq(payment_source)
      end
    end
  end

  describe '#payment_method_to_use' do
    context 'when the subscription has a payment method without source' do
      it 'returns the payment method on the subscription' do
        payment_method = create(:check_payment_method)
        subscription = create(:subscription, payment_method: payment_method)

        expect(subscription.payment_method_to_use).to eq(payment_method)
      end
    end

    context 'when the subscription has a payment method with a source' do
      it 'returns the payment method on the subscription' do
        user = create(:user)
        payment_method = create(:credit_card_payment_method)
        payment_source = create(:credit_card,
          payment_method: payment_method,
          gateway_customer_profile_id: 'BGS-123',
          user: user,)

        subscription = create(:subscription,
          user: user,
          payment_method: payment_method,
          payment_source: payment_source,)

        expect(subscription.payment_method_to_use).to eq(payment_method)
      end
    end

    context 'when the subscription has no payment method' do
      it "returns the method from the default source in the user's wallet_payment_source" do
        user = create(:user)
        payment_source = create(:credit_card, gateway_customer_profile_id: 'BGS-123', user: user)
        wallet_payment_source = user.wallet.add(payment_source)
        user.wallet.default_wallet_payment_source = wallet_payment_source

        subscription = create(:subscription, user: user)

        expect(subscription.payment_method_to_use).to eq(payment_source.payment_method)
      end
    end
  end

  describe '#billing_address_to_use' do
    context 'when the subscription has a billing address' do
      it 'returns the billing address on the subscription' do
        billing_address = create(:bill_address)

        subscription = create(:subscription, billing_address: billing_address)

        expect(subscription.billing_address_to_use).to eq(billing_address)
      end
    end

    context 'when the subscription has no billing address' do
      it 'returns the billing address on the user' do
        user = create(:user)
        billing_address = create(:bill_address)
        user.bill_address = billing_address

        subscription = create(:subscription, user: user)

        expect(subscription.billing_address_to_use).to eq(billing_address)
      end
    end
  end

  describe '#shipping_address_to_use' do
    context 'when the subscription has a shipping address' do
      it 'returns the shipping address on the subscription' do
        shipping_address = create(:ship_address)

        subscription = create(:subscription, shipping_address: shipping_address)

        expect(subscription.shipping_address_to_use).to eq(shipping_address)
      end
    end

    context 'when the subscription has no shipping address' do
      it 'returns the shipping address on the user' do
        user = create(:user)
        shipping_address = create(:ship_address)
        user.ship_address = shipping_address

        subscription = create(:subscription, user: user)

        expect(subscription.shipping_address_to_use).to eq(shipping_address)
      end
    end
  end

  describe "#update_actionable_date_if_interval_changed" do
    context "with installments" do
      context "when the last installment date would cause the interval to be in the past" do
        it "sets the actionable_date to the current day" do
          subscription = create(:subscription, actionable_date: Time.zone.parse('2016-08-22'))
          create(:installment, subscription: subscription, created_at: Time.zone.parse('2016-07-22'))

          subscription.update!(interval_length: 1, interval_units: 'month')

          expect(subscription.actionable_date).to eq(Time.zone.today)
        end
      end

      context "when the last installment date would cause the interval to be in the future" do
        it "sets the actionable_date to an interval from the last installment" do
          subscription = create(:subscription, actionable_date: Time.zone.parse('2016-08-22'))
          create(:installment, subscription: subscription, created_at: 4.days.ago)

          subscription.update!(interval_length: 1, interval_units: 'month')

          expect(subscription.actionable_date).to eq((4.days.ago + 1.month).to_date)
        end
      end
    end

    context "when there are no installments" do
      context "when the subscription creation date would cause the interval to be in the past" do
        it "sets the actionable_date to the current day" do
          subscription = create(:subscription, created_at: Time.zone.parse('2016-08-22'))

          subscription.update!(interval_length: 1, interval_units: 'month')

          expect(subscription.actionable_date).to eq(Time.zone.today)
        end
      end

      context "when the subscription creation date would cause the interval to be in the future" do
        it "sets the actionable_date to one interval past the subscription creation date" do
          subscription = create(:subscription, created_at: 4.days.ago)

          subscription.update!(interval_length: 1, interval_units: 'month')

          expect(subscription.actionable_date).to eq((4.days.ago + 1.month).to_date)
        end
      end
    end
  end

  describe '#failing_since' do
    context 'when the subscription is not failing' do
      it 'returns nil' do
        subscription = create(:subscription, installments: [
          create(:installment, details: [
            create(:installment_detail, success: false, created_at: '2020-11-11'),
            create(:installment_detail, success: false, created_at: '2020-11-12'),
            create(:installment_detail, success: true, created_at: '2020-11-13'),
          ]),
          create(:installment, details: [
            create(:installment_detail, success: false, created_at: '2020-11-24'),
            create(:installment_detail, success: true, created_at: '2020-11-25'),
          ]),
        ])

        expect(subscription.failing_since).to eq(nil)
      end
    end

    context 'when the subscription is failing with a previous success' do
      it 'returns the date of the first failure' do
        subscription = create(:subscription, installments: [
          create(:installment, details: [
            create(:installment_detail, success: false, created_at: '2020-11-11'),
            create(:installment_detail, success: false, created_at: '2020-11-12'),
            create(:installment_detail, success: true, created_at: '2020-11-13'),
          ]),
          create(:installment, details: [
            create(:installment_detail, success: false, created_at: '2020-11-24'),
            create(:installment_detail, success: false, created_at: '2020-11-25'),
          ]),
          create(:installment, details: [
            create(:installment_detail, success: false, created_at: '2020-11-26'),
            create(:installment_detail, success: false, created_at: '2020-11-27'),
          ]),
        ])

        expect(subscription.failing_since).to eq(Time.zone.parse('2020-11-24'))
      end
    end

    context 'when the subscription is failing with no previous success' do
      it 'returns the date of the first failure' do
        subscription = create(:subscription, installments: [
          create(:installment, details: [
            create(:installment_detail, success: false, created_at: '2020-11-11'),
            create(:installment_detail, success: false, created_at: '2020-11-12'),
            create(:installment_detail, success: false, created_at: '2020-11-13'),
          ]),
          create(:installment, details: [
            create(:installment_detail, success: false, created_at: '2020-11-24'),
            create(:installment_detail, success: false, created_at: '2020-11-25'),
          ]),
          create(:installment, details: [
            create(:installment_detail, success: false, created_at: '2020-11-26'),
            create(:installment_detail, success: false, created_at: '2020-11-27'),
          ]),
        ])

        expect(subscription.failing_since).to eq(Time.zone.parse('2020-11-11'))
      end
    end
  end

  describe '#maximum_reprocessing_time_reached?' do
    context 'when maximum_reprocessing_time is not configured' do
      it 'returns false' do
        stub_config(maximum_reprocessing_time: 5.days)
        subscription = create(:subscription)

        expect(subscription.maximum_reprocessing_time_reached?).to eq(false)
      end
    end

    context 'when maximum_reprocessing_time is configured' do
      context 'when the subscription has been failing for too long' do
        it 'returns true' do
          stub_config(maximum_reprocessing_time: 15.days)

          subscription = create(:subscription, installments: [
            create(:installment, details: [
              create(:installment_detail, success: false, created_at: 20.days.ago),
              create(:installment_detail, success: false, created_at: 19.days.ago),
              create(:installment_detail, success: true, created_at: 18.days.ago),
            ]),
            create(:installment, details: [
              create(:installment_detail, success: false, created_at: 17.days.ago),
              create(:installment_detail, success: false, created_at: 16.days.ago),
            ]),
            create(:installment, details: [
              create(:installment_detail, success: false, created_at: 15.days.ago),
              create(:installment_detail, success: false, created_at: 14.days.ago),
            ]),
          ])

          expect(subscription.maximum_reprocessing_time_reached?).to eq(true)
        end
      end

      context 'when the subscription has not been failing for too long' do
        it 'returns false' do
          stub_config(maximum_reprocessing_time: 15.days)

          subscription = create(:subscription, installments: [
            create(:installment, details: [
              create(:installment_detail, success: false, created_at: 15.days.ago),
              create(:installment_detail, success: false, created_at: 14.days.ago),
              create(:installment_detail, success: true, created_at: 13.days.ago),
            ]),
            create(:installment, details: [
              create(:installment_detail, success: false, created_at: 12.days.ago),
              create(:installment_detail, success: false, created_at: 11.days.ago),
            ]),
            create(:installment, details: [
              create(:installment_detail, success: false, created_at: 10.days.ago),
              create(:installment_detail, success: false, created_at: 9.days.ago),
            ]),
          ])

          expect(subscription.maximum_reprocessing_time_reached?).to eq(false)
        end
      end

      context 'when the subscription is not failing' do
        it 'returns false' do
          stub_config(maximum_reprocessing_time: 2.days)

          subscription = create(:subscription, installments: [
            create(:installment, details: [
              create(:installment_detail, success: false, created_at: 15.days.ago),
              create(:installment_detail, success: false, created_at: 14.days.ago),
              create(:installment_detail, success: true, created_at: 13.days.ago),
            ]),
            create(:installment, details: [
              create(:installment_detail, success: false, created_at: 12.days.ago),
              create(:installment_detail, success: false, created_at: 11.days.ago),
            ]),
            create(:installment, details: [
              create(:installment_detail, success: false, created_at: 10.days.ago),
              create(:installment_detail, success: true, created_at: 9.days.ago),
            ]),
          ])

          expect(subscription.maximum_reprocessing_time_reached?).to eq(false)
        end
      end
    end
  end

  describe '#actionable?' do
    context 'when the actionable date is nil' do
      it 'is not actionable' do
        subscription = build_stubbed(:subscription, actionable_date: nil)

        expect(subscription).not_to be_actionable
      end
    end

    context 'when the actionable date is in the future' do
      it 'is not actionable' do
        subscription = build_stubbed(:subscription, actionable_date: Time.zone.today + 5.days)

        expect(subscription).not_to be_actionable
      end
    end

    context 'when the state is either canceled or inactive' do
      it 'is not actionable' do
        canceled_subscription = build_stubbed(:subscription, :canceled)
        inactive_subscription = build_stubbed(:subscription, :inactive)

        [canceled_subscription, inactive_subscription].each do |subscription|
          expect(subscription).not_to be_actionable
        end
      end
    end

    context 'when the active subscription actionable date is today or in the past' do
      it 'is actionable' do
        subscription = build_stubbed(:subscription, actionable_date: Time.zone.today)

        expect(subscription).to be_actionable
      end
    end
  end
end