# encoding: utf-8

require 'spec_helper'
require 'monetize'

describe Monetize do
  bar = <<-JSON
    {
      "priority": 1,
      "iso_code": "BAR",
      "iso_numeric": "840",
      "name": "Dollar with 4 decimal places",
      "symbol": "$",
      "subunit": "Cent",
      "subunit_to_unit": 10000,
      "symbol_first": true,
      "html_entity": "$",
      "decimal_mark": ".",
      "thousands_separator": ","
    }
  JSON

  eu4 = <<-JSON
    {
      "priority": 1,
      "iso_code": "EU4",
      "iso_numeric": "841",
      "name": "Euro with 4 decimal places",
      "symbol": "€",
      "subunit": "Cent",
      "subunit_to_unit": 10000,
      "symbol_first": true,
      "html_entity": "€",
      "decimal_mark": ",",
      "thousands_separator": "."
    }
  JSON

  describe '.parse' do
    it 'parses european-formatted inputs under 10EUR' do
      expect(Monetize.parse('EUR 5,95')).to eq Money.new(595, 'EUR')
    end

    it 'parses european-formatted inputs with multiple thousands-seperators' do
      expect(Monetize.parse('EUR 1.234.567,89')).to eq Money.new(1_234_567_89, 'EUR')
      expect(Monetize.parse('EUR 1.111.234.567,89')).to eq Money.new(1_111_234_567_89, 'EUR')
    end

    describe 'currency detection' do
      context 'opted in' do
        before :all do
          Monetize.assume_from_symbol = true
        end

        after :all do
          Monetize.assume_from_symbol = false
        end

        Monetize::Parser::CURRENCY_SYMBOLS.each_pair do |symbol, iso_code|
          context iso_code do
            let(:currency) { Money::Currency.find(iso_code) }
            let(:amount) { 5_95 }
            let(:amount_in_units) { amount.to_f / currency.subunit_to_unit }

            it 'ensures correct amount calculations for test' do
              expect(amount_in_units * currency.subunit_to_unit).to eq(amount)
            end

            it "parses formatted inputs with #{iso_code} passed as a symbol" do
              expect(Monetize.parse("#{symbol}#{amount_in_units}")).to eq Money.new(amount, iso_code)
            end

            it "parses formatted inputs with #{iso_code} symbol is after the amount" do
              expect(Monetize.parse("#{amount_in_units}#{symbol}")).to eq Money.new(amount, iso_code)
            end

            context 'prefix' do
              it 'parses formatted inputs with plus sign and currency as a symbol' do
                expect(Monetize.parse("+#{symbol}#{amount_in_units}")).to eq Money.new(amount, iso_code)
              end

              it 'parses formatted inputs with minus sign and currency as a symbol' do
                expect(Monetize.parse("-#{symbol}#{amount_in_units}")).to eq Money.new(-amount, iso_code)
              end
            end

            context 'postfix' do
              it 'parses formatted inputs with currency symbol and postfix minus sign' do
                expect(Monetize.parse("#{symbol}#{amount_in_units}-")).to eq Money.new(-amount, iso_code)
              end

              it 'parses formatted inputs with currency symbol and postfix plus sign' do
                expect(Monetize.parse("#{symbol}#{amount_in_units}+")).to eq Money.new(amount, iso_code)
              end
            end

            context 'amount suffixes' do
              it 'parses formatted inputs with amounts given with suffixes' do
                expect(Monetize.parse("#{symbol}1.26K")).to eq Money.new(1_260 * currency.subunit_to_unit, iso_code)
                expect(Monetize.parse("#{symbol}126.36M")).to eq Money.new(126_360_000 * currency.subunit_to_unit, iso_code)
                expect(Monetize.parse("#{symbol}.45B")).to eq Money.new(450_000_000 * currency.subunit_to_unit, iso_code)
                expect(Monetize.parse("-#{symbol}2.45B")).to eq Money.new(-2_450_000_000 * currency.subunit_to_unit, iso_code)
                expect(Monetize.parse("#{symbol}1.65T")).to eq Money.new(1_650_000_000_000 * currency.subunit_to_unit, iso_code)
              end
            end

            it 'parses formatted inputs with symbol and surrounding spaces' do
              expect(Monetize.parse(" #{symbol}#{amount_in_units} ")).to eq Money.new(amount, iso_code)
            end

            it 'parses formatted inputs without currency detection when overridden' do
              expect(Monetize.parse("#{symbol}5.95", nil, assume_from_symbol: false)).to eq Money.new(amount, 'USD')
            end
          end
        end

        it 'should assume default currency if not a recognised symbol' do
          expect(Monetize.parse('L9.99')).to eq Money.new(999, 'USD')
        end

        it 'should use provided currency over symbol' do
          expect(Monetize.parse('$1.05 CAD')).to eq Money.new(105, 'CAD')
        end

        it 'ignores ZAR symbols that is part of a text' do
          expect(Monetize.parse('EUR 9.99')).to eq Money.new(999, 'EUR')
          expect(Monetize.parse('9.99 EUR')).to eq Money.new(999, 'EUR')
        end

        context 'negatives' do
          it 'ignores the ambiguous kr symbol' do
            # Could mean either of DKK, EEK, ISK, NOK, SEK
            expect(Monetize.parse('kr9.99')).to eq Money.new(999, 'USD')
          end
        end
      end

      context 'opted out' do
        before do
          Monetize.assume_from_symbol = false
        end

        it 'ignores the Euro symbol' do
          expect(Monetize.parse('€5.95')).to eq Money.new(595, 'USD')
        end

        it 'ignores the South African Rand symbol' do
          expect(Monetize.parse('R5.95')).to eq Money.new(595, 'USD')
        end

        it 'ignores the Euro symbol with surrounding spaces' do
          expect(Monetize.parse(' €5.95 ')).to eq Money.new(595, 'USD')
        end

        it 'ignores the British Pounds Sterling symbol' do
          expect(Monetize.parse('£9.99')).to eq Money.new(999, 'USD')
        end

        it 'parses formatted inputs with currency detection when overridden' do
          expect(Monetize.parse('£9.99', nil, assume_from_symbol: true)).to eq Money.new(999, 'GBP')
        end
      end

      it 'should opt out by default' do
        expect(Monetize.assume_from_symbol).to be_falsy
      end

      context 'ISO code' do
        it 'parses currency given as ISO code' do
          expect('20.00 USD'.to_money).to eq Money.new(20_00, 'USD')
          expect('20.00 EUR'.to_money).to eq Money.new(20_00, 'EUR')
          expect('20.00 GBP'.to_money).to eq Money.new(20_00, 'GBP')
        end

        it 'raises an error if currency code is invalid' do
          expect { '20.00 OMG'.to_money }.to raise_error Monetize::ParseError
        end
      end
    end

    it 'parses USD-formatted inputs under $10' do
      five_ninety_five = Money.new(595, 'USD')

      expect(Monetize.parse(5.95)).to eq five_ninety_five
      expect(Monetize.parse('5.95')).to eq five_ninety_five
      expect(Monetize.parse('$5.95')).to eq five_ninety_five
      expect(Monetize.parse("\n $5.95 \n")).to eq five_ninety_five
      expect(Monetize.parse('$ 5.95')).to eq five_ninety_five
      expect(Monetize.parse('$5.95 ea.')).to eq five_ninety_five
      expect(Monetize.parse('$5.95, each')).to eq five_ninety_five
    end

    it 'parses USD-formatted inputs with multiple thousands-seperators' do
      expect(Monetize.parse('1,234,567.89')).to eq Money.new(1_234_567_89, 'USD')
      expect(Monetize.parse('1,111,234,567.89')).to eq Money.new(1_111_234_567_89, 'USD')
    end

    it 'parses DKK-formatted inputs' do
      expect(Monetize.parse('kr.123,45', 'DKK')).to eq Money.new(123_45, 'DKK')
      expect(Monetize.parse('kr.123.45', 'DKK')).to eq Money.new(123_45, 'DKK')
      expect(Monetize.parse('kr.45k', 'DKK')).to eq Money.new(45_000_00, 'DKK')
    end

    it 'returns nil if input is a price range' do
      expect(Monetize.parse('$5.95-10.95')).to be_nil
      expect(Monetize.parse('$5.95 - 10.95')).to be_nil
      expect(Monetize.parse('$5.95 - $10.95')).to be_nil
    end

    it 'does not return a price for completely invalid input' do
      expect(Monetize.parse(nil)).to eq Money.empty
      expect(Monetize.parse('hellothere')).to eq Money.empty
      expect(Monetize.parse('')).to eq Money.empty
    end

    it 'handles negative inputs' do
      five_ninety_five = Money.new(-595, 'USD')

      expect(Monetize.parse('$-5.95')).to eq five_ninety_five
      expect(Monetize.parse('-$5.95')).to eq five_ninety_five
      expect(Monetize.parse('$5.95-')).to eq five_ninety_five
    end

    it 'returns nil when unable to detect polarity' do
      expect(Monetize.parse('-$5.95-')).to be_nil
    end

    it 'returns nil when more than 2 digit separators are used' do
      expect(Monetize.parse("123.34,56'89 EUR")).to be_nil
    end

    it 'parses correctly strings with repeated digit separator' do
      expect(Monetize.parse('19.12.89', 'EUR')).to eq Money.new(191_289_00, 'EUR')
    end

    it 'parses correctly strings with exactly 3 decimal digits' do
      expect(Monetize.parse('6,534', 'EUR')).to eq Money.new(653, 'EUR')
      expect(Monetize.parse('6.534', 'EUR')).to eq Money.new(653, 'EUR')

      Monetize.enforce_currency_delimiters = true
      expect(Monetize.parse('6.534', 'EUR')).to eq Money.new(6_534_00, 'EUR')
      Monetize.enforce_currency_delimiters = false
    end

    context 'Money object attempting to be parsed' do
      let(:money) { Money.new(595, 'GBP') }

      it 'returns the original Money object' do
        expect(Monetize.parse(money)).to eq money
        expect(Monetize.parse(money).currency).to eq 'GBP'
        expect(Monetize.parse(money).cents).to eq 595
      end
    end

    context 'parsing an instance of Numeric class' do
      let(:integer)     { 10 }
      let(:float)       { 10.0 }
      let(:big_decimal) { BigDecimal('10') }

      [:integer, :float, :big_decimal].each do |type|
        it "returns a new Money object based on the #{type} input" do
          money = Monetize.parse(send(type), 'USD')

          expect(money).to be_instance_of(Money)
          expect(money.currency).to eq('USD')
          expect(money.cents).to eq(10_00)
        end
      end
    end

    context 'custom currencies with 4 decimal places' do
      before :each do
        Money::Currency.register(JSON.parse(bar, symbolize_names: true))
        Money::Currency.register(JSON.parse(eu4, symbolize_names: true))
      end

      after :each do
        Money::Currency.unregister(JSON.parse(bar, symbolize_names: true))
        Money::Currency.unregister(JSON.parse(eu4, symbolize_names: true))
      end

      # String#to_money(Currency) is equivalent to Monetize.parse(String, Currency)
      it 'parses strings respecting subunit to unit, decimal and thousands separator' do
        expect('$0.4'.to_money('BAR')).to eq Money.new(4000, 'BAR')
        expect('€0,4'.to_money('EU4')).to eq Money.new(4000, 'EU4')

        expect('$0.04'.to_money('BAR')).to eq Money.new(400, 'BAR')
        expect('€0,04'.to_money('EU4')).to eq Money.new(400, 'EU4')

        expect('$0.004'.to_money('BAR')).to eq Money.new(40, 'BAR')
        expect('€0,004'.to_money('EU4')).to eq Money.new(40, 'EU4')

        expect('$0.0004'.to_money('BAR')).to eq Money.new(4, 'BAR')
        expect('€0,0004'.to_money('EU4')).to eq Money.new(4, 'EU4')

        expect('$0.0024'.to_money('BAR')).to eq Money.new(24, 'BAR')
        expect('€0,0024'.to_money('EU4')).to eq Money.new(24, 'EU4')

        expect('$0.0324'.to_money('BAR')).to eq Money.new(324, 'BAR')
        expect('€0,0324'.to_money('EU4')).to eq Money.new(324, 'EU4')

        expect('$0.5324'.to_money('BAR')).to eq Money.new(5324, 'BAR')
        expect('€0,5324'.to_money('EU4')).to eq Money.new(5324, 'EU4')

        # Following currencies consider 4 decimal places
        # rubocop:disable Style/NumericLiterals
        expect('$6.5324'.to_money('BAR')).to eq Money.new(6_5324, 'BAR')
        expect('€6,5324'.to_money('EU4')).to eq Money.new(6_5324, 'EU4')

        expect('$86.5324'.to_money('BAR')).to eq Money.new(86_5324, 'BAR')
        expect('€86,5324'.to_money('EU4')).to eq Money.new(86_5324, 'EU4')

        expect('$186.5324'.to_money('BAR')).to eq Money.new(186_5324, 'BAR')
        expect('€186,5324'.to_money('EU4')).to eq Money.new(186_5324, 'EU4')

        expect('$3,331.0034'.to_money('BAR')).to eq Money.new(3_331_0034, 'BAR')
        expect('€3.331,0034'.to_money('EU4')).to eq Money.new(3_331_0034, 'EU4')

        expect('$8,883,331.0034'.to_money('BAR')).to eq Money.new(8_883_331_0034, 'BAR')
        expect('€8.883.331,0034'.to_money('EU4')).to eq Money.new(8_883_331_0034, 'EU4')
        # rubocop:enable Style/NumericLiterals
      end

      it 'parses strings deducing a decimal mark when a single delimiter is used and major is 0' do
        expect('$0,4'.to_money('BAR')).to eq Money.new(4000, 'BAR')
        expect('€0.4'.to_money('EU4')).to eq Money.new(4000, 'EU4')

        expect('$0,04'.to_money('BAR')).to eq Money.new(400, 'BAR')
        expect('€0.04'.to_money('EU4')).to eq Money.new(400, 'EU4')

        expect('$0,004'.to_money('BAR')).to eq Money.new(40, 'BAR')
        expect('€0.004'.to_money('EU4')).to eq Money.new(40, 'EU4')

        expect('$0,0004'.to_money('BAR')).to eq Money.new(4, 'BAR')
        expect('€0.0004'.to_money('EU4')).to eq Money.new(4, 'EU4')

        expect('$0,0024'.to_money('BAR')).to eq Money.new(24, 'BAR')
        expect('€0.0024'.to_money('EU4')).to eq Money.new(24, 'EU4')

        expect('$0,0324'.to_money('BAR')).to eq Money.new(324, 'BAR')
        expect('€0.0324'.to_money('EU4')).to eq Money.new(324, 'EU4')

        expect('$0,5324'.to_money('BAR')).to eq Money.new(5324, 'BAR')
        expect('€0.5324'.to_money('EU4')).to eq Money.new(5324, 'EU4')
      end
    end
  end

  describe '.parse!' do
    it 'does not return a price if there is a price range' do
      expect { Monetize.parse!('$5.95-10.95') }.to raise_error Monetize::ParseError
      expect { Monetize.parse!('$5.95 - 10.95') }.to raise_error Monetize::ParseError
      expect { Monetize.parse!('$5.95 - $10.95') }.to raise_error Monetize::ParseError
    end

    it 'raises ArgumentError when unable to detect polarity' do
      expect { Monetize.parse!('-$5.95-') }.to raise_error Monetize::ParseError
    end

    it 'raises ArgumentError with invalid format' do
      expect { Monetize.parse!('11..0') }.to raise_error Monetize::ParseError
    end
  end

  describe '.parse_collection' do
    it 'parses into a Money::Collection' do
      expect(Monetize.parse_collection('$7')).to be_a Monetize::Collection
    end

    it 'parses comma separated values' do
      collection = Monetize.parse_collection('$5, $7')
      expect(collection.first).to eq Monetize.parse('$5')
      expect(collection.last).to eq Monetize.parse('$7')
    end

    it 'parses slash separated values' do
      collection = Monetize.parse_collection('£4.50/€6')
      expect(collection.first).to eq Monetize.parse('£4.50')
      expect(collection.last).to eq Monetize.parse('€6')
    end

    it 'parses hyphens as ranges' do
      collection = Monetize.parse_collection('$4 - $10')
      expect(collection.first).to eq Monetize.parse('$4')
      expect(collection.last).to eq Monetize.parse('$10')
    end

    it 'raises an error if argument is invalid' do
      expect { Monetize.parse_collection(nil) }.to raise_error Monetize::ArgumentError
    end
  end

  describe '.from_string' do
    it 'converts given amount to cents' do
      expect(Monetize.from_string('1')).to eq Money.new(1_00)
      expect(Monetize.from_string('1')).to eq Money.new(1_00, 'USD')
      expect(Monetize.from_string('1', 'EUR')).to eq Money.new(1_00, 'EUR')
    end

    it 'respects :subunit_to_unit currency property' do
      expect(Monetize.from_string('1', 'USD')).to eq Money.new(1_00, 'USD')
      expect(Monetize.from_string('1', 'TND')).to eq Money.new(1_000, 'TND')
      expect(Monetize.from_string('1', 'JPY')).to eq Money.new(1, 'JPY')
    end

    it 'accepts a currency options' do
      m = Monetize.from_string('1')
      expect(m.currency).to eq Money.default_currency

      m = Monetize.from_string('1', Money::Currency.wrap('EUR'))
      expect(m.currency).to eq Money::Currency.wrap('EUR')

      m = Monetize.from_string('1', 'EUR')
      expect(m.currency).to eq Money::Currency.wrap('EUR')
    end
  end

  describe '.from_fixnum' do
    it 'converts given amount to cents' do
      expect(Monetize.from_fixnum(1)).to eq Money.new(1_00)
      expect(Monetize.from_fixnum(1)).to eq Money.new(1_00, 'USD')
      expect(Monetize.from_fixnum(1, 'EUR')).to eq Money.new(1_00, 'EUR')
    end

    it 'should respect :subunit_to_unit currency property' do
      expect(Monetize.from_fixnum(1, 'USD')).to eq Money.new(1_00, 'USD')
      expect(Monetize.from_fixnum(1, 'TND')).to eq Money.new(1_000, 'TND')
      expect(Monetize.from_fixnum(1, 'JPY')).to eq Money.new(1, 'JPY')
    end

    it 'accepts a currency options' do
      m = Monetize.from_fixnum(1)
      expect(m.currency).to eq Money.default_currency

      m = Monetize.from_fixnum(1, Money::Currency.wrap('EUR'))
      expect(m.currency).to eq Money::Currency.wrap('EUR')

      m = Monetize.from_fixnum(1, 'EUR')
      expect(m.currency).to eq Money::Currency.wrap('EUR')
    end

    it 'is aliased as from_integer' do
      expect(Monetize.from_integer(1)).to eq(Monetize.from_fixnum(1))
    end
  end

  describe '.from_float' do
    it 'converts given amount to cents' do
      expect(Monetize.from_float(1.2)).to eq Money.new(1_20)
      expect(Monetize.from_float(1.2)).to eq Money.new(1_20, 'USD')
      expect(Monetize.from_float(1.2, 'EUR')).to eq Money.new(1_20, 'EUR')
    end

    it 'respects :subunit_to_unit currency property' do
      expect(Monetize.from_float(1.2, 'USD')).to eq Money.new(1_20, 'USD')
      expect(Monetize.from_float(1.2, 'TND')).to eq Money.new(1_200, 'TND')
      expect(Monetize.from_float(1.2, 'JPY')).to eq Money.new(1, 'JPY')
    end

    it 'accepts a currency options' do
      m = Monetize.from_float(1.2)
      expect(m.currency).to eq Money.default_currency

      m = Monetize.from_float(1.2, Money::Currency.wrap('EUR'))
      expect(m.currency).to eq Money::Currency.wrap('EUR')

      m = Monetize.from_float(1.2, 'EUR')
      expect(m.currency).to eq Money::Currency.wrap('EUR')
    end
  end

  describe '.from_bigdecimal' do
    it 'converts given amount to cents' do
      expect(Monetize.from_bigdecimal(BigDecimal('1'))).to eq Money.new(1_00)
      expect(Monetize.from_bigdecimal(BigDecimal('1'))).to eq Money.new(1_00, 'USD')
      expect(Monetize.from_bigdecimal(BigDecimal('1'), 'EUR')).to eq Money.new(1_00, 'EUR')
    end

    it 'respects :subunit_to_unit currency property' do
      expect(Monetize.from_bigdecimal(BigDecimal('1'), 'USD')).to eq Money.new(1_00, 'USD')
      expect(Monetize.from_bigdecimal(BigDecimal('1'), 'TND')).to eq Money.new(1_000, 'TND')
      expect(Monetize.from_bigdecimal(BigDecimal('1'), 'JPY')).to eq Money.new(1, 'JPY')
    end

    it 'respects rounding mode when rounding amount to the nearest cent' do
      amount = BigDecimal('1.005')

      expect(Monetize.from_bigdecimal(amount, 'USD')).to eq Money.from_amount(amount, 'USD')
    end

    it 'accepts a currency options' do
      m = Monetize.from_bigdecimal(BigDecimal('1'))
      expect(m.currency).to eq Money.default_currency

      m = Monetize.from_bigdecimal(BigDecimal('1'), Money::Currency.wrap('EUR'))
      expect(m.currency).to eq Money::Currency.wrap('EUR')

      m = Monetize.from_bigdecimal(BigDecimal('1'), 'EUR')
      expect(m.currency).to eq Money::Currency.wrap('EUR')
    end

    context 'infinite_precision = true' do
      before do
        Money.infinite_precision = true
      end

      after do
        Money.infinite_precision = false
      end

      it 'keeps precision' do
        expect(Monetize.from_bigdecimal(BigDecimal('1'))).to eq Money.new(100)
        expect(Monetize.from_bigdecimal(BigDecimal('1.23456'))).to eq Money.new(123.456)
        expect(Monetize.from_bigdecimal(BigDecimal('-1.23456'))).to eq Money.new(-123.456)
        expect(Monetize.from_bigdecimal(BigDecimal('1.23456'))).to eq Money.new(123.456, 'USD')
        expect(Monetize.from_bigdecimal(BigDecimal('1.23456'), 'EUR')).to eq Money.new(123.456, 'EUR')

        expect('1'.to_money).to eq Money.new(100)
        expect('1.23456'.to_money).to eq Money.new(123.456)
        expect('-1.23456'.to_money).to eq Money.new(-123.456)
        expect('$1.23456'.to_money).to eq Money.new(123.456, 'USD')
        expect('1.23456 EUR'.to_money).to eq Money.new(123.456, 'EUR')
      end
    end
  end

  describe '.from_numeric' do
    it 'converts given amount to cents' do
      expect(Monetize.from_numeric(1)).to eq Money.new(1_00)
      expect(Monetize.from_numeric(1.0)).to eq Money.new(1_00)
      expect(Monetize.from_numeric(BigDecimal('1'))).to eq Money.new(1_00)
    end

    it 'raises ArgumentError with unsupported argument' do
      expect { Monetize.from_numeric('100') }.to raise_error(Monetize::ArgumentError)
    end

    it 'respects :subunit_to_unit currency property' do
      expect(Monetize.from_numeric(1, 'USD')).to eq Money.new(1_00, 'USD')
      expect(Monetize.from_numeric(1, 'TND')).to eq Money.new(1_000, 'TND')
      expect(Monetize.from_numeric(1, 'JPY')).to eq Money.new(1, 'JPY')
    end

    it 'accepts a bank option' do
      expect(Monetize.from_numeric(1)).to eq Money.new(1_00)
      expect(Monetize.from_numeric(1)).to eq Money.new(1_00, 'USD')
      expect(Monetize.from_numeric(1, 'EUR')).to eq Money.new(1_00, 'EUR')
    end

    it 'accepts a currency options' do
      m = Monetize.from_numeric(1)
      expect(m.currency).to eq Money.default_currency

      m = Monetize.from_numeric(1, Money::Currency.wrap('EUR'))
      expect(m.currency).to eq Money::Currency.wrap('EUR')

      m = Monetize.from_numeric(1, 'EUR')
      expect(m.currency).to eq Money::Currency.wrap('EUR')
    end
  end

  describe '.extract_cents' do
    it 'is deprecated' do
      allow(Monetize).to receive(:warn)

      Monetize.extract_cents('100')

      expect(Monetize)
        .to have_received(:warn)
        .with('[DEPRECATION] Monetize.extract_cents is deprecated. Use Monetize.parse().cents')
    end

    it 'extracts cents from a given string' do
      expect(Monetize.extract_cents('10.99')).to eq(1099)
    end

    it "correctly treats pipe marks '|' in input (regression test)" do
      expect(Monetize.extract_cents('100|0')).to eq Monetize.extract_cents('100!0')
    end
  end

  context 'given the same inputs to .parse and .from_*' do
    it 'gives the same results' do
      expect(4.635.to_money).to eq '4.635'.to_money
    end
  end
end
