# encoding: UTF-8

require File.expand_path("./helper", File.dirname(__FILE__))

require "ostruct"

class Post < Ohm::Model
  attribute :body
  list :related, Post
end

class User < Ohm::Model
  attribute :email
  set :posts, Post
end

class Person < Ohm::Model
  attribute :name
  index :initial

  def validate
    assert_present :name
  end

  def initial
    name[0, 1].upcase
  end
end

class Event < Ohm::Model
  attribute :name
  counter :votes
  set :attendees, Person

  attribute :slug

  def write
    self.slug = name.to_s.downcase
    super
  end
end

module SomeNamespace
  class Foo < Ohm::Model
    attribute :name
  end
end

class Meetup < Ohm::Model
  attribute :name
  attribute :location

  def validate
    assert_present :name
  end
end

test "assign attributes from the hash" do
  event = Event.new(:name => "Ruby Tuesday")
  assert event.name == "Ruby Tuesday"
end

test "assign an ID and save the object" do
  event1 = Event.create(:name => "Ruby Tuesday")
  event2 = Event.create(:name => "Ruby Meetup")

  assert "1" == event1.id
  assert "2" == event2.id
end

test "return the unsaved object if validation fails" do
  assert Person.create(:name => nil).kind_of?(Person)
end

test "updates attributes" do
  event = Meetup.create(:name => "Ruby Tuesday")
  event.update(:name => "Ruby Meetup")
  assert "Ruby Meetup" == event.name
end

test "return false if the validation fails" do
  event = Meetup.create(:name => "Ruby Tuesday")
  assert !event.update(:name => nil)
end

test "save the attributes in UTF8" do
 event = Meetup.create(:name => "32° Kisei-sen")
 assert "32° Kisei-sen" == Meetup[event.id].name
end

test "delete the attribute if set to nil" do
  event = Meetup.create(:name => "Ruby Tuesday", :location => "Los Angeles")
  assert "Los Angeles" == Meetup[event.id].location
  assert event.update(:location => nil)
  assert nil == Meetup[event.id].location
end

test "delete the attribute if set to an empty string" do
  event = Meetup.create(:name => "Ruby Tuesday", :location => "Los Angeles")
  assert "Los Angeles" == Meetup[event.id].location
  assert event.update(:location => "")
  assert nil == Meetup[event.id].location
end

test "not raise if an attribute is redefined" do
  class RedefinedModel < Ohm::Model
    attribute :name

    silence_warnings do
      attribute :name
    end
  end
end

test "not raise if a counter is redefined" do
  class RedefinedModel < Ohm::Model
    counter :age

    silence_warnings do
      counter :age
    end
  end
end

test "not raise if a list is redefined" do
  class RedefinedModel < Ohm::Model
    list :todo, lambda { }

    silence_warnings do
      list :todo, lambda { }
    end
  end
end

test "not raise if a set is redefined" do
  class RedefinedModel < Ohm::Model
    set :friends, lambda { }

    silence_warnings do
      set :friends, lambda { }
    end
  end
end

test "not raise if a collection is redefined" do
  class RedefinedModel < Ohm::Model
    list :toys, lambda { }

    silence_warnings do
      set :toys, lambda { }
    end
  end
end

test "not raise if a index is redefined" do
  class RedefinedModel < Ohm::Model
    attribute :color
    index :color
    index :color
  end
end

test "allow arbitrary IDs" do
  Event.create(:id => "abc123", :name => "Concert")

  assert Event.all.size == 1
  assert Event["abc123"].name == "Concert"
end

test "forbid assignment of IDs on a new object" do
  event = Event.new(:name => "Concert")

  assert_raise(NoMethodError) do
    event.id = "abc123"
  end
end

setup do
  Ohm.redis.sadd("Event:all", 1)
  Ohm.redis.hset("Event:1", "name", "Concert")
end

test "return an instance of Event" do
  assert Event[1].kind_of?(Event)
  assert 1 == Event[1].id
  assert "Concert" == Event[1].name
end

setup do
  Ohm.redis.sadd("User:all", 1)
  Ohm.redis.hset("User:1", "email", "albert@example.com")
end

test "return an instance of User" do
  assert User[1].kind_of?(User)
  assert 1 == User[1].id
  assert "albert@example.com" == User[1].email
end

test "allow to map key to models" do
  assert [User[1]] == [1].map(&User)
end

setup do
  Ohm.redis.sadd("User:all", 1)
  Ohm.redis.set("User:1:email", "albert@example.com")

  @user = User[1]
end

test "change its attributes" do
  @user.email = "maria@example.com"
  assert "maria@example.com" == @user.email
end

test "save the new values" do
  @user.email = "maria@example.com"
  @user.save

  @user.email = "maria@example.com"
  @user.save

  assert "maria@example.com" == User[1].email
end

test "assign a new id to the event" do
  event1 = Event.new
  event1.create

  event2 = Event.new
  event2.create

  assert !event1.new?
  assert !event2.new?

  assert "1" == event1.id
  assert "2" == event2.id
end

# Saving a model
test "create the model if it is new" do
  event = Event.new(:name => "Foo").save
  assert "Foo" == Event[event.id].name
end

test "save it only if it was previously created" do
  event = Event.new
  event.name = "Lorem ipsum"
  event.create

  event.name = "Lorem"
  event.save

  assert "Lorem" == Event[event.id].name
end

test "allow to hook into write" do
  event = Event.create(:name => "Foo")

  assert "foo" == event.slug
end

test "save counters" do
  event = Event.create(:name => "Foo")

  event.incr(:votes)
  event.save

  assert 1 == Event[event.id].votes
end

# Delete
test "delete an existing model" do
  class ModelToBeDeleted < Ohm::Model
    attribute :name
    set :foos, Post
    list :bars, Post
  end

  @model = ModelToBeDeleted.create(:name => "Lorem")

  @model.foos << Post.create
  @model.bars << Post.create

  id = @model.id

  @model.delete

  assert Ohm.redis.get(ModelToBeDeleted.key[id]).nil?
  assert Ohm.redis.get(ModelToBeDeleted.key[id][:name]).nil?
  assert Array.new == Ohm.redis.smembers(ModelToBeDeleted.key[id][:foos])
  assert Array.new == Ohm.redis.lrange(ModelToBeDeleted.key[id][:bars], 0, -1)

  assert ModelToBeDeleted.all.empty?
end

setup do
end

test "be no leftover keys" do
  class ::Foo < Ohm::Model
    attribute :name
    index :name
  end

  assert [] == Ohm.redis.keys("*")

  Foo.create(:name => "Bar")

  assert ["Foo:1:_indices", "Foo:1", "Foo:all", "Foo:id", "Foo:name:QmFy"].sort == Ohm.redis.keys("*").sort

  Foo[1].delete

  assert ["Foo:id"] == Ohm.redis.keys("*")
end

# Listing
test "find all" do
  event1 = Event.new
  event1.name = "Ruby Meetup"
  event1.create

  event2 = Event.new
  event2.name = "Ruby Tuesday"
  event2.create

  all = Event.all

  assert all.detect {|e| e.name == "Ruby Meetup" }
  assert all.detect {|e| e.name == "Ruby Tuesday" }
end

# Sorting
test "sort all" do
  Person.create :name => "D"
  Person.create :name => "C"
  Person.create :name => "B"
  Person.create :name => "A"

  assert %w[A B C D] == Person.all.sort_by(:name, :order => "ALPHA").map { |person| person.name }
end

test "return an empty array if there are no elements to sort" do
  assert [] == Person.all.sort_by(:name)
end

test "return the first element sorted by id when using first" do
  Person.create :name => "A"
  Person.create :name => "B"
  assert "A" == Person.all.first.name
end

test "return the first element sorted by name if first receives a sorting option" do
  Person.create :name => "B"
  Person.create :name => "A"
  assert "A" == Person.all.first(:by => :name, :order => "ALPHA").name
end

test "return attribute values when the get parameter is specified" do
  Person.create :name => "B"
  Person.create :name => "A"

  assert "A" == Person.all.sort_by(:name, :get => :name, :order => "ALPHA").first
end

test "work on lists" do
  @post = Post.create :body => "Hello world!"
  @post.related << Post.create(:body => "C")
  @post.related << Post.create(:body => "B")
  @post.related << Post.create(:body => "A")

  assert ["A", "B", "C"] == @post.related.sort_by(:body, :order => "ALPHA ASC").map { |model| model.body }
end

# Loading attributes
setup do
  event = Event.new
  event.name = "Ruby Tuesday"
  @id = event.create.id
end

def monitor
  log = []

  monitor = Thread.new do
    Redis.connect.monitor do |line|
      break if line =~ /ping/
      log << line
    end
  end

  sleep 0.01

  log.clear.tap do
    yield
    Ohm.redis.ping
    monitor.join
  end
end

test "load attributes lazily" do
  event = Event[@id]

  log = monitor { event.name }

  assert !log.empty?

  log = monitor { event.name }

  assert log.empty?
end

test "load attributes as a strings" do
  event = Event.create(:name => 1)

  assert "1" == Event[event.id].name
end

# Attributes of type Set
setup do
  @person1 = Person.create(:name => "Albert")
  @person2 = Person.create(:name => "Bertrand")
  @person3 = Person.create(:name => "Charles")

  @event = Event.new
  @event.name = "Ruby Tuesday"
end

test "not be available if the model is new" do
  assert_raise Ohm::Model::MissingID do
    @event.attendees << Person.new
  end
end

test "remove an element if sent delete" do
  @event.create
  @event.attendees << @person1
  @event.attendees << @person2
  @event.attendees << @person3
  assert ["1", "2", "3"] == @event.attendees.key.sort
  @event.attendees.delete(@person2)
  assert ["1", "3"] == Event[@event.id].attendees.key.sort
end

test "return true if the set includes some member" do
  @event.create
  @event.attendees << @person1
  @event.attendees << @person2
  assert @event.attendees.include?(@person2)
  assert !@event.attendees.include?(@person3)
end

test "return instances of the passed model" do
  @event.create
  @event.attendees << @person1

  assert [@person1] == @event.attendees.all
  assert @person1 == @event.attendees[@person1.id]
end

test "return the size of the set" do
  @event.create
  @event.attendees << @person1
  @event.attendees << @person2
  @event.attendees << @person3
  assert 3 == @event.attendees.size
end

test "empty the set" do
  @event.create
  @event.attendees << @person1

  @event.attendees.clear

  assert @event.attendees.empty?
end

test "replace the values in the set" do
  @event.create
  @event.attendees << @person1

  assert [@person1] == @event.attendees.all

  @event.attendees.replace([@person2, @person3])

  assert [@person2, @person3] == @event.attendees.all.sort_by(&:id)
end

test "filter elements" do
  @event.create
  @event.attendees.add(@person1)
  @event.attendees.add(@person2)

  assert [@person1] == @event.attendees.find(:initial => "A").all
  assert [@person2] == @event.attendees.find(:initial => "B").all
  assert [] == @event.attendees.find(:initial => "Z").all
end

# Attributes of type List
setup do
  @post = Post.new
  @post.body = "Hello world!"
  @post.create
end

test "return an array" do
  assert @post.related.all.kind_of?(Array)
end

test "append elements with push" do
  @post.related.push Post.create
  @post.related << Post.create

  assert ["2", "3"] == @post.related.all.map { |model| model.id }
end

test "keep the inserting order" do
  @post.related << Post.create
  @post.related << Post.create
  @post.related << Post.create
  assert ["2", "3", "4"] == @post.related.all.map { |model| model.id }
end

test "keep the inserting order after saving" do
  @post.related << Post.create
  @post.related << Post.create
  @post.related << Post.create
  @post.save
  assert ["2", "3", "4"] == Post[@post.id].related.map { |model| model.id }
end

test "allow slicing the list" do
  post1 = Post.create
  post2 = Post.create
  post3 = Post.create

  @post.related << post1
  @post.related << post2
  @post.related << post3

  assert post1 == @post.related[0]
  assert post2 == @post.related[1]
  assert post3 == @post.related[-1]

  assert nil == @post.related[3]

  assert [post2, post3] == @post.related[1, 2]
  assert [post2, post3] == @post.related[1, -1]

  assert [] == @post.related[4, 5]

  assert [post2, post3] == @post.related[1..2]
  assert [post2, post3] == @post.related[1..5]

  assert [] == @post.related[4..5]
end

test "respond to each" do
  @post.related << Post.create
  @post.related << Post.create
  @post.related << Post.create

  i = 2
  @post.related.each do |c|
    assert i == c.id.to_i
    i += 1
  end
end

test "return the size of the list" do
  @post.related << Post.create
  @post.related << Post.create
  @post.related << Post.create
  assert 3 == @post.related.size
end

test "return the last element with pop" do
  @post.related << Post.create
  @post.related << Post.create
  assert "3" == @post.related.pop.id
  assert "2" == @post.related.pop.id
  assert @post.related.empty?
end

test "return the first element with shift" do
  @post.related << Post.create
  @post.related << Post.create
  assert "2" == @post.related.shift.id
  assert "3" == @post.related.shift.id
  assert @post.related.empty?
end

test "push to the head of the list with unshift" do
  @post.related.unshift Post.create
  @post.related.unshift Post.create
  assert "2" == @post.related.pop.id
  assert "3" == @post.related.pop.id
  assert @post.related.empty?
end

test "empty the list" do
  @post.related.unshift Post.create
  @post.related.clear

  assert @post.related.empty?
end

test "replace the values in the list" do
  @post.related.replace([Post.create, Post.create])

  assert ["2", "3"] == @post.related.map { |model| model.id }
end

test "add models" do
  @post.related.add(Post.create(:body => "Hello"))
  assert ["2"] == @post.related.map { |model| model.id }
end

test "find elements in the list" do
  another_post = Post.create

  @post.related.add(another_post)

  assert  @post.related.include?(another_post)
  assert !@post.related.include?(Post.create)
end

test "unshift models" do
  @post.related.unshift(Post.create(:body => "Hello"))
  @post.related.unshift(Post.create(:body => "Goodbye"))

  assert ["3", "2"] == @post.related.map { |model| model.id }

  assert "3" == @post.related.shift.id

  assert "2" == @post.related.pop.id

  assert @post.related.pop.nil?
end

# Applying arbitrary transformations
require "date"

class MyActiveRecordModel
  def self.find(id)
    return new if id.to_i == 1
  end

  def id
    1
  end

  def ==(other)
    id == other.id
  end
end

class ::Calendar < Ohm::Model
  list :holidays, lambda { |v| Date.parse(v) }
  list :subscribers, lambda { |id| MyActiveRecordModel.find(id) }
  list :appointments, Appointment

  set :events, lambda { |id| MyActiveRecordModel.find(id) }
end

class ::Appointment < Ohm::Model
  attribute :text
  reference :subscriber, lambda { |id| MyActiveRecordModel.find(id) }
end

setup do
  @calendar = Calendar.create

  @calendar.holidays.key.rpush "2009-05-25"
  @calendar.holidays.key.rpush "2009-07-09"

  @calendar.subscribers << MyActiveRecordModel.find(1)

  @calendar.events << MyActiveRecordModel.find(1)
end

test "apply a transformation" do
  assert [Date.new(2009, 5, 25), Date.new(2009, 7, 9)] == @calendar.holidays.all

  assert [1] == @calendar.subscribers.all.map { |model| model.id }
  assert [MyActiveRecordModel.find(1)] == @calendar.subscribers.all
end

test "doing an each on lists" do
  arr = []
  @calendar.subscribers.each do |sub|
    arr << sub
  end

  assert [MyActiveRecordModel.find(1)] == arr
end

test "doing an each on sets" do
  arr = []
  @calendar.events.each do |sub|
    arr << sub
  end

  assert [MyActiveRecordModel.find(1)] == arr
end

test "allow lambdas in references" do
  appointment = Appointment.create(:subscriber => MyActiveRecordModel.find(1))
  assert MyActiveRecordModel.find(1) == appointment.subscriber
end

test "work with models too" do
  @calendar.appointments.add(Appointment.create(:text => "Meet with Bertrand"))

  assert [Appointment[1]] == Calendar[1].appointments.sort
end

# Sorting lists and sets
setup do
  @post = Post.create(:body => "Lorem")
  @post.related << Post.create
  @post.related << Post.create
  @post.related << Post.create
end

test "sort values" do
  assert %w{2 3 4} == @post.related.sort.map { |model| model.id }
end

# Sorting lists and sets by model attributes
setup do
  @event = Event.create(:name => "Ruby Tuesday")
  @event.attendees << Person.create(:name => "D")
  @event.attendees << Person.create(:name => "C")
  @event.attendees << Person.create(:name => "B")
  @event.attendees << Person.create(:name => "A")
end

test "sort the model instances by the values provided" do
  people = @event.attendees.sort_by(:name, :order => "ALPHA")
  assert %w[A B C D] == people.map { |person| person.name }
end

test "accept a number in the limit parameter" do
  people = @event.attendees.sort_by(:name, :limit => 2, :order => "ALPHA")
  assert %w[A B] == people.map { |person| person.name }
end

test "use the start parameter as an offset if the limit is provided" do
  people = @event.attendees.sort_by(:name, :limit => 2, :start => 1, :order => "ALPHA")
  assert %w[B C] == people.map { |person| person.name }
end

# Collections initialized with a Model parameter
setup do
  @user = User.create(:email => "albert@example.com")
  @user.posts.add Post.create(:body => "D")
  @user.posts.add Post.create(:body => "C")
  @user.posts.add Post.create(:body => "B")
  @user.posts.add Post.create(:body => "A")
end

test "return instances of the passed model" do
  assert Post == @user.posts.first.class
end

# Counters
setup do
  @event = Event.create(:name => "Ruby Tuesday")
end

test "raise ArgumentError if the attribute is not a counter" do
  assert_raise ArgumentError do
    @event.incr(:name)
  end
end

test "be zero if not initialized" do
  assert 0 == @event.votes
end

test "be able to increment a counter" do
  @event.incr(:votes)
  assert 1 == @event.votes

  @event.incr(:votes, 2)
  assert 3 == @event.votes
end

test "be able to decrement a counter" do
  @event.decr(:votes)
  assert @event.votes == -1

  @event.decr(:votes, 2)
  assert @event.votes == -3
end

# Comparison
setup do
  @user = User.create(:email => "foo")
end

test "be comparable to other instances" do
  assert @user == User[@user.id]

  assert @user != User.create
  assert User.new != User.new
end

test "not be comparable to instances of other models" do
  assert @user != Event.create(:name => "Ruby Tuesday")
end

test "be comparable to non-models" do
  assert @user != 1
  assert @user != true

  # Not equal although the other object responds to #key.
  assert @user != OpenStruct.new(:key => @user.send(:key))
end

# Debugging
class ::Bar < Ohm::Model
  attribute :name
  counter :visits
  set :friends, self
  list :comments, self

  def foo
    bar.foo
  end

  def baz
    bar.new.foo
  end

  def bar
    SomeMissingConstant
  end
end

test "provide a meaningful inspect" do
  bar = Bar.new

  assert "#<Bar:? name=nil friends=nil comments=nil visits=0>" == bar.inspect

  bar.update(:name => "Albert")
  bar.friends << Bar.create
  bar.comments << Bar.create
  bar.incr(:visits)

  assert %Q{#<Bar:#{bar.id} name="Albert" friends=#<Set (Bar): ["2"]> comments=#<List (Bar): ["3"]> visits=1>} == Bar[bar.id].inspect
end

def assert_wrapper_exception(&block)
  begin
    block.call
  rescue NoMethodError => exception_raised
  end

  assert exception_raised.message =~
  /You tried to call SomeMissingConstant#\w+, but SomeMissingConstant is not defined on #{__FILE__}:\d+:in `bar'/
end

test "inform about a miscatch by Wrapper when calling class methods" do
  assert_wrapper_exception { Bar.new.baz }
end

test "inform about a miscatch by Wrapper when calling instance methods" do
  assert_wrapper_exception { Bar.new.foo }
end

# Overwriting write
class ::Baz < Ohm::Model
  attribute :name

  def write
    self.name = "Foobar"
    super
  end
end

test "work properly" do
  baz = Baz.new
  baz.name = "Foo"
  baz.save
  baz.name = "Foo"
  baz.save
  assert "Foobar" == Baz[baz.id].name
end

# Models connected to different databases
class ::Car < Ohm::Model
  attribute :name
end

class ::Make < Ohm::Model
  attribute :name
end

setup do
  Car.connect(:db => 15)
  Car.db.flushdb
end

test "save to the selected database" do
  car = Car.create(:name => "Twingo")
  make = Make.create(:name => "Renault")

  assert ["1"] == Redis.connect.smembers("Make:all")
  assert [] == Redis.connect.smembers("Car:all")

  assert ["1"] == Car.db.smembers("Car:all")
  assert [] == Car.db.smembers("Make:all")

  assert car == Car[1]
  assert make == Make[1]

  Make.db.flushdb

  assert car == Car[1]
  assert Make[1].nil?
end

test "allow changing the database" do
  Car.create(:name => "Twingo")

  assert ["1"] == Car.all.key.smembers

  Car.connect
  assert [] == Car.all.key.smembers

  Car.connect :db => 15
  assert ["1"] == Car.all.key.smembers
end

# Persistence
test "persist attributes to a hash" do
  event = Event.create(:name => "Redis Meetup")
  event.incr(:votes)

  assert "hash" == Ohm.redis.type("Event:1")

  assert [
    "Event:1", "Event:all", "Event:id"
  ].sort == Ohm.redis.keys("Event:*").sort

  assert "Redis Meetup" == Event[1].name
  assert 1 == Event[1].votes
end

# a Model with individual attribute writing needs
class Order < Ohm::Model
  attribute :state

  def authorize!
    write_remote :state, 'authorized'
  end
end

test "writes locally" do
  order = Order.create(:state => "pending")
  order.authorize!
  assert 'authorized' == order.state
end

test "writes remotely" do
  order = Order.create(:state => "pending")
  order.authorize!
  order = Order[order.id]
  assert 'authorized' == order.state
end

# namespaced models
test "be persisted" do
  SomeNamespace::Foo.create(:name => "foo")

  assert "hash" == Ohm.redis.type("SomeNamespace::Foo:1")

  assert "foo" == SomeNamespace::Foo[1].name
end