require File.join(File.dirname(__FILE__), "spec_helper")

describe Sequel::Model do
  it "should have class method aliased as model" do
    Sequel::Model.instance_methods.should include("model")

    model_a = Class.new Sequel::Model
    model_a.new.model.should be(model_a)
  end

  it "should be associated with a dataset" do
    model_a = Class.new(Sequel::Model) { set_dataset MODEL_DB[:as] }

    model_a.dataset.should be_a_kind_of(MockDataset)
    model_a.dataset.opts[:from].should == [:as]

    model_b = Class.new(Sequel::Model) { set_dataset MODEL_DB[:bs] }

    model_b.dataset.should be_a_kind_of(MockDataset)
    model_b.dataset.opts[:from].should == [:bs]

    model_a.dataset.opts[:from].should == [:as]
  end

end

describe Sequel::Model, "dataset & schema" do

  before do
    @model = Class.new(Sequel::Model(:items))
  end

  it "creates dynamic model subclass with set table name" do
    @model.table_name.should == :items
  end

  it "defaults to primary key of id" do
    @model.primary_key.should == :id
  end

  it "allow primary key change" do
    @model.set_primary_key :ssn
    @model.primary_key.should == :ssn
  end

  it "allows dataset change" do
    @model.set_dataset(MODEL_DB[:foo])
    @model.table_name.should == :foo
  end

  it "sets schema with implicit table name" do
    @model.set_schema do
      primary_key :ssn, :string
    end
    @model.primary_key.should == :ssn
    @model.table_name.should == :items
  end

  it "sets schema with explicit table name" do
    @model.set_schema :foo do
      primary_key :id
    end
    @model.primary_key.should == :id
    @model.table_name.should == :foo
  end

  it "puts the lotion in the basket or it gets the hose again" do
    # just kidding!
  end
end

describe Sequel::Model, "constructor" do
  
  before(:each) do
    @m = Class.new(Sequel::Model)
    @m.columns :a, :b
  end

  it "should accept a hash" do
    m = @m.new(:a => 1, :b => 2)
    m.values.should == {:a => 1, :b => 2}
    m.should be_new
  end
  
  it "should accept a block and yield itself to the block" do
    block_called = false
    m = @m.new {|i| block_called = true; i.should be_a_kind_of(@m); i.values[:a] = 1}
    
    block_called.should be_true
    m.values[:a].should == 1
  end
  
end

describe Sequel::Model, "new" do

  before(:each) do
    @m = Class.new(Sequel::Model) do
      set_dataset MODEL_DB[:items]
    end
  end

  it "should be marked as new?" do
    o = @m.new
    o.should be_new
  end

  it "should not be marked as new? once it is saved" do
    o = @m.new(:x => 1)
    o.should be_new
    o.save
    o.should_not be_new
  end

  it "should use the last inserted id as primary key if not in values" do
    d = @m.dataset
    def d.insert(*args)
      super
      1234
    end

    def d.first
      {:x => 1, :id => 1234}
    end

    o = @m.new(:x => 1)
    o.save
    o.id.should == 1234

    o = @m.load(:x => 1, :id => 333)
    o.save
    o.id.should == 333
  end

end

describe Sequel::Model, ".subset" do

  before(:each) do
    MODEL_DB.reset

    @c = Class.new(Sequel::Model(:items))
  end

  it "should create a filter on the underlying dataset" do
    proc {@c.new_only}.should raise_error(NoMethodError)
    
    @c.subset(:new_only) {:age == 'new'}
    
    @c.new_only.sql.should == "SELECT * FROM items WHERE (age = 'new')"
    @c.dataset.new_only.sql.should == "SELECT * FROM items WHERE (age = 'new')"
    
    @c.subset(:pricey) {:price > 100}
    
    @c.pricey.sql.should == "SELECT * FROM items WHERE (price > 100)"
    @c.dataset.pricey.sql.should == "SELECT * FROM items WHERE (price > 100)"
    
    # check if subsets are composable
    @c.pricey.new_only.sql.should == "SELECT * FROM items WHERE (price > 100) AND (age = 'new')"
    @c.new_only.pricey.sql.should == "SELECT * FROM items WHERE (age = 'new') AND (price > 100)"
  end

end

describe Sequel::Model, ".find" do

  before(:each) do
    MODEL_DB.reset
    
    @c = Class.new(Sequel::Model(:items))
    
    $cache_dataset_row = {:name => 'sharon', :id => 1}
    @dataset = @c.dataset
    $sqls = []
    @dataset.extend(Module.new {
      def fetch_rows(sql)
        $sqls << sql
        yield $cache_dataset_row
      end
    })
  end
  
  it "should return the first record matching the given filter" do
    @c.find(:name => 'sharon').should be_a_kind_of(@c)
    $sqls.last.should == "SELECT * FROM items WHERE (name = 'sharon') LIMIT 1"

    @c.find {"name LIKE 'abc%'".lit}.should be_a_kind_of(@c)
    $sqls.last.should == "SELECT * FROM items WHERE name LIKE 'abc%' LIMIT 1"
  end
  
  it "should accept filter blocks" do
    @c.find {:id == 1}.should be_a_kind_of(@c)
    $sqls.last.should == "SELECT * FROM items WHERE (id = 1) LIMIT 1"

    @c.find {:x > 1 && :y < 2}.should be_a_kind_of(@c)
    $sqls.last.should == "SELECT * FROM items WHERE ((x > 1) AND (y < 2)) LIMIT 1"
  end

end

describe Sequel::Model, ".fetch" do

  before(:each) do
    MODEL_DB.reset
    @c = Class.new(Sequel::Model(:items))
  end
  
  it "should return instances of Model" do
    @c.fetch("SELECT * FROM items").first.should be_a_kind_of(@c)
  end

  it "should return true for .empty? and not raise an error on empty selection" do
    rows = @c.fetch("SELECT * FROM items WHERE FALSE")
    @c.class_def(:fetch_rows) {|sql| yield({:count => 0})}
    proc {rows.empty?}.should_not raise_error
  end
  
end

### DEPRECATED
describe Sequel::Model, "magic methods" do

  before(:each) do
    @c = Class.new(Sequel::Dataset) do
      @@sqls = []
    
      def self.sqls; @@sqls; end
    
      def fetch_rows(sql)
        @@sqls << sql
        yield({:id => 123, :name => 'hey'})
      end
    end
  
    @m = Class.new(Sequel::Model(@c.new(nil).from(:items)))
  end
  
  it "should support order_by_xxx" do
    @m.order_by_name.should be_a_kind_of(@c)
    @m.order_by_name.sql.should == "SELECT * FROM items ORDER BY name"
  end

  it "should support group_by_xxx" do
    @m.group_by_name.should be_a_kind_of(@c)
    @m.group_by_name.sql.should == "SELECT * FROM items GROUP BY name"
  end

  it "should support count_by_xxx" do
    @m.count_by_name.should be_a_kind_of(@c)
    @m.count_by_name.sql.should == "SELECT name, count(*) AS count FROM items GROUP BY name ORDER BY count"
  end

  it "should support filter_by_xxx" do
    @m.filter_by_name('sharon').should be_a_kind_of(@c)
    @m.filter_by_name('sharon').sql.should == "SELECT * FROM items WHERE (name = 'sharon')"
  end
  
  it "should support all_by_xxx" do
    all = @m.all_by_name('sharon')
    all.class.should == Array
    all.size.should == 1
    all.first.should be_a_kind_of(@m)
    all.first.values.should == {:id => 123, :name => 'hey'}
    @c.sqls.should == ["SELECT * FROM items WHERE (name = 'sharon')"]
  end
  
  it "should support find_by_xxx" do
    @m.find_by_name('sharon').should be_a_kind_of(@m)
    @m.find_by_name('sharon').values.should == {:id => 123, :name => 'hey'}
    @c.sqls.should == ["SELECT * FROM items WHERE (name = 'sharon') LIMIT 1"] * 2
  end

  it "should support first_by_xxx" do
    @m.first_by_name('sharon').should be_a_kind_of(@m)
    @m.first_by_name('sharon').values.should == {:id => 123, :name => 'hey'}
    @c.sqls.should == ["SELECT * FROM items ORDER BY name LIMIT 1"] * 2
  end

  it "should support last_by_xxx" do
    @m.last_by_name('sharon').should be_a_kind_of(@m)
    @m.last_by_name('sharon').values.should == {:id => 123, :name => 'hey'}
    @c.sqls.should == ["SELECT * FROM items ORDER BY name DESC LIMIT 1"] * 2
  end
  
end

describe Sequel::Model, ".find_or_create" do

  before(:each) do
    MODEL_DB.reset
    @c = Class.new(Sequel::Model(:items)) do
      no_primary_key
      columns :x
    end
  end

  it "should find the record" do
    @c.find_or_create(:x => 1)
    MODEL_DB.sqls.should == ["SELECT * FROM items WHERE (x = 1) LIMIT 1"]
    
    MODEL_DB.reset
  end
  
  it "should create the record if not found" do
    @c.meta_def(:find) do |*args|
      dataset.filter(*args).first
      nil
    end
    
    @c.find_or_create(:x => 1)
    MODEL_DB.sqls.should == [
      "SELECT * FROM items WHERE (x = 1) LIMIT 1",
      "INSERT INTO items (x) VALUES (1)"
    ]
  end
end

describe Sequel::Model, ".delete_all" do

  before(:each) do
    MODEL_DB.reset
    @c = Class.new(Sequel::Model(:items)) do
      no_primary_key
    end
    
    @c.dataset.meta_def(:delete) {MODEL_DB << delete_sql}
  end

  it "should delete all records in the dataset" do
    @c.delete_all
    MODEL_DB.sqls.should == ["DELETE FROM items"]
  end

end

describe Sequel::Model, ".destroy_all" do

  before(:each) do
    MODEL_DB.reset
    @c = Class.new(Sequel::Model(:items)) do
      no_primary_key
    end

    @c.dataset.meta_def(:delete) {MODEL_DB << delete_sql}
  end

  it "should delete all records in the dataset" do
    @c.dataset.meta_def(:destroy) {MODEL_DB << "DESTROY this stuff"}
    @c.destroy_all
    MODEL_DB.sqls.should == ["DESTROY this stuff"]
  end
  
  it "should call dataset.destroy" do
    @c.dataset.should_receive(:destroy).and_return(true)
    @c.destroy_all
  end
end

describe Sequel::Model, ".all" do
  
  before(:each) do
    MODEL_DB.reset
    @c = Class.new(Sequel::Model(:items)) do
      no_primary_key
    end
    
    @c.dataset.meta_def(:all) {1234}
  end
  
  it "should return all records in the dataset" do
    @c.all.should == 1234
  end
  
end

class DummyModelBased < Sequel::Model(:blog)
end

describe Sequel::Model, "(:tablename)" do

  it "should allow reopening of descendant classes" do
    proc do
      eval "class DummyModelBased < Sequel::Model(:blog); end"
    end.should_not raise_error
  end

end

describe Sequel::Model, "A model class without a primary key" do

  before(:each) do
    MODEL_DB.reset
    @c = Class.new(Sequel::Model(:items)) do
      columns :x
      no_primary_key
    end
  end

  it "should be able to insert records without selecting them back" do
    i = nil
    proc {i = @c.create(:x => 1)}.should_not raise_error
    i.class.should be(@c)
    i.values.to_hash.should == {:x => 1}

    MODEL_DB.sqls.should == ['INSERT INTO items (x) VALUES (1)']
  end

  it "should raise when deleting" do
    o = @c.new
    proc {o.delete}.should raise_error
  end

  it "should insert a record when saving" do
    o = @c.new(:x => 2)
    o.should be_new
    o.save
    MODEL_DB.sqls.should == ['INSERT INTO items (x) VALUES (2)']
  end

end

describe Sequel::Model, "attribute accessors" do

  before(:each) do
    MODEL_DB.reset

    @c = Class.new(Sequel::Model) do
      def self.columns; orig_columns; end
    end
    @dataset = Object.new
    def @dataset.db; end
    def @dataset.set_model(blah); end
    def @dataset.naked; self; end
    def @dataset.columns; [:x, :y]; end
    def @dataset.def_mutation_method(*names);  end
  end

  it "should be created on set_dataset" do
    %w'x y x= y='.each do |x|
      @c.instance_methods.include?(x).should == false
    end
    @c.set_dataset(@dataset)
    %w'x y x= y='.each do |x|
      @c.instance_methods.include?(x).should == true
    end
    o = @c.new
    %w'x y x= y='.each do |x|
      o.methods.include?(x).should == true
    end

    o.x.should be_nil
    o.x = 34
    o.x.should == 34
  end

  it "should be only accept one argument for the write accessor" do
    @c.set_dataset(@dataset)
    o = @c.new

    o.x = 34
    o.x.should == 34
    proc{o.send(:x=)}.should raise_error
    proc{o.send(:x=, 3, 4)}.should raise_error
  end
end

describe Sequel::Model, ".[]" do

  before(:each) do
    MODEL_DB.reset

    @c = Class.new(Sequel::Model(:items))

    $cache_dataset_row = {:name => 'sharon', :id => 1}
    @dataset = @c.dataset
    $sqls = []
    @dataset.extend(Module.new {
      def fetch_rows(sql)
        $sqls << sql
        yield $cache_dataset_row
      end
    })
  end

  it "should return the first record for the given pk" do
    @c[1].should be_a_kind_of(@c)
    $sqls.last.should == "SELECT * FROM items WHERE (id = 1) LIMIT 1"
    @c[9999].should be_a_kind_of(@c)
    $sqls.last.should == "SELECT * FROM items WHERE (id = 9999) LIMIT 1"
  end

  it "should raise for boolean argument (mistaken comparison)" do
    # This in order to prevent stuff like Model[:a == 'b']
    proc {@c[:a == 1]}.should raise_error(Sequel::Error)
    proc {@c[:a != 1]}.should raise_error(Sequel::Error)
  end

  it "should work correctly for custom primary key" do
    @c.set_primary_key :name
    @c['sharon'].should be_a_kind_of(@c)
    $sqls.last.should == "SELECT * FROM items WHERE (name = 'sharon') LIMIT 1"
  end

  it "should work correctly for composite primary key" do
    @c.set_primary_key [:node_id, :kind]
    @c[3921, 201].should be_a_kind_of(@c)
    $sqls.last.should =~ \
    /^SELECT \* FROM items WHERE (\(node_id = 3921\) AND \(kind = 201\))|(\(kind = 201\) AND \(node_id = 3921\)) LIMIT 1$/
  end
end

context "Model#inspect" do
  setup do
    @o = Sequel::Model.load(:x => 333)
  end
  
  specify "should include the class name and the values" do
    @o.inspect.should == '#<Sequel::Model @values={:x=>333}>'
  end
end
