require 'spec_helper'

module Ransack
  describe Search do
    describe '#initialize' do
      it 'removes empty conditions before building' do
        expect_any_instance_of(Search).to receive(:build).with({})
        Search.new(Person, name_eq: '')
      end

      it 'keeps conditions with a false value before building' do
        expect_any_instance_of(Search).to receive(:build)
        .with({ 'name_eq' => false })
        Search.new(Person, name_eq: false)
      end

      it 'keeps conditions with a value before building' do
        expect_any_instance_of(Search).to receive(:build)
        .with({ 'name_eq' => 'foobar' })
        Search.new(Person, name_eq: 'foobar')
      end

      context 'whitespace stripping' do
        context 'when whitespace_strip option is true' do
          before do
            Ransack.configure { |c| c.strip_whitespace = true }
          end

          it 'strips leading & trailing whitespace before building' do
            expect_any_instance_of(Search).to receive(:build)
            .with({ 'name_eq' => 'foobar' })
            Search.new(Person, name_eq: '   foobar     ')
          end
        end

        context 'when whitespace_strip option is false' do
          before do
            Ransack.configure { |c| c.strip_whitespace = false }
          end

          it 'doesn\'t strip leading & trailing whitespace before building' do
            expect_any_instance_of(Search).to receive(:build)
            .with({ 'name_eq' => '   foobar     ' })
            Search.new(Person, name_eq: '   foobar     ')
          end
        end

        it 'strips leading & trailing whitespace when strip_whitespace search parameter is true' do
          expect_any_instance_of(Search).to receive(:build)
          .with({ 'name_eq' => 'foobar' })
          Search.new(Person, { name_eq: '   foobar     ' }, { strip_whitespace: true })
        end

        it 'doesn\'t strip leading & trailing whitespace when strip_whitespace search parameter is false' do
          expect_any_instance_of(Search).to receive(:build)
          .with({ 'name_eq' => '   foobar     ' })
          Search.new(Person, { name_eq: '   foobar     ' }, { strip_whitespace: false })
        end
      end

      it 'removes empty suffixed conditions before building' do
        expect_any_instance_of(Search).to receive(:build).with({})
        Search.new(Person, name_eq_any: [''])
      end

      it 'keeps suffixed conditions with a false value before building' do
        expect_any_instance_of(Search).to receive(:build)
        .with({ 'name_eq_any' => [false] })
        Search.new(Person, name_eq_any: [false])
      end

      it 'keeps suffixed conditions with a value before building' do
        expect_any_instance_of(Search).to receive(:build)
        .with({ 'name_eq_any' => ['foobar'] })
        Search.new(Person, name_eq_any: ['foobar'])
      end

      it 'does not raise exception for string :params argument' do
        expect { Search.new(Person, '') }.not_to raise_error
      end

      it 'accepts a context option' do
        shared_context = Context.for(Person)
        s1 = Search.new(Person, { name_eq: 'A' }, context: shared_context)
        s2 = Search.new(Person, { name_eq: 'B' }, context: shared_context)
        expect(s1.context).to be s2.context
      end
    end

    describe '#build' do
      it 'creates conditions for top-level attributes' do
        s = Search.new(Person, name_eq: 'Ernie')
        condition = s.base[:name_eq]
        expect(condition).to be_a Nodes::Condition
        expect(condition.predicate.name).to eq 'eq'
        expect(condition.attributes.first.name).to eq 'name'
        expect(condition.value).to eq 'Ernie'
      end

      it 'creates conditions for association attributes' do
        s = Search.new(Person, children_name_eq: 'Ernie')
        condition = s.base[:children_name_eq]
        expect(condition).to be_a Nodes::Condition
        expect(condition.predicate.name).to eq 'eq'
        expect(condition.attributes.first.name).to eq 'children_name'
        expect(condition.value).to eq 'Ernie'
      end

      it 'creates conditions for polymorphic belongs_to association attributes' do
        s = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie')
        condition = s.base[:notable_of_Person_type_name_eq]
        expect(condition).to be_a Nodes::Condition
        expect(condition.predicate.name).to eq 'eq'
        expect(condition.attributes.first.name)
          .to eq 'notable_of_Person_type_name'
        expect(condition.value).to eq 'Ernie'
      end

      it 'creates conditions for multiple polymorphic belongs_to association
        attributes' do
        s = Search.new(Note,
          notable_of_Person_type_name_or_notable_of_Article_type_title_eq: 'Ernie')
        condition = s.
          base[:notable_of_Person_type_name_or_notable_of_Article_type_title_eq]
        expect(condition).to be_a Nodes::Condition
        expect(condition.predicate.name).to eq 'eq'
        expect(condition.attributes.first.name)
          .to eq 'notable_of_Person_type_name'
        expect(condition.attributes.last.name)
          .to eq 'notable_of_Article_type_title'
        expect(condition.value).to eq 'Ernie'
      end

      it 'creates conditions for aliased attributes',
      if: Ransack::SUPPORTS_ATTRIBUTE_ALIAS do
        s = Search.new(Person, full_name_eq: 'Ernie')
        condition = s.base[:full_name_eq]
        expect(condition).to be_a Nodes::Condition
        expect(condition.predicate.name).to eq 'eq'
        expect(condition.attributes.first.name).to eq 'full_name'
        expect(condition.value).to eq 'Ernie'
      end

      it 'preserves default scope and conditions for associations' do
        s = Search.new(Person, published_articles_title_eq: 'Test')
        expect(s.result.to_sql).to include 'default_scope'
        expect(s.result.to_sql).to include 'published'
      end

      # The failure/oversight in Ransack::Nodes::Condition#arel_predicate or deeper is beyond my understanding of the structures
      it 'preserves (inverts) default scope and conditions for negative subqueries' do
        # the positive case (published_articles_title_eq) is
        # SELECT "people".* FROM "people"
        # LEFT OUTER JOIN "articles" ON "articles"."person_id" = "people"."id"
        #   AND "articles"."published" = 't'
        #   AND ('default_scope' = 'default_scope')
        # WHERE "articles"."title" = 'Test' ORDER BY "people"."id" DESC
        #
        # negative case was
        # SELECT "people".* FROM "people" WHERE "people"."id" NOT IN (
        #   SELECT "articles"."person_id" FROM "articles"
        #   WHERE "articles"."person_id" = "people"."id"
        #     AND NOT ("articles"."title" != 'Test')
        # ) ORDER BY "people"."id" DESC
        #
        # Should have been like
        # SELECT "people".* FROM "people" WHERE "people"."id" NOT IN (
        #   SELECT "articles"."person_id" FROM "articles"
        #   WHERE "articles"."person_id" = "people"."id"
        #     AND "articles"."title" = 'Test' AND "articles"."published" = 't' AND ('default_scope' = 'default_scope')
        # ) ORDER BY "people"."id" DESC
        #
        # With tenanting (eg default_scope with column reference), NOT IN should be like
        # SELECT "people".* FROM "people" WHERE "people"."tenant_id" = 'tenant_id' AND "people"."id" NOT IN (
        #   SELECT "articles"."person_id" FROM "articles"
        #   WHERE "articles"."person_id" = "people"."id"
        #     AND "articles"."tenant_id" = 'tenant_id'
        #     AND "articles"."title" = 'Test' AND "articles"."published" = 't' AND ('default_scope' = 'default_scope')
        # ) ORDER BY "people"."id" DESC

        s = Search.new(Person, published_articles_title_not_eq: 'Test')
        expect(s.result.to_sql).to include 'default_scope'
        expect(s.result.to_sql).to include 'published'
      end

      it 'discards empty conditions' do
        s = Search.new(Person, children_name_eq: '')
        condition = s.base[:children_name_eq]
        expect(condition).to be_nil
      end

      it 'accepts base grouping condition as an option' do
        expect(Nodes::Grouping).to receive(:new).with(kind_of(Context), 'or')
        Search.new(Person, {}, { grouping: 'or' })
      end

      it 'accepts arrays of groupings' do
        s = Search.new(Person,
          g: [
            { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
            { m: 'or', name_eq: 'Bert', children_name_eq: 'Bert' },
          ]
        )
        ors = s.groupings
        expect(ors.size).to eq(2)
        or1, or2 = ors
        expect(or1).to be_a Nodes::Grouping
        expect(or1.combinator).to eq 'or'
        expect(or2).to be_a Nodes::Grouping
        expect(or2.combinator).to eq 'or'
      end

      it 'accepts attributes hashes for groupings' do
        s = Search.new(Person,
          g: {
            '0' => { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
            '1' => { m: 'or', name_eq: 'Bert',  children_name_eq: 'Bert' },
          }
        )
        ors = s.groupings
        expect(ors.size).to eq(2)
        or1, or2 = ors
        expect(or1).to be_a Nodes::Grouping
        expect(or1.combinator).to eq 'or'
        expect(or2).to be_a Nodes::Grouping
        expect(or2.combinator).to eq 'or'
      end

      it 'accepts attributes hashes for conditions' do
        s = Search.new(Person,
          c: {
            '0' => { a: ['name'], p: 'eq', v: ['Ernie'] },
            '1' => {
                     a: ['children_name', 'parent_name'],
                     p: 'eq', v: ['Ernie'], m: 'or'
                   }
          }
        )
        conditions = s.base.conditions
        expect(conditions.size).to eq(2)
        expect(conditions.map { |c| c.class })
        .to eq [Nodes::Condition, Nodes::Condition]
      end

      it 'creates conditions for custom predicates that take arrays' do
        Ransack.configure do |config|
          config.add_predicate 'ary_pred', wants_array: true
        end

        s = Search.new(Person, name_ary_pred: ['Ernie', 'Bert'])
        condition = s.base[:name_ary_pred]
        expect(condition).to be_a Nodes::Condition
        expect(condition.predicate.name).to eq 'ary_pred'
        expect(condition.attributes.first.name).to eq 'name'
        expect(condition.value).to eq ['Ernie', 'Bert']
      end

      it 'does not evaluate the query on #inspect' do
        s = Search.new(Person, children_id_in: [1, 2, 3])
        expect(s.inspect).not_to match /ActiveRecord/
      end

      context 'with an invalid condition' do
        subject { Search.new(Person, unknown_attr_eq: 'Ernie') }

        context 'when ignore_unknown_conditions configuration option is false' do
          before do
            Ransack.configure { |c| c.ignore_unknown_conditions = false }
          end

          specify { expect { subject }.to raise_error ArgumentError }
          specify { expect { subject }.to raise_error InvalidSearchError }
        end

        context 'when ignore_unknown_conditions configuration option is true' do
          before do
            Ransack.configure { |c| c.ignore_unknown_conditions = true }
          end

          specify { expect { subject }.not_to raise_error }
        end

        subject(:with_ignore_unknown_conditions_false) {
          Search.new(Person,
            { unknown_attr_eq: 'Ernie' },
            { ignore_unknown_conditions: false }
          )
        }

        subject(:with_ignore_unknown_conditions_true) {
          Search.new(Person,
            { unknown_attr_eq: 'Ernie' },
            { ignore_unknown_conditions: true }
          )
        }

        context 'when ignore_unknown_conditions search parameter is absent' do
          specify { expect { subject }.not_to raise_error }
        end

        context 'when ignore_unknown_conditions search parameter is false' do
          specify { expect { with_ignore_unknown_conditions_false }.to raise_error ArgumentError }
          specify { expect { with_ignore_unknown_conditions_false }.to raise_error InvalidSearchError }
        end

        context 'when ignore_unknown_conditions search parameter is true' do
          specify { expect { with_ignore_unknown_conditions_true }.not_to raise_error }
        end
      end

      it 'does not modify the parameters' do
        params = { name_eq: '' }
        expect { Search.new(Person, params) }.not_to change { params }
      end

      context 'with empty search parameters' do
        it 'handles completely empty parameters' do
          search = Search.new(Person, {})
          expect(search.result.to_sql).not_to match(/WHERE/)
        end

        it 'handles nil parameters' do
          search = Search.new(Person, nil)
          expect(search.result.to_sql).not_to match(/WHERE/)
        end
      end

      context 'with whitespace-only values' do
        before do
          Ransack.configure { |c| c.strip_whitespace = true }
        end

        it 'removes whitespace-only values' do
          expect_any_instance_of(Search).to receive(:build).with({})
          Search.new(Person, name_eq: '   ')
        end

        it 'keeps values with content after whitespace stripping' do
          expect_any_instance_of(Search).to receive(:build).with({ 'name_eq' => 'test' })
          Search.new(Person, name_eq: '  test  ')
        end
      end

      context 'with special characters in values' do
        it 'handles values with special regex characters' do
          search = Search.new(Person, name_cont: 'test[(){}^$|?*+.\\')
          expect { search.result }.not_to raise_error
        end

        it 'handles values with SQL injection attempts' do
          search = Search.new(Person, name_cont: "'; DROP TABLE people; --")
          expect { search.result }.not_to raise_error
        end
      end

      context "ransackable_scope" do
        around(:each) do |example|
          Person.define_singleton_method(:name_eq) do |name|
            self.where(name: name)
          end

          begin
            example.run
          ensure
            Person.singleton_class.undef_method :name_eq
          end
        end

        it "is prioritized over base predicates" do
          allow(Person).to receive(:ransackable_scopes)
            .and_return(Person.ransackable_scopes + [:name_eq])

          s = Search.new(Person, name_eq: "Johny")
          expect(s.instance_variable_get(:@scope_args)["name_eq"]).to eq("Johny")
          expect(s.base[:name_eq]).to be_nil
        end
      end

      context "ransackable_scope with array arguments" do
        around(:each) do |example|
          Person.define_singleton_method(:domestic) do |countries|
            self.where(name: countries)
          end

          Person.define_singleton_method(:flexible_scope) do |*args|
            self.where(id: args)
          end

          Person.define_singleton_method(:two_param_scope) do |param1, param2|
            self.where(name: param1, id: param2)
          end

          begin
            example.run
          ensure
            Person.singleton_class.undef_method :domestic
            Person.singleton_class.undef_method :flexible_scope
            Person.singleton_class.undef_method :two_param_scope
          end
        end

        it "handles scopes that take arrays as single arguments (arity 1)" do
          allow(Person).to receive(:ransackable_scopes)
            .and_return(Person.ransackable_scopes + [:domestic])

          # This should not raise ArgumentError
          expect {
            s = Search.new(Person, domestic: ['US', 'JP'])
            s.result # This triggers the actual scope call
          }.not_to raise_error

          s = Search.new(Person, domestic: ['US', 'JP'])
          expect(s.instance_variable_get(:@scope_args)["domestic"]).to eq("US")
        end

        it "handles scopes with flexible arity (negative arity)" do
          allow(Person).to receive(:ransackable_scopes)
            .and_return(Person.ransackable_scopes + [:flexible_scope])

          expect {
            s = Search.new(Person, flexible_scope: ['US', 'JP'])
            s.result
          }.not_to raise_error
        end

        it "handles scopes with arity > 1" do
          allow(Person).to receive(:ransackable_scopes)
            .and_return(Person.ransackable_scopes + [:two_param_scope])

          expect {
            s = Search.new(Person, two_param_scope: ['param1', 'param2'])
            s.result
          }.not_to raise_error
        end

        it "still supports the workaround with nested arrays" do
          allow(Person).to receive(:ransackable_scopes)
            .and_return(Person.ransackable_scopes + [:domestic])

          # The workaround from the issue should still work
          expect {
            s = Search.new(Person, domestic: [['US', 'JP']])
            s.result
          }.not_to raise_error
        end
      end
    end

    describe '#result' do
      let(:people_name_field) {
        "#{quote_table_name("people")}.#{quote_column_name("name")}"
      }
      let(:children_people_name_field) {
        "#{quote_table_name("children_people")}.#{quote_column_name("name")}"
      }
      let(:notable_type_field) {
        "#{quote_table_name("notes")}.#{quote_column_name("notable_type")}"
      }

      it 'evaluates conditions contextually' do
        s = Search.new(Person, children_name_eq: 'Ernie')
        expect(s.result).to be_an ActiveRecord::Relation
        expect(s.result.to_sql).to match /#{
          children_people_name_field} = 'Ernie'/
      end

      it 'use appropriate table alias' do
        s = Search.new(Person, {
          name_eq: "person_name_query",
          articles_title_eq: "person_article_title_query",
          parent_name_eq: "parent_name_query",
          parent_articles_title_eq: 'parents_article_title_query'
        }).result

        real_query = remove_quotes_and_backticks(s.to_sql)

        expect(real_query)
                .to match(%r{LEFT OUTER JOIN articles ON (\('default_scope' = 'default_scope'\) AND )?articles.person_id = people.id})
        expect(real_query)
                .to match(%r{LEFT OUTER JOIN articles articles_people ON (\('default_scope' = 'default_scope'\) AND )?articles_people.person_id = parents_people.id})

        expect(real_query)
          .to include "people.name = 'person_name_query'"
        expect(real_query)
          .to include "articles.title = 'person_article_title_query'"
        expect(real_query)
          .to include "parents_people.name = 'parent_name_query'"
        expect(real_query)
          .to include "articles_people.title = 'parents_article_title_query'"
      end

      it 'evaluates conditions for multiple `belongs_to` associations to the same table contextually' do
        s = Search.new(
          Recommendation,
          person_name_eq: 'Ernie',
          target_person_parent_name_eq: 'Test'
        ).result
        expect(s).to be_an ActiveRecord::Relation
        real_query = remove_quotes_and_backticks(s.to_sql)
        expected_query = <<-SQL
          SELECT recommendations.* FROM recommendations
          LEFT OUTER JOIN people ON people.id = recommendations.person_id
          LEFT OUTER JOIN people target_people_recommendations
            ON target_people_recommendations.id = recommendations.target_person_id
          LEFT OUTER JOIN people parents_people
            ON parents_people.id = target_people_recommendations.parent_id
          WHERE (people.name = 'Ernie' AND parents_people.name = 'Test')
        SQL
        .squish
        expect(real_query).to eq expected_query
      end

      it 'evaluates compound conditions contextually' do
        s = Search.new(Person, children_name_or_name_eq: 'Ernie').result
        expect(s).to be_an ActiveRecord::Relation
        expect(s.to_sql).to match /#{children_people_name_field
          } = 'Ernie' OR #{people_name_field} = 'Ernie'/
      end

      it 'evaluates polymorphic belongs_to association conditions contextually' do
        s = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie').result
        expect(s).to be_an ActiveRecord::Relation
        expect(s.to_sql).to match /#{people_name_field} = 'Ernie'/
        expect(s.to_sql).to match /#{notable_type_field} = 'Person'/
      end

      it 'evaluates nested conditions' do
        s = Search.new(Person, children_name_eq: 'Ernie',
          g: [
            { m: 'or', name_eq: 'Ernie', children_children_name_eq: 'Ernie' }
          ]
        ).result
        expect(s).to be_an ActiveRecord::Relation
        first, last = s.to_sql.split(/ AND /)
        expect(first).to match /#{children_people_name_field} = 'Ernie'/
        expect(last).to match /#{
          people_name_field} = 'Ernie' OR #{
          quote_table_name("children_people_2")}.#{
          quote_column_name("name")} = 'Ernie'/
      end

      it 'evaluates arrays of groupings' do
        s = Search.new(Person,
          g: [
            { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
            { m: 'or', name_eq: 'Bert', children_name_eq: 'Bert' }
          ]
        ).result
        expect(s).to be_an ActiveRecord::Relation
        first, last = s.to_sql.split(/ AND /)
        expect(first).to match /#{people_name_field} = 'Ernie' OR #{
          children_people_name_field} = 'Ernie'/
        expect(last).to match /#{people_name_field} = 'Bert' OR #{
          children_people_name_field} = 'Bert'/
      end

      it 'returns distinct records when passed distinct: true' do
        s = Search.new(Person,
          g: [
            { m: 'or', comments_body_cont: 'e', articles_comments_body_cont: 'e' }
          ]
        )

        all_or_load, uniq_or_distinct = :load, :distinct
        expect(s.result.send(all_or_load).size)
        .to eq(8998)
        expect(s.result(distinct: true).size)
        .to eq(10)
        expect(s.result.send(all_or_load).send(uniq_or_distinct))
        .to eq s.result(distinct: true).send(all_or_load)
      end

      it 'evaluates joins with belongs_to join' do
        s = Person.joins(:parent).ransack(parent_name_eq: 'Ernie').result(distinct: true)
        expect(s).to be_an ActiveRecord::Relation
      end

      private

        def remove_quotes_and_backticks(str)
          str.gsub(/["`]/, '')
        end
    end

    describe '#sorts=' do
      before do
        @s = Search.new(Person)
      end

      it 'doesn\'t creates sorts' do
        @s.sorts = ''
        expect(@s.sorts.size).to eq(0)
      end

      it 'creates sorts based on a single attribute/direction' do
        @s.sorts = 'id desc'
        expect(@s.sorts.size).to eq(1)
        sort = @s.sorts.first
        expect(sort).to be_a Nodes::Sort
        expect(sort.name).to eq 'id'
        expect(sort.dir).to eq 'desc'
      end

      it 'creates sorts based on a single attribute and uppercase direction' do
        @s.sorts = 'id DESC'
        expect(@s.sorts.size).to eq(1)
        sort = @s.sorts.first
        expect(sort).to be_a Nodes::Sort
        expect(sort.name).to eq 'id'
        expect(sort.dir).to eq 'desc'
      end

      it 'creates sorts based on a single attribute and without direction' do
        @s.sorts = 'id'
        expect(@s.sorts.size).to eq(1)
        sort = @s.sorts.first
        expect(sort).to be_a Nodes::Sort
        expect(sort.name).to eq 'id'
        expect(sort.dir).to eq 'asc'
      end

      it 'creates sorts based on a single alias/direction' do
        @s.sorts = 'daddy desc'
        expect(@s.sorts.size).to eq(1)
        sort = @s.sorts.first
        expect(sort).to be_a Nodes::Sort
        expect(sort.name).to eq 'parent_name'
        expect(sort.dir).to eq 'desc'
      end

      it 'creates sorts based on a single alias and uppercase direction' do
        @s.sorts = 'daddy DESC'
        expect(@s.sorts.size).to eq(1)
        sort = @s.sorts.first
        expect(sort).to be_a Nodes::Sort
        expect(sort.name).to eq 'parent_name'
        expect(sort.dir).to eq 'desc'
      end

      it 'creates sorts based on a single alias and without direction' do
        @s.sorts = 'daddy'
        expect(@s.sorts.size).to eq(1)
        sort = @s.sorts.first
        expect(sort).to be_a Nodes::Sort
        expect(sort.name).to eq 'parent_name'
        expect(sort.dir).to eq 'asc'
      end

      it 'creates sorts based on attributes, alias and directions in array format' do
        @s.sorts = ['id desc', { name: 'daddy', dir: 'asc' }]
        expect(@s.sorts.size).to eq(2)
        sort1, sort2 = @s.sorts
        expect(sort1).to be_a Nodes::Sort
        expect(sort1.name).to eq 'id'
        expect(sort1.dir).to eq 'desc'
        expect(sort2).to be_a Nodes::Sort
        expect(sort2.name).to eq 'parent_name'
        expect(sort2.dir).to eq 'asc'
      end

      it 'creates sorts based on attributes, alias and uppercase directions in array format' do
        @s.sorts = ['id DESC', { name: 'daddy', dir: 'ASC' }]
        expect(@s.sorts.size).to eq(2)
        sort1, sort2 = @s.sorts
        expect(sort1).to be_a Nodes::Sort
        expect(sort1.name).to eq 'id'
        expect(sort1.dir).to eq 'desc'
        expect(sort2).to be_a Nodes::Sort
        expect(sort2.name).to eq 'parent_name'
        expect(sort2.dir).to eq 'asc'
      end

      it 'creates sorts based on attributes, alias and different directions
        in array format' do
        @s.sorts = ['id DESC', { name: 'daddy', dir: nil }]
        expect(@s.sorts.size).to eq(2)
        sort1, sort2 = @s.sorts
        expect(sort1).to be_a Nodes::Sort
        expect(sort1.name).to eq 'id'
        expect(sort1.dir).to eq 'desc'
        expect(sort2).to be_a Nodes::Sort
        expect(sort2.name).to eq 'parent_name'
        expect(sort2.dir).to eq 'asc'
      end

      it 'creates sorts based on attributes, alias and directions in hash format' do
        @s.sorts = {
          '0' => { name: 'id', dir: 'desc' },
          '1' => { name: 'daddy', dir: 'asc' }
        }
        expect(@s.sorts.size).to eq(2)
        expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
        id_sort = @s.sorts.detect { |s| s.name == 'id' }
        daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
        expect(id_sort.dir).to eq 'desc'
        expect(daddy_sort.dir).to eq 'asc'
      end

      it 'creates sorts based on attributes, alias and uppercase directions
        in hash format' do
        @s.sorts = {
          '0' => { name: 'id', dir: 'DESC' },
          '1' => { name: 'daddy', dir: 'ASC' }
        }
        expect(@s.sorts.size).to eq(2)
        expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
        id_sort = @s.sorts.detect { |s| s.name == 'id' }
        daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
        expect(id_sort.dir).to eq 'desc'
        expect(daddy_sort.dir).to eq 'asc'
      end

      it 'creates sorts based on attributes, alias and different directions
        in hash format' do
        @s.sorts = {
          '0' => { name: 'id', dir: 'DESC' },
          '1' => { name: 'daddy', dir: nil }
        }
        expect(@s.sorts.size).to eq(2)
        expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
        id_sort = @s.sorts.detect { |s| s.name == 'id' }
        daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
        expect(id_sort.dir).to eq 'desc'
        expect(daddy_sort.dir).to eq 'asc'
      end

      it 'overrides existing sort' do
        @s.sorts = 'id asc'
        expect(@s.result.first.id).to eq 1
      end

      it 'raises ArgumentError when an invalid argument is sent' do
        expect do
          @s.sorts = 1234
        end.to raise_error(ArgumentError,  "Invalid argument (Integer) supplied to sorts=")
      end

      it 'raises InvalidSearchError when an invalid argument is sent' do
        expect do
          @s.sorts = 1234
        end.to raise_error(Ransack::InvalidSearchError,  "Invalid argument (Integer) supplied to sorts=")
      end

      it "PG's sort option", if: ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" do
        default = Ransack.options.clone

        s = Search.new(Person, s: 'name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" ASC"

        Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_first }
        s = Search.new(Person, s: 'name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" ASC NULLS FIRST"
        s = Search.new(Person, s: 'name desc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" DESC NULLS LAST"

        Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_last }
        s = Search.new(Person, s: 'name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" ASC NULLS LAST"
        s = Search.new(Person, s: 'name desc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" DESC NULLS FIRST"

        Ransack.options = default
      end

      it "PG's sort option with double name", if: ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" do
        default = Ransack.options.clone

        s = Search.new(Person, s: 'doubled_name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC"

        Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_first }
        s = Search.new(Person, s: 'doubled_name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS FIRST"
        s = Search.new(Person, s: 'doubled_name desc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS LAST"

        Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_last }
        s = Search.new(Person, s: 'doubled_name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS LAST"
        s = Search.new(Person, s: 'doubled_name desc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS FIRST"

        Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_always_first }
        s = Search.new(Person, s: 'doubled_name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS FIRST"
        s = Search.new(Person, s: 'doubled_name desc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS FIRST"

        Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_always_last }
        s = Search.new(Person, s: 'doubled_name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS LAST"
        s = Search.new(Person, s: 'doubled_name desc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS LAST"

        Ransack.options = default
      end
    end

    describe '#method_missing' do
      before do
        @s = Search.new(Person)
      end

      it 'raises NoMethodError when sent an invalid attribute' do
        expect { @s.blah }.to raise_error NoMethodError
      end

      it 'sets condition attributes when sent valid attributes' do
        @s.name_eq = 'Ernie'
        expect(@s.name_eq).to eq 'Ernie'
      end

      it 'allows chaining to access nested conditions' do
        @s.groupings = [
          { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' }
        ]
        expect(@s.groupings.first.children_name_eq).to eq 'Ernie'
      end
    end
  end
end
