module Danconia
  describe Money do
    context 'instantiation' do
      it 'should accept integers and strings' do
        expect(Money(10).amount).to eq BigDecimal('10')
        expect(Money('10.235').amount).to eq BigDecimal('10.235')
      end

      it 'non numeric values are treated as zero' do
        expect(Money(nil)).to eq Money(0)
        expect(Money('a')).to eq Money(0)
      end

      it 'should use the default currency if not specified' do
        TestHelpers.with_config do |config|
          config.default_currency = 'ARS'
          expect(Money(0).currency.code).to eq 'ARS'

          config.default_currency = 'EUR'
          expect(Money(0).currency.code).to eq 'EUR'
        end
      end
    end

    context 'arithmetic' do
      it 'does the operation on the amount' do
        expect(Money(3) + Money(2)).to eq Money(5)
        expect(Money(3.5) + 0.5).to eq Money(4)
        expect(Money(78.55) * 0.25).to eq Money(19.6375)
        expect(Money(3) / 2).to eq Money(1.5)
        expect { Money(3.5) + 'a' }.to raise_error TypeError
      end

      it 'should preserve the currency' do
        expect(Money(1, 'ARS') + 2).to eq Money(3, 'ARS')
        expect(Money(1, 'ARS') + Money(2, 'ARS')).to eq Money(3, 'ARS')
      end

      it 'should exchange the other currency if it is different' do
        m1 = Money(1, 'ARS', exchange_opts: {exchange: fake_exchange(rate: 4)})
        expect(m1 + Money(1, 'USD')).to eq Money(5, 'ARS')
      end

      it 'should return a new object with the same options' do
        m1 = Money(4, decimals: 3)
        m2 = m1 * 2
        expect(m2).to_not eql m1
        expect(m2.decimals).to eq 3
      end

      it 'round should return a money object with the same currency' do
        expect(Money(1.9, 'ARS').round).to eq Money(2, 'ARS')
      end
    end

    context 'comparisson' do
      it 'two money objects are equal when the amount and currency are the same' do
        expect(Money(1.01)).to eq Money(1.01)
        expect(Money(1.01)).not_to eq Money(1.02)
        expect(Money(1, 'ARS')).to eq Money(1, 'ARS')
        expect(Money(1, 'USD')).not_to eq Money(1, 'ARS')
      end

      it 'allows to compare against numeric values when using the default currency' do
        expect(Money(1)).to eq 1
        expect(Money(1.35)).to eq 1.35
        expect(Money(1)).not_to eq 2
        expect(Money(1, 'ARS')).not_to eq 1
      end

      it 'should exchange to the source currency if they differ' do
        TestHelpers.with_rates 'USDARS' => 4 do |_config|
          expect(Money(3, 'ARS') < Money(1, 'USD')).to be true
          expect(Money(4, 'ARS') < Money(1, 'USD')).to be false
        end
      end

      it 'should work with uniq' do
        expect([Money(1), Money(1)].uniq.size).to eq 1
        expect([Money(1), Money(1.1)].uniq.size).to eq 2
        expect([Money(1, 'ARS'), Money(1, 'USD')].uniq.size).to eq 2
      end
    end

    context 'format' do
      it 'allow to override the number of decimals' do
        expect(Money(3.561, decimals: 3).format(decimals: 1)).to eq '$3.6'
      end

      it 'pass the options to the activesupport helper' do
        expect(Money(2).format(format: '%n %u')).to eq '2.00 $'
      end
    end

    context 'to_s' do
      it 'should add the currency symbol' do
        expect(Money(3.25).to_s).to eq '$3.25'

        TestHelpers.with_config do |config|
          config.available_currencies = [{code: 'EUR', symbol: '€'}, {code: 'JPY', symbol: '¥'}]

          expect(Money(1, 'EUR').to_s).to eq '€1.00'
          expect(Money(1, 'JPY').to_s).to eq '¥1.00'
          expect(Money(1, 'OTHER').to_s).to eq '$1.00'
        end
      end

      it 'should round according to decimals' do
        expect(Money(3.256, decimals: 2).to_s).to eq '$3.26'
        expect(Money(3.2517, decimals: 3).to_s).to eq '$3.252'
      end
    end

    context '.inspect' do
      it 'should display the object internals' do
        expect(Money(10.25, 'ARS', decimals: 3).inspect).to eq '10.25 ARS'
      end
    end

    context 'exchange_to' do
      it 'should use a default exchange if not overriden' do
        TestHelpers.with_rates 'USDEUR' => 3, 'USDARS' => 4 do
          expect(Money(2, 'USD').exchange_to('EUR')).to eq Money(6, 'EUR')
          expect(Money(2, 'USD').exchange_to('ARS')).to eq Money(8, 'ARS')
        end
      end

      it 'should allow to pass the exchange to the instance' do
        m = Money(2, 'USD', exchange_opts: {exchange: fake_exchange(rate: 3)})
        expect(m.exchange_to('ARS')).to eq Money(6, 'ARS')
      end

      it 'should allow to pass the exchange when converting' do
        expect(Money(2, 'USD').exchange_to('ARS', exchange: fake_exchange(rate: 4))).to eq Money(8, 'ARS')
      end

      it 'when overriding the exchange, should preserve it in the new instances' do
        m1 = Money(1, 'USD').exchange_to('ARS', exchange: fake_exchange(rate: 2))
        m2 = m1 + Money(3, 'USD')
        m3 = m2 * Money(1, 'USD')
        expect(m2).to eq Money(8, 'ARS')
        expect(m3).to eq Money(16, 'ARS')
      end

      it 'if no rate if found should raise error' do
        expect { Money(2, 'USD').exchange_to('ARS') }.to raise_error Errors::ExchangeRateNotFound
      end

      it 'exchange between the same currency is always 1' do
        expect(Money(5, 'EUR').exchange_to('EUR')).to eq Money(5, 'EUR')
      end

      it 'should return a new object with the same opts' do
        m1 = Money(1, 'USD', decimals: 0)
        m2 = m1.exchange_to('ARS', exchange: fake_exchange(rate: 3))
        expect(m2).to_not eql m1
        expect(m2.decimals).to eq 0
        expect(m1).to eq Money(1, 'USD')
      end

      it 'if the destination currency is blank should not do the conversion' do
        expect(Money(1, 'USD').exchange_to('')).to eq Money(1, 'USD')
        expect(Money(1, 'ARS').exchange_to('')).to eq Money(1, 'ARS')
      end

      context 'opts' do
        let(:exchange) do
          Class.new(Exchange) do
            def rates opts
              case opts[:type]
              when 'divisa' then {'USDARS' => 7}
              when 'billete' then {'USDARS' => 8}
              else {}
              end
            end
          end.new
        end

        it 'allows to specify opts to pass to the exchange (filters for example)' do
          expect(Money(1, 'USD').exchange_to('ARS', type: 'divisa', exchange: exchange)).to eq Money(7, 'ARS')
          expect { Money(1, 'USD').exchange_to('ARS', exchange: exchange) }.to raise_error Errors::ExchangeRateNotFound
        end

        it 'should be preserved after operations' do
          m1 = Money(1, 'USD').exchange_to('ARS', type: 'divisa', exchange: exchange)
          m2 = m1 + Money(2, 'USD')

          expect(m2).to eq Money(21, 'ARS')
          expect(m1 < Money(2, 'USD')).to eq true
        end

        it 'should use the instance exchange_opts by default' do
          m = Money(1, 'USD', exchange_opts: {exchange: exchange, type: 'billete'})
          expect(m.exchange_to('ARS')).to eq Money(8, 'ARS')
        end
      end
    end

    context 'default_currency?' do
      it 'is true if the currency is the configured default' do
        TestHelpers.with_config do |config|
          config.default_currency = 'ARS'
          expect(Money(1, 'ARS')).to be_default_currency
          expect(Money(1, 'USD')).to_not be_default_currency
        end
      end
    end

    context 'delegation' do
      it 'should delegate missing methods to the amount' do
        expect(Money(10).positive?).to be true
        expect(Money(10).respond_to?(:positive?)).to be true
      end
    end

    def fake_exchange args = {}
      double 'Danconia::Exchange', args.reverse_merge(rate: nil)
    end
  end
end