require 'minitest_helper'

describe 'Collection' do

  describe 'Specification' do

    it 'Implicit' do
      Users.collection_name.must_equal :users
      Users.model.must_equal User
      Users.primary_key.must_equal :id
      Users.foreign_key.must_equal :user_id
    end

    it 'Explicit' do
      People.collection_name.must_equal :people
      People.model.must_equal Person
      People.primary_key.must_equal :document_number
      People.foreign_key.must_equal :document_number
    end

    it 'Lazy model name' do
      collection_class = Class.new(Rasti::DB::Collection) do
        set_model :User
      end

      collection_class.model.must_equal User
    end

  end

  describe 'Insert, Update and Delete' do

    it 'Insert' do
      id = users.insert name: 'User 1'

      db[:users][id: id][:name].must_equal 'User 1'
    end

    it 'Insert with many to many' do
      user_id = db[:users].insert name: 'User 1'

      1.upto(2) do |i| 
        db[:posts].insert user_id: user_id, title: "Post #{i}", body: '...'
        db[:categories].insert name: "Category #{i}"
      end

      post_id = posts.insert user_id: user_id, title: 'Post title', body: '...', categories: [1,2]
      category_id = categories.insert name: 'Category', posts: [1,2]

      db[:categories_posts].where(post_id: post_id).map(:category_id).must_equal [1,2]
      db[:categories_posts].where(category_id: category_id).map(:post_id).must_equal [1,2]
    end

    it 'Insert only many to many' do
      1.upto(3) do |i| 
        db[:categories].insert name: "Category #{i}"
      end

      user_id = db[:users].insert name: 'User 1'
      post_id = db[:posts].insert user_id: user_id, title: 'Post title', body: '...'
      1.upto(2) { |category_id| db[:categories_posts].insert post_id: post_id, category_id: category_id }

      posts.insert_relations post_id, categories: [3]

      db[:categories_posts].where(post_id: post_id).map(:category_id).must_equal [1,2,3]
    end

    it 'Bulk insert' do
      users_attrs = 1.upto(2).map { |i| {name: "User #{i}"} }

      ids = users.bulk_insert users_attrs, return: :primary_key

      ids.must_equal [1,2]
      db[:users][id: 1][:name].must_equal 'User 1'
      db[:users][id: 2][:name].must_equal 'User 2'
    end

    it 'Update' do
      id = db[:users].insert name: 'User 1'

      db[:users][id: id][:name].must_equal 'User 1'

      users.update id, name: 'updated'

      db[:users][id: id][:name].must_equal 'updated'
    end

    it 'Update with many to many' do
      user_id = db[:users].insert name: 'User 1'

      1.upto(3) do |i| 
        db[:posts].insert user_id: user_id, title: "Post #{i}", body: '...'
        db[:categories].insert name: "Category #{i}"
      end

      db[:categories_posts].insert post_id: 1, category_id: 1
      db[:categories_posts].insert post_id: 1, category_id: 2
      db[:categories_posts].insert post_id: 2, category_id: 2

      db[:categories_posts].where(post_id: 1).map(:category_id).must_equal [1,2]

      posts.update 1, categories: [2,3]

      db[:categories_posts].where(post_id: 1).map(:category_id).must_equal [2,3]
      
      db[:categories_posts].where(category_id: 2).map(:post_id).must_equal [1,2]

      categories.update 2, posts: [2,3]
      
      db[:categories_posts].where(category_id: 2).map(:post_id).must_equal [2,3]
    end

    it 'Bulk update' do
      user_id = db[:users].insert name: 'User 1'
      1.upto(3) { |i| db[:posts].insert user_id: user_id, title: "Post #{i}", body: '...' }

      posts.bulk_update(body: 'Updated ...') { where id: [1,2] }

      db[:posts][id: 1][:body].must_equal 'Updated ...'
      db[:posts][id: 2][:body].must_equal 'Updated ...'
      db[:posts][id: 3][:body].must_equal '...'
    end

    it 'Delete' do
      id = db[:users].insert name: 'User 1'

      db[:users].count.must_equal 1

      users.delete id

      db[:users].count.must_equal 0
    end

    it 'Delete only many to many' do
      1.upto(3) do |i| 
        db[:categories].insert name: "Category #{i}"
      end

      user_id = db[:users].insert name: 'User 1'
      post_id = db[:posts].insert user_id: user_id, title: 'Post title', body: '...'
      1.upto(3) { |category_id| db[:categories_posts].insert post_id: post_id, category_id: category_id }

      posts.delete_relations post_id, categories: [3]

      db[:categories_posts].where(post_id: post_id).map(:category_id).must_equal [1,2]
    end

    it 'Bulk delete' do
      1.upto(3) { |i| db[:users].insert name: "User #{i}" }

      users.bulk_delete { where id: [1,2] }

      db[:users].map(:id).must_equal [3]
    end

    describe 'Delete cascade' do

      before :each do
        1.upto(3) do |i|
          user_id = db[:users].insert name: "User #{i}"

          db[:people].insert document_number: "document_#{i}", 
                             first_name: "John #{i}",
                             last_name: "Doe #{i}",
                             birth_date: Time.now - i,
                             user_id: user_id

          category_id = db[:categories].insert name: "Category #{i}" 
          
          1.upto(3) do |n|
            post_id = db[:posts].insert user_id: user_id, title: "Post #{i}.#{n}", body: '...'
            db[:categories_posts].insert post_id: post_id, category_id: category_id
          end
        end

        {1 => 4..6, 2 => 7..9, 3 => 1..3}.each do |user_id, post_ids|
          post_ids.each do |post_id|
            db[:comments].insert post_id: post_id, user_id: user_id, text: 'Comment'
          end
        end
      end

      it 'Self relations' do
        db[:posts].where(id: 1).count.must_equal 1
        db[:categories_posts].where(post_id: 1).count.must_equal 1
        db[:comments].where(post_id: 1).count.must_equal 1

        posts.delete_cascade 1

        db[:posts].where(id: 1).count.must_equal 0
        db[:categories_posts].where(post_id: 1).count.must_equal 0
        db[:comments].where(post_id: 1).count.must_equal 0

        db[:users].count.must_equal 3
        db[:categories].count.must_equal 3
        db[:posts].count.must_equal 8
        db[:categories_posts].count.must_equal 8
        db[:comments].count.must_equal 8
      end

      it 'Deep relations' do
        db[:users].where(id: 1).count.must_equal 1
        db[:people].where(user_id: 1).count.must_equal 1
        db[:comments].where(user_id: 1).count.must_equal 3
        db[:posts].where(user_id: 1).count.must_equal 3
        db[:comments].join(:posts, id: :post_id).where(Sequel[:posts][:user_id] => 1).count.must_equal 3
        db[:categories_posts].join(:posts, id: :post_id).where(Sequel[:posts][:user_id] => 1).count.must_equal 3

        users.delete_cascade 1

        db[:users].where(id: 1).count.must_equal 0
        db[:people].where(user_id: 1).count.must_equal 0
        db[:comments].where(user_id: 1).count.must_equal 0
        db[:posts].where(user_id: 1).count.must_equal 0
        db[:comments].join(:posts, id: :post_id).where(Sequel[:posts][:user_id] => 1).count.must_equal 0
        db[:categories_posts].join(:posts, id: :post_id).where(Sequel[:posts][:user_id] => 1).count.must_equal 0
        
        db[:users].count.must_equal 2
        db[:people].count.must_equal 2
        db[:categories].count.must_equal 3
        db[:posts].count.must_equal 6
        db[:categories_posts].count.must_equal 6
        db[:comments].count.must_equal 3
      end

    end

  end

  describe 'Queries' do

    it 'Find' do
      id = db[:users].insert name: 'User 1'
      
      users.find(id).must_equal User.new(id: id, name: 'User 1')
    end

    it 'Find graph' do
      user_id = db[:users].insert name: 'User 1'
      db[:posts].insert user_id: user_id, title: 'Post 1', body: '...'

      users.find_graph(user_id, :posts).must_equal User.new id: user_id, name: 'User 1', posts: posts.all
    end

    it 'Count' do
      1.upto(10) { |i| db[:users].insert name: "User #{i}" }

      users.count.must_equal 10
    end

    it 'All' do
      id = db[:users].insert name: 'User 1'

      users.all.must_equal [User.new(id: id, name: 'User 1')]
    end

    it 'First' do
      1.upto(10) { |i| db[:users].insert name: "User #{i}" }

      users.first.must_equal User.new(id: 1, name: 'User 1')
    end

    it 'Exists' do
      1.upto(10) { |i| db[:users].insert name: "User #{i}" }

      users.exists?(id: 1).must_equal true
      users.exists?(id: 0).must_equal false

      users.exists? { where id: 1 }.must_equal true
      users.exists? { where id: 0 }.must_equal false
    end

    it 'Detect' do
      1.upto(10) { |i| db[:users].insert name: "User #{i}" }

      users.detect(id: 1).must_equal User.new(id: 1, name: 'User 1')
      users.detect(id: 0).must_be_nil

      users.detect { where id: 1 }.must_equal User.new(id: 1, name: 'User 1')
      users.detect { where id: 0 }.must_be_nil
    end

    it 'Chained query' do
      1.upto(10) { |i| db[:users].insert name: "User #{i}" }

      models = users.where(id: [1,2]).reverse_order(:id).all

      models.must_equal [2,1].map { |i| User.new(id: i, name: "User #{i}") }
    end

    it 'Chain dataset as query' do
      1.upto(2) { |i| db[:users].insert name: "User #{i}" }
      1.upto(3) { |i| db[:posts].insert user_id: 1, title: "Post #{i}", body: '...' }
      1.upto(2) { |i| db[:comments].insert post_id: i, user_id: 2, text: 'Comment' }

      models = posts.commented_by(2).all
      models.must_equal [1,2].map { |i| Post.new(id: i, user_id: 1, title: "Post #{i}", body: '...') }
    end

    it 'Custom query' do
      1.upto(2) { |i| db[:users].insert name: "User #{i}" }
      1.upto(3) { |i| db[:posts].insert user_id: 1, title: "Post #{i}", body: '...' }
      1.upto(2) { |i| db[:comments].insert post_id: i, user_id: 2, text: 'Comment' }

      models = comments.posts_commented_by(2)
      models.must_equal [1,2].map { |i| Post.new(id: i, user_id: 1, title: "Post #{i}", body: '...') }
    end

    describe 'Named queries' do

      before do
        1.upto(2) do |i|
          db[:categories].insert name: "Category #{i}"
          db[:users].insert name: "User #{i}"
          db[:people].insert document_number: "document_#{i}", 
                             first_name: "John #{i}",
                             last_name: "Doe #{i}",
                             birth_date: Time.now - i,
                             user_id: i
        end

        1.upto(3) do |i| 
          db[:posts].insert user_id: 1, title: "Post #{i}", body: '...'
          db[:categories_posts].insert category_id: 1, post_id: i
        end
        
        4.upto(5) do |i| 
          db[:posts].insert user_id: 2, title: "Post #{i}", body: '...'
          db[:categories_posts].insert category_id: 2, post_id: i
        end
      end

      describe 'Relations' do

        it 'Many to Many' do
          posts.order(:id).with_categories(1).primary_keys.must_equal [1,2,3]
        end

        it 'One to Many' do
          users.with_posts([1,4]).primary_keys.must_equal [1,2]
        end

        it 'Many to One' do
          posts.with_users(2).primary_keys.must_equal [4,5]
        end

        it 'One to One' do
          users.with_people('document_1').primary_keys.must_equal [1]
        end

      end

      it 'Global' do
        result_1 = posts.created_by(1)
        result_1.primary_keys.must_equal [1,2,3]
        
        result_2 = posts.created_by(2)
        result_2.primary_keys.must_equal [4,5]
      end

      it 'Chained' do
        result = posts.created_by(2).entitled('Post 4')
        result.primary_keys.must_equal [4]
      end

    end

    it 'Graph' do
      1.upto(3) do |i|
        db[:users].insert name: "User #{i}"
        db[:people].insert document_number: "document_#{i}", 
                           first_name: "John #{i}",
                           last_name: "Doe #{i}",
                           birth_date: Time.now - i,
                           user_id: i
        db[:categories].insert name: "Category #{i}"
        db[:posts].insert user_id: i, title: "Post #{i}", body: '...'
        db[:categories_posts].insert post_id: i, category_id: i
      end

      db[:posts].map(:id).each do |post_id|
        db[:users].map(:id).each do |user_id|
          db[:comments].insert post_id: post_id, user_id: user_id, text: 'Comment'
        end
      end

      posts_graph = posts.where(id: 1).graph('user.person', :categories, 'comments.user.posts.categories').all

      posts_graph.count.must_equal 1

      posts_graph[0].user.id.must_equal 1
      posts_graph[0].user.person.must_equal people.detect(user_id: 1)

      posts_graph[0].categories.must_equal [categories.find(1)]

      posts_graph[0].comments.count.must_equal 3
      posts_graph[0].comments.each_with_index do |comment, index|
        i = index + 1

        comment.post_id.must_equal 1
        comment.user_id.must_equal i
        
        comment.user.id.must_equal i
        comment.user.name.must_equal "User #{i}"
        
        comment.user.posts.count.must_equal 1
        comment.user.posts[0].id.must_equal i
        comment.user.posts[0].title.must_equal "Post #{i}"
        comment.user.posts[0].user_id.must_equal i

        comment.user.posts[0].categories.count.must_equal 1
        comment.user.posts[0].categories[0].id.must_equal i
        comment.user.posts[0].categories[0].name.must_equal "Category #{i}"
      end
    end

  end

  describe 'Schemas' do

    let :stub_db do
      stubs = Proc.new do |sql|
        case sql

        when 'SELECT * FROM custom_schema.users', 
             'SELECT * FROM custom_schema.users WHERE (id IN (2, 1))'
          [
            {id: 1},
            {id: 2}
          ]

        when 'SELECT * FROM custom_schema.posts',
             'SELECT * FROM custom_schema.posts WHERE (user_id IN (1, 2))'
          [
            {id: 3, user_id: 1},
            {id: 4, user_id: 2}
          ]

        when 'SELECT * FROM custom_schema.comments WHERE (post_id IN (3, 4))'
          [
            {id: 5, user_id: 2, post_id: 3},
            {id: 6, user_id: 1, post_id: 3},
            {id: 7, user_id: 1, post_id: 4},
            {id: 8, user_id: 2, post_id: 4}
          ]

        else
          nil
        end
      end

      Sequel.mock(fetch: stubs, autoid: 1).tap do |mock|
        get_schema_block = ->(table_name) { db.schema table_name }
        mock.define_singleton_method(:schema_parse_table) do |table_name, opts| 
          get_schema_block.call table_name
        end
      end
    end

    let(:stub_users)    { Users.new    stub_db, :custom_schema }
    let(:stub_posts)    { Posts.new    stub_db, :custom_schema }
    let(:stub_comments) { Comments.new stub_db, :custom_schema }

    it 'Insert' do
      stub_users.insert name: 'User 1'
      stub_db.sqls.must_equal [
        'BEGIN',
        "INSERT INTO custom_schema.users (name) VALUES ('User 1')",
        'COMMIT'
      ]
    end

    it 'Insert with many to many relation' do
      stub_posts.insert user_id: 1, title: 'Post 1', body: '...', categories: [2,3]
      stub_db.sqls.must_equal [
        'BEGIN',
        "INSERT INTO custom_schema.posts (user_id, title, body) VALUES (1, 'Post 1', '...')",
        'DELETE FROM custom_schema.categories_posts WHERE (post_id IN (1))',
        'INSERT INTO custom_schema.categories_posts (post_id, category_id) VALUES (1, 2)',
        'INSERT INTO custom_schema.categories_posts (post_id, category_id) VALUES (1, 3)',
        'COMMIT'
      ]
    end

    it 'Update' do
      stub_users.update 1, name: 'Updated name'
      stub_db.sqls.must_equal [
        'BEGIN',
        "UPDATE custom_schema.users SET name = 'Updated name' WHERE (id = 1)",
        'COMMIT'
      ]
    end

    it 'Delete' do
      stub_users.delete 1
      stub_db.sqls.must_equal ['DELETE FROM custom_schema.users WHERE (id = 1)']
    end

    it 'Chained query' do
      stub_users.where(id: [1,2]).limit(1).order(:name).all
      stub_db.sqls.must_equal ['SELECT * FROM custom_schema.users WHERE (id IN (1, 2)) ORDER BY name LIMIT 1']
    end

    it 'Graph' do
      stub_posts.graph(:user, :categories, 'comments.user.posts.categories').all
      stub_db.sqls.must_equal [
        'SELECT * FROM custom_schema.posts',
        'SELECT * FROM custom_schema.users WHERE (id IN (1, 2))',
        'SELECT custom_schema.categories.*, custom_schema.categories_posts.post_id AS source_foreign_key FROM custom_schema.categories INNER JOIN custom_schema.categories_posts ON (custom_schema.categories_posts.category_id = custom_schema.categories.id) WHERE (custom_schema.categories_posts.post_id IN (3, 4))', 
        'SELECT * FROM custom_schema.comments WHERE (post_id IN (3, 4))',
        'SELECT * FROM custom_schema.users WHERE (id IN (2, 1))',
        'SELECT * FROM custom_schema.posts WHERE (user_id IN (1, 2))',
        'SELECT custom_schema.categories.*, custom_schema.categories_posts.post_id AS source_foreign_key FROM custom_schema.categories INNER JOIN custom_schema.categories_posts ON (custom_schema.categories_posts.category_id = custom_schema.categories.id) WHERE (custom_schema.categories_posts.post_id IN (3, 4))'
      ]
    end

    it 'Named query' do
      stub_posts.commented_by(1).all
      stub_db.sqls.must_equal [
        'SELECT DISTINCT custom_schema.posts.* FROM custom_schema.posts INNER JOIN custom_schema.comments ON (custom_schema.comments.post_id = custom_schema.posts.id) WHERE (custom_schema.comments.user_id = 1)'
      ]
    end

    it 'Custom query' do
      stub_comments.posts_commented_by(2)
      stub_db.sqls.must_equal [
        'SELECT custom_schema.posts.* FROM custom_schema.comments INNER JOIN custom_schema.posts ON (custom_schema.posts.id = custom_schema.comments.post_id) WHERE (comments.user_id = 2)'
      ]
    end

  end

end