require "test_helper"

class Account::Balance::SyncerTest < ActiveSupport::TestCase
  include Account::EntriesTestHelper

  setup do
    @account = families(:empty).accounts.create!(name: "Test", balance: 20000, currency: "USD", accountable: Depository.new)
    @investment_account = families(:empty).accounts.create!(name: "Test Investment", balance: 50000, currency: "USD", accountable: Investment.new)
  end

  test "syncs account with no entries" do
    assert_equal 0, @account.balances.count

    run_sync_for @account

    assert_equal [ @account.balance ], @account.balances.chronological.map(&:balance)
  end

  test "syncs account with valuations only" do
    create_valuation(account: @account, date: 2.days.ago.to_date, amount: 22000)

    run_sync_for @account

    assert_equal 22000, @account.balance
    assert_equal [ 22000, 22000, 22000 ], @account.balances.chronological.map(&:balance)
  end

  test "syncs account with transactions only" do
    create_transaction(account: @account, date: 4.days.ago.to_date, amount: 100)
    create_transaction(account: @account, date: 2.days.ago.to_date, amount: -500)

    run_sync_for @account

    assert_equal 20000, @account.balance
    assert_equal [ 19600, 19500, 19500, 20000, 20000, 20000 ], @account.balances.chronological.map(&:balance)
  end

  test "syncs account with valuations and transactions when valuation starts" do
    create_valuation(account: @account, date: 5.days.ago.to_date, amount: 20000)
    create_transaction(account: @account, date: 3.days.ago.to_date, amount: -500)
    create_transaction(account: @account, date: 2.days.ago.to_date, amount: 100)
    create_valuation(account: @account, date: 1.day.ago.to_date, amount: 25000)

    run_sync_for(@account)

    assert_equal 25000, @account.balance
    assert_equal [ 20000, 20000, 20500, 20400, 25000, 25000 ], @account.balances.chronological.map(&:balance)
  end

  test "syncs account with valuations and transactions when transaction starts" do
    new_account = families(:empty).accounts.create!(name: "Test Account", balance: 1000, currency: "USD", accountable: Depository.new)
    create_transaction(account: new_account, date: 2.days.ago.to_date, amount: 250)
    create_valuation(account: new_account, date: Date.current, amount: 1000)

    run_sync_for(new_account)

    assert_equal 1000, new_account.balance
    assert_equal [ 1250, 1000, 1000, 1000 ], new_account.balances.chronological.map(&:balance)
  end

  test "syncs account with transactions in multiple currencies" do
    ExchangeRate.create! date: 1.day.ago.to_date, from_currency: "EUR", to_currency: "USD", rate: 1.2

    create_transaction(account: @account, date: 3.days.ago.to_date, amount: 100, currency: "USD")
    create_transaction(account: @account, date: 2.days.ago.to_date, amount: 300, currency: "USD")
    create_transaction(account: @account, date: 1.day.ago.to_date, amount: 500, currency: "EUR") # €500 * 1.2 = $600

    run_sync_for(@account)

    assert_equal 20000, @account.balance
    assert_equal [ 21000, 20900, 20600, 20000, 20000 ], @account.balances.chronological.map(&:balance)
  end

  test "converts foreign account balances to family currency" do
    @account.update! currency: "EUR"

    create_transaction(date: 1.day.ago.to_date, amount: 1000, account: @account, currency: "EUR")

    create_exchange_rate(2.days.ago.to_date, from: "EUR", to: "USD", rate: 2)
    create_exchange_rate(1.day.ago.to_date, from: "EUR", to: "USD", rate: 2)
    create_exchange_rate(Date.current, from: "EUR", to: "USD", rate: 2)

    with_env_overrides SYNTH_API_KEY: ENV["SYNTH_API_KEY"] || "fookey" do
      run_sync_for(@account)
    end

    usd_balances = @account.balances.where(currency: "USD").chronological.map(&:balance)
    eur_balances = @account.balances.where(currency: "EUR").chronological.map(&:balance)

    assert_equal 20000, @account.balance
    assert_equal [ 21000, 20000, 20000 ], eur_balances # native account balances
    assert_equal [ 42000, 40000, 40000 ], usd_balances # converted balances at rate of 2:1
  end

  test "raises issue if missing exchange rates" do
    create_transaction(date: Date.current, account: @account, currency: "EUR")

    ExchangeRate.expects(:find_rate).with(from: "EUR", to: "USD", date: Date.current).returns(nil)
    @account.expects(:observe_missing_exchange_rates).with(from: "EUR", to: "USD", dates: [ Date.current ])

    syncer = Account::Balance::Syncer.new(@account)

    syncer.run
  end

  # Account is able to calculate balances in its own currency (i.e. can still show a historical graph), but
  # doesn't have exchange rates available to convert those calculated balances to the family currency
  test "observes issue if exchange rate provider is not configured" do
    @account.update! currency: "EUR"

    syncer = Account::Balance::Syncer.new(@account)

    @account.expects(:observe_missing_exchange_rate_provider)

    with_env_overrides SYNTH_API_KEY: nil do
      syncer.run
    end
  end

  test "overwrites existing balances and purges stale balances" do
    assert_equal 0, @account.balances.size

    @account.balances.create! date: Date.current, currency: "USD", balance: 30000 # incorrect balance, will be updated
    @account.balances.create! date: 10.years.ago.to_date, currency: "USD", balance: 35000 # Out of range balance, will be deleted

    assert_equal 2, @account.balances.size

    run_sync_for(@account)

    assert_equal [ @account.balance ], @account.balances.chronological.map(&:balance)
  end

  test "partial sync does not affect balances prior to sync start date" do
    existing_balance = @account.balances.create! date: 2.days.ago.to_date, currency: "USD", balance: 30000

    transaction = create_transaction(account: @account, date: 1.day.ago.to_date, amount: 100, currency: "USD")

    run_sync_for(@account, start_date: 1.day.ago.to_date)

    assert_equal [ existing_balance.balance, existing_balance.balance - transaction.amount, @account.balance ], @account.balances.chronological.map(&:balance)
  end

  private

    def run_sync_for(account, start_date: nil)
      syncer = Account::Balance::Syncer.new(account, start_date: start_date)
      syncer.run
    end

    def create_exchange_rate(date, from:, to:, rate:)
      ExchangeRate.create! date: date, from_currency: from, to_currency: to, rate: rate
    end
end
