require "spec_helper"

describe ScopedSearch::QueryBuilder do

  let(:klass) { Class.new(ActiveRecord::Base) }

  before(:each) do
    @definition = double('ScopedSearch::Definition')
    @definition.stub(:klass).and_return(klass)
    @definition.stub(:profile).and_return(:default)
    @definition.stub(:default_order).and_return(nil)
    @definition.stub(:profile=).and_return(true)
    @definition.klass.stub(:connection).and_return(double())
  end

  it "should raise an ArgumentError if the query is not set" do
    lambda { ScopedSearch::QueryBuilder.build_query(@definition, nil) }.should raise_error(ArgumentError)
  end

  it "should return empty conditions if the query is blank" do
    ScopedSearch::QueryBuilder.build_query(@definition, "").should == { }
  end

  it "should return empty conditions if the query is whitespace only" do
    ScopedSearch::QueryBuilder.build_query(@definition, "\t ").should == {  }
  end

  it "should use default adapter when connection type is unknown" do
    ScopedSearch::QueryBuilder.class_for(@definition).should == ScopedSearch::QueryBuilder
  end

  it "should use postgres adapter for postgres-like connection" do
    connection = double()
    connection.stub("name").and_return("SomePostgreSQLAdapter")
    @definition.klass.connection.stub("class").and_return(connection)
    ScopedSearch::QueryBuilder.class_for(@definition).should == ScopedSearch::QueryBuilder::PostgreSQLAdapter
  end

  it "should validate value if validator selected" do
    field = double('field')
    field.stub(:virtual?).and_return(false)
    field.stub(:only_explicit).and_return(true)
    field.stub(:field).and_return(:test_field)
    field.stub(:validator).and_return(->(_value) { false })
    field.stub(:special_values).and_return([])

    @definition.stub(:field_by_name).and_return(field)

    lambda { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val') }.should raise_error(ScopedSearch::QueryNotSupported)
  end

  it "should validate value if validator selected" do
    field = double('field')
    field.stub(:virtual?).and_return(false)
    field.stub(:only_explicit).and_return(true)
    field.stub(:field).and_return(:test_field)
    field.stub(:ext_method).and_return(nil)
    field.stub(:key_field).and_return(nil)
    field.stub(:set?).and_return(false)
    field.stub(:to_sql).and_return('')
    field.stub(:validator).and_return(->(value) { value =~ /^\d+$/ })
    field.stub(:value_translation).and_return(nil)
    field.stub(:special_values).and_return([])

    @definition.stub(:field_by_name).and_return(field)

    lambda { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field ^ (1,2)') }.should_not raise_error
    lambda { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field ^ (1,a)') }.should raise_error(ScopedSearch::QueryNotSupported)
  end

  it "should display custom error from validator" do
    field = double('field')
    field.stub(:virtual?).and_return(false)
    field.stub(:only_explicit).and_return(true)
    field.stub(:field).and_return(:test_field)
    field.stub(:validator).and_return(->(_value) { raise ScopedSearch::QueryNotSupported, 'my custom message' })
    field.stub(:special_values).and_return([])

    @definition.stub(:field_by_name).and_return(field)

    lambda { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val') }.should raise_error('my custom message')
  end

  context 'with value_translation' do
    let(:translator) do
      ->(value) do
        if %w(a b c).include?(value)
          'good'
        end
      end
    end
    let(:special_values) { %w(a b c) }
    before do
      field = double('field')
      field.stub(:field).and_return(:test_field)
      field.stub(:key_field).and_return(nil)
      field.stub(:to_sql).and_return('test_field')
      [:virtual?, :set?, :temporal?, :relation, :offset].each { |key| field.stub(key).and_return(false) }
      field.stub(:validator).and_return(->(value) { value == 'x' }) # Nothing except for special_values and x is valid
      field.stub(:special_values).and_return(special_values)
      field.stub(:value_translation).and_return(translator)
      @definition.stub(:field_by_name).and_return(field)
    end

    it 'should translate the value' do
      ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = a').should eq(conditions: ['(test_field = ?)', 'good'])
      ScopedSearch::QueryBuilder.build_query(@definition, 'test_field ^ (a, b, c)').should eq(conditions: ['(test_field IN (?,?,?))', 'good', 'good', 'good'])
    end

    it 'should validate before translation' do
      proc { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = d') }.should raise_error(ScopedSearch::QueryNotSupported, /Value 'd' is not valid for field/)
    end

    it 'should raise an error if translated value is nil' do
      proc { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = x') }.should raise_error(ScopedSearch::QueryNotSupported, /Translation from any value to nil is not allowed/)
    end
  end

  context "with ext_method" do
    before do
      @definition = ScopedSearch::Definition.new(klass)
      @definition.define(:test_field, ext_method: :ext_test)
    end

    it "should return combined :conditions and :parameter" do
      klass.should_receive(:ext_test).with('test_field', '=', 'test_val').and_return(conditions: 'field = ?', parameter: ['test_val'])
      ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val').should eq(conditions: ['(field = ?)', 'test_val'])
    end

    it "should return :joins and :include" do
      klass.should_receive(:ext_test).with('test_field', '=', 'test_val').and_return(include: 'test1', joins: 'test2')
      ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val').should eq(include: ['test1'], joins: ['test2'])
    end

    it "should support LIKE query on a virtual field" do
      klass.should_receive(:ext_test).with('test_field', 'LIKE', 'test_val').and_return(conditions: 'field LIKE ?', parameter: ['%test_val%'])
      ScopedSearch::QueryBuilder.build_query(@definition, 'test_field ~ test_val').should eq(conditions: ['(field LIKE ?)', '%test_val%'])
    end

    it "should raise error when non-hash returned" do
      klass.should_receive(:ext_test).and_return('test')
      lambda { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val') }.should raise_error(ScopedSearch::QueryNotSupported, /should return hash/)
    end

    it "should raise error when ext_method doesn't exist" do
      lambda { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val') }.should raise_error(ScopedSearch::QueryNotSupported, /doesn't respond to 'ext_test'/)
    end

    it "should raise error when method raises exception" do
      klass.should_receive(:ext_test).and_raise('test')
      lambda { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val') }.should raise_error(ScopedSearch::QueryNotSupported, /failed with error: test/)
    end
  end
end