# Enable codecov.io on Ruby 1.9 and later on Travis CI
if RUBY_VERSION >= '1.9'
  require 'simplecov'
  SimpleCov.start do
    add_filter '/spec/'
  end
  if ENV['CI']=='true'
    require 'codecov'
    SimpleCov.formatter = SimpleCov::Formatter::Codecov
  end
end

# Require rspec and the actual library under test
require 'rspec'
require File.join(File.dirname(__FILE__), '..', 'lib', 'values')

describe Value do

  describe 'Value.new' do
    it 'raises argument error if given zero fields' do
      expect { Value.new }.to raise_error(ArgumentError, 'wrong number of arguments (0 for 1+)')
    end
  end

  Cell = Value.new(:alive)

  Point = Value.new(:x, :y)

  describe '.new and the fields of a value class' do
    it 'stores a single field' do
      expect(Cell.new(true).alive).to eq(true)
    end

    it 'stores multiple values' do
      p = Point.new(0,1)
      expect(p.x).to eq(0)
      expect(p.y).to eq(1)
    end

    it 'raises argument errors if not given the right number of arguments' do
      expect { Point.new }.to raise_error(ArgumentError, 'wrong number of arguments, 0 for 2')
    end
  end

  class GraphPoint < Value.new(:x, :y)
    def inspect
      "GraphPoint at #{@x},#{@y}"
    end
  end

  it 'can be inherited from to add methods' do
    expect(GraphPoint.new(0,0).inspect).to eq('GraphPoint at 0,0')
  end

  it 'has a pretty string representation' do
    expect(Point.new(0, 1).inspect).to eq('#<Point x=0, y=1>')
  end

  Line = Value.new(:slope, :y_intercept) do
    def inspect
      "<Line: y=#{slope}x+#{y_intercept}>"
    end
  end

  it 'can be customized with a block' do
    expect(Line.new(2, 3).inspect).to eq('<Line: y=2x+3>')
  end

  it 'cannot be mutated' do
    p = Point.new(0,1)
    expect { p.x = 1 }.to raise_error
  end

  class Cow < Value.new(:color)
    def change_color(new_color)
      @color = new_color
    end
  end

  it 'cannot even be mutated inside a subclass with methods' do
    c = Cow.new('red')
    expect { c.change_color('blue') }.to raise_error
  end

  Money = Value.new(:amount, :denomination)

  it 'cannot be mutated using #instance_variable_set' do
    m = Money.new(1, 'USD')
    expect { m.instance_variable_set('@amount', 2) }.to raise_error
  end

  it 'can be instantiated with a hash' do
    one_dollar = Money.with(:amount => 1, :denomination => 'USD')
    expect(one_dollar.amount).to eq(1)
    expect(one_dollar.denomination).to eq('USD')
  end

  it 'errors if you instantiate it from a hash with unrecognised fields' do
    expect { Money.with(:unrecognized_field => 1, :amount => 2, :denomination => 'USD') }.to raise_error(ArgumentError)
  end

  it 'errors if you instantiate it from a hash with missing fields' do
    expect { Money.with({}) }.to raise_error(ArgumentError)
  end

  it 'does not error when fields are explicitly nil' do
    expect { Money.with(:amount => 1, :denomination => nil) }.not_to raise_error
  end

  describe '#hash and equality' do
    Y = Value.new(:x, :y)

    it 'is equal to another value with the same fields' do
      expect(Point.new(0,0)).to eq(Point.new(0,0))
    end

    it 'is not equal to an object with a different class' do
      expect(Point.new(0,0)).not_to eq(Y.new(0,0))
    end

    it 'is not equal to another value with different fields' do
      expect(Point.new(0,0)).not_to eq(Point.new(0,1))
      expect(Point.new(0,0)).not_to eq(Point.new(1,0))
    end

    it 'has an equal hash if the fields are equal' do
      expect(Point.new(0,0).hash).to eq(Point.new(0,0).hash)
    end

    it 'has a non-equal hash if the fields are different' do
      expect(Point.new(0,0).hash).not_to eq(Point.new(1,0).hash)
    end

    it 'does not have an equal hash if the class is different' do
      expect(Point.new(0,0).hash).not_to eq(Y.new(0,0).hash)
    end
  end

  describe '#values' do
    it 'returns an array of field values' do
      expect(Point.new(10, 13).values).to eq([10, 13])
    end
  end

  ManyAttrs = Value.new(:f, :e, :d, :c, :b, :a)

  describe '#inspect' do
    let(:v) { ManyAttrs.new(6, 5, 4, 3, 2, 1) }

    it 'returns a string containing attributes in their expected order' do
      expect(v.inspect).to eq('#<ManyAttrs f=6, e=5, d=4, c=3, b=2, a=1>')
    end
  end

  describe '#with' do
    let(:p) { Point.new(1, -1) }

    describe 'with no arguments' do
      it 'returns an object equal by value' do
        expect(p.with).to eq(p)
      end

      it 'returns an object equal by identity' do
        expect(p.with).to equal(p)
      end
    end

    describe 'with hash arguments' do
      it 'replaces all field values' do
        expect(p.with({ :x => 1, :y => 2 })).to eq(Point.new(1, 2))
      end

      it 'defaults to current values if missing' do
        expect(p.with({ :y => 2 })).to eq(Point.new(1, 2))
      end

      it 'raises argument error if unknown field' do
        expect { p.with({ :foo => 3 }) }.to raise_error(ArgumentError)
      end
    end
  end

  describe '#to_h' do
    it 'returns a hash of fields and values' do
      expect(Point.new(1, -1).to_h).to eq({ :x => 1, :y => -1 })
    end
  end

  describe '#to_a' do
    it 'returns an array of pairs of fields and values' do
      expect(Point.new(1, -1).to_a).to eq([[:x, 1], [:y, -1]])
    end
  end
end