-- Eventloop behavior tests for the CraftOS-PC harness.
local createEventLoop = require('/apis/eventloop');
local createLibTest = require('/apis/libtest');

local testlib = createLibTest({ ... });

testlib.test('register dispatches queued event args', function()
  local events = createEventLoop();
  local called = 0;

  events.register('eventloop_test_basic', function(a, b)
    called = called + 1;
    testlib.assertEquals(a, 'first');
    testlib.assertEquals(b, 42);
    events.stopLoop();
  end);

  os.queueEvent('eventloop_test_basic', 'first', 42);
  events.runLoop();

  testlib.assertEquals(called, 1);
  testlib.assertTrue(not events.isRunningLoop());
end);

testlib.test('STOP unregisters handler after first call', function()
  local events = createEventLoop();
  local stoppedHandlerCalls = 0;
  local observerCalls = 0;

  events.register('eventloop_test_stop', function()
    stoppedHandlerCalls = stoppedHandlerCalls + 1;
    return events.STOP;
  end);

  events.register('eventloop_test_stop', function()
    observerCalls = observerCalls + 1;
    if observerCalls == 1 then
      os.queueEvent('eventloop_test_stop');
    else
      events.stopLoop();
    end
  end);

  os.queueEvent('eventloop_test_stop');
  events.runLoop();

  testlib.assertEquals(stoppedHandlerCalls, 1);
  testlib.assertEquals(observerCalls, 2);
end);

testlib.test('manual unregister prevents dispatch', function()
  local events = createEventLoop();
  local called = 0;
  local dispose = events.register('eventloop_test_unregister', function()
    called = called + 1;
  end);

  events.setTimeout(function()
    events.stopLoop();
  end, 0);
  dispose();

  os.queueEvent('eventloop_test_unregister');
  events.runLoop();

  testlib.assertEquals(called, 0);
end);

testlib.test('setTimeout before runLoop fires once', function()
  local events = createEventLoop();
  local called = 0;

  events.setTimeout(function()
    called = called + 1;
    events.stopLoop();
  end, 0);

  events.runLoop();

  testlib.assertEquals(called, 1);
end);

testlib.test('setTimeout during runLoop fires after event handler', function()
  local events = createEventLoop();
  local order = '';

  events.register('eventloop_test_runtime_timeout', function()
    order = order .. 'event>';
    events.setTimeout(function()
      order = order .. 'timeout';
      events.stopLoop();
    end, 0);
    return events.STOP;
  end);

  os.queueEvent('eventloop_test_runtime_timeout');
  events.runLoop();

  testlib.assertEquals(order, 'event>timeout');
end);

testlib.test('cleared timeout before runLoop does not fire', function()
  local events = createEventLoop();
  local called = 0;

  local clear = events.setTimeout(function()
    called = called + 1;
  end, 0);
  clear();

  events.setTimeout(function()
    events.stopLoop();
  end, 0);
  events.runLoop();

  testlib.assertEquals(called, 0);
end);

testlib.test('onStart and onStop run around loop', function()
  local events = createEventLoop();
  local order = '';

  events.onStart(function()
    order = order .. 'start>';
  end);

  events.onStop(function()
    order = order .. 'stop';
  end);

  events.setTimeout(function()
    order = order .. 'timeout>';
    events.stopLoop();
  end, 0);

  events.runLoop();

  testlib.assertEquals(order, 'start>timeout>stop');
end);

testlib.test('same-loop stop does not leak to a sibling loop with the same id', function()
  local createEventLoopA = assert(loadfile('/apis/eventloop.lua'))();
  local createEventLoopB = assert(loadfile('/apis/eventloop.lua'))();
  local bootEvents = createEventLoopA();
  local verifierEvents = createEventLoopB();
  local bootProbeHandled = false;

  bootEvents.register('eventloop_test_same_loop_probe', function()
    bootProbeHandled = true;
    bootEvents.stopLoop();
  end);

  verifierEvents.register('eventloop_test_same_loop_stop', function()
    verifierEvents.stopLoop();
    os.queueEvent('eventloop_test_same_loop_probe');
  end);

  os.queueEvent('eventloop_test_same_loop_stop');
  parallel.waitForAll(function() bootEvents.runLoop(true); end, function() verifierEvents.runLoop(); end);

  testlib.assertTrue(bootProbeHandled);
end);

testlib.test('external stop events use ids unique across module instances', function()
  local createEventLoopA = assert(loadfile('/apis/eventloop.lua'))();
  local createEventLoopB = assert(loadfile('/apis/eventloop.lua'))();
  local bootEvents = createEventLoopA();
  local verifierEvents = createEventLoopB();
  local bootProbeHandled = false;

  bootEvents.register('eventloop_test_external_stop', function()
    verifierEvents.stopLoop();
    os.queueEvent('eventloop_test_external_probe');
  end);

  bootEvents.register('eventloop_test_external_probe', function()
    bootProbeHandled = true;
    bootEvents.stopLoop();
  end);

  os.queueEvent('eventloop_test_external_stop');
  parallel.waitForAll(function() bootEvents.runLoop(true); end, function() verifierEvents.runLoop(true); end);

  testlib.assertTrue(bootProbeHandled);
end);

testlib.test('protected dispatch reports errors and keeps the loop alive', function()
  local errors = {};
  local events = createEventLoop({
    onError = function(eventName, err)
      errors[#errors + 1] = { eventName = eventName, err = err };
    end,
  });
  local siblingCalled = false;
  local laterCalled = false;

  events.register('eventloop_test_protected', function()
    error('handler boom', 0);
  end);

  events.register('eventloop_test_protected', function()
    siblingCalled = true;
    os.queueEvent('eventloop_test_protected_later');
  end);

  events.register('eventloop_test_protected_later', function()
    laterCalled = true;
    events.stopLoop();
  end);

  os.queueEvent('eventloop_test_protected');
  events.runLoop();

  testlib.assertEquals(#errors, 1);
  testlib.assertEquals(errors[1].eventName, 'eventloop_test_protected');
  testlib.assertTrue(string.find(tostring(errors[1].err), 'handler boom', 1, true) ~= nil);
  testlib.assertTrue(siblingCalled);
  testlib.assertTrue(laterCalled);
end);

testlib.test('protected timeout dispatch reports errors and keeps the loop alive', function()
  local errors = {};
  local events = createEventLoop({
    onError = function(eventName, err)
      errors[#errors + 1] = { eventName = eventName, err = err };
    end,
  });
  local laterCalled = false;

  events.setTimeout(function()
    error('timeout boom', 0);
  end, 0);

  events.setTimeout(function()
    laterCalled = true;
    events.stopLoop();
  end, 0);

  events.runLoop();

  testlib.assertEquals(#errors, 1);
  testlib.assertEquals(errors[1].eventName, 'timer');
  testlib.assertTrue(string.find(tostring(errors[1].err), 'timeout boom', 1, true) ~= nil);
  testlib.assertTrue(laterCalled);
end);

testlib.test('timeout handler can stop a loop with persistent handlers', function()
  local events = createEventLoop();

  events.register('eventloop_test_never_fired', function()
  end);

  events.setTimeout(function()
    events.stopLoop();
  end, 0);

  events.runLoop();

  testlib.assertTrue(not events.isRunningLoop());
end);

testlib.test('empty loop returns and runs onStop', function()
  local events = createEventLoop();
  local stopped = false;

  events.onStop(function()
    stopped = true;
  end);

  events.runLoop();

  testlib.assertTrue(stopped);
  testlib.assertTrue(not events.isRunningLoop());
end);

testlib.test('error contracts are enforced', function()
  local events = createEventLoop();
  local function handler()
  end

  events.register('eventloop_test_errors', handler);

  testlib.assertErrors(function()
    events.register('eventloop_test_errors', handler);
  end, 'handler already registered');

  testlib.assertErrors(function()
    events.stopLoop();
  end, 'loop is already stopped');

  testlib.assertErrors(function()
    events.register(1, handler);
  end, 'string expected');

  testlib.assertErrors(function()
    events.register('eventloop_test_errors_2', 'not a function');
  end, 'function expected');
end);

testlib.run();
