require File.dirname(__FILE__) + '/test_helper'

class ModelSetTest < Test::Unit::TestCase
  class CreateTables < ActiveRecord::Migration    
    def self.up
      create_table :heroes do |t|
        t.column :name, :string
        t.column :universe, :string
      end

      create_table :superpowers do |t|
        t.column :name, :string
      end

      create_table :mutations do |t|
        t.column :name, :string
      end

      create_table :superpets do |t|
        t.column :name, :string
        t.column :species, :string
        t.column :owner_id, :bigint
      end

      create_table :hero_superpowers do |t|
        t.column :hero_id,    :bigint
        t.column :power_type, :string
        t.column :power_id,   :bigint
      end

      create_table :hero_birthdays do |t|
        t.column :hero_id, :bigint
        t.column :birthday, :date
      end

      create_table :robots do |t|
        t.string :name
        t.string :classification
      end
    end

    def self.down
      drop_table :heroes
      drop_table :superpowers
      drop_table :mutations
      drop_table :superpets
      drop_table :hero_superpowers
      drop_table :hero_birthdays
      drop_table :robots
    end
  end
  
  class Superpower < ActiveRecord::Base
  end

  class Mutation < ActiveRecord::Base
  end

  class Superpet < ActiveRecord::Base
  end

  class HeroSuperpower < ActiveRecord::Base
  end

  class Hero < ActiveRecord::Base
    set_table_name 'heroes'
    has_set :superpowers, :through => :hero_superpowers, :other_key => :power_id
    has_set :pets, :class_name => 'Superpet', :own_key => :owner_id do
      def dogs!
        add_conditions!("species = 'dog'")
      end
    end
  end
  
  class HeroSet < ModelSet
    constructor  :with_universe
    clone_method :with_universe
    def with_universe!(universe)
      add_conditions!("universe = '#{universe}'")
    end    

    clone_method :add_birthday
    def add_birthday!
      add_fields!( "hero_birthdays.birthday" => "LEFT OUTER JOIN hero_birthdays ON heroes.id = hero_birthdays.hero_id" )
    end
  end

  context 'with a db connection' do
    setup do
      CreateTables.verbose = false
      CreateTables.up
    end
  
    teardown do
      CreateTables.down
    end
  
    should "construct a model set" do
      captain  = Hero.create(:name => 'Captain America', :universe => 'Marvel')
      spidey   = Hero.create(:name => 'Spider Man',      :universe => 'Marvel')
      batman   = Hero.create(:name => 'Batman',          :universe => 'D.C.'  )
      superman = Hero.create(:name => 'Superman',        :universe => 'D.C.'  )
      ironman  = Hero.create(:name => 'Iron Man',        :universe => 'Marvel')
      
      set = HeroSet.with_universe('Marvel')
      assert_equal [captain.id, spidey.id, ironman.id], set.ids
    end

    should "maintain initial order when adding conditions" do
      captain  = Hero.create(:name => 'Captain America', :universe => 'Marvel')
      spidey   = Hero.create(:name => 'Spider Man',      :universe => 'Marvel')
      batman   = Hero.create(:name => 'Batman',          :universe => 'D.C.'  )
      superman = Hero.create(:name => 'Superman',        :universe => 'D.C.'  )
      ironman  = Hero.create(:name => 'Iron Man',        :universe => 'Marvel')

      set = HeroSet.new([ironman, captain, superman, spidey, batman])

      set.add_conditions!("universe = 'Marvel'")

      assert_equal [ironman.id, captain.id, spidey.id], set.ids
    end

    should "order and reverse set" do
      captain   = Hero.create(:name => 'Captain America', :universe => 'Marvel')
      spidey    = Hero.create(:name => 'Spider Man',      :universe => 'Marvel')
      wolverine = Hero.create(:name => 'Wolverine',       :universe => 'Marvel'  )
      phoenix   = Hero.create(:name => 'Phoenix',         :universe => 'Marvel'  )
      ironman   = Hero.create(:name => 'Iron Man',        :universe => 'Marvel')
      
      ids = [captain.id, ironman.id, phoenix.id, spidey.id, wolverine.id]
      set = HeroSet.with_universe('Marvel')

      set.order_by!('name')
      assert_equal ids, set.ids

      set.reverse!
      assert_equal ids.reverse, set.ids

      set.order_by!('name DESC')
      assert_equal ids.reverse, set.ids

      set.reverse!
      assert_equal ids, set.ids

      # Make sure that a comma in a function call works.
      set.order_by!("lower(ltrim(name, 'C'))")
      assert_equal ids, set.ids

      set.reverse!
      assert_equal ids.reverse, set.ids
    end

    should "have missing ids" do
      missing_id = 5555
      spidey = Hero.create(:name => 'Spider Man', :universe => 'Marvel')
      set = HeroSet.new([spidey.id, missing_id])
      
      # Iterate through the profiles so the missing ones will be detected.
      set.each {}
      assert_equal [missing_id], set.missing_ids
    end
  
    should "have missing ids with add_fields" do
      missing_id = 5555
      spidey = Hero.create(:name => 'Spider Man', :universe => 'Marvel')
      set = HeroSet.new([spidey.id, missing_id]).add_birthday
      
      # Iterate through the profiles so the missing ones will be detected.
      set.each {}
      assert_equal [missing_id], set.missing_ids
    end
  
    should "support has_set" do
      hero = Hero.create(:name => 'Mr. Invisible')
      mighty_mouse = Superpet.create(:name => 'Mighty Mouse', :owner_id => hero.id)
      underdog     = Superpet.create(:name => 'Underdog', :owner_id => hero.id)
      
      set = hero.pets
      assert_equal SuperpetSet, set.class
      assert_equal [mighty_mouse.id, underdog.id], set.ids
    end
    
    should "support has_set with through" do
      hero = Hero.create(:name => 'Mr. Invisible')
      invisibility = Superpower.create(:name => 'Invisibility')
      flying       = Superpower.create(:name => 'Flying')
      HeroSuperpower.create(:hero_id => hero.id, :power_id => invisibility.id)
      HeroSuperpower.create(:hero_id => hero.id, :power_id => flying.id)
      
      set = hero.superpowers
      assert_equal SuperpowerSet, set.class
      assert_equal [invisibility.id, flying.id], set.ids
    end
  
    should "allow set extensions" do
      hero = Hero.create(:name => 'Mr. Invisible')
      mighty_mouse = Superpet.create(:name => 'Mighty Mouse', :owner_id => hero.id, :species => 'mouse')
      sammy        = Superpet.create(:name => 'Sammy Davis Jr. Jr.', :owner_id => hero.id, :species => 'dog')
      underdog     = Superpet.create(:name => 'Underdog', :owner_id => hero.id, :species => 'dog')
      
      set = hero.pets
      assert_equal ['mouse', 'dog', 'dog'], set.collect {|pet| pet.species}
      
      assert_equal [sammy.id, underdog.id], set.dogs!.ids
    end

    class Robot < ActiveRecord::Base
    end
    
    class RobotSet < ModelSet
    end

    setup do
      @bender       = Robot.create(:name => 'Bender',     :classification => :smart_ass )
      @r2d2         = Robot.create(:name => 'R2D2',       :classification => :droid     )
      @c3po         = Robot.create(:name => 'C3PO',       :classification => :droid     )
      @rosie        = Robot.create(:name => 'Rosie',      :classification => :domestic  )
      @small_wonder = Robot.create(:name => 'Vicki',      :classification => :child     )
      @t1000        = Robot.create(:name => 'Terminator', :classification => :assasin   )
      @johnny5      = Robot.create(:name => 'Johnny 5',   :classification => :miltary   )
      
      @bot_set = RobotSet.new([@bender,@r2d2,@c3po,@rosie,@small_wonder,@t1000,@johnny5])
      
      @data    = Robot.create(:name => 'Data',       :classification => :positronic)
      @number8 = Robot.create(:name => 'Boomer',     :classification => :cylon     )
    end
  
    should "be empty" do
      set = RobotSet.empty
      assert_equal 0, set.size
      assert set.empty?
      
      set = RobotSet.new(@bender)
      assert !set.empty?
    end

    should "create a set with single model" do
      set = RobotSet.new(@bender)
      assert_equal [@bender.id], set.ids
    end

    should "include models" do
      set = RobotSet.new([@bender, @r2d2.id, @c3po.id])
      assert set.include?(@bender)
      assert set.include?(@r2d2.id)
      assert set.include?(@c3po)
    end

    should "delete models from a set" do
      set = RobotSet.new([@rosie, @small_wonder, @c3po])
      
      set.delete(@c3po)
      assert_equal [@rosie.id, @small_wonder.id], set.ids
      
      set.delete(@rosie.id)
      assert_equal [@small_wonder.id], set.ids
      
      set.delete(@small_wonder)
      assert_equal [], set.ids
      assert set.empty?
    end

    should "select models from a set" do
      assert_equal [@r2d2, @c3po], @bot_set.select {|bot| bot.classification == :droid}.to_a
      assert_equal 7, @bot_set.size
      
      @bot_set.select! {|bot| bot.classification == :miltary}
      assert_equal [@johnny5], @bot_set.to_a 
    end
    
    should "sort a set" do
      assert_equal [@bender,@c3po,@johnny5,@r2d2,@rosie,@t1000,@small_wonder], @bot_set.sort {|a,b| a.name <=> b.name}.to_a
      assert_equal @johnny5, @bot_set.last
      
      @bot_set.sort! {|a,b| b.name <=> a.name}
      assert_equal [@bender,@c3po,@johnny5,@r2d2,@rosie,@t1000,@small_wonder].reverse, @bot_set.to_a 

      @bot_set.reverse!
      assert_equal [@bender,@c3po,@johnny5,@r2d2,@rosie,@t1000,@small_wonder], @bot_set.to_a 
    end
    
    should "sort a set by name" do
      assert_equal [@bender,@c3po,@johnny5,@r2d2,@rosie,@t1000,@small_wonder], @bot_set.sort_by {|bot| bot.name}.to_a
    end
    
    should "reject models from a set" do
      @bot_set.reject! {|bot| bot.classification == :domestic}
      assert !@bot_set.include?(@rosie)
    end

    should "do set arithmetic" do
      droids    = RobotSet.new([@c3po, @r2d2])
      womanoids = RobotSet.new([@rosie, @small_wonder, @number8])
      humanoids = RobotSet.new([@small_wonder, @t1000, @data, @number8])
      metalics  = RobotSet.new([@r2d2, @c3po, @johnny5])
      cartoons  = RobotSet.new([@bender, @rosie])
      
      assert_equal ['C3PO', 'R2D2', 'Johnny 5'],                    (droids + metalics).collect {|bot| bot.name}
      assert_equal ['Bender', 'Rosie', 'C3PO', 'R2D2', 'Johnny 5'], (cartoons + droids + metalics).collect {|bot| bot.name}
      assert_equal 5, (cartoons + droids + metalics).size
      assert_equal 5, (cartoons + droids + metalics).count
      
      assert_equal [],                     (droids - metalics).collect {|bot| bot.name}
      assert_equal ['Johnny 5'],           (metalics - droids).collect {|bot| bot.name}
      assert_equal ['Terminator', 'Data'], (humanoids - womanoids).collect {|bot| bot.name}
      assert_equal ['Bender'],             (cartoons - womanoids).collect {|bot| bot.name}
      assert_equal 2, (humanoids - womanoids).size
      assert_equal 2, (humanoids - womanoids).count
      
      assert_equal ['C3PO', 'R2D2'],    (droids & metalics).collect {|bot| bot.name}
      assert_equal ['R2D2', 'C3PO'],    (metalics & droids).collect {|bot| bot.name}
      assert_equal ['Vicki', 'Boomer'], (humanoids & womanoids).collect {|bot| bot.name}
      assert_equal ['Rosie'],           (cartoons & womanoids).collect {|bot| bot.name}
      assert_equal 2, (humanoids & womanoids).size
      assert_equal 2, (humanoids & womanoids).count

      set = (droids + @johnny5)
      assert_equal ['C3PO', 'R2D2', 'Johnny 5'], set.collect {|bot| bot.name}
      set -= @r2d2
      assert_equal ['C3PO', 'Johnny 5'], set.collect {|bot| bot.name}
    end
    
    should "clone a set" do
      set     = RobotSet.new([1])
      new_set = set.clone
      assert new_set.object_id != set.object_id
    end
  end
end