require 'dentaku/calculator'

describe Dentaku::Calculator do
  let(:calculator)  { described_class.new }
  let(:with_memory) { described_class.new.store(:apples => 3) }

  it 'evaluates an expression' do
    expect(calculator.evaluate('7+3')).to eq(10)
  end

  describe 'memory' do
    it { expect(calculator).to be_empty }
    it { expect(with_memory).not_to be_empty   }
    it { expect(with_memory.clear).to be_empty }

    it 'discards local values' do
      expect(calculator.evaluate('pears * 2', :pears => 5)).to eq(10)
      expect(calculator).to be_empty
    end
  end

  it 'evaluates a statement with no variables' do
    expect(calculator.evaluate('5+3')).to eq(8)
    expect(calculator.evaluate('(1+1+1)/3*100')).to eq(100)
  end

  it 'fails to evaluate unbound statements' do
    unbound = 'foo * 1.5'
    expect { calculator.evaluate!(unbound) }.to raise_error(Dentaku::UnboundVariableError)
    expect { calculator.evaluate!(unbound) }.to raise_error do |error|
      expect(error.unbound_variables).to eq [:foo]
    end
    expect(calculator.evaluate(unbound)).to be_nil
    expect(calculator.evaluate(unbound) { :bar }).to eq :bar
    expect(calculator.evaluate(unbound) { |e| e }).to eq unbound
  end

  it 'evaluates unbound statements given a binding in memory' do
    expect(calculator.evaluate('foo * 1.5', :foo => 2)).to eq(3)
    expect(calculator.bind(:monkeys => 3).evaluate('monkeys < 7')).to be_truthy
    expect(calculator.evaluate('monkeys / 1.5')).to eq(2)
  end

  it 'rebinds for each evaluation' do
    expect(calculator.evaluate('foo * 2', :foo => 2)).to eq(4)
    expect(calculator.evaluate('foo * 2', :foo => 4)).to eq(8)
  end

  it 'accepts strings or symbols for binding keys' do
    expect(calculator.evaluate('foo * 2', :foo => 2)).to eq(4)
    expect(calculator.evaluate('foo * 2', 'foo' => 4)).to eq(8)
  end

  it 'accepts digits in identifiers' do
    expect(calculator.evaluate('foo1 * 2', :foo1 => 2)).to eq(4)
    expect(calculator.evaluate('foo1 * 2', 'foo1' => 4)).to eq(8)
    expect(calculator.evaluate('1foo * 2', '1foo' => 2)).to eq(4)
    expect(calculator.evaluate('fo1o * 2', :fo1o => 4)).to eq(8)
  end

  it 'compares string literals with string variables' do
    expect(calculator.evaluate('fruit = "apple"', :fruit => 'apple')).to be_truthy
    expect(calculator.evaluate('fruit = "apple"', :fruit => 'pear')).to be_falsey
  end

  it 'performs case-sensitive comparison' do
    expect(calculator.evaluate('fruit = "Apple"', :fruit => 'apple')).to be_falsey
    expect(calculator.evaluate('fruit = "Apple"', :fruit => 'Apple')).to be_truthy
  end

  it 'allows binding logical values' do
    expect(calculator.evaluate('some_boolean AND 7 > 5', :some_boolean => true)).to be_truthy
    expect(calculator.evaluate('some_boolean AND 7 < 5', :some_boolean => true)).to be_falsey
    expect(calculator.evaluate('some_boolean AND 7 > 5', :some_boolean => false)).to be_falsey

    expect(calculator.evaluate('some_boolean OR 7 > 5', :some_boolean => true)).to be_truthy
    expect(calculator.evaluate('some_boolean OR 7 < 5', :some_boolean => true)).to be_truthy
    expect(calculator.evaluate('some_boolean OR 7 < 5', :some_boolean => false)).to be_falsey

  end

  describe 'functions' do
    it 'include IF' do
      expect(calculator.evaluate('if(foo < 8, 10, 20)', :foo => 2)).to eq(10)
      expect(calculator.evaluate('if(foo < 8, 10, 20)', :foo => 9)).to eq(20)
      expect(calculator.evaluate('if (foo < 8, 10, 20)', :foo => 2)).to eq(10)
      expect(calculator.evaluate('if (foo < 8, 10, 20)', :foo => 9)).to eq(20)
    end

    it 'include ROUND' do
      expect(calculator.evaluate('round(8.2)')).to eq(8)
      expect(calculator.evaluate('round(8.8)')).to eq(9)
      expect(calculator.evaluate('round(8.75, 1)')).to eq(BigDecimal.new('8.8'))

      expect(calculator.evaluate('ROUND(apples * 0.93)', { :apples => 10 })).to eq(9)
    end

    it 'include NOT' do
      expect(calculator.evaluate('NOT(some_boolean)', :some_boolean => true)).to be_falsey
      expect(calculator.evaluate('NOT(some_boolean)', :some_boolean => false)).to be_truthy

      expect(calculator.evaluate('NOT(some_boolean) AND 7 > 5', :some_boolean => true)).to be_falsey
      expect(calculator.evaluate('NOT(some_boolean) OR 7 < 5', :some_boolean => false)).to be_truthy
    end
  end
end