require 'spec_helper'

class Term
  def call_me
    #stub, because flexmock doesn't work for new instances during constructor
    @@called=true
  end
  def self.called
    @@called
  end
end

describe Term do

  before :all do
    @calc = CalculationSet.find("transport")[:transport]
  end

  it 'can be initialized via DSL block' do
    Term.new {call_me}
    Term.called.should be_true
  end

  it "has label" do
    Term.new {label :hello}.label.should eql :hello
  end

  it "has name" do
    Term.new {name :hello}.name.should eql :hello
  end

  it "has path" do
    Term.new {path :hello}.path.should eql :hello
  end

  it "has note" do
    Term.new {note 'hello'}.note.should eql 'hello'
  end

  it "has note with no '\"' character" do
     Term.new {note 'hello "some quote"'}.note.should eql "hello 'some quote'"
  end

  it 'has parent' do
    @calc[:distance].parent.should eql @calc
  end
  it "has name defaulting to label" do
    Term.new {label :hello}.name.should eql 'Hello'
    Term.new {label :hello ; name 'goodbye'}.name.should eql 'goodbye'
    Term.new {name 'goodbye' ; label :hello }.name.should eql 'goodbye'
  end
  it 'has path defaulting to label' do
    Term.new {label :hello}.path.should eql 'hello'
    Term.new {label :hello ; path 'goodbye'}.path.should eql 'goodbye'
    Term.new {path 'goodbye' ; label :hello }.path.should eql 'goodbye'
  end
  it 'has value' do
    Term.new {value :hello}.value.should eql :hello
  end
  it 'knows if it is set' do
    Term.new {value :hello}.set?.should be_true
    Term.new.set?.should be_false
  end
  it 'has interface' do
    Term.new {interface :text_box}.interface.should eql :text_box
  end
  it 'raises exception on invalid interface' do
    lambda{Term.new {interface :bobby}}.should raise_error Exceptions::InvalidInterface
  end
  it 'knows what it''s interface is' do
    Term.new {interface :text_box}.text_box?.should be_true
    Term.new {interface :text_box}.drop_down?.should be_false
    Term.new {interface :drop_down}.text_box?.should be_false
    Term.new {interface :drop_down}.drop_down?.should be_true
  end
  it 'can be enabled or disabled' do
    t=Term.new
    t.enabled?.should be_true
    t.disabled?.should be_false
    t.disable!
    t.enabled?.should be_false
    t.disabled?.should be_true
    t.enable!
    t.enabled?.should be_true
    t.disabled?.should be_false
  end
  it 'can be visible or invisible' do
    t=Term.new
    t.visible?.should be_true
    t.hidden?.should be_false
    t.hide!
    t.visible?.should be_false
    t.hidden?.should be_true
    t.show!
    t.visible?.should be_true
    t.hidden?.should be_false
  end
  it 'knows which terms come before or after it' do
    @calc.terms.
      select{|x|x.before?(:distance)}.map(&:label).
      should eql [:fuel,:size]
    @calc.terms.
      select{|x|x.after?(:distance)}.map(&:label).
      should eql [:co2]
  end

  it "should respond to unit methods" do
    Term.new.methods.should include "unit","per_unit","default_unit","default_per_unit",
                                    "alternative_units","alternative_per_units"
  end

  it "has no default unit if none declared" do
    Term.new {path :hello}.default_unit.should be_nil
    Term.new {path :hello}.default_per_unit.should be_nil
    Term.new {path :hello}.unit.should be_nil
    Term.new {path :hello}.per_unit.should be_nil
  end

  it "has default unit if specified" do
    Term.new {path :hello; default_unit :kg}.default_unit.name.should == 'kilogram'
    Term.new {path :hello; default_per_unit :kWh}.default_per_unit.pluralized_name.should == 'kilowatt hours'
    Term.new {path :hello; default_unit :kg}.default_unit.should be_a Quantify::Unit::SI
  end

  it "has current unit defaulting to default unit if none specified" do
    Term.new {path :hello; default_unit :kg}.unit.name.should == 'kilogram'
    Term.new {path :hello; default_per_unit :kWh}.per_unit.pluralized_name.should == 'kilowatt hours'
    Term.new {path :hello; default_unit :kg}.unit.should be_a Quantify::Unit::SI
  end

  it "should have no alternative units if none specified and no default" do
    term = Term.new {path :hello}
    term.default_unit.should be_nil
    term.unit.should be_nil
    term.default_per_unit.should be_nil
    term.per_unit.should be_nil
    term.alternative_units.should be_nil
    term.alternative_per_units.should be_nil
  end

  it "has default unit alternatives if none specified" do
    term = Term.new {path :hello; default_unit :kg; default_per_unit :kWh}
    units = term.alternative_units.map(&:name)
    units.should include "gigagram", "pound", "tonne"
    per_units = term.alternative_per_units.map(&:name)
    per_units.should include "joule", "british thermal unit", "megawatt hour"
  end

  it "has unit choices which include default and alternative" do
    term = Term.new {path :hello; default_unit :kg; default_per_unit :kWh}
    units = term.alternative_units.map(&:name)
    units.should include "gigagram", "pound", "tonne"
    units.should_not include "kilogram", "kelvin"

    units = term.unit_choices.map(&:name)
    units.first.should eql "kilogram"
    units.should include "kilogram", "gigagram", "pound", "tonne"
    units.should_not include "kelvin"

    per_units = term.alternative_per_units.map(&:name)
    per_units.should include "joule", "british thermal unit", "megawatt hour"
    per_units.should_not include "kilowatt hour"

    per_units = term.per_unit_choices.map(&:name)
    per_units.first.should eql "kilowatt hour"
    per_units.should include "kilowatt hour", "joule", "british thermal unit", "megawatt hour"
  end

  it "has limited set of alternative units if specified" do
    term = Term.new {path :hello; default_unit :kg; alternative_units :t, :ton_us, :lb}
    units = term.alternative_units.map(&:name)
    units.should include "tonne", "pound", "short ton"
    units.should_not include "gigagram", "ounce", "gram"
  end

  it "has unit choices which include default and alternative with limited set of alternative units" do
    term = Term.new {path :hello; default_unit :kg; alternative_units :t, :ton_us, :lb}
    units = term.alternative_units.map(&:name)
    units.should include "tonne", "pound", "short ton"
    units.should_not include "kilogram", "gigagram", "ounce", "gram"

    units = term.unit_choices.map(&:name)
    units.first.should eql "kilogram"
    units.should include "kilogram", "tonne", "pound", "short ton"
    units.should_not include "gigagram", "ounce", "gram"
  end

  it "should raise error when specifying incompatible alternative units" do
    lambda{Term.new {path :hello; default_unit :kg; alternative_units :kWh, :km}}.should raise_error
  end

  it "should make a clone" do
    term = Term.new {path :hello; default_unit :kg; alternative_units :t, :ton_us, :lb}
    term.path.should eql :hello
    term.default_unit.should be_a Quantify::Unit::Base
    original_unit_instance = term.default_unit
    term.default_unit.symbol.should eql 'kg'
    new_term = term.clone
    new_term.path.should eql :hello
    new_term.default_unit.should be_a Quantify::Unit::Base
    new_term.default_unit.symbol.should eql 'kg'
    new_unit_instance = new_term.default_unit
    original_unit_instance.should_not eql new_unit_instance
  end

  it "should represent term as string with unit symbol if no argument provided" do
    Term.new {path :hello; value 12; default_unit :kg}.to_s.should == '12.0 kg'
    Term.new {path :hello; value 12; default_unit :kg; per_unit :h}.to_s.should == '12.0 kg/h'
    Term.new {path :hello; value 12; per_unit :h}.to_s.should == '12.0 h^-1'
  end

  it "should represent term as string with unit label" do
    Term.new {path :hello; value 12; default_unit :kg}.to_s(:label).should == '12.0 kg'
    Term.new {path :hello; value 12; default_unit :kg; per_unit :h}.to_s(:label).should == '12.0 kg/h'
    Term.new {path :hello; value 12; per_unit :h}.to_s(:label).should == '12.0 h^-1'
  end

  it "should represent term as string with unit name" do
    Term.new {path :hello; value 12; default_unit :kg}.to_s(:name).should == '12.0 kilograms'
    Term.new {path :hello; value 12; default_unit :kg; per_unit :h}.to_s(:name).should == '12.0 kilograms per hour'
    Term.new {path :hello; value 12; per_unit :h}.to_s(:name).should == '12.0 per hour'
  end

  it "should represent term as string with unit pluralized name" do
    Term.new {path :hello; value 12; default_unit :kg}.to_s(:pluralized_name).should == '12.0 kilograms'
    Term.new {path :hello; value 12; default_unit :kg; per_unit :h}.to_s(:pluralized_name).should == '12.0 kilograms per hour'
    Term.new {path :hello; value 12; per_unit :h}.to_s(:pluralized_name).should == '12.0 per hour'
  end

  it "should represent term as string with no units" do
    Term.new {path :hello; value 12}.to_s(:pluralized_name).should == '12'
    Term.new {path :hello; value 12}.to_s(:name).should == '12'
    Term.new {path :hello; value 12}.to_s(:symbol).should == '12'
    Term.new {path :hello; value 12}.to_s(:label).should == '12'
    Term.new {path :hello; value 12}.to_s.should == '12'
  end

  it "should be recognised as numeric" do
    Term.new {path :hello; value 12; default_unit :kg}.has_numeric_value?.should be_true
  end

  it "should be recognised as non numeric" do
    Term.new {path :hello; value 'bob'; default_unit :kg}.has_numeric_value?.should be_false
  end
  
  it "should convert the input to a String if the type is specified as such" do
    Term.new {type :string; value 54}.value.should == "54"
  end
  
  it "should convert the input to a Fixnum if the type is specified as such" do
    Term.new {type :fixnum; value '54'}.value.should == 54
  end
  
  it "should convert the input to a Float if the type is specified as such" do
    Term.new {type :float; value '54'}.value.should == 54.0
  end
  
  it "should convert the input to a Date if the type is specified as such" do
    Term.new {type :date; value '2011-01-01'}.value.should == Date.parse("2011-01-01")
  end
  
  it "should convert the input to a Time if the type is specified as such" do
    Term.new {type :time; value "2011-01-01 10:00:00"}.value.should == Time.parse("2011-01-01 10:00:00")
  end
  
  it "should convert the input to a DateTime if the type is specified as such" do
    now = DateTime.now
    Term.new {type :datetime; value now.to_s}.value.should === now
  end
  
  it "should store the pre cast value" do
    term = Term.new {type :float; value '54'}
    term.value.should == 54.0
    term.value_before_cast.should == "54"
  end

    it "should return self if no unit or per unit attribute" do
    @term = Term.new { value 20 }
    @term.unit.should be_nil
    @term.per_unit.should be_nil
    @term.value.should eql 20
    new_term = @term.convert_unit(:unit => :t)
    new_term.should === @term
    new_term.value.should eql 20
    new_term.unit.should be_nil
    new_term.per_unit.should be_nil
  end

  it "should return self if not a numeric unit" do
    @term = Term.new { value 'plane'; unit :kg }
    @term.unit.label.should eql 'kg'
    @term.value.should eql 'plane'
    new_term = @term.convert_unit(:unit => :t)
    new_term.should === @term
  end

  it "should convert unit" do
    @term = Term.new { value 20; unit :kg }
    @term.unit.symbol.should eql 'kg'
    @term.value.should eql 20
    new_term = @term.convert_unit(:unit => :t)
    new_term.unit.symbol.should eql 't'
    new_term.value.should eql 0.020
  end

  it "should convert per unit" do
    @term = Term.new { value 20; unit :kg; per_unit :min }
    @term.unit.symbol.should eql 'kg'
    @term.per_unit.symbol.should eql 'min'
    @term.value.should eql 20
    new_term = @term.convert_unit(:per_unit => :h)
    new_term.unit.symbol.should eql 'kg'
    new_term.per_unit.symbol.should eql 'h'
    new_term.value.should eql 1200.0
  end

  it "should convert unit and per unit" do
    @term = Term.new { value 20; unit :kg; per_unit :min }
    @term.unit.symbol.should eql 'kg'
    @term.per_unit.symbol.should eql 'min'
    @term.value.should eql 20
    new_term = @term.convert_unit( :unit => :t, :per_unit => :h )
    new_term.unit.symbol.should eql 't'
    new_term.per_unit.symbol.should eql 'h'
    new_term.value.should eql 1.2000
  end

  it "should convert unit if value a string" do
    @term = Term.new { value "20"; unit :kg }
    @term.unit.symbol.should eql 'kg'
    @term.value.should eql "20"
    new_term = @term.convert_unit(:unit => :t)
    new_term.unit.symbol.should eql 't'
    new_term.value.should eql 0.020
  end

  it "should convert per unit if value a string" do
    @term = Term.new { value "20"; unit :kg; per_unit :min }
    @term.unit.symbol.should eql 'kg'
    @term.per_unit.symbol.should eql 'min'
    @term.value.should eql "20"
    new_term = @term.convert_unit(:per_unit => :h)
    new_term.unit.symbol.should eql 'kg'
    new_term.per_unit.symbol.should eql 'h'
    new_term.value.should eql 1200.0
  end

  it "should convert unit and per unit if value a string" do
    @term = Term.new { value "20"; unit :kg; per_unit :min }
    @term.unit.symbol.should eql 'kg'
    @term.per_unit.symbol.should eql 'min'
    @term.value.should eql "20"
    new_term = @term.convert_unit( :unit => :t, :per_unit => :h )
    new_term.unit.symbol.should eql 't'
    new_term.per_unit.symbol.should eql 'h'
    new_term.value.should eql 1.2000
  end

  it "should raise error if trying to convert to non dimensionally equivalent unit" do
    @term = Term.new { value 20; unit :kg; per_unit :min }
    @term.unit.symbol.should eql 'kg'
    @term.per_unit.symbol.should eql 'min'
    @term.value.should eql 20
    lambda{new_term = @term.convert_unit( :unit => :J, :per_unit => :h )}.should raise_error
  end

  it "should raise error if trying to convert to non dimensionally equivalent unit" do
    @term = Term.new { value 20; unit :kg; per_unit :min }
    @term.unit.symbol.should eql 'kg'
    @term.per_unit.symbol.should eql 'min'
    @term.value.should eql 20
    lambda{new_term = @term.convert_unit( :unit => :J, :per_unit => :h )}.should raise_error
  end

  describe "quantities" do

    it "should convert term with unit to quantity object" do
      @term = Term.new { value 20; unit :kg }
      quantity = @term.to_quantity
      quantity.unit.name.should eql "kilogram"
      quantity.unit.symbol.should eql "kg"
      quantity.value.should eql 20.0
      quantity.to_s.should eql "20.0 kg"
    end

    it "should convert term with per unit to quantity object" do
      @term = Term.new { value 20; per_unit :h }
      quantity = @term.to_quantity
      quantity.unit.name.should eql "per hour"
      quantity.unit.symbol.should eql "h^-1"
      quantity.value.should eql 20.0
      quantity.to_s.should eql "20.0 h^-1"
    end

    it "should convert term with unit and per unit to quantity object" do
      @term = Term.new { value 20; unit :kg; per_unit :h }
      quantity = @term.to_quantity
      quantity.unit.name.should eql "kilogram per hour"
      quantity.unit.symbol.should eql "kg/h"
      quantity.value.should eql 20.0
      quantity.to_s.should eql "20.0 kg/h"
    end

    it "should return nil with no unit or per unit" do
      @term = Term.new { value 20 }
      quantity = @term.to_quantity
      quantity.should eql 20
      quantity.should be_a Integer
    end

    it "should return nil with non numeric term" do
      @term = Term.new { value "taxi" }
      quantity = @term.to_quantity
      quantity.should be_nil
    end

    it "should convert term with unit to quantity object with alias method" do
      @term = Term.new { value 20; unit :kg }
      quantity = @term.to_q
      quantity.unit.name.should eql "kilogram"
      quantity.unit.symbol.should eql "kg"
      quantity.value.should eql 20.0
      quantity.to_s.should eql "20.0 kg"
    end

    it "should convert term with per unit to quantity object with alias method" do
      @term = Term.new { value 20; per_unit :h }
      quantity = @term.to_q
      quantity.unit.name.should eql "per hour"
      quantity.unit.symbol.should eql "h^-1"
      quantity.value.should eql 20.0
      quantity.to_s.should eql "20.0 h^-1"
    end
  end

  it "should recognise similar terms" do
    @term1 = Term.new { value 20; per_unit :h }
    @term2 = Term.new { value 20; per_unit :h }
    (@term1 == @term2).should be_true
  end

  it "should recognise dissimilar terms" do
    @term1 = Term.new { value 20; per_unit :h }
    @term2 = Term.new { value 40; per_unit :h }
    (@term1 == @term2).should be_false
  end
  
end