require "spec_helper"

require "sqlite3"
require "liquor/drop/drop"

ActiveRecord::Base.establish_connection(
  adapter:  'sqlite3',
  database: ':memory:',
)

ActiveRecord::Schema.define force: true do
  create_table "users", force: true do |t|
    t.string "login"
    t.string "email"
    t.string "occupation"
  end

  create_table "articles", force: true do |t|
    t.integer "user_id"
    t.string  "name"
    t.boolean "published"
  end
end

# Models

class User < ActiveRecord::Base
  include Liquor::Dropable

  has_many :articles

  scope :with_login, ->(login) { where('login = ?', login) }
  scope :with_id,    ->(id)    { where('id = ?', id) }
end

class Article < ActiveRecord::Base
  include Liquor::Dropable

  belongs_to :user

  scope :published, -> { where('published = ?', true) }
end

dhh = User.create login: 'dhh', email: 'dhh@loudthinking.org', occupation: 'developer'
dhh.articles.create name: 'java sucks',  published: false
dhh.articles.create name: 'rails rules', published: true

me = User.create login: 'me', email: 'vassily@poupkin.org', occupation: 'developer'
me.articles.create name: 'hello world', published: true

nate = User.create login: 'xnutsive', email: 'nat@evl.ms', occupation: 'manager'

# Drops

class UserDrop < Liquor::Drop
  attributes :id, :login, :email
  scopes :with_login, :with_id

  has_many :articles, scope: [ :published ]
end

class ArticleDrop < Liquor::Drop
  attributes :name, :published
  scopes :published

  belongs_to :user
end

describe Liquor::Drop do
  before do
    @me = User.find_by_login 'me'
    @dhh = User.find_by_login 'dhh'
  end

  it "should export attributes" do
    strukt = Struct.new(:a, :b)
    klass = Class.new(Liquor::Drop) do
      attributes :a, :b
    end

    datum = strukt.new(1, "hello")
    drop  = klass.new(datum)

    exec('{{ drop.a }} {{ drop.b }}', drop: drop).should == '1 hello'
    expect { exec('{{ drop.c }}') }.to raise_error
  end

  it "should allow iterating" do
    expect {
      exec(%|{% for user in: users do: %}{{ user.id }}{% end for %}|, users: User.to_drop)
    }.not_to raise_error
  end

  it "should walk relations and stuff" do
    exec('{{ user.login }}', user: @dhh.to_drop).should == 'dhh'
    exec(%|{% for article in: user.articles do: %}{{ article.name }}, {% end for %}|,
         user: @dhh.to_drop).strip.should == 'rails rules,'
    exec(%|{{ size(articles) }}|, articles: Article.to_drop).should == '3'
    exec(%|{{ size(articles.published) }}|, articles: Article.to_drop).should == '2'
    exec(%|{{ size(users.with_login('dhh')) }}|, users: User.to_drop).should == '1'
    exec(%|{% if article.user == null then: %}ok{% end if %}|, article: Article.new.to_drop).should == 'ok'
  end

  it "should support generic find_by" do
    exec(%|{{ users.find_by(login: "dhh").email }}|, users: User.to_drop).should == 'dhh@loudthinking.org'
    exec(%|{{ users.find_by(email: "vassily@poupkin.org").login }}|, users: User.to_drop).should == 'me'
    exec(%|{{ users.find_by(id: user).email }}|, users: User.to_drop, user: @dhh.to_drop).should == 'dhh@loudthinking.org'
  end

  it "should return null from find_by" do
    exec(%|{% if users.find_by(login: "123") == null then: %}ok{% end if %}|, users: User.to_drop).should == 'ok'
  end

  it "should support generic find_all_by and return a tuple" do
    exec(%|{{ size(users.find_all_by(occupation: "developer")) }}|, users: User.to_drop).should == '2'
    exec(%|{{ size(users.find_all_by(occupation: "manager")) }}|, users: User.to_drop).should == '1'
  end

  it "should support reversing" do
    ltr = exec(%|{% for user in: users do: %}{{ user.id }}{% end for %}|, users: User.to_drop)
    rtl = exec(%|{% for user in: users.reverse do: %}{{ user.id }}{% end for %}|, users: User.to_drop)
    ltr.should == rtl.reverse
  end

  it "should support except" do
    res = exec(%|{% for user in: users.except(users.find_by(login: 'xnutsive')) do: %}{{ user.login }},{% end for %}|, users: User.to_drop)
    res.split(',').sort.should == %w(me dhh).sort

    res = exec(%|{% for user in: users.except([users.find_by(login: 'xnutsive'), users.find_by(login: 'dhh')]) do: %}{{ user.login }},{% end for %}|, users: User.to_drop)
    res.split(',').sort.should == %w(me).sort
  end

  it "should support find_except_by" do
    res = exec(%|{% for user in: users.find_except_by(login: 'xnutsive') do: %}{{ user.login }},{% end for %}|, users: User.to_drop)
    res.split(',').sort.should == %w(me dhh).sort
  end

  it "should not fail if the [] argument is out of range" do
    exec(%|{% if users[5] == null then: %}ok{% end if %}|, users: User.to_drop).should == 'ok'
  end

  it "should provide [] access to the elements, returned by find_all_by function" do
    exec(%|
      {% assign found_users = users.find_all_by(occupation: "developer") %}
      {{ found_users[0].login }}
    |, users: User.to_drop).strip.should == 'dhh'
  end

  it "should accept scope returned by find_all_by in for statements" do
    exec(%|
      {% for user in: users.find_all_by(occupation: "developer") do: %}
        {{ user.login }}
      {% end for %}
    |, users: User.to_drop).split.should == %w[dhh me]
  end

  it "should accept scope returned by find_all into empty() function" do
    exec(%|{% if !is_empty(users.find_all_by(occupation: "developer")) then: %}it works{%end if%}|, users: User.to_drop).should == "it works"
    exec(%|{% if is_empty(users.find_all_by(occupation: "idiot")) then: %}it works{% end if%}|, users: User.to_drop).should == "it works"
  end

  it "should support #pluck" do
    exec(%!{{ users.pluck('login') | join with: ', ' }}!, users: User.to_drop).should == 'dhh, me, xnutsive'
  end

  it "should export model name from Drop and Drop::Scope" do
    exec(%!{{ users.entity }}!, users: User.to_drop).should == 'User'
    exec(%!{{ users.first.entity }}!, users: User.to_drop).should == 'User'
  end

  it "should return intact source" do
    @dhh.to_drop.source.should == @dhh
  end

  it "should support equality" do
    (@dhh.to_drop == @dhh.to_drop).should == true
    (@dhh.to_drop.eql? @dhh.to_drop).should == true
  end

  it "should support include?" do
    lib = Module.new do
      include Liquor::Library

      function "check", {
                  mandatory_named_args: {
                    collection: :any,
                    element:    :any,
                  }
                } do |arg, kw|
        kw[:collection].include? kw[:element]
      end
    end

    compiler = Liquor::Compiler.new
    lib.export compiler
    code = compiler.compile!(parse(%|
      {% if check(collection: collection element: element) then: %}
        yes
      {% end if %}
    |, compiler), [:collection, :element])

    code.call(collection: User.to_drop, element: @dhh.to_drop).
        strip.should == 'yes'
    code.call(collection: User.to_drop, element: User.new.to_drop).
        strip.should == ''
  end

  it "should support uniq'ing" do
    @dhh.to_drop.hash.should == @dhh.to_drop.hash
    [@me, @dhh, @dhh].map(&:to_drop).uniq.size.should == 2
  end

  it "should unwrap arguments to derivative scopes" do
    Liquor::Drop.unwrap_scope_arguments([ @me.to_drop ]).
        should == [@me.id]
    Liquor::Drop.unwrap_scope_arguments([ [@me.to_drop, @dhh.to_drop] ]).
        should ==  [ [@me.id, @dhh.id] ]
    Liquor::Drop.unwrap_scope_arguments([ 1, { a: @me.to_drop, b: @dhh.to_drop } ]).
        should == [ 1, { a: @me.id, b: @dhh.id } ]
    exec('{{ scope.with_id(obj).first.login }}', scope: User.to_drop, obj: @me.to_drop).
        should == @me.login
    Liquor::Drop.unwrap_scope_arguments([ User.order(:id).to_drop ]).
        should == [ User.pluck(:id).sort ]
  end
end
