require 'test_helper'

class InvoiceTest < ActiveSupport::TestCase
  fixtures :countries

  should belong_to :buyer_account
  should belong_to :provider_account
  should validate_presence_of :provider_account
  should validate_presence_of :buyer_account

  should have_many :line_items
  should have_many :payment_transactions
  should have_attached_file :pdf

  def buyer_vat_rate(vat_rate)
    @invoice.buyer.update_attribute(:vat_rate, vat_rate)
    @invoice.reload
  end

  def setup
    Timecop.return
    @provider = FactoryGirl.create(:simple_provider)
    @buyer = FactoryGirl.create(:simple_buyer, provider_account: @provider)

    @invoice = FactoryGirl.create(:invoice,
                                  period: Month.new(Time.zone.local(1984, 1, 1)),
                                  provider_account: @provider,
                                  buyer_account: @buyer,
                                  friendly_id: '0000-00-00000001')
  end

  test 'be exportable to XML' do
    # TODO: some heavier testing and selecting needed
    assert_not_nil @invoice.to_xml
  end

  test 'have period when loaded' do
    @invoice.reload
    assert_instance_of Month, @invoice.period
  end

  test 'have period, period_start and period_end' do
    @invoice.update_attribute(:period, Month.new(Time.utc(1986, 4, 23)))
    assert_equal Time.utc(1986, 4, 1).to_date, @invoice.period_start
    assert_equal Time.utc(1986, 4, 1).end_of_month.to_date, @invoice.period_end
  end

  test 'have no due_on and issued_on date on creation' do
    assert_nil @invoice.due_on
    assert_nil @invoice.issued_on
    assert_nil @invoice.finalized_at
  end

  test 'finalize state' do
    time = Time.zone.now
    Timecop.freeze(time) { @invoice.finalize! }

    assert_equal time.round, @invoice.finalized_at.round
    assert @invoice.finalized?
  end

  test 'finalized_before ignores cancelled invoices' do
    time = Time.now.utc
    assert Invoice.finalized_before(time).empty?

    cancelled = FactoryGirl.create(:invoice, provider_account: @provider, buyer_account: @buyer)
    finalized = FactoryGirl.create(:invoice, provider_account: @provider, buyer_account: @buyer)

    Timecop.freeze(time) { [finalized, cancelled].each(&:finalize!) }
    cancelled.cancel!

    assert Invoice.finalized_before(time - 1.hour).empty?
    assert_equal 1, Invoice.finalized_before(time).count
    assert_equal finalized, Invoice.finalized_before(time).first
  end

  test 'fail on incorrect friendly_id format' do
    @invoice.friendly_id = "#{Time.now.year}-0000000a"
    @invoice.valid?
    assert !@invoice.errors[:friendly_id].empty?
  end

  [:open, :finalized, :pending, :unpaid, :failed].each do |state|
    test "be cancellable when #{state}" do
      @invoice.cancel!
      assert_equal 'cancelled', @invoice.state
    end
  end

  test 'invoice is editable only when open or finalized' do
    assert @invoice.editable?

    @invoice.finalize!
    assert @invoice.editable?

    assert @invoice.issue_and_pay_if_free!
    assert(!@invoice.editable?)
  end

  test 'bill accepts block' do
    @item = stub(name: 'Fake', cost: 10, description: 'really', quantity: 1)

    @invoice.bill do |i|
      i.name = 'Name in block'
      i.cost = 0
    end

    assert_equal 'Name in block', @invoice.line_items.first.name
    assert_equal 0, @invoice.reload.cost
  end

  test 'bill creates new line item' do
    @item = stub(name: 'Fake', cost: 10, description: 'really', quantity: 1)
    @invoice.bill(@item)
    assert_equal 1, @invoice.line_items.size
    assert_equal 'Fake', @invoice.line_items.first.name
    assert_equal 10, @invoice.reload.cost
  end

  test 'bill raises when argument is a duck and not "item"' do
    assert_raises(ArgumentError) { @invoice.bill('duck') }
  end

  test 'bill raises unless the invoice is open' do
    @item = stub(name: 'Fake', cost: 10, description: 'really', quantity: 1)

    @invoice.update_attribute(:state, :cancelled)
    assert_raises(Invoice::InvalidInvoiceStateException) { @invoice.bill(@item) }

    @invoice.update_attribute(:state, :finalized)
    assert_raises(Invoice::InvalidInvoiceStateException) { @invoice.bill(@item) }
  end

  test 'be due and paid if issued with 0 cost' do
    @invoice.stubs(:cost).returns(0.to_has_money('EUR'))
    @invoice.finalize!

    Timecop.freeze(@now = Time.zone.now) { @invoice.issue_and_pay_if_free! }

    assert_equal @now.utc.to_date, @invoice.issued_on
    assert_equal @now.round, @invoice.paid_at.round
    assert_equal 'paid', @invoice.state
  end

  test 'have PDF after issued' do
    @invoice.stubs(:cost).returns(0.to_has_money('EUR'))
    @invoice.issue_and_pay_if_free!
    assert @invoice.pdf.file?, 'Test have PDF when issued'
  end

  test 'issuing with non-zero cost sets meaningful dates' do
    @invoice.stubs(:cost).returns(100.to_has_money('EUR'))
    @invoice.finalize!

    Timecop.freeze(@now = Time.zone.now) { @invoice.issue_and_pay_if_free! }

    assert_equal @now.utc.to_date, @invoice.issued_on
    assert_not_nil @invoice.due_on
    assert_equal 'pending', @invoice.state
  end

  test 'have due_on after issued_on' do
    @invoice.stubs(:cost).returns(100.to_has_money('EUR'))
    Timecop.freeze(@now = Time.zone.now) { @invoice.issue_and_pay_if_free! }
    assert @invoice.issued_on < @invoice.due_on
  end

  test '#to_param returns id' do
    invoice = FactoryGirl.create(:invoice, created_at: Time.utc(1986, 4, 23),
                                 provider_account: @provider, buyer_account: @buyer)
    assert_equal invoice.id.to_s, invoice.to_param
  end

  test '#to returns by presence billing_address and legal_address' do
    assert_equal 'Perdido Street 123', @invoice.to.line1
    @invoice.buyer.update_attribute :org_legaladdress, nil
    @invoice.buyer.reload
    assert_equal 'Booked 2', @invoice.to.line1
  end

  test 'have cost that is sum of line items plus VAT' do
    invoice = FactoryGirl.create(:invoice, provider_account: @provider, buyer_account: @buyer)
    assert_equal 0, invoice.cost

    invoice.line_items.build(cost: 100)
    invoice.line_items.build(cost: 900)
    invoice.save!
    assert_equal 1000, invoice.cost

    # take vat_rate from buyer
    assert_equal 'open', invoice.state
    invoice.buyer.update_attribute(:vat_rate, 12.3)
    assert_equal 1123, invoice.reload.cost

    # pending should take the frozen vat_rate
    invoice.issue_and_pay_if_free!
    invoice.buyer_account.update_attribute(:vat_rate, 333)

    assert_equal 'pending', invoice.state
    invoice.buyer_account.vat_rate = 12.3
    assert_equal 1123, invoice.cost
  end

  # TODO: remove - use open? instead
  test 'have current? method' do
    Timecop.freeze do
      invoice_one = FactoryGirl.create(:invoice, period: Month.new(Time.zone.local(1984, 1, 1)), provider_account: @provider, buyer_account: @buyer)
      invoice_two = FactoryGirl.create(:invoice, provider_account: @provider, buyer_account: @buyer)

      refute invoice_one.current?
      assert invoice_two.current?
    end
  end

  test "invoice is 'chargeable' if it is pending or unpaid, due and it wasn't charged in 3 days or more" do
    params = { provider_account: @provider, buyer_account: @buyer }
    now = Time.zone.now

    # chargeable invoices
    FactoryGirl.create(:invoice, params.merge(state: 'unpaid', due_on: now))
    FactoryGirl.create(:invoice, params.merge(state: 'pending', due_on: now))

    # not chargeable decoys
    FactoryGirl.create(:invoice, params.merge(state: 'pending', due_on: now, last_charging_retry: now - 2.days))
    FactoryGirl.create(:invoice, params.merge(state: 'pending', due_on: now + 1.day))

    assert_equal 2, Invoice.chargeable(now).count
  end

  test 'charge! should raise if cancelled or paid' do
    @invoice.cancel!
    assert_raises(Invoice::InvalidInvoiceStateException) { @invoice.charge! }

    @invoice.update_attribute(:state, 'paid')
    assert_raises(Invoice::InvalidInvoiceStateException) { @invoice.charge! }
  end

  def setup_1964_and_2009_invoices
    @invoice_one = FactoryGirl.create(:invoice,
                                      buyer_account: @buyer,
                                      provider_account: @provider,
                                      period: Month.new(Time.utc(2009, 6, 1)))

    @invoice_two = FactoryGirl.create(:invoice,
                                      provider_account: @provider,
                                      buyer_account: @buyer,
                                      period: Month.new(Time.utc(1964, 10, 1)))
  end

  private :setup_1964_and_2009_invoices

  test 'find only "old ones" with #before scope' do
    setup_1964_and_2009_invoices
    assert_equal 0, Invoice.before(Time.utc(1964, 10, 2)).count
  end

  test 'find_by_month' do
    setup_1964_and_2009_invoices
    assert_equal @invoice_one, Invoice.find_by_month('2009-06')
    assert_equal @invoice_two, Invoice.find_by_month('1964-10')
  end

  test 'opened_by_buyer' do
    setup_1964_and_2009_invoices
    assert_equal @invoice_one, Invoice.opened_by_buyer(@buyer)
  end

  test 'currency is frozen after issuing' do
    before = @invoice.provider.currency

    @invoice.issue_and_pay_if_free!
    @invoice.provider.stubs(:currency).returns('XXX')

    assert_equal before, @invoice.currency
  end

  # Regression test for:
  # `issued_on` and `due_on` not in PDF (#2381)
  test 'issued_on and due_on is present while generating PDF' do
    @invoice.expects(:generate_pdf!).with do
      @data_for_pdf = [@invoice.issued_on, @invoice.due_on]
    end

    @invoice.issue_and_pay_if_free!

    assert_not_nil @data_for_pdf[0]
    assert_not_nil @data_for_pdf[1]
  end

  test 'vat_rate and address is frozen after issuing' do
    spain = Country.find_by_code!('ES')
    usa = Country.find_by_code!('US')

    buyer = @invoice.buyer

    buyer.country = spain
    buyer.vat_rate = 25
    buyer.save!

    # make sure the values are dynamic when 'open'
    @invoice.reload

    assert_equal 'Spain', @invoice.to.country
    assert_equal 'Barcelona', @invoice.to.city
    assert_equal 25, @invoice.vat_rate

    # that should freeze all
    @invoice.issue_and_pay_if_free!
    buyer.city = 'BCN'
    buyer.country = usa
    buyer.vat_rate = 99
    buyer.save!

    # make sure invoice remains the same
    @invoice.reload
    assert_equal 'Spain', @invoice.to.country
    assert_equal 'Barcelona', @invoice.to.city
  end

  test 'unresolved invoices' do
    Invoice.delete_all
    [:open, :finalized, :pending, :unpaid, :failed, :cancelled, :paid].each do |state|
      FactoryGirl.create(:invoice, state: state, provider_account: @provider, buyer_account: @buyer)
    end

    assert_equal 4, @buyer.invoices.unresolved.count
  end

  test 'exact cost without vat' do
    buyer_vat_rate(10)
    item = stub(name: 'Fake', cost: 1.233, description: 'really', quantity: 1)
    @invoice.bill(item)
    item = stub(name: 'Fake', cost: 0.0001, description: 'really2', quantity: 1)
    @invoice.bill(item)
    assert_equal 1.2331, @invoice.exact_cost_without_vat
  end

  test 'charge cost without vat' do
    buyer_vat_rate(10)
    item = stub(name: 'Fake', cost: 1.233, description: 'really', quantity: 1)
    @invoice.bill(item)
    item = stub(name: 'Fake', cost: 0.0001, description: 'really2', quantity: 1)
    @invoice.bill(item)
    assert_equal 1.23, @invoice.charge_cost_without_vat
  end

  test 'exact vat' do
    buyer_vat_rate(10)
    item = stub(name: 'Fake', cost: 1.233, description: 'really', quantity: 1)
    @invoice.bill(item)
    item = stub(name: 'Fake', cost: 0.0001, description: 'really2', quantity: 1)
    @invoice.bill(item)
    assert_equal 0.12331, @invoice.vat_amount
  end

  test 'VAT cost' do
    buyer_vat_rate(10)
    item = stub(name: 'Fake', cost: 1.233, description: 'really', quantity: 1)
    @invoice.bill(item)
    item = stub(name: 'Fake', cost: 0.0001, description: 'really2', quantity: 1)
    @invoice.bill(item)
    assert_equal 0.12, @invoice.charge_cost_vat_amount
  end

  test 'charged cost' do
    buyer_vat_rate(10)
    item = stub(name: 'Fake', cost: 1.233, description: 'really', quantity: 1)
    @invoice.bill(item)
    item = stub(name: 'Fake', cost: 0.0001, description: 'really2', quantity: 1)
    @invoice.bill(item)
    assert_equal((0.12 + 1.23), @invoice.charge_cost)
  end

  test '#charge! is successful' do
    @buyer.expects(:charge!).returns(true)
    @provider.stubs(:payment_gateway_unconfigured?).returns(false)
    @invoice.update_attribute(:state, 'pending')

    assert @invoice.charge!, 'Invoice should charge!'
  end

  test '#charge! failed if provider payment_gateway is unconfigured' do
    @buyer.expects(:charge!).never
    @provider.stubs(:payment_gateway_unconfigured?).returns(true)
    @invoice.update_attribute(:state, 'pending')

    refute @invoice.charge!, 'Invoice should not charge!'
  end

  test '#buyer_field_label' do
    @buyer.expects(:field_label).with('vat_rate')
    @invoice.buyer_field_label('vat_rate')

    @invoice.stubs(buyer_account: nil)
    assert_equal 'Vat rate', @invoice.buyer_field_label('vat_rate')
  end
end
