local TimerTask = require('resty.concurrent.timer_task')
local ngx_timer = ngx.timer

describe('TimerTask', function()
  local test_task = function() end

  before_each(function()
    TimerTask.active_tasks = {}
    TimerTask.last_one = {}
  end)

  after_each(function()
    -- We need to trigger a GC cycle after each test.
    -- Instances of TimerTask mark themselves as inactive by removing their ID
    -- from TimerTask.active_tasks, which is shared between tests.
    -- The sequence of random numbers generated by resty.jit-uuid starts from
    -- the beginning on each nested describe clause. That means that IDs are
    -- reused from different tests.
    -- That means that the following situation might happen:
    -- A test with some ID runs, a second test that reuses the same ID runs an
    -- assumes that during the test the ID will be marked as active. However,
    -- in the middle of the test a garbage collector cycle is triggered and
    -- runs the __gc method of an instance from the first test. Now the ID is
    -- marked as inactive and the second test fails.
    collectgarbage()
  end)

  describe('.register_task', function()
    it('adds an ID to the list of active tasks', function()
      local id = '1'

      TimerTask.register_task(id)

      assert.is_true(TimerTask.task_is_active(id))
    end)
  end)

  describe('.unregister_task', function()
    local id = '1'

    setup(function()
      TimerTask.register_task(id)
    end)

    it('removes an ID to the list of active tasks', function()
      TimerTask.unregister_task(id)
      assert.is_false(TimerTask.task_is_active(id))
    end)
  end)

  describe(':new', function()
    it('adds the task to the list of active tasks', function()
      local task = TimerTask.new(test_task)

      assert.is_true(TimerTask.task_is_active(task.id))
    end)

    it('sets a default interval of 60s when not specified', function()
      local task = TimerTask.new(test_task)

      assert.equals(60, task.interval)
    end)

    it('allows to set a running interval', function()
      local interval = 10

      local task = TimerTask.new(test_task, { interval = interval })

      assert.equals(interval, task.interval)
    end)

    it('allows to set arguments for the task', function()
      local args = { '1', '2' }

      local task = TimerTask.new(test_task, { args = args })

      assert.same(args, task.args)
    end)
  end)

  describe(':cancel', function()
    it('removes the task from the list of active tasks', function()
      local task = TimerTask.new(test_task)

      task:cancel()

      assert.is_false(TimerTask.task_is_active(task.id))
    end)

    it('marks the task to run for the last time when specified in the params', function()
      local task = TimerTask.new(test_task)

      task:cancel(true)

      assert.is_true(TimerTask.last_one[task.id])
    end)
  end)

  describe(':execute', function()
    local func = test_task
    local ngx_timer_stub

    local args = { '1', '2', '3' }
    local interval = 10

    before_each(function()
      ngx_timer_stub = stub(ngx_timer, 'at')
    end)

    describe('when the task is active', function()
      it('runs the task', function()
        local timer_task = TimerTask.new(func, { args = args, interval = interval })
        local func_spy = spy.on(timer_task, 'task')

        timer_task:execute(true)

        assert.spy(func_spy).was_called_with(unpack(args))
      end)

      it('schedules the next one', function()
        local timer_task = TimerTask.new(func, { args = args, interval = interval })

        timer_task:execute(true)

        assert.stub(ngx_timer_stub).was_called()

        -- Can't check all the arguments of ngx.timer.at because it calls an
        -- private function but at least we can check the interval (first arg),
        -- and that the second argument is a function.
        assert.equals(interval, ngx_timer_stub.calls[1].vals[1])
        assert.is_function(ngx_timer_stub.calls[1].vals[2])
      end)
    end)

    describe('when the task is not active', function()
      it('does not run the task', function()
        local timer_task = TimerTask.new(func, { args = args, interval = interval })
        local func_spy = spy.on(timer_task, 'task')
        timer_task:cancel()

        timer_task:execute(true)

        assert.spy(func_spy).was_not_called()
      end)

      it('does not schedule another task', function()
        local timer_task = TimerTask.new(func, { args = args, interval = interval })
        timer_task:cancel()

        timer_task:execute(true)

        assert.stub(ngx_timer_stub).was_not_called()
      end)
    end)

    describe('when the option to wait an interval instead of running now is passed', function()
      it('does not run the task inmediately', function()
        local timer_task = TimerTask.new(func, { args = args, interval = interval })
        local func_spy = spy.on(timer_task, 'task')

        timer_task:execute(false)

        -- It will be called in 'interval' seconds, but not now
        assert.spy(func_spy).was_not_called()
      end)

      it('schedules the next one', function()
        local timer_task = TimerTask.new(func, { args = args, interval = interval })

        timer_task:execute(false)

        assert.stub(ngx_timer_stub).was_called()
      end)
    end)

    describe('when the task should run for the last time', function()
      it('runs the task', function()
        local timer_task = TimerTask.new(func, { args = args, interval = interval })
        local func_spy = spy.on(timer_task, 'task')
        timer_task:cancel(true)

        timer_task:execute(true)

        assert.spy(func_spy).was_called_with(unpack(args))
      end)

      it('does not schedule another task', function()
        local timer_task = TimerTask.new(func, { args = args, interval = interval })
        timer_task:cancel(true)

        timer_task:execute(true)

        assert.stub(ngx_timer_stub).was_not_called()
      end)
    end)
  end)

  it('cancels itself when it is garbage collected', function()
    local timer_task = TimerTask.new(test_task)
    local id = timer_task.id

    timer_task = nil
    collectgarbage()

    assert.is_false(TimerTask.task_is_active(id))
  end)

  it('does not ensure a last run when garbage collected', function()
    local timer_task = TimerTask.new(test_task)
    local id = timer_task.id

    timer_task = nil
    collectgarbage()

    assert.is_falsy(TimerTask.last_one[id])
  end)
end)
