# frozen_string_literal: true

require 'spec_helper'

describe ThinkingSphinx::ActiveRecord::SQLBuilder do
  let(:source)       { double('source', :model => model, :offset => 3,
    :fields => [], :attributes => [], :disable_range? => false,
    :delta_processor => nil, :conditions => [], :groupings => [],
    :adapter => adapter, :associations => [], :primary_key => :id,
    :options => {}, :properties => []) }
  let(:model)        { double('model', :connection => connection,
    :descends_from_active_record? => true, :column_names => [],
    :inheritance_column => 'type', :unscoped => relation,
    :quoted_table_name => '`users`', :name => 'User') }
  let(:connection)   { double('connection') }
  let(:relation)     { double('relation') }
  let(:config)       { double('config', :indices => indices, :settings => {}) }
  let(:indices)      { double('indices', :count => 5) }
  let(:presenter)    { double('presenter', :to_select => '`name` AS `name`',
    :to_group => '`name`') }
  let(:adapter)      { double('adapter',
    :time_zone_query_pre => ['SET TIME ZONE']) }
  let(:associations) { double('associations', :join_values => []) }
  let(:builder)      { ThinkingSphinx::ActiveRecord::SQLBuilder.new source }

  before :each do
    allow(ThinkingSphinx::Configuration).to receive_messages :instance => config
    allow(ThinkingSphinx::ActiveRecord::PropertySQLPresenter).to receive_messages :new => presenter
    allow(Joiner::Joins).to receive_messages :new => associations
    allow(relation).to receive_messages :select => relation, :where => relation, :group => relation,
      :order => relation, :joins => relation, :to_sql => ''
    allow(connection).to receive(:quote_column_name) { |column| "`#{column}`"}
  end

  describe 'sql_query' do
    before :each do
      allow(source).to receive_messages :type => 'mysql'
    end

    it "adds source associations to the joins of the query" do
      source.associations << double('association',
        :stack => [:user, :posts], :string? => false)

      expect(associations).to receive(:add_join_to).with([:user, :posts])

      builder.sql_query
    end

    it "adds string joins directly to the relation" do
      source.associations << double('association',
        :to_s => 'my string', :string? => true)

      expect(relation).to receive(:joins).with(['my string']).and_return(relation)

      builder.sql_query
    end

    context 'MySQL adapter' do
      before :each do
        allow(source).to receive_messages :type => 'mysql'
      end

      it "returns the relation's query" do
        allow(relation).to receive_messages :to_sql => 'SELECT * FROM people'

        expect(builder.sql_query).to eq('SELECT * FROM people')
      end

      it "ensures results aren't from cache" do
        expect(relation).to receive(:select) do |string|
          expect(string).to match(/^SQL_NO_CACHE /)
          relation
        end

        builder.sql_query
      end

      it "adds the document id using the offset and index count" do
        expect(relation).to receive(:select) do |string|
          expect(string).to match(/`users`.`id` \* 5 \+ 3 AS `id`/)
          relation
        end

        builder.sql_query
      end

      it "adds each field to the SELECT clause" do
        source.fields << double('field')

        expect(relation).to receive(:select) do |string|
          expect(string).to match(/`name` AS `name`/)
          relation
        end

        builder.sql_query
      end

      it "adds each attribute to the SELECT clause" do
        source.attributes << double('attribute')
        allow(presenter).to receive_messages(:to_select => '`created_at` AS `created_at`')

        expect(relation).to receive(:select) do |string|
          expect(string).to match(/`created_at` AS `created_at`/)
          relation
        end

        builder.sql_query
      end

      it "limits results to a set range" do
        expect(relation).to receive(:where) do |string|
          expect(string).to match(/`users`.`id` BETWEEN \$start AND \$end/)
          relation
        end

        builder.sql_query
      end

      it "shouldn't limit results to a range if ranges are disabled" do
        allow(source).to receive_messages :disable_range? => true

        expect(relation).to receive(:where) do |string|
          expect(string).not_to match(/`users`.`id` BETWEEN \$start AND \$end/)
          relation
        end

        builder.sql_query
      end

      it "adds source conditions" do
        source.conditions << 'created_at > NOW()'

        expect(relation).to receive(:where) do |string|
          expect(string).to match(/created_at > NOW()/)
          relation
        end

        builder.sql_query
      end

      it "groups by the primary key" do
        expect(relation).to receive(:group) do |string|
          expect(string).to match(/`users`.`id`/)
          relation
        end

        builder.sql_query
      end

      it "groups each field" do
        source.fields << double('field')

        expect(relation).to receive(:group) do |string|
          expect(string).to match(/`name`/)
          relation
        end

        builder.sql_query
      end

      it "groups each attribute" do
        source.attributes << double('attribute')
        allow(presenter).to receive_messages(:to_group => '`created_at`')

        expect(relation).to receive(:group) do |string|
          expect(string).to match(/`created_at`/)
          relation
        end

        builder.sql_query
      end

      it "groups by source groupings" do
        source.groupings << '`latitude`'

        expect(relation).to receive(:group) do |string|
          expect(string).to match(/`latitude`/)
          relation
        end

        builder.sql_query
      end

      it "orders by NULL" do
        expect(relation).to receive(:order).with('NULL').and_return(relation)

        builder.sql_query
      end

      context 'STI model' do
        before :each do
          model.column_names << 'type'
          allow(model).to receive_messages :descends_from_active_record? => false
          allow(model).to receive_messages :store_full_sti_class => true
        end

        it "groups by the inheritance column" do
          expect(relation).to receive(:group) do |string|
            expect(string).to match(/`users`.`type`/)
            relation
          end

          builder.sql_query
        end

        context 'with a custom inheritance column' do
          before :each do
            model.column_names << 'custom_type'
            allow(model).to receive_messages :inheritance_column => 'custom_type'
          end

          it "groups by the right column" do
            expect(relation).to receive(:group) do |string|
              expect(string).to match(/`users`.`custom_type`/)
              relation
            end

            builder.sql_query
          end
        end
      end

      context 'with a delta processor' do
        let(:processor) { double('processor') }

        before :each do
          allow(source).to receive_messages :delta_processor => processor
          allow(source).to receive_messages :delta? => true
        end

        it "filters by the provided clause" do
          expect(processor).to receive(:clause).with(true).and_return('`delta` = 1')
          expect(relation).to receive(:where) do |string|
            expect(string).to match(/`delta` = 1/)
            relation
          end

          builder.sql_query
        end
      end
    end

    context 'PostgreSQL adapter' do
      let(:presenter) { double('presenter', :to_select => '"name" AS "name"',
        :to_group => '"name"') }

      before :each do
        allow(source).to receive_messages :type => 'pgsql'
        allow(model).to receive_messages :quoted_table_name => '"users"'
        allow(connection).to receive(:quote_column_name) { |column| "\"#{column}\""}
      end

      it "returns the relation's query" do
        allow(relation).to receive_messages :to_sql => 'SELECT * FROM people'

        expect(builder.sql_query).to eq('SELECT * FROM people')
      end

      it "adds the document id using the offset and index count" do
        expect(relation).to receive(:select) do |string|
          expect(string).to match(/"users"."id" \* 5 \+ 3 AS "id"/)
          relation
        end

        builder.sql_query
      end

      it "adds each field to the SELECT clause" do
        source.fields << double('field')

        expect(relation).to receive(:select) do |string|
          expect(string).to match(/"name" AS "name"/)
          relation
        end

        builder.sql_query
      end

      it "adds each attribute to the SELECT clause" do
        source.attributes << double('attribute')
        allow(presenter).to receive_messages(:to_select => '"created_at" AS "created_at"')

        expect(relation).to receive(:select) do |string|
          expect(string).to match(/"created_at" AS "created_at"/)
          relation
        end

        builder.sql_query
      end

      it "limits results to a set range" do
        expect(relation).to receive(:where) do |string|
          expect(string).to match(/"users"."id" BETWEEN \$start AND \$end/)
          relation
        end

        builder.sql_query
      end

      it "shouldn't limit results to a range if ranges are disabled" do
        allow(source).to receive_messages :disable_range? => true

        expect(relation).to receive(:where) do |string|
          expect(string).not_to match(/"users"."id" BETWEEN \$start AND \$end/)
          relation
        end

        builder.sql_query
      end

      it "adds source conditions" do
        source.conditions << 'created_at > NOW()'

        expect(relation).to receive(:where) do |string|
          expect(string).to match(/created_at > NOW()/)
          relation
        end

        builder.sql_query
      end

      it "groups by the primary key" do
        expect(relation).to receive(:group) do |string|
          expect(string).to match(/"users"."id"/)
          relation
        end

        builder.sql_query
      end

      it "groups each field" do
        source.fields << double('field')

        expect(relation).to receive(:group) do |string|
          expect(string).to match(/"name"/)
          relation
        end

        builder.sql_query
      end

      it "groups each attribute" do
        source.attributes << double('attribute')
        allow(presenter).to receive_messages(:to_group => '"created_at"')

        expect(relation).to receive(:group) do |string|
          expect(string).to match(/"created_at"/)
          relation
        end

        builder.sql_query
      end

      it "groups by source groupings" do
        source.groupings << '"latitude"'

        expect(relation).to receive(:group) do |string|
          expect(string).to match(/"latitude"/)
          relation
        end

        builder.sql_query
      end

      it "has no ORDER clause" do
        expect(relation).not_to receive(:order)

        builder.sql_query
      end

      context 'group by shortcut' do
        before :each do
          source.options[:minimal_group_by?] = true
        end

        it "groups by the primary key" do
          expect(relation).to receive(:group) do |string|
            expect(string).to match(/"users"."id"/)
            relation
          end

          builder.sql_query
        end

        it "does not group by fields" do
          source.fields << double('field')

          expect(relation).to receive(:group) do |string|
            expect(string).not_to match(/"name"/)
            relation
          end

          builder.sql_query
        end

        it "does not group by attributes" do
          source.attributes << double('attribute')
          allow(presenter).to receive_messages(:to_group => '"created_at"')

          expect(relation).to receive(:group) do |string|
            expect(string).not_to match(/"created_at"/)
            relation
          end

          builder.sql_query
        end

        it "groups by source groupings" do
          source.groupings << '"latitude"'

          expect(relation).to receive(:group) do |string|
            expect(string).to match(/"latitude"/)
            relation
          end

          builder.sql_query
        end
      end

      context 'group by shortcut in global configuration' do
        before :each do
          config.settings['minimal_group_by'] = true
        end

        it "groups by the primary key" do
          expect(relation).to receive(:group) do |string|
            expect(string).to match(/"users"."id"/)
            relation
          end

          builder.sql_query
        end

        it "does not group by fields" do
          source.fields << double('field')

          expect(relation).to receive(:group) do |string|
            expect(string).not_to match(/"name"/)
            relation
          end

          builder.sql_query
        end

        it "does not group by attributes" do
          source.attributes << double('attribute')
          allow(presenter).to receive_messages(:to_group => '"created_at"')

          expect(relation).to receive(:group) do |string|
            expect(string).not_to match(/"created_at"/)
            relation
          end

          builder.sql_query
        end

        it "groups by source groupings" do
          source.groupings << '"latitude"'

          expect(relation).to receive(:group) do |string|
            expect(string).to match(/"latitude"/)
            relation
          end

          builder.sql_query
        end
      end

      context 'STI model' do
        before :each do
          model.column_names << 'type'
          allow(model).to receive_messages :descends_from_active_record? => false
          allow(model).to receive_messages :store_full_sti_class => true
        end

        it "groups by the inheritance column" do
          expect(relation).to receive(:group) do |string|
            expect(string).to match(/"users"."type"/)
            relation
          end

          builder.sql_query
        end

        context 'with a custom inheritance column' do
          before :each do
            model.column_names << 'custom_type'
            allow(model).to receive_messages :inheritance_column => 'custom_type'
          end

          it "groups by the right column" do
            expect(relation).to receive(:group) do |string|
              expect(string).to match(/"users"."custom_type"/)
              relation
            end

            builder.sql_query
          end
        end
      end

      context 'with a delta processor' do
        let(:processor) { double('processor') }

        before :each do
          allow(source).to receive_messages :delta_processor => processor
          allow(source).to receive_messages :delta? => true
        end

        it "filters by the provided clause" do
          expect(processor).to receive(:clause).with(true).and_return('"delta" = 1')
          expect(relation).to receive(:where) do |string|
            expect(string).to match(/"delta" = 1/)
            relation
          end

          builder.sql_query
        end
      end
    end
  end

  describe 'sql_query_pre' do
    let(:processor) { double('processor', :reset_query => 'RESET DELTAS') }

    before :each do
      allow(source).to receive_messages :options => {}, :delta_processor => nil, :delta? => false
      allow(adapter).to receive_messages :utf8_query_pre => ['SET UTF8']
    end

    it "adds a reset delta query if there is a delta processor and this is the core source" do
      allow(source).to receive_messages :delta_processor => processor

      expect(builder.sql_query_pre).to include('RESET DELTAS')
    end

    it "does not add a reset query if there is no delta processor" do
      expect(builder.sql_query_pre).not_to include('RESET DELTAS')
    end

    it "does not add a reset query if this is a delta source" do
      allow(source).to receive_messages :delta_processor => processor
      allow(source).to receive_messages :delta? => true

      expect(builder.sql_query_pre).not_to include('RESET DELTAS')
    end

    it "sets the group_concat_max_len value if set" do
      source.options[:group_concat_max_len] = 123

      expect(builder.sql_query_pre).
        to include('SET SESSION group_concat_max_len = 123')
    end

    it "does not set the group_concat_max_len if not provided" do
      source.options[:group_concat_max_len] = nil

      expect(builder.sql_query_pre.select { |sql|
        sql[/SET SESSION group_concat_max_len/]
      }).to be_empty
    end

    it "sets the connection to use UTF-8 if required" do
      source.options[:utf8?] = true

      expect(builder.sql_query_pre).to include('SET UTF8')
    end

    it "does not set the connection to use UTF-8 if not required" do
      source.options[:utf8?] = false

      expect(builder.sql_query_pre).not_to include('SET UTF8')
    end

    it "adds a time-zone query by default" do
      expect(builder.sql_query_pre).to include('SET TIME ZONE')
    end

    it "does not add a time-zone query if requested" do
      config.settings['skip_time_zone'] = true

      expect(builder.sql_query_pre).to_not include('SET TIME ZONE')
    end
  end

  describe 'sql_query_range' do
    before :each do
      allow(adapter).to receive(:convert_nulls) { |string, default|
        "ISNULL(#{string}, #{default})"
      }
    end

    it "returns the relation's query" do
      allow(relation).to receive_messages :to_sql => 'SELECT * FROM people'

      expect(builder.sql_query_range).to eq('SELECT * FROM people')
    end

    it "returns nil if ranges are disabled" do
      allow(source).to receive_messages :disable_range? => true

      expect(builder.sql_query_range).to be_nil
    end

    it "selects the minimum primary key value, allowing for nulls" do
      expect(relation).to receive(:select) do |string|
        expect(string).to match(/ISNULL\(MIN\(`users`.`id`\), 1\)/)
        relation
      end

      builder.sql_query_range
    end

    it "selects the maximum primary key value, allowing for nulls" do
      expect(relation).to receive(:select) do |string|
        expect(string).to match(/ISNULL\(MAX\(`users`.`id`\), 1\)/)
        relation
      end

      builder.sql_query_range
    end

    it "shouldn't limit results to a range" do
      expect(relation).to receive(:where) do |string|
        expect(string).not_to match(/`users`.`id` BETWEEN \$start AND \$end/)
        relation
      end

      builder.sql_query_range
    end

    it "does not add source conditions" do
      source.conditions << 'created_at > NOW()'

      expect(relation).to receive(:where) do |string|
        expect(string).not_to match(/created_at > NOW()/)
        relation
      end

      builder.sql_query_range
    end

    context 'with a delta processor' do
      let(:processor) { double('processor') }

      before :each do
        allow(source).to receive_messages :delta_processor => processor
        allow(source).to receive_messages :delta? => true
      end

      it "filters by the provided clause" do
        expect(processor).to receive(:clause).with(true).and_return('`delta` = 1')
        expect(relation).to receive(:where) do |string|
          expect(string).to match(/`delta` = 1/)
          relation
        end

        builder.sql_query_range
      end
    end
  end
end
