# Copyright (C) 2011-2022  Sutou Kouhei <kou@clear-code.com>
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library.  If not, see <http://www.gnu.org/licenses/>.

class TestEventLoop < Test::Unit::TestCase
  include MilterTestUtils
  include MilterEventLoopTestUtils

  def setup
    @loop = create_event_loop
    @tags = []
  end

  def teardown
    @tags.each do |tag|
      @loop.remove(tag)
    end
  end

  def test_timeout
    timeouted = false
    interval = 0.001
    @tags << @loop.add_timeout(interval) do
      timeouted = true
      false
    end
    sleep(interval)
    assert_true(@loop.iterate(:may_block => false))
    assert_true(timeouted)
  end

  def test_timeout_not_timeouted
    timeouted = false
    interval = 1
    @tags << @loop.add_timeout(interval) do
      timeouted = true
      false
    end
    assert_false(@loop.iterate(:may_block => false))
    assert_false(timeouted)
  end

  def test_timeout_without_block
    assert_raise(ArgumentError.new("timeout block is missing")) do
      @tags << @loop.add_timeout(1)
    end
  end

  def test_timeout_exception
    n_called_before = n_called_after = 0
    interval = 0.001
    @tags << @loop.add_timeout(interval) do
      n_called_before += 1
      raise "should be rescued"
      n_called_after += 1
    end
    sleep(interval)
    assert_nothing_raised {@loop.iterate(:may_block => false)}
    assert_equal([1, 0], [n_called_before, n_called_after])
    sleep(interval)
    assert_nothing_raised {@loop.iterate(:may_block => false)}
    assert_equal([1, 0], [n_called_before, n_called_after])
  end

  def test_idle
    idled = false
    @tags << @loop.add_idle do
      idled = true
      false
    end
    assert_true(@loop.iterate(:may_block => false))
    assert_true(idled)
  end

  def test_idle_not_idled
    idled = false
    @tags << @loop.add_idle do
      idled = true
      false
    end
    @tags << @loop.add_timeout(0.000001) do
      false
    end
    assert_true(@loop.iterate(:may_block => false))
    assert_false(idled)
  end

  def test_idle_without_block
    assert_raise(ArgumentError.new("idle block is missing")) do
      @tags << @loop.add_idle
    end
  end

  def test_idle_exception
    n_called_before = n_called_after = 0
    @tags << @loop.add_idle do
      n_called_before += 1
      raise "should be rescued"
      n_called_after += 1
    end
    assert_nothing_raised {@loop.iterate(:may_block => false)}
    assert_equal([1, 0], [n_called_before, n_called_after])
    assert_nothing_raised {@loop.iterate(:may_block => false)}
    assert_equal([1, 0], [n_called_before, n_called_after])
  end

  def test_watch_child
    omit("watch_child() from Ruby isn't stable...")
    callback_arguments = nil
    IO.pipe do |parent_read, child_write|
      pid = spawn(RbConfig.ruby, "-e", "puts('write')", out: child_write)
      @tags << @loop.watch_child(pid) do |*args|
        callback_arguments = args
        false
      end
      child_write.close
      parent_read.gets
      sleep(0.1)
      assert_true(@loop.iterate(:may_block => false))
      assert_equal(1, callback_arguments.size)
      status = callback_arguments[0]
      assert_equal([pid, true, true],
                   [status.pid, status.exited?, status.success?])
    end
  end

  def test_watch_child_not_reaped
    omit("watch_child() from Ruby isn't stable...")
    callback_arguments = nil
    pid = spawn(RbConfig.ruby, "-e", "sleep(0.1)")
    @tags << @loop.watch_child(pid) do |*args|
      callback_arguments = args
      false
    end
    assert_false(@loop.iterate(:may_block => false))
    assert_nil(callback_arguments)
  end

  def test_watch_child_without_block
    omit("watch_child() from Ruby isn't stable...")
    pid = spawn(RbConfig.ruby, "-e", "nil")
    begin
      assert_raise(ArgumentError.new("watch child block is missing")) do
        @tags << @loop.watch_child(pid)
      end
    ensure
      Process.wait(pid)
    end
  end

  def test_watch_child_exception
    omit("watch_child() from Ruby isn't stable...")
    IO.pipe do |parent_read, child_write|
      n_called_before = n_called_after = 0
      pid = spawn(RbConfig.ruby, "-e", "puts('write')", out: child_write)
      @tags << @loop.watch_child(pid) do |*args|
        n_called_before += 1
        raise "should be rescued"
        n_called_after += 1
      end
      child_write.close
      parent_read.gets
      sleep(0.1)
      assert_nothing_raised {@loop.iterate(:may_block => false)}
      assert_equal([1, 0], [n_called_before, n_called_after])
    end
  end

  def test_watch_io
    callback_arguments = nil
    read_data = nil
    IO.pipe do |parent_read, child_write|
      IO.pipe do |flush_notify_read, flush_notify_write|
        pid = spawn(RbConfig.ruby,
                    "-e",
                    "$stdout.puts('child'); " +
                    "$stdout.flush; " +
                    "$stderr.puts('flushed'); " +
                    "$stderr.flush; " +
                    "sleep(0.1)",
                    out: child_write,
                    err: flush_notify_write)
        begin
          child_write.close
          flush_notify_write.close
          input = GLib::IOChannel.new(parent_read)
          @tags << @loop.watch_io(input,
                                  GLib::IOChannel::IN) do |channel, condition|
            callback_arguments = [channel.class, condition]
            read_data = channel.readline
            false
          end
          flush_notify_read.gets
          assert_true(@loop.iterate(:may_block => false))
          assert_equal([[input.class, GLib::IOChannel::IN], "child\n"],
                       [callback_arguments, read_data])
        ensure
          Process.waitpid(pid)
        end
      end
    end
  end

  def test_watch_io_without_block
    IO.pipe do |parent_read, child_write|
      pid = spawn(RbConfig.ruby, "-e", "puts('child')", out: child_write)
      begin
        child_write.close
        input = GLib::IOChannel.new(parent_read)
        assert_raise(ArgumentError.new("watch IO block is missing")) do
          @tags << @loop.watch_io(input, GLib::IOChannel::IN)
        end
      ensure
        Process.waitpid(pid)
      end
    end
  end

  def test_watch_io_exception
    n_called_before = n_called_after = 0
    sleep_time = 0.01
    IO.pipe do |parent_read, child_write|
      IO.pipe do |notify_read, notify_write|
        pid = spawn(RbConfig.ruby,
                    "-e",
                    "$stdout.puts('child'); " +
                    "$stdout.flush; " +
                    "$stderr.puts('notify'); " +
                    "sleep(#{sleep_time}); " +
                    "$stdout.puts('child')",
                    out: child_write,
                    err: notify_write)
        begin
          child_write.close
          notify_write.close
          input = GLib::IOChannel.new(parent_read)
          @tags << @loop.watch_io(input,
                                  GLib::IOChannel::IN) do |channel, condition|
            n_called_before += 1
            raise "should be rescued"
            n_called_after += 1
          end
          notify_read.gets
          sleep(sleep_time * 2)
          assert_nothing_raised {@loop.iterate(:may_block => false)}
          assert_equal([1, 0], [n_called_before, n_called_after])
          assert_nothing_raised {@loop.iterate(:may_block => false)}
          assert_equal([1, 0], [n_called_before, n_called_after])
        ensure
          Process.waitpid(pid)
        end
      end
    end
  end

  def test_run
    n_called = 0
    quitted = false
    @tags << @loop.add_idle do
      n_called += 1
      if n_called == 3
        @loop.quit
        false
      else
        true
      end
    end
    @loop.run
    assert_equal(3, n_called)
  end
end
