# -*- coding: utf-8 -*-
require 'spec_helper'
require 'tengine/core'

describe Tengine::Core::Mutex do
  before do
    Tengine::Core::Mutex::Mutex.delete_all
  end

  context "#new" do
    subject { Tengine::Core::Mutex.new "test mutex 01" }

    it { should be_kind_of(Tengine::Core::Mutex) }
    its(:mutex) { should be_kind_of(Tengine::Core::Mutex::Mutex) }
    its(:recursive) { should be_zero }

    context "negative ttl" do
      it { expect { Tengine::Core::Mutex.new "test negative ttl", -1 }.to raise_exception(ArgumentError) }
    end

    context "infinite ttl" do
      it { expect { Tengine::Core::Mutex.new "test negative ttl", (1.0 / 0.0) }.to raise_exception(TypeError) }
    end
  end

  context "#synchronize" do
    subject { Tengine::Core::Mutex.new "test mutex 01", Math::PI / 10 }

    it "requires block" do
      expect {
        subject.synchronize
      }.to raise_exception(ArgumentError)
    end

    it "synchronizes #0: no other lockers" do
      block_called = false
      EM.run_block do
        subject.synchronize do
          block_called = true
        end
      end
      block_called.should be_true
    end

    it "synchronizes #1: with another locker, which is expired" do
      # "stub" waiters
      m = subject.mutex
      m.waiters ||= []
      m.waiters << { :_id => 1, :timeout => Time.at(0) }
      m.save

      block_called = false
      t0 = Time.now
      EM.run_block do
        subject.synchronize do
          block_called = true
        end
      end
      t1 = Time.now
      block_called.should be_true
      m.reload.waiters.should be_empty
      (t1 - t0).should be_within(0.5).of(0) #immediate
    end

    it "synchronizes #2: with another locker, which is expiring" do
      # "stub" waiters
      m = subject.mutex
      m.waiters ||= []
      m.waiters << { :_id => 1, :timeout => Time.now + m.ttl / 2 }
      m.save

      block_called = false
      t0 = Time.now
      EM.run do
        subject.synchronize do
          block_called = true
          EM.stop
        end
      end
      t1 = Time.now
      block_called.should be_true
      m.reload.waiters.should be_empty
      (t1 - t0).should be_within(0.5).of(0.3 + m.ttl)
    end

    it "synchronizes #3: with another locker, which is not expiring" do
      # "stub" waiters
      m = subject.mutex
      s = mock("mutex")
      s.stub("_id").and_return(1)
      m.waiters ||= []
      m.waiters << { :_id => s._id, :timeout => Time.now + 10 }
      m.save

      block_called = false
      t0 = Time.now
      EM.run do
        EM.add_timer 5 do
          m.unlock s
        end
        subject.synchronize do
          block_called = true
          EM.stop
        end
      end
      t1 = Time.now
      block_called.should be_true
      m.reload.waiters.should be_empty
      (t1 - t0).should be_within(0.5).of(5 + m.ttl)
    end

    it "synchronizes #4: multi-threaded situation" do
      x = y = z = nil

      EM.run do
        EM.defer do
          m = Tengine::Core::Mutex.new "test mutex 02"
          m.synchronize do
            x = Time.now.to_f
          end
          y = Time.now.to_f
        end

        EM.defer do
          sleep 0.5
          m = Tengine::Core::Mutex.new "test mutex 02"
          m.synchronize do
            z = Time.now.to_f
          end
        end

        EM.add_timer(2.5) do
          EM.stop
        end
      end

      x.should_not be_nil
      y.should_not be_nil
      z.should_not be_nil
      x.should < z
      y.should <= z
    end
  end

  context "#heartbeat" do
    subject { Tengine::Core::Mutex.new "test mutex 01", Math::PI / 10 }

    it "prevents automatic unlocking" do
      m = subject.mutex
      t1 = nil
      t0 = Time.now.to_f
      EM.run do
        EM.defer do
          subject.synchronize do
            20.times do
              subject.heartbeat
              sleep(m.ttl / 2)
            end
          end
        end
        EM.defer do
          sleep m.ttl
          loop do
            # hacky...
            if h = m.reload.waiters.first
              if h["timeout"] < Time.now
                t1 = Time.now.to_f
                EM.stop
                break
              end
            else
              t1 = Time.now.to_f
              EM.stop
              break
            end
          end
        end
      end

      t1.should_not be_nil
      (t1 - t0).should be_within(0.5).of(10 * subject.mutex.ttl)
    end
  end
end