require 'helper'

class Flight < ActiveRecord::Base
  col :origin
  col :dest
  col :year, :type => :integer
  col :airline
  col :origin_city
  col :dest_city
end
Flight.auto_upgrade!

FactoryGirl.define do
  factory :lax_sfo, :class => Flight do
    origin 'LAX'
    dest 'SFO'
  end
  factory :lax_ord, :class => Flight do
    origin 'LAX'
    dest 'ORD'
  end
  factory :ord_sfo, :class => Flight do
    origin 'ORD'
    dest 'SFO'
  end
end

shared_examples_for 'an adapter the provides #cohort' do
  def moot_condition
    Arel.sql('9 = 9')
  end

  describe :cohort do
    it "finds the biggest set of records matching the characteristics" do
      FactoryGirl.create(:lax_ord)
      FactoryGirl.create(:lax_sfo)
      assert_count 2, model.cohort(:origin => 'LAX')
      assert_count 1, model.cohort(:dest => 'SFO')
      assert_count 1, model.cohort(:origin => 'LAX', :dest => 'SFO')
      assert_count 0, model.cohort(:dest => 'MSN')
    end

    it "handles arrays of values" do
      FactoryGirl.create(:lax_ord)
      FactoryGirl.create(:lax_sfo)
      assert_count 2, model.cohort(:dest => ['ORD','SFO'])
      assert_count 2, model.cohort(:origin => ['LAX'])
      assert_count 1, model.cohort(:dest => ['SFO'])
      assert_count 1, model.cohort(:origin => ['LAX'], :dest => ['SFO'])
      assert_count 0, model.cohort(:dest => ['MSN'])
      assert_count 1, model.cohort(:dest => ['MSN','SFO'])
    end

    it "handles ranges of values" do
      FactoryGirl.create(:lax_ord, :year => 20)
      FactoryGirl.create(:lax_ord, :year => 15)
      FactoryGirl.create(:lax_ord, :year => 10)
      assert_count 3, model.cohort(:year => 10..20)
      assert_count 2, model.cohort(:year => 11..20)
      assert_count 1, model.cohort(:year => 11..19)
    end

    it "matches everything if empty characteristics" do
      FactoryGirl.create(:lax_ord)
      FactoryGirl.create(:lax_sfo)
      assert_count 2, model.cohort({})
    end

    it "discards characteristics to maximize size until the minimum size is met" do
      a = FactoryGirl.create(:lax_ord)
      b = FactoryGirl.create(:lax_sfo)
      cohort = model.cohort({:origin => 'LAX', :dest => 'SFO'}, :minimum_size => 2)
      assert_count 2, cohort
      assert_members [a,b], cohort
    end

    it "returns an empty cohort (basically an impossible condition) unless the minimum size is set" do
      FactoryGirl.create(:lax_ord)
      cohort = model.cohort({:origin => 'LAX'}, :minimum_size => 2)
      assert_count 0, cohort
    end

    it "discards characteristics in order until a minimum size is met" do
      a = FactoryGirl.create(:lax_ord)
      b = FactoryGirl.create(:lax_sfo)
      cohort = model.cohort({:origin => 'LAX', :dest => 'MSN'}, :minimum_size => 2, :priority => [:origin, :dest])
      assert_count 2, cohort
      assert_members [a,b], cohort
    end

    it "returns an empty cohort if discarding characteristics in order has that effect" do
      FactoryGirl.create(:lax_ord)
      FactoryGirl.create(:lax_sfo)
      cohort = model.cohort({:origin => 'LAX', :dest => 'MSN'}, :minimum_size => 2, :priority => [:dest, :origin])
      assert_count 0, cohort
    end

    it "obeys conditions already added" do
      FactoryGirl.create(:lax_ord, :year => 1900)
      FactoryGirl.create(:lax_sfo, :year => 1900)
      FactoryGirl.create(:lax_sfo, :year => 2009)
      FactoryGirl.create(:ord_sfo, :year => 2009)
      year_is_2009 = f_t[:year].eq(2009)

      assert_count 1, model.where(year_is_2009).cohort(:origin => 'LAX').where(moot_condition)
      assert_count 1, model.where(year_is_2009).cohort(:origin => 'LAX', :dest => 'MSN').where(moot_condition)

      assert_count 1, model.where(year_is_2009).cohort({:origin => 'LAX', :dest => 'MSN'}, :priority => [:origin, :dest]).where(moot_condition)
      assert_count 0, model.where(year_is_2009).cohort({:origin => 'LAX', :dest => 'MSN'}, :priority => [:dest, :origin]).where(moot_condition)

      assert_count 0, model.where(year_is_2009).cohort({:origin => 'LAX'}, :minimum_size => 2).where(moot_condition)
      assert_count 0, model.where(year_is_2009).cohort({:origin => 'LAX', :dest => 'MSN'}, :minimum_size => 2).where(moot_condition)
      

      assert_count 1, model.where(year_is_2009).cohort(:origin => 'LAX').where(moot_condition)
      assert_count 1, model.where(year_is_2009).cohort(:origin => 'LAX', :dest => 'SFO').where(moot_condition)

      assert_count 1, model.where(year_is_2009).cohort({:origin => 'LAX', :dest => 'SFO'}, :priority => [:origin, :dest]).where(moot_condition)
      assert_count 1, model.where(year_is_2009).cohort({:origin => 'LAX', :dest => 'SFO'}, :priority => [:dest, :origin]).where(moot_condition)

      assert_count 0, model.where(year_is_2009).cohort({:origin => 'LAX'}, :minimum_size => 2).where(moot_condition)
      assert_count 2, model.where(year_is_2009).cohort({:origin => 'LAX', :dest => 'SFO'}, :minimum_size => 2).where(moot_condition)


      assert_count 1, model.where(year_is_2009).cohort(:origin => 'LAX').where(moot_condition)
      assert_count 1, model.where(year_is_2009).cohort(:origin => 'LAX', :dest => 'ORD').where(moot_condition)

      assert_count 1, model.where(year_is_2009).cohort({:origin => 'LAX', :dest => 'ORD'}, :priority => [:origin, :dest]).where(moot_condition)
      assert_count 0, model.where(year_is_2009).cohort({:origin => 'LAX', :dest => 'ORD'}, :priority => [:dest, :origin]).where(moot_condition)

      assert_count 0, model.where(year_is_2009).cohort({:origin => 'LAX'}, :minimum_size => 2).where(moot_condition)
      assert_count 0, model.where(year_is_2009).cohort({:origin => 'LAX', :dest => 'ORD'}, :minimum_size => 2).where(moot_condition)


      assert_count 1, model.where(year_is_2009).cohort(:origin => 'ORD').where(moot_condition)
      assert_count 1, model.where(year_is_2009).cohort(:origin => 'ORD', :dest => 'MSN').where(moot_condition)

      assert_count 1, model.where(year_is_2009).cohort({:origin => 'ORD', :dest => 'MSN'}, :priority => [:origin, :dest]).where(moot_condition)
      assert_count 0, model.where(year_is_2009).cohort({:origin => 'ORD', :dest => 'MSN'}, :priority => [:dest, :origin]).where(moot_condition)

      assert_count 0, model.where(year_is_2009).cohort({:origin => 'ORD'}, :minimum_size => 2).where(moot_condition)
      assert_count 0, model.where(year_is_2009).cohort({:origin => 'ORD', :dest => 'MSN'}, :minimum_size => 2).where(moot_condition)
    end

    it "carries over into conditions added later" do
      FactoryGirl.create(:lax_ord, :year => 1900)
      FactoryGirl.create(:lax_sfo, :year => 1900)
      FactoryGirl.create(:lax_sfo, :year => 2009)
      FactoryGirl.create(:ord_sfo, :year => 2009)
      year_is_2009 = f_t[:year].eq(2009)

      assert_count 1, model.where(moot_condition).cohort(:origin => 'LAX').where(year_is_2009)
      assert_count 1, model.where(moot_condition).cohort(:origin => 'LAX', :dest => 'MSN').where(year_is_2009)

      assert_count 1, model.where(moot_condition).cohort({:origin => 'LAX', :dest => 'MSN'}, :priority => [:origin, :dest]).where(year_is_2009)
      assert_count 0, model.where(moot_condition).cohort({:origin => 'LAX', :dest => 'MSN'}, :priority => [:dest, :origin]).where(year_is_2009)

      assert_count 0, model.where(moot_condition).cohort({:origin => 'LAX'}, :minimum_size => 2).where(year_is_2009)
      assert_count 0, model.where(moot_condition).cohort({:origin => 'LAX', :dest => 'MSN'}, :minimum_size => 2).where(year_is_2009)
      

      assert_count 1, model.where(moot_condition).cohort(:origin => 'LAX').where(year_is_2009)
      assert_count 1, model.where(moot_condition).cohort(:origin => 'LAX', :dest => 'SFO').where(year_is_2009)

      assert_count 1, model.where(moot_condition).cohort({:origin => 'LAX', :dest => 'SFO'}, :priority => [:origin, :dest]).where(year_is_2009)
      assert_count 1, model.where(moot_condition).cohort({:origin => 'LAX', :dest => 'SFO'}, :priority => [:dest, :origin]).where(year_is_2009)

      assert_count 0, model.where(moot_condition).cohort({:origin => 'LAX'}, :minimum_size => 2).where(year_is_2009)
      assert_count 2, model.where(moot_condition).cohort({:origin => 'LAX', :dest => 'SFO'}, :minimum_size => 2).where(year_is_2009)


      assert_count 1, model.where(moot_condition).cohort(:origin => 'LAX').where(year_is_2009)
      assert_count 1, model.where(moot_condition).cohort(:origin => 'LAX', :dest => 'ORD').where(year_is_2009)

      assert_count 1, model.where(moot_condition).cohort({:origin => 'LAX', :dest => 'ORD'}, :priority => [:origin, :dest]).where(year_is_2009)
      assert_count 0, model.where(moot_condition).cohort({:origin => 'LAX', :dest => 'ORD'}, :priority => [:dest, :origin]).where(year_is_2009)

      assert_count 0, model.where(moot_condition).cohort({:origin => 'LAX'}, :minimum_size => 2).where(year_is_2009)
      assert_count 0, model.where(moot_condition).cohort({:origin => 'LAX', :dest => 'ORD'}, :minimum_size => 2).where(year_is_2009)


      assert_count 1, model.where(moot_condition).cohort(:origin => 'ORD').where(year_is_2009)
      assert_count 1, model.where(moot_condition).cohort(:origin => 'ORD', :dest => 'MSN').where(year_is_2009)

      assert_count 1, model.where(moot_condition).cohort({:origin => 'ORD', :dest => 'MSN'}, :priority => [:origin, :dest]).where(year_is_2009)
      assert_count 0, model.where(moot_condition).cohort({:origin => 'ORD', :dest => 'MSN'}, :priority => [:dest, :origin]).where(year_is_2009)

      assert_count 0, model.where(moot_condition).cohort({:origin => 'ORD'}, :minimum_size => 2).where(year_is_2009)
      assert_count 0, model.where(moot_condition).cohort({:origin => 'ORD', :dest => 'MSN'}, :minimum_size => 2).where(year_is_2009)
    end

    it "can get where sql" do
      FactoryGirl.create(:lax_ord)
      FactoryGirl.create(:lax_sfo)
      model.cohort(:origin => 'LAX').where_sql.delete('"`').must_equal %{WHERE flights.origin = 'LAX'}
    end

    it "will resolve independently from other cohorts" do
      FactoryGirl.create(:lax_ord)
      FactoryGirl.create(:lax_sfo)
      assert_count 0, model.cohort(:dest => 'SFO').cohort(:dest => 'ORD')
    end

    it "will resolve independently from other cohorts (complex example)" do
      FactoryGirl.create(:lax_ord)
      FactoryGirl.create(:lax_ord, :origin_city => 'Los Angeles', :airline => 'Delta')
      FactoryGirl.create(:lax_sfo)
      FactoryGirl.create(:lax_ord, :origin_city => 'Los Angeles', :airline => 'Delta', :year => 2000)
      FactoryGirl.create(:lax_sfo, :year => 2000)
      year_condition = f_t[:year].eq(2000)

      # sanity check
      assert_count 2, model.where(year_condition)
      assert_count 2, model.cohort(:origin => 'LAX', :dest => 'SFO')
      assert_count 2, model.cohort(:origin_city => 'Los Angeles', :airline => 'Delta')
      
      assert_count 1, model.cohort(:origin => 'LAX', :dest => 'SFO').where(year_condition)
      assert_count 1, model.where(year_condition).cohort(:origin => 'LAX', :dest => 'SFO')
      
      assert_count 2, model.cohort({:origin => 'LAX', :dest => 'SFO'}, :minimum_size => 2).where(year_condition)
      assert_count 2, model.where(year_condition).cohort({:origin => 'LAX', :dest => 'SFO'}, :minimum_size => 2)

      assert_count 1, model.cohort(:origin_city => 'Los Angeles', :airline => 'Delta').where(year_condition)
      assert_count 1, model.where(year_condition).cohort(:origin_city => 'Los Angeles', :airline => 'Delta')
      
      assert_count 1, model.cohort(:origin_city => 'Los Angeles', :airline => 'Delta').where(year_condition)
      assert_count 1, model.where(year_condition).cohort(:origin_city => 'Los Angeles', :airline => 'Delta')
      #--

      assert_count 0, model.cohort(:origin => 'LAX', :dest => 'SFO').cohort(:origin_city => 'Los Angeles', :airline => 'Delta')
      assert_count 0, model.cohort(:origin_city => 'Los Angeles', :airline => 'Delta').cohort(:origin => 'LAX', :dest => 'SFO')
    end

    describe "when used with UNION" do
      before do
        @ord = FactoryGirl.create(:lax_ord)
        @sfo = FactoryGirl.create(:lax_sfo)
      end

      # sanity check!
      it "has tests that use unions properly" do
        ord = model.where(f_t[:dest].eq('ORD'))
        sfo = model.where(f_t[:dest].eq('SFO'))
        ord.projections = [Arel.star]
        sfo.projections = [Arel.star]
        Flight.find_by_sql("SELECT * FROM #{ord.union(sfo).to_sql} AS subquery").must_equal [@ord, @sfo]
      end
        
      it "builds successful cohorts" do
        ord = model.cohort(:dest => 'ORD').project(Arel.star)
        sfo = model.cohort(:dest => 'SFO').project(Arel.star)
        Flight.find_by_sql("SELECT * FROM #{ord.union(sfo).to_sql} AS subquery").must_equal [@ord, @sfo]

        msn = model.cohort(:origin => 'LAX', :dest => 'MSN').project(Arel.star)
        lhr = model.cohort(:origin => 'LAX', :dest => 'LHR').project(Arel.star)
        Flight.find_by_sql("SELECT * FROM #{msn.union(lhr).to_sql} AS subquery").must_equal [@ord, @sfo]
      end

      it "doesn't somehow create unions with false positives" do
        msn = model.cohort(:dest => 'MSN').project(Arel.star)
        lhr = model.cohort(:dest => 'LHR').project(Arel.star)
        count = ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM #{msn.union(lhr).to_sql} AS subquery")
        flunk "count was nil" if count.nil?
        count.to_i.must_equal 0
      end

      it "builds unions where only one side has rows" do
        msn = model.cohort(:dest => 'MSN').project(Arel.star)
        ord = model.cohort(:dest => 'ORD').project(Arel.star)
        Flight.find_by_sql("SELECT * FROM #{msn.union(ord).to_sql} AS subquery").must_equal [@ord]
      end
    end

    describe :cohort_possible? do
      it "can be used after the cohort is resolved" do
        FactoryGirl.create(:lax_ord)
        FactoryGirl.create(:lax_sfo)
        yes = model.cohort(:origin => 'LAX')
        yes.to_sql # force the cohort to resolve
        no = model.cohort(:dest => 'MSN')
        no.to_sql
        assert yes.cohort_possible?
        refute no.cohort_possible?
      end

      it "may be available even after further composition" do
        FactoryGirl.create(:lax_ord)
        FactoryGirl.create(:lax_sfo)
        yes = model.cohort(:origin => 'LAX').where(moot_condition)
        yes.to_sql # force the cohort to resolve
        no = model.cohort(:dest => 'MSN').where(moot_condition)
        no.to_sql
        assert yes.cohort_possible?
        refute no.cohort_possible?
      end
    end
  end
end

describe CohortAnalysis do
  def assert_count(expected_count, relation)
    relation = relation.clone
    relation.projections = [Arel.sql('COUNT(*)')]
    sql = relation.to_sql
    count = ActiveRecord::Base.connection.select_value(sql)
    flunk "count was nil" if count.nil?
    count.to_i.must_equal expected_count
  end

  def assert_members(expected_members, relation)
    relation = relation.clone
    table = relation.source.left
    relation.projections = [Arel.star]
    actual_members = Flight.find_by_sql relation.to_sql
    actual_members.map(&:id).sort.must_equal expected_members.map(&:id).sort
  end

  def f_t
    Arel::Table.new(:flights)
  end

  describe 'ArelSelectManagerInstanceMethods' do
    it_behaves_like 'an adapter the provides #cohort'
    def model
      Arel::SelectManager.new(ActiveRecord::Base, Arel::Table.new(:flights))
    end
  end

  describe 'ArelTableInstanceMethods' do
    it_behaves_like 'an adapter the provides #cohort'
    def model
      Arel::Table.new(:flights, ActiveRecord::Base)
    end
  end

  describe 'ActiveRecordBaseClassMethods' do
    it_behaves_like 'an adapter the provides #cohort'
    def model
      Flight
    end
  end

  describe 'ActiveRecordRelationInstanceMethods' do
    it_behaves_like 'an adapter the provides #cohort'
    def model
      Flight.scoped
    end
  end
end
