# frozen_string_literal: false
require "monitor"

require "test/unit"

class TestMonitor < Test::Unit::TestCase
  Queue = Thread::Queue

  def setup
    @monitor = Monitor.new
  end

  def test_enter_in_different_fibers
    @monitor.enter
    Fiber.new {
      assert_equal false, @monitor.try_enter
    }.resume
  end

  def test_enter
    ary = []
    queue = Thread::Queue.new
    th = Thread.start {
      queue.pop
      @monitor.enter
      for i in 6 .. 10
        ary.push(i)
        Thread.pass
      end
      @monitor.exit
    }
    th2 = Thread.start {
      @monitor.enter
      queue.enq(nil)
      for i in 1 .. 5
        ary.push(i)
        Thread.pass
      end
      @monitor.exit
    }
    assert_join_threads([th, th2])
    assert_equal((1..10).to_a, ary)
  end

  def test_exit
    m = Monitor.new
    m.enter
    assert_equal true, m.mon_owned?
    m.exit
    assert_equal false, m.mon_owned?

    assert_raise ThreadError do
      m.exit
    end

    assert_equal false, m.mon_owned?

    m.enter
    Thread.new{
      assert_raise(ThreadError) do
        m.exit
      end
    }.join
    assert_equal true, m.mon_owned?
    m.exit
  end

  def test_enter_second_after_killed_thread
    th = Thread.start {
      @monitor.enter
      Thread.current.kill
      @monitor.exit
    }
    th.join
    @monitor.enter
    @monitor.exit
    th2 = Thread.start {
      @monitor.enter
      @monitor.exit
    }
    assert_join_threads([th, th2])
  end

  def test_synchronize
    ary = []
    queue = Thread::Queue.new
    th = Thread.start {
      queue.pop
      @monitor.synchronize do
        for i in 6 .. 10
          ary.push(i)
          Thread.pass
        end
      end
    }
    th2 = Thread.start {
      @monitor.synchronize do
        queue.enq(nil)
        for i in 1 .. 5
          ary.push(i)
          Thread.pass
        end
      end
    }
    assert_join_threads([th, th2])
    assert_equal((1..10).to_a, ary)
  end

  def test_killed_thread_in_synchronize
    ary = []
    queue = Thread::Queue.new
    t1 = Thread.start {
      queue.pop
      @monitor.synchronize {
        ary << :t1
      }
    }
    t2 = Thread.start {
      queue.pop
      @monitor.synchronize {
        ary << :t2
      }
    }
    t3 = Thread.start {
      @monitor.synchronize do
        queue.enq(nil)
        queue.enq(nil)
        assert_equal([], ary)
        t1.kill
        t2.kill
        ary << :main
      end
      assert_equal([:main], ary)
    }
    assert_join_threads([t1, t2, t3])
  end

  def test_try_enter
    queue1 = Thread::Queue.new
    queue2 = Thread::Queue.new
    th = Thread.start {
      queue1.deq
      @monitor.enter
      queue2.enq(nil)
      queue1.deq
      @monitor.exit
      queue2.enq(nil)
    }
    th2 = Thread.start {
      assert_equal(true, @monitor.try_enter)
      @monitor.exit
      queue1.enq(nil)
      queue2.deq
      assert_equal(false, @monitor.try_enter)
      queue1.enq(nil)
      queue2.deq
      assert_equal(true, @monitor.try_enter)
    }
    assert_join_threads([th, th2])
  end

  def test_try_enter_second_after_killed_thread
    th = Thread.start {
      assert_equal(true, @monitor.try_enter)
      Thread.current.kill
      @monitor.exit
    }
    th.join
    assert_equal(true, @monitor.try_enter)
    @monitor.exit
    th2 = Thread.start {
      assert_equal(true, @monitor.try_enter)
      @monitor.exit
    }
    assert_join_threads([th, th2])
  end

  def test_mon_locked_and_owned
    queue1 = Thread::Queue.new
    queue2 = Thread::Queue.new
    th = Thread.start {
      @monitor.enter
      queue1.enq(nil)
      queue2.deq
      @monitor.exit
      queue1.enq(nil)
    }
    queue1.deq
    assert(@monitor.mon_locked?)
    assert(!@monitor.mon_owned?)

    queue2.enq(nil)
    queue1.deq
    assert(!@monitor.mon_locked?)

    @monitor.enter
    assert @monitor.mon_locked?
    assert @monitor.mon_owned?
    @monitor.exit

    @monitor.synchronize do
      assert @monitor.mon_locked?
      assert @monitor.mon_owned?
    end
  ensure
    th.join
  end

  def test_cond
    cond = @monitor.new_cond

    a = "foo"
    queue1 = Thread::Queue.new
    th = Thread.start do
      queue1.deq
      @monitor.synchronize do
        a = "bar"
        cond.signal
      end
    end
    th2 = Thread.start do
      @monitor.synchronize do
        queue1.enq(nil)
        assert_equal("foo", a)
        result1 = cond.wait
        assert_equal(true, result1)
        assert_equal("bar", a)
      end
    end
    assert_join_threads([th, th2])
  end

  class NewCondTest
    include MonitorMixin
    attr_reader :cond
    def initialize
      @cond = new_cond
      super # mon_initialize
    end
  end

  def test_new_cond_before_initialize
    assert NewCondTest.new.cond.instance_variable_get(:@monitor) != nil
  end

  class KeywordInitializeParent
    def initialize(x:)
    end
  end

  class KeywordInitializeChild < KeywordInitializeParent
    include MonitorMixin
    def initialize
      super(x: 1)
    end
  end

  def test_initialize_with_keyword_arg
    assert KeywordInitializeChild.new
  end

  def test_timedwait
    cond = @monitor.new_cond
    b = "foo"
    queue2 = Thread::Queue.new
    th = Thread.start do
      queue2.deq
      @monitor.synchronize do
        b = "bar"
        cond.signal
      end
    end
    th2 = Thread.start do
      @monitor.synchronize do
        queue2.enq(nil)
        assert_equal("foo", b)
        result2 = cond.wait(0.1)
        assert_equal(true, result2)
        assert_equal("bar", b)
      end
    end
    assert_join_threads([th, th2])

    c = "foo"
    queue3 = Thread::Queue.new
    th = Thread.start do
      queue3.deq
      @monitor.synchronize do
        c = "bar"
        cond.signal
      end
    end
    th2 = Thread.start do
      @monitor.synchronize do
        assert_equal("foo", c)
        result3 = cond.wait(0.1)
        assert_equal(false, result3)
        assert_equal("foo", c)
        queue3.enq(nil)
        result4 = cond.wait
        assert_equal(true, result4)
        assert_equal("bar", c)
      end
    end
    assert_join_threads([th, th2])

#     d = "foo"
#     cumber_thread = Thread.start {
#       loop do
#         @monitor.synchronize do
#           d = "foo"
#         end
#       end
#     }
#     queue3 = Thread::Queue.new
#     Thread.start do
#       queue3.pop
#       @monitor.synchronize do
#         d = "bar"
#         cond.signal
#       end
#     end
#     @monitor.synchronize do
#       queue3.enq(nil)
#       assert_equal("foo", d)
#       result5 = cond.wait
#       assert_equal(true, result5)
#       # this thread has priority over cumber_thread
#       assert_equal("bar", d)
#     end
#     cumber_thread.kill
  end

  def test_wait_interruption
    cond = @monitor.new_cond

    th = Thread.start {
      @monitor.synchronize do
        begin
          cond.wait(0.1)
          @monitor.mon_owned?
        rescue Interrupt
          @monitor.mon_owned?
        end
      end
    }
    sleep(0.1)
    th.raise(Interrupt)

    begin
      assert_equal true, th.value
    rescue Interrupt
    end
  end
end
