require 'spec_helper'

RSpec.describe 'AuxiliaryStatement' do
  before :each do
    User.auxiliary_statements_list = {}
  end

  context 'on relation' do
    let(:klass) { User }
    let(:true_value) { 'TRUE' }
    subject { klass.unscoped }

    it 'has its method' do
      expect(subject).to respond_to(:with)
    end

    it 'can perform simple queries' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.all
        cte.attributes content: :comment_content
      end

      result = 'WITH "comments" AS'
      result << ' (SELECT "comments"."content" AS comment_content, "comments"."user_id" FROM "comments")'
      result << ' SELECT "users".*, "comments"."comment_content" FROM "users"'
      result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
      expect(subject.with(:comments).arel.to_sql).to eql(result)
    end

    it 'can perform more complex queries' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.distinct_on(:user_id).order(:user_id, id: :desc)
        cte.attributes content: :last_comment
      end

      result = 'WITH "comments" AS (SELECT DISTINCT ON ( "comments"."user_id" )'
      result << ' "comments"."content" AS last_comment, "comments"."user_id"'
      result << ' FROM "comments" ORDER BY "comments"."user_id" ASC,'
      result << ' "comments"."id" DESC) SELECT "users".*,'
      result << ' "comments"."last_comment" FROM "users" INNER JOIN "comments"'
      result << ' ON "comments"."user_id" = "users"."id"'
      expect(subject.with(:comments).arel.to_sql).to eql(result)
    end

    it 'accepts extra select columns' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.all
        cte.attributes content: :comment_content
      end

      result = 'WITH "comments" AS'
      result << ' (SELECT "comments"."content" AS comment_content, "comments"."slug" AS comment_slug, "comments"."user_id" FROM "comments")'
      result << ' SELECT "users".*, "comments"."comment_content", "comments"."comment_slug" FROM "users"'
      result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
      expect(subject.with(:comments, select: {slug: :comment_slug}).arel.to_sql).to eql(result)
    end

    it 'accepts extra join columns' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.all
        cte.attributes content: :comment_content
      end

      result = 'WITH "comments" AS'
      result << ' (SELECT "comments"."content" AS comment_content, "comments"."user_id", "comments"."active" FROM "comments")'
      result << ' SELECT "users".*, "comments"."comment_content" FROM "users"'
      result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id" AND "comments"."active" = "users"."active"'
      expect(subject.with(:comments, join: {active: :active}).arel.to_sql).to eql(result)
    end

    it 'accepts extra conditions' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.all
        cte.attributes content: :comment_content
      end

      result = 'WITH "comments" AS'
      result << ' (SELECT "comments"."content" AS comment_content, "comments"."user_id"'
      result << ' FROM "comments" WHERE "comments"."active" = $1)'
      result << ' SELECT "users".*, "comments"."comment_content" FROM "users"'
      result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
      expect(subject.with(:comments, where: {active: true}).arel.to_sql).to eql(result)
    end

    it 'accepts scopes from both sides' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.where(id: 1).all
        cte.attributes content: :comment_content
      end

      query = subject.where(id: 2).with(:comments)

      result = 'WITH "comments" AS'
      result << ' (SELECT "comments"."content" AS comment_content, "comments"."user_id" FROM "comments"'
      result << ' WHERE "comments"."id" = $1)'
      result << ' SELECT "users".*, "comments"."comment_content" FROM "users"'
      result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
      result << ' WHERE "users"."id" = $2'

      expect(query.arel.to_sql).to eql(result)
      expect(query.send(:bound_attributes).map(&:value_before_type_cast)).to eql([1, 2])
    end

    it 'accepts string as attributes' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.all
        cte.attributes sql('MAX(id)') => :comment_id
      end

      result = 'WITH "comments" AS'
      result << ' (SELECT MAX(id) AS comment_id, "comments"."user_id" FROM "comments")'
      result << ' SELECT "users".*, "comments"."comment_id" FROM "users"'
      result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
      expect(subject.with(:comments).arel.to_sql).to eql(result)
    end

    it 'accepts complex string as attributes' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.all
        cte.attributes sql('ROW_NUMBER() OVER (PARTITION BY ORDER BY "comments"."id")') => :comment_id
      end

      result = 'WITH "comments" AS'
      result << ' (SELECT ROW_NUMBER() OVER (PARTITION BY ORDER BY "comments"."id") AS comment_id, "comments"."user_id" FROM "comments")'
      result << ' SELECT "users".*, "comments"."comment_id" FROM "users"'
      result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
      expect(subject.with(:comments).arel.to_sql).to eql(result)
    end

    it 'accepts arel attribute as attributes' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.all
        cte.attributes col(:id).minimum => :comment_id
      end

      result = 'WITH "comments" AS'
      result << ' (SELECT MIN("comments"."id") AS comment_id, "comments"."user_id" FROM "comments")'
      result << ' SELECT "users".*, "comments"."comment_id" FROM "users"'
      result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
      expect(subject.with(:comments).arel.to_sql).to eql(result)
    end

    it 'accepts custom join properties' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.all
        cte.attributes content: :comment_content
        cte.join name: :id, 'a.col' => :col
      end

      result = 'WITH "comments" AS (SELECT "comments"."content" AS comment_content,'
      result << ' "comments"."id", "comments"."col" FROM "comments") SELECT "users".*,'
      result << ' "comments"."comment_content" FROM "users" INNER JOIN "comments"'
      result << ' ON "comments"."id" = "users"."name" AND "comments"."col" = "a"."col"'
      expect(subject.with(:comments).arel.to_sql).to eql(result)
    end

    it 'can perform other types of joins' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.all
        cte.attributes content: :comment_content
        cte.join_type :left
      end

      result = 'WITH "comments" AS (SELECT "comments"."content" AS comment_content,'
      result << ' "comments"."user_id" FROM "comments") SELECT "users".*,'
      result << ' "comments"."comment_content" FROM "users" LEFT OUTER JOIN "comments"'
      result << ' ON "comments"."user_id" = "users"."id"'
      expect(subject.with(:comments).arel.to_sql).to eql(result)
    end

    it 'can manually define the association' do
      klass.has_many :sample_comment, class_name: 'Comment', foreign_key: :a_user_id
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.all
        cte.through :sample_comment
        cte.attributes content: :sample_content
      end

      result = 'WITH "comments" AS'
      result << ' (SELECT "comments"."content" AS sample_content, "comments"."a_user_id" FROM "comments")'
      result << ' SELECT "users".*, "comments"."sample_content" FROM "users"'
      result << ' INNER JOIN "comments" ON "comments"."a_user_id" = "users"."id"'
      expect(subject.with(:comments).arel.to_sql).to eql(result)
    end

    it 'accepts complex scopes from dependencies' do
      klass.send(:auxiliary_statement, :comments1) do |cte|
        cte.query Comment.where(id: 1).all
        cte.attributes content: :comment_content1
      end

      klass.send(:auxiliary_statement, :comments2) do |cte|
        cte.requires :comments1
        cte.query Comment.where(id: 2).all
        cte.attributes content: :comment_content2
      end

      query = subject.where(id: 3).with(:comments2)

      result = 'WITH '
      result << '"comments1" AS (SELECT "comments"."content" AS comment_content1, "comments"."user_id" FROM "comments" WHERE "comments"."id" = $1), '
      result << '"comments2" AS (SELECT "comments"."content" AS comment_content2, "comments"."user_id" FROM "comments" WHERE "comments"."id" = $2)'
      result << ' SELECT "users".*, "comments1"."comment_content1", "comments2"."comment_content2" FROM "users"'
      result << ' INNER JOIN "comments1" ON "comments1"."user_id" = "users"."id"'
      result << ' INNER JOIN "comments2" ON "comments2"."user_id" = "users"."id"'
      result << ' WHERE "users"."id" = $3'

      expect(query.arel.to_sql).to eql(result)
      expect(query.send(:bound_attributes).map(&:value_before_type_cast)).to eql([1, 2, 3])
    end

    context 'with dependency' do
      before :each do
        klass.send(:auxiliary_statement, :comments1) do |cte|
          cte.query Comment.all
          cte.attributes content: :comment_content1
        end

        klass.send(:auxiliary_statement, :comments2) do |cte|
          cte.requires :comments1
          cte.query Comment.all
          cte.attributes content: :comment_content2
        end
      end

      it 'can requires another statement as dependency' do
        result = 'WITH '
        result << '"comments1" AS (SELECT "comments"."content" AS comment_content1, "comments"."user_id" FROM "comments"), '
        result << '"comments2" AS (SELECT "comments"."content" AS comment_content2, "comments"."user_id" FROM "comments")'
        result << ' SELECT "users".*, "comments1"."comment_content1", "comments2"."comment_content2" FROM "users"'
        result << ' INNER JOIN "comments1" ON "comments1"."user_id" = "users"."id"'
        result << ' INNER JOIN "comments2" ON "comments2"."user_id" = "users"."id"'
        expect(subject.with(:comments2).arel.to_sql).to eql(result)
      end

      it 'can uses already already set dependent' do
        result = 'WITH '
        result << '"comments1" AS (SELECT "comments"."content" AS comment_content1, "comments"."user_id" FROM "comments"), '
        result << '"comments2" AS (SELECT "comments"."content" AS comment_content2, "comments"."user_id" FROM "comments")'
        result << ' SELECT "users".*, "comments1"."comment_content1", "comments2"."comment_content2" FROM "users"'
        result << ' INNER JOIN "comments1" ON "comments1"."user_id" = "users"."id"'
        result << ' INNER JOIN "comments2" ON "comments2"."user_id" = "users"."id"'
        expect(subject.with(:comments1, :comments2).arel.to_sql).to eql(result)
      end

      it 'raises an error if the dependent does not exist' do
        klass.send(:auxiliary_statement, :comments2) do |cte|
          cte.requires :comments3
          cte.query Comment.all
          cte.attributes content: :comment_content2
        end
        expect{ subject.with(:comments2).arel.to_sql }.to raise_error(ArgumentError)
      end
    end

    context 'query as string' do
      it 'performs correctly' do
        klass.send(:auxiliary_statement, :comments) do |cte|
          cte.query :comments, 'SELECT * FROM comments'
          cte.attributes content: :comment
          cte.join id: :user_id
        end

        result = 'WITH "comments" AS (SELECT * FROM comments)'
        result << ' SELECT "users".*, "comments"."comment" FROM "users"'
        result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
        expect(subject.with(:comments).arel.to_sql).to eql(result)
      end

      it 'accepts arguments to format the query' do
        klass.send(:auxiliary_statement, :comments) do |cte|
          cte.query :comments, 'SELECT * FROM comments WHERE active = %{active}'
          cte.attributes content: :comment
          cte.join id: :user_id
        end

        result = "WITH \"comments\" AS (SELECT * FROM comments WHERE active = #{true_value})"
        result << ' SELECT "users".*, "comments"."comment" FROM "users"'
        result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
        expect(subject.with(:comments, args: {active: true}).arel.to_sql).to eql(result)
      end

      it 'raises an error when join columns are not given' do
        klass.send(:auxiliary_statement, :comments) do |cte|
          cte.query :comments, 'SELECT * FROM comments'
          cte.attributes content: :comment
        end

        expect{ subject.with(:comments).arel.to_sql }.to raise_error(ArgumentError, /join columns/)
      end

      it 'not raises an error when not given the table name as first argument' do
        klass.send(:auxiliary_statement, :comments) do |cte|
          cte.query 'SELECT * FROM comments'
          cte.attributes content: :comment
          cte.join id: :user_id
        end

        expect{ subject.with(:comments).arel.to_sql }.not_to raise_error
      end
    end

    context 'query as proc' do
      it 'performs correctly for result as relation' do
        klass.send(:auxiliary_statement, :comments) do |cte|
          cte.query :comments, -> { Comment.all }
          cte.attributes content: :comment
          cte.join id: :user_id
        end

        result = 'WITH "comments" AS'
        result << ' (SELECT "comments"."content" AS comment, "comments"."user_id" FROM "comments")'
        result << ' SELECT "users".*, "comments"."comment" FROM "users"'
        result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
        expect(subject.with(:comments).arel.to_sql).to eql(result)
      end

      it 'performs correctly for anything that has a call method' do
        obj = Struct.new(:call, :arity).new('SELECT * FROM comments', 0)
        klass.send(:auxiliary_statement, :comments) do |cte|
          cte.query :comments, obj
          cte.attributes content: :comment
          cte.join id: :user_id
        end

        result = 'WITH "comments" AS (SELECT * FROM comments)'
        result << ' SELECT "users".*, "comments"."comment" FROM "users"'
        result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
        expect(subject.with(:comments).arel.to_sql).to eql(result)
      end

      it 'performs correctly for result as string' do
        klass.send(:auxiliary_statement, :comments) do |cte|
          cte.query :comments, -> { 'SELECT * FROM comments' }
          cte.attributes content: :comment
          cte.join id: :user_id
        end

        result = 'WITH "comments" AS (SELECT * FROM comments)'
        result << ' SELECT "users".*, "comments"."comment" FROM "users"'
        result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
        expect(subject.with(:comments).arel.to_sql).to eql(result)
      end

      it 'performs correctly when the proc requires arguments' do
        klass.send(:auxiliary_statement, :comments) do |cte|
          cte.query :comments, -> (args) { Comment.where(id: args.id) }
          cte.attributes content: :comment
          cte.join id: :user_id
        end

        query = subject.with(:comments, args: {id: 1})

        result = 'WITH "comments" AS'
        result << ' (SELECT "comments"."content" AS comment, "comments"."user_id"'
        result << ' FROM "comments" WHERE "comments"."id" = $1)'
        result << ' SELECT "users".*, "comments"."comment" FROM "users"'
        result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'

        expect(query.arel.to_sql).to eql(result)
        expect(query.send(:bound_attributes).map(&:value_before_type_cast)).to eql([1])
      end

      it 'raises an error when join columns are not given' do
        klass.send(:auxiliary_statement, :comments) do |cte|
          cte.query :comments, -> { Author.all }
          cte.attributes content: :comment
        end

        expect{ subject.with(:comments).arel.to_sql }.to raise_error(ArgumentError, /join columns/)
      end

      it 'not raises an error when not given the table name as first argument' do
        klass.send(:auxiliary_statement, :comments) do |cte|
          cte.query -> { Comment.all }
          cte.attributes content: :comment
          cte.join id: :user_id
        end

        expect{ subject.with(:comments).arel.to_sql }.not_to raise_error
      end

      it 'raises an error when the result of the proc is an invalid type' do
        klass.send(:auxiliary_statement, :comments) do |cte|
          cte.query :comments, -> { false }
          cte.attributes content: :comment
          cte.join id: :user_id
        end

        expect{ subject.with(:comments).arel.to_sql }.to raise_error(ArgumentError, /query objects/)
      end
    end

    context 'with inheritance' do
      let(:base) { Activity }
      let(:klass) { ActivityBook }

      it 'accepts ancestors auxiliary statements' do
        base.send(:auxiliary_statement, :authors) do |cte|
          cte.query Author.all
          cte.attributes name: :author_name
          cte.join author_id: :id
        end

        result = 'WITH "authors" AS'
        result << ' (SELECT "authors"."name" AS author_name, "authors"."id" FROM "authors")'
        result << ' SELECT "activity_books".*, "authors"."author_name" FROM "activity_books"'
        result << ' INNER JOIN "authors" ON "authors"."id" = "activity_books"."author_id"'
        expect(subject.with(:authors).arel.to_sql).to eql(result)
      end

      it 'can replace ancestors auxiliary statements' do
        base.send(:auxiliary_statement, :authors) do |cte|
          cte.query Author.all
          cte.attributes name: :author_name
          cte.join author_id: :id
        end

        klass.send(:auxiliary_statement, :authors) do |cte|
          cte.query Author.all
          cte.attributes type: :author_type
          cte.join author_id: :id
        end

        result = 'WITH "authors" AS'
        result << ' (SELECT "authors"."type" AS author_type, "authors"."id" FROM "authors")'
        result << ' SELECT "activity_books".*, "authors"."author_type" FROM "activity_books"'
        result << ' INNER JOIN "authors" ON "authors"."id" = "activity_books"."author_id"'
        expect(subject.with(:authors).arel.to_sql).to eql(result)
      end

      it 'raises an error when no class has the auxiliary statement' do
        expect{ subject.with(:comments).arel.to_sql }.to raise_error(ArgumentError)
      end
    end

    context 'recursive' do
      let(:klass) { Course }

      it 'correctly build a recursive cte' do
        klass.send(:recursive_auxiliary_statement, :all_categories) do |cte|
          cte.query Category.all
          cte.join id: :parent_id
        end

        result = 'WITH RECURSIVE "all_categories" AS ('
        result << ' SELECT "categories"."id", "categories"."parent_id"'
        result << ' FROM "categories"'
        result << ' WHERE "categories"."parent_id" IS NULL'
        result << ' UNION'
        result << ' SELECT "categories"."id", "categories"."parent_id"'
        result << ' FROM "categories", "all_categories"'
        result << ' WHERE "categories"."parent_id" = "all_categories"."id"'
        result << ' ) SELECT "courses".* FROM "courses" INNER JOIN "all_categories"'
        result << ' ON "all_categories"."parent_id" = "courses"."id"'
        expect(subject.with(:all_categories).arel.to_sql).to eql(result)
      end

      it 'allows connect to be set to something different using a single value' do
        klass.send(:recursive_auxiliary_statement, :all_categories) do |cte|
          cte.query Category.all
          cte.join id: :parent_id
          cte.connect :name
        end

        result = 'WITH RECURSIVE "all_categories" AS ('
        result << ' SELECT "categories"."name", "categories"."parent_id"'
        result << ' FROM "categories"'
        result << ' WHERE "categories"."parent_name" IS NULL'
        result << ' UNION'
        result << ' SELECT "categories"."name", "categories"."parent_id"'
        result << ' FROM "categories", "all_categories"'
        result << ' WHERE "categories"."parent_name" = "all_categories"."name"'
        result << ' ) SELECT "courses".* FROM "courses" INNER JOIN "all_categories"'
        result << ' ON "all_categories"."parent_id" = "courses"."id"'
        expect(subject.with(:all_categories).arel.to_sql).to eql(result)
      end

      it 'allows a complete different set of connect' do
        klass.send(:recursive_auxiliary_statement, :all_categories) do |cte|
          cte.query Category.all
          cte.join id: :parent_id
          cte.connect left: :right
        end

        result = 'WITH RECURSIVE "all_categories" AS ('
        result << ' SELECT "categories"."left", "categories"."parent_id"'
        result << ' FROM "categories"'
        result << ' WHERE "categories"."right" IS NULL'
        result << ' UNION'
        result << ' SELECT "categories"."left", "categories"."parent_id"'
        result << ' FROM "categories", "all_categories"'
        result << ' WHERE "categories"."right" = "all_categories"."left"'
        result << ' ) SELECT "courses".* FROM "courses" INNER JOIN "all_categories"'
        result << ' ON "all_categories"."parent_id" = "courses"."id"'
        expect(subject.with(:all_categories).arel.to_sql).to eql(result)
      end

      it 'allows using an union all' do
        klass.send(:recursive_auxiliary_statement, :all_categories) do |cte|
          cte.query Category.all
          cte.join id: :parent_id
          cte.union_all!
        end

        result = 'WITH RECURSIVE "all_categories" AS ('
        result << ' SELECT "categories"."id", "categories"."parent_id"'
        result << ' FROM "categories"'
        result << ' WHERE "categories"."parent_id" IS NULL'
        result << ' UNION ALL'
        result << ' SELECT "categories"."id", "categories"."parent_id"'
        result << ' FROM "categories", "all_categories"'
        result << ' WHERE "categories"."parent_id" = "all_categories"."id"'
        result << ' ) SELECT "courses".* FROM "courses" INNER JOIN "all_categories"'
        result << ' ON "all_categories"."parent_id" = "courses"."id"'
        expect(subject.with(:all_categories).arel.to_sql).to eql(result)
      end

      it 'allows having a complete different initiator' do
        klass.send(:recursive_auxiliary_statement, :all_categories) do |cte|
          cte.query Category.where(parent_id: 5)
          cte.join id: :parent_id
        end

        result = 'WITH RECURSIVE "all_categories" AS ('
        result << ' SELECT "categories"."id", "categories"."parent_id"'
        result << ' FROM "categories"'
        result << ' WHERE "categories"."parent_id" = $1'
        result << ' UNION'
        result << ' SELECT "categories"."id", "categories"."parent_id"'
        result << ' FROM "categories", "all_categories"'
        result << ' WHERE "categories"."parent_id" = "all_categories"."id"'
        result << ' ) SELECT "courses".* FROM "courses" INNER JOIN "all_categories"'
        result << ' ON "all_categories"."parent_id" = "courses"."id"'
        expect(subject.with(:all_categories).arel.to_sql).to eql(result)
      end

      it 'can process the depth of the query' do
        klass.send(:recursive_auxiliary_statement, :all_categories) do |cte|
          cte.query Category.all
          cte.join id: :parent_id
          cte.with_depth
        end

        result = 'WITH RECURSIVE "all_categories" AS ('
        result << ' SELECT "categories"."id", "categories"."parent_id", 0 AS depth'
        result << ' FROM "categories"'
        result << ' WHERE "categories"."parent_id" IS NULL'
        result << ' UNION'
        result << ' SELECT "categories"."id", "categories"."parent_id", ("all_categories"."depth" + 1) AS depth'
        result << ' FROM "categories", "all_categories"'
        result << ' WHERE "categories"."parent_id" = "all_categories"."id"'
        result << ' ) SELECT "courses".* FROM "courses" INNER JOIN "all_categories"'
        result << ' ON "all_categories"."parent_id" = "courses"."id"'
        expect(subject.with(:all_categories).arel.to_sql).to eql(result)
      end

      it 'can process and expose the depth of the query' do
        klass.send(:recursive_auxiliary_statement, :all_categories) do |cte|
          cte.query Category.all
          cte.join id: :parent_id
          cte.with_depth 'd', start: 10, as: :category_depth
        end

        result = 'WITH RECURSIVE "all_categories" AS ('
        result << ' SELECT "categories"."id", "categories"."parent_id", 10 AS d'
        result << ' FROM "categories"'
        result << ' WHERE "categories"."parent_id" IS NULL'
        result << ' UNION'
        result << ' SELECT "categories"."id", "categories"."parent_id", ("all_categories"."d" + 1) AS d'
        result << ' FROM "categories", "all_categories"'
        result << ' WHERE "categories"."parent_id" = "all_categories"."id"'
        result << ' ) SELECT "courses".*, "all_categories"."d" AS category_depth FROM "courses" INNER JOIN "all_categories"'
        result << ' ON "all_categories"."parent_id" = "courses"."id"'
        expect(subject.with(:all_categories).arel.to_sql).to eql(result)
      end

      it 'can process the path of the query' do
        klass.send(:recursive_auxiliary_statement, :all_categories) do |cte|
          cte.query Category.all
          cte.join id: :parent_id
          cte.with_path
        end

        result = 'WITH RECURSIVE "all_categories" AS ('
        result << ' SELECT "categories"."id", "categories"."parent_id", ARRAY["categories"."id"]::varchar[] AS path'
        result << ' FROM "categories"'
        result << ' WHERE "categories"."parent_id" IS NULL'
        result << ' UNION'
        result << ' SELECT "categories"."id", "categories"."parent_id", ARRAY_APPEND("all_categories"."path", "categories"."id"::varchar) AS path'
        result << ' FROM "categories", "all_categories"'
        result << ' WHERE "categories"."parent_id" = "all_categories"."id"'
        result << ' ) SELECT "courses".* FROM "courses" INNER JOIN "all_categories"'
        result << ' ON "all_categories"."parent_id" = "courses"."id"'
        expect(subject.with(:all_categories).arel.to_sql).to eql(result)
      end

      it 'can process and expose the path of the query' do
        klass.send(:recursive_auxiliary_statement, :all_categories) do |cte|
          cte.query Category.all
          cte.join id: :parent_id
          cte.with_path 'p', source: :name, as: :category_path
        end

        result = 'WITH RECURSIVE "all_categories" AS ('
        result << ' SELECT "categories"."id", "categories"."parent_id", ARRAY["categories"."name"]::varchar[] AS p'
        result << ' FROM "categories"'
        result << ' WHERE "categories"."parent_id" IS NULL'
        result << ' UNION'
        result << ' SELECT "categories"."id", "categories"."parent_id", ARRAY_APPEND("all_categories"."p", "categories"."name"::varchar) AS p'
        result << ' FROM "categories", "all_categories"'
        result << ' WHERE "categories"."parent_id" = "all_categories"."id"'
        result << ' ) SELECT "courses".*, "all_categories"."p" AS category_path FROM "courses" INNER JOIN "all_categories"'
        result << ' ON "all_categories"."parent_id" = "courses"."id"'
        expect(subject.with(:all_categories).arel.to_sql).to eql(result)
      end

      it 'works with string queries' do
        klass.send(:recursive_auxiliary_statement, :all_categories) do |cte|
          cte.query 'SELECT * FROM categories WHERE a IS NULL'
          cte.sub_query 'SELECT * FROM categories, all_categories WHERE all_categories.a = b'
          cte.join id: :parent_id
        end

        result = 'WITH RECURSIVE "all_categories" AS ('
        result << 'SELECT * FROM categories WHERE a IS NULL'
        result << ' UNION '
        result << ' SELECT * FROM categories, all_categories WHERE all_categories.a = b'
        result << ') SELECT "courses".* FROM "courses" INNER JOIN "all_categories"'
        result << ' ON "all_categories"."parent_id" = "courses"."id"'
        expect(subject.with(:all_categories).arel.to_sql).to eql(result)
      end

      it 'raises an error when query is a string and there is no sub query' do
        klass.send(:recursive_auxiliary_statement, :all_categories) do |cte|
          cte.query 'SELECT * FROM categories WHERE a IS NULL'
          cte.join id: :parent_id
        end

        expect{ subject.with(:all_categories).arel.to_sql }.to raise_error(ArgumentError, /generate sub query/)
      end

      it 'raises an error when sub query has an invalid type' do
        klass.send(:recursive_auxiliary_statement, :all_categories) do |cte|
          cte.query 'SELECT * FROM categories WHERE a IS NULL'
          cte.sub_query -> { 1 }
          cte.join id: :parent_id
        end

        expect{ subject.with(:all_categories).arel.to_sql }.to raise_error(ArgumentError, /query and sub query objects/)
      end

      it 'raises an error when connect can be resolved automatically' do
        allow(klass).to receive(:primary_key).and_return(nil)
        klass.send(:recursive_auxiliary_statement, :all_categories) do |cte|
          cte.query Category.all
          cte.join id: :parent_id
        end

        expect{ subject.with(:all_categories).arel.to_sql }.to raise_error(ArgumentError, /setting up a proper way to connect/)
      end
    end

    it 'works with count and does not add extra columns' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.all
        cte.attributes content: :comment_content
      end

      result = 'WITH "comments" AS'
      result << ' (SELECT "comments"."content" AS comment_content, "comments"."user_id" FROM "comments")'
      result << ' SELECT COUNT(*) FROM "users"'
      result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'

      query = get_last_executed_query { subject.with(:comments).count }
      expect(query).to eql(result)
    end

    it 'works with sum and does not add extra columns' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.all
        cte.attributes id: :value
      end

      result = 'WITH "comments" AS'
      result << ' (SELECT "comments"."id" AS value, "comments"."user_id" FROM "comments")'
      result << ' SELECT SUM("comments"."value") FROM "users"'
      result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'

      query = get_last_executed_query { subject.with(:comments).sum(comments: :value) }
      expect(query).to eql(result)
    end

    it 'raises an error when using an invalid type of object as query' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query :string, String
      end

      expect{ subject.with(:comments).arel.to_sql }.to raise_error(ArgumentError, /object types/)
    end

    it 'raises an error when trying to use a statement that is not defined' do
      expect{ subject.with(:does_not_exist).arel.to_sql }.to raise_error(ArgumentError)
    end

    it 'raises an error when using an invalid type of join' do
      klass.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.all
        cte.attributes content: :comment_content
        cte.join_type :invalid
      end

      expect{ subject.with(:comments).arel.to_sql }.to raise_error(ArgumentError)
    end
  end

  context 'on model' do
    subject { User }

    it 'has its configurator' do
      expect(subject.protected_methods).to include(:cte)
      expect(subject.protected_methods).to include(:auxiliary_statement)
    end

    it 'has the recursive configuration' do
      expect(subject.protected_methods).to include(:recursive_cte)
      expect(subject.protected_methods).to include(:recursive_auxiliary_statement)
    end

    it 'allows configure new auxiliary statements' do
      subject.send(:auxiliary_statement, :cte1)
      expect(subject.auxiliary_statements_list).to include(:cte1)
      expect(subject.const_defined?('Cte1_AuxiliaryStatement')).to be_truthy
    end

    it 'has its query method' do
      expect(subject).to respond_to(:with)
    end

    it 'returns a relation when using the method' do
      subject.send(:auxiliary_statement, :comments) do |cte|
        cte.query Comment.all
        cte.attributes content: :comment_content
      end
      expect(subject.with(:comments)).to be_a(ActiveRecord::Relation)
    end
  end

  context 'on external' do
    let(:klass) { Torque::PostgreSQL::AuxiliaryStatement }
    subject { User }

    it 'has the external method available' do
      expect(klass).to respond_to(:create)
    end

    it 'accepts simple auxiliary statement definition' do
      sample = klass.create(Comment.all)
      query = subject.with(sample, select: {content: :comment_content}).arel.to_sql

      result = 'WITH "comment" AS'
      result << ' (SELECT "comments"."content" AS comment_content, "comments"."user_id" FROM "comments")'
      result << ' SELECT "users".*, "comment"."comment_content" FROM "users"'
      result << ' INNER JOIN "comment" ON "comment"."user_id" = "users"."id"'
      expect(query).to eql(result)
    end

    it 'accepts a hash auxiliary statement definition' do
      sample = klass.create(query: Comment.all, select: {content: :comment_content})
      query = subject.with(sample).arel.to_sql

      result = 'WITH "comment" AS'
      result << ' (SELECT "comments"."content" AS comment_content, "comments"."user_id" FROM "comments")'
      result << ' SELECT "users".*, "comment"."comment_content" FROM "users"'
      result << ' INNER JOIN "comment" ON "comment"."user_id" = "users"."id"'
      expect(query).to eql(result)
    end

    it 'accepts a block when creating the auxiliary statement' do
      sample = klass.create(:all_comments) do |cte|
        cte.query Comment.all
        cte.select content: :comment_content
      end

      result = 'WITH "all_comments" AS'
      result << ' (SELECT "comments"."content" AS comment_content, "comments"."user_id" FROM "comments")'
      result << ' SELECT "users".*, "all_comments"."comment_content" FROM "users"'
      result << ' INNER JOIN "all_comments" ON "all_comments"."user_id" = "users"."id"'

      query = subject.with(sample).arel.to_sql
      expect(query).to eql(result)
    end

    context 'recursive' do
      let(:klass) { Torque::PostgreSQL::AuxiliaryStatement::Recursive }
      subject { Course }

      it 'has the external method available' do
        expect(klass).to respond_to(:create)
      end

      it 'accepts simple recursive auxiliary statement definition' do
        settings = { join: { id: :parent_id } }
        query = subject.with(klass.create(Category.all), **settings).arel.to_sql

        result = 'WITH RECURSIVE "category" AS ('
        result << ' SELECT "categories"."id", "categories"."parent_id"'
        result << ' FROM "categories"'
        result << ' WHERE "categories"."parent_id" IS NULL'
        result << ' UNION'
        result << ' SELECT "categories"."id", "categories"."parent_id"'
        result << ' FROM "categories", "category"'
        result << ' WHERE "categories"."parent_id" = "category"."id"'
        result << ' ) SELECT "courses".* FROM "courses" INNER JOIN "category"'
        result << ' ON "category"."parent_id" = "courses"."id"'
        expect(query).to eql(result)
      end

      it 'accepts a connect option' do
        settings = { join: { id: :parent_id }, connect: { a: :b } }
        query = subject.with(klass.create(Category.all), **settings).arel.to_sql

        result = 'WITH RECURSIVE "category" AS ('
        result << ' SELECT "categories"."a", "categories"."parent_id"'
        result << ' FROM "categories"'
        result << ' WHERE "categories"."b" IS NULL'
        result << ' UNION'
        result << ' SELECT "categories"."a", "categories"."parent_id"'
        result << ' FROM "categories", "category"'
        result << ' WHERE "categories"."b" = "category"."a"'
        result << ' ) SELECT "courses".* FROM "courses" INNER JOIN "category"'
        result << ' ON "category"."parent_id" = "courses"."id"'
        expect(query).to eql(result)
      end

      it 'accepts an union all option' do
        settings = { join: { id: :parent_id }, union_all: true }
        query = subject.with(klass.create(Category.all), **settings).arel.to_sql

        result = 'WITH RECURSIVE "category" AS ('
        result << ' SELECT "categories"."id", "categories"."parent_id"'
        result << ' FROM "categories"'
        result << ' WHERE "categories"."parent_id" IS NULL'
        result << ' UNION ALL'
        result << ' SELECT "categories"."id", "categories"."parent_id"'
        result << ' FROM "categories", "category"'
        result << ' WHERE "categories"."parent_id" = "category"."id"'
        result << ' ) SELECT "courses".* FROM "courses" INNER JOIN "category"'
        result << ' ON "category"."parent_id" = "courses"."id"'
        expect(query).to eql(result)
      end

      it 'accepts a sub query option' do
        settings = { join: { id: :parent_id }, sub_query: Category.where(active: true) }
        query = subject.with(klass.create(Category.all), **settings).arel.to_sql

        result = 'WITH RECURSIVE "category" AS ('
        result << ' SELECT "categories"."id", "categories"."parent_id" FROM "categories"'
        result << ' UNION'
        result << ' SELECT "categories"."id", "categories"."parent_id" FROM "categories", "category" WHERE "categories"."active" = $1'
        result << ' ) SELECT "courses".* FROM "courses" INNER JOIN "category"'
        result << ' ON "category"."parent_id" = "courses"."id"'
        expect(query).to eql(result)
      end

      it 'accepts a depth option' do
        settings = { join: { id: :parent_id }, with_depth: { name: 'a', start: 5, as: 'b' } }
        query = subject.with(klass.create(Category.all), **settings).arel.to_sql

        result = 'WITH RECURSIVE "category" AS ('
        result << ' SELECT "categories"."id", "categories"."parent_id", 5 AS a'
        result << ' FROM "categories"'
        result << ' WHERE "categories"."parent_id" IS NULL'
        result << ' UNION'
        result << ' SELECT "categories"."id", "categories"."parent_id", ("category"."a" + 1) AS a'
        result << ' FROM "categories", "category"'
        result << ' WHERE "categories"."parent_id" = "category"."id"'
        result << ' ) SELECT "courses".*, "category"."a" AS b FROM "courses" INNER JOIN "category"'
        result << ' ON "category"."parent_id" = "courses"."id"'
        expect(query).to eql(result)
      end

      it 'accepts a path option' do
        settings = { join: { id: :parent_id }, with_path: { name: 'a', source: 'b', as: 'c' } }
        query = subject.with(klass.create(Category.all), **settings).arel.to_sql

        result = 'WITH RECURSIVE "category" AS ('
        result << ' SELECT "categories"."id", "categories"."parent_id", ARRAY["categories"."b"]::varchar[] AS a'
        result << ' FROM "categories"'
        result << ' WHERE "categories"."parent_id" IS NULL'
        result << ' UNION'
        result << ' SELECT "categories"."id", "categories"."parent_id", ARRAY_APPEND("category"."a", "categories"."b"::varchar) AS a'
        result << ' FROM "categories", "category"'
        result << ' WHERE "categories"."parent_id" = "category"."id"'
        result << ' ) SELECT "courses".*, "category"."a" AS c FROM "courses" INNER JOIN "category"'
        result << ' ON "category"."parent_id" = "courses"."id"'
        expect(query).to eql(result)
      end
    end
  end

  context 'on settings' do
    let(:base) { User }
    let(:statement_klass) do
      base.send(:auxiliary_statement, :statement)
      base::Statement_AuxiliaryStatement
    end

    subject do
      Torque::PostgreSQL::AuxiliaryStatement::Settings.new(base, statement_klass)
    end

    it 'has access to base' do
      expect(subject.base).to eql(User)
      expect(subject.base_table).to be_a(Arel::Table)
    end

    it 'has access to statement table' do
      expect(subject.table_name).to eql('statement')
      expect(subject.table).to be_a(Arel::Table)
    end

    it 'has access to the query arel table' do
      subject.query Comment.all
      expect(subject.query_table).to be_a(Arel::Table)
    end

    it 'raises an error when trying to access query table before defining the query' do
      expect{ subject.with(:comments).arel.to_sql }.to raise_error(StandardError)
    end
  end
end
