--------------------------------------------------------------------------------
-- 0100-log.lua: tests for logging system
-- This file is a part of lua-nucleo library
-- Copyright (c) lua-nucleo authors (see file `COPYRIGHT` for the license)
--------------------------------------------------------------------------------

local make_suite = assert(loadfile('test/test-lib/init/strict.lua'))(...)

--------------------------------------------------------------------------------

local is_string,
      is_function
      = import 'lua-nucleo/type.lua'
      {
        'is_string',
        'is_function'
      }

local assert_is_string,
      assert_is_table,
      assert_is_nil,
      assert_is_function,
      assert_is_number
      = import 'lua-nucleo/typeassert.lua'
      {
        'assert_is_string',
        'assert_is_table',
        'assert_is_nil',
        'assert_is_function',
        'assert_is_number'
      }

local tstr,
      tstr_cat,
      tcount_elements,
      tijoin_many,
      tclone,
      tset
      = import 'lua-nucleo/table.lua'
      {
        'tstr',
        'tstr_cat',
        'tcount_elements',
        'tijoin_many',
        'tclone',
        'tset'
      }



local is_table
      = import 'lua-nucleo/type.lua'
      {
        'is_table'
      }

local ensure,
      ensure_equals,
      ensure_strequals,
      ensure_tequals,
      ensure_fails_with_substring
      = import 'lua-nucleo/ensure.lua'
      {
        'ensure',
        'ensure_equals',
        'ensure_strequals',
        'ensure_tequals',
        'ensure_fails_with_substring'
      }

local arguments,
      method_arguments,
      optional_arguments
      = import 'lua-nucleo/args.lua'
      {
        'arguments',
        'method_arguments',
        'optional_arguments'
      }

local create_escape_subst
      = import 'lua-nucleo/string.lua'
      {
        'create_escape_subst'
      }

local make_concatter = import 'lua-nucleo/string.lua' { 'make_concatter' }

local unique_object
      = import 'lua-nucleo/misc.lua'
      {
        'unique_object'
      }

--------------------------------------------------------------------------------

local LOG_LEVEL,
      LOG_FLUSH_MODE,
      FLUSH_SECONDS_DEFAULT,
      END_OF_LOG_MESSAGE,
      format_logsystem_date,
      get_current_logsystem_date,
      make_common_logging_config,
      make_logging_system,
      wrap_file_sink,
      make_loggers,
      log_exports
      = import 'lua-nucleo/log.lua'
      {
        'LOG_LEVEL',
        'LOG_FLUSH_MODE',
        'FLUSH_SECONDS_DEFAULT',
        'END_OF_LOG_MESSAGE',
        'format_logsystem_date',
        'get_current_logsystem_date',
        'make_common_logging_config',
        'make_logging_system',
        'wrap_file_sink',
        'make_loggers'
      }

--------------------------------------------------------------------------------

local test = make_suite("log", log_exports)

--------------------------------------------------------------------------------

test:test_for "LOG_LEVEL" (function()
  assert_is_table(LOG_LEVEL) -- Smoke test
end)

test:test_for "LOG_FLUSH_MODE" (function()
  assert_is_table(LOG_FLUSH_MODE) -- Smoke test
end)

test:test_for "FLUSH_SECONDS_DEFAULT" (function()
  assert_is_number(FLUSH_SECONDS_DEFAULT) -- Smoke test
end)

test:test_for "END_OF_LOG_MESSAGE" (function()
  assert_is_string(END_OF_LOG_MESSAGE) -- Smoke test, arbitrary limit
end)

--------------------------------------------------------------------------------

test:test_for "format_logsystem_date" (function()
  ensure_strequals(
      "format_logsystem_date",
      format_logsystem_date(0),
      os.date("%Y-%m-%d %H:%M:%S", 0) -- Silly smoke test
    )
end)

test:test_for "get_current_logsystem_date" (function()
  local ts_before = 0
  local ts_after = nil
  local formatted_date

  -- We should get time inside one second
  -- to avoid false positives in check below
  local n = 0
  while ts_before ~= ts_after do -- Assuming integer seconds
    ts_before = os.time()
    formatted_date = get_current_logsystem_date()
    ts_after = os.time()
    n = n + 1
    assert(n < 10, "infinite loop detected")
  end

  ensure_strequals(
      "get_current_logsystem_date",
      formatted_date,
      os.date("%Y-%m-%d %H:%M:%S", ts_after)
    )
end)

--------------------------------------------------------------------------------

test:test_for "wrap_file_sink" (function()
  local flush_tag = unique_object()

  local buf = {}

  local file_mock =
  {
    write = function(self, v)
      method_arguments(
          self,
          "string", v
        )
      buf[#buf + 1] = v
    end;

    flush = function(self)
      method_arguments(
          self
        )
      buf[#buf + 1] = flush_tag
    end;
  }

  local sink = assert_is_function(wrap_file_sink(file_mock))

  ensure_equals("aaa", sink("aaa"), sink)
  ensure_equals("flush", sink(END_OF_LOG_MESSAGE), sink)
  ensure_equals("bbb", sink("bbb"), sink)

  ensure_tequals(
      "call order check",
      buf,
      { "aaa", END_OF_LOG_MESSAGE, "bbb" }
    )
end)

--------------------------------------------------------------------------------

test:factory "make_logging_system" (
    make_logging_system, "", function() end, {}
  )

--------------------------------------------------------------------------------

-- same as used in log
local escape_subst = create_escape_subst("\\%03d")

-- TODO: Generalize
local make_test_concatter = function()
  local concatter =
  {
    buf_ = { };
  }

  concatter.cat = function(v)
    local buf = concatter.buf_
    buf[#buf + 1] = assert_is_string(v)
    return concatter.cat
  end

  concatter.reset = function()
    concatter.buf_ = { }
  end

  concatter.buf = function()
    return tclone(concatter.buf_)
  end

  return concatter
end

local check_date_str = function(date_str, time_before, time_after)
  arguments(
      "string", date_str,
      "number", time_before,
      "number", time_after
    )

  assert(time_before <= time_after)

  local t = {}
  t.year, t.month, t.day,
  t.hour, t.min, t.sec = date_str:match(
      "^(%d%d%d%d)-(%d%d)-(%d%d) (%d%d):(%d%d):(%d%d)$"
    )

  local timestamp = ensure("time format valid", os.time(t))

  ensure("time after before", time_before <= timestamp)
  ensure("time before after", timestamp >= time_after)
end

local check_logger = function(
    concatter,
    logger,
    date_checker,
    logging_system_id,
    module_suffix,
    ...
  )
  arguments(
         "table", concatter,
      "function", logger,
      "function", date_checker,
        --"string", logging_system_id,
        "string", module_suffix
    )

  assert(is_function(logging_system_id) or is_string(logging_system_id))

  concatter.reset()

  local ts_before = os.time()
  logger(...)
  local ts_after = os.time()

  local actual = concatter.buf()

  -- Prepend system info to expected data
  local date = assert_is_string(actual[2])
  date_checker(date, ts_before, ts_after)

  if is_function(logging_system_id) then
    logging_system_id = assert_is_string(logging_system_id())
  end

  local expected = { "[", date, "] ", logging_system_id, "[", module_suffix, "] " }

  local function expected_cat(v)
    expected[#expected + 1] = assert_is_string(v)
    return expected_cat
  end

  for i = 1, select("#", ...) do
    local v = select(i, ...)

    if i > 1 then
      expected_cat(" ")
    end

    if is_table(v) then
      expected_cat(tstr(v))
    else
      -- same as used in log
      expected_cat(tostring(v):gsub("[%c%z\128-\255]",  escape_subst))
    end
  end

  expected_cat(END_OF_LOG_MESSAGE)

  local res, err = pcall(ensure_tequals, "check_logger", actual, expected)
  if not res then
    error(
        "err: " .. err
     .. "\nactual:", tstr(actual)
     .. "\nexpected", tstr(expected)
      )
  end

  concatter.reset()
end

local check_make_module_logger = function(
    concatter,
    date_checker,
    logging_system,
    logging_system_id,
    ...
  )
  arguments(
      "table", concatter,
      "function", date_checker,
      "table", logging_system
      --"string", logging_system_id
    )
  for _, log_level in pairs(LOG_LEVEL) do
    local logger = ensure(
        "make_module_logger",
        logging_system:make_module_logger(
            "module_name", log_level, "MOD"
          )
      )

    check_logger(
        concatter, logger, date_checker,
        logging_system_id, "MOD",
        ...
      )
  end
end

--------------------------------------------------------------------------------

test:methods "get_config" -- TODO: no explicit tests
             "make_module_logger"

test "make_module_logger-empty" (function()
  local concatter = make_test_concatter()
  local logging_system_id = "{logger_id} "
  local logging_system = ensure(
      "make logging system",
      make_logging_system(
          logging_system_id,
          concatter.cat,
          make_common_logging_config(tset(LOG_LEVEL))
        )
    )

  check_make_module_logger(
      concatter, check_date_str, logging_system, logging_system_id
      -- Empty
    )
end)

test "make_module_logger-simple" (function()
  local concatter = make_test_concatter()
  local logging_system_id = "{logger_id} "
  local logging_system = ensure(
      "make logging system",
      make_logging_system(
          logging_system_id,
          concatter.cat,
          make_common_logging_config(tset(LOG_LEVEL))
        )
    )

  check_make_module_logger(
      concatter, check_date_str, logging_system, logging_system_id,
      42, "embedded\0zero", nil, true, nil
    )
end)

test "make_module_logger-id-callback-simple" (function()
  local concatter = make_test_concatter()
  local logging_system_id = "{logger_id} "
  local logging_system = ensure(
      "make logging system",
      make_logging_system(
          function() return logging_system_id end,
          concatter.cat,
          make_common_logging_config(tset(LOG_LEVEL))
        )
    )

  check_make_module_logger(
      concatter, check_date_str, logging_system, logging_system_id,
      42, "embedded\0zero", nil, true, nil
    )
end)

test "make_module_logger-id-callback-dynamic" (function()
  local concatter = make_test_concatter()

  local make_logging_system_id = function()
    local count = 0
    return function()
      count = count + 1
      return tostring(count)
    end
  end

  local logging_system = ensure(
      "make logging system",
      make_logging_system(
          make_logging_system_id(),
          concatter.cat,
          make_common_logging_config(tset(LOG_LEVEL))
        )
    )

  check_make_module_logger(
      concatter, check_date_str, logging_system, make_logging_system_id(),
      42, "embedded\0zero", nil, true, nil
    )
end)

test "make_module_logger-get_current_logsystem_date-simple" (function()
  local concatter = make_test_concatter()
  local logging_system_id = "{logger_id} "

  local date = "MYDATE"
  local date_checker = function(date_str, time_before, time_after)
    ensure_strequals("date match expected", date_str, date)
  end

  local logging_system = ensure(
      "make logging system",
      make_logging_system(
          logging_system_id,
          concatter.cat,
          make_common_logging_config(tset(LOG_LEVEL)),
          function() return date end
        )
    )

  check_make_module_logger(
      concatter, date_checker, logging_system, logging_system_id,
      42, "embedded\0zero", nil, true, nil
    )
end)

test "make_module_logger-table" (function()
  local concatter = make_test_concatter()
  local logging_system_id = "{logger_id} "
  local logging_system = ensure(
      "make logging system",
      make_logging_system(
          logging_system_id,
          concatter.cat,
          make_common_logging_config(tset(LOG_LEVEL))
        )
    )

  check_make_module_logger(
      concatter, check_date_str, logging_system, logging_system_id,
      nil, { [{ 42, nil, 24 }] = { a = 42 } }, nil
    )
end)

test "make_module_logger-binary_data" (function()
  local concatter = make_test_concatter()
  local logging_system_id = "{logger_id} "
  local logging_system = ensure(
      "make logging system",
      make_logging_system(
          logging_system_id,
          concatter.cat,
          make_common_logging_config(tset(LOG_LEVEL))
        )
    )

  local str_test = ""
  for i = 0, 255 do
    str_test = str_test .. string.char(i)
  end

  check_make_module_logger(
      concatter, check_date_str, logging_system, logging_system_id, str_test
    )
end)

--------------------------------------------------------------------------------

test:factory "make_common_logging_config" (
    make_common_logging_config
  )

--------------------------------------------------------------------------------

local check_is_log_enabled = function(
    levels_config,
    modules_config,
    module_name,
    log_level,
    actually_enabled
  )
  optional_arguments(
      "table", levels_config,
      "table", modules_config
    )
  arguments(
       "string", module_name,
       "number", log_level,
      "boolean", actually_enabled
    )

  local concatter = make_test_concatter()

  local logging_system_id = "{logger_id} "
  local module_suffix = "MOD"

  local common_logging_config = make_common_logging_config(
      levels_config,
      modules_config
    )

  local logging_system = make_logging_system(
      logging_system_id,
      concatter.cat,
      common_logging_config
    )

  ensure_equals(
      "is_log_enabled",
      common_logging_config:is_log_enabled(module_name, log_level),
      actually_enabled
    )

  local logger = ensure(
      "make_module_logger",
      logging_system:make_module_logger(
          module_name, log_level, module_suffix
        )
    )

  if actually_enabled then
    check_logger(
        concatter, logger, check_date_str,
        logging_system_id, module_suffix,
        42, "embedded\0zero", nil, true, nil
      )
  else
    logger(42, "embedded\0zero", nil, true, nil)
    ensure_equals("logging is disabled", next(concatter.buf()), nil)
  end

  actually_enabled = not actually_enabled

  common_logging_config:set_log_enabled(module_name, log_level, actually_enabled)

  if actually_enabled then
    check_logger(
        concatter, logger, check_date_str,
        logging_system_id, module_suffix,
        42, "embedded\0zero", nil, true, nil
      )
  else
    logger(42, "embedded\0zero", nil, true, nil)
    ensure_equals("logging is disabled", next(concatter.buf()), nil)
  end
end

test:methods "set_log_enabled" -- TODO: no explicit tests found
             "is_log_enabled"

test "is_log_enabled-levels-default" (function()
  local module_name = "module_name"
  local levels_config = nil -- Default
  local modules_config = nil -- Default
  for _, log_level in pairs(LOG_LEVEL) do
    check_is_log_enabled(
        levels_config,
        modules_config,
        module_name,
        log_level,
        false -- actually disabled
      )
  end
end)

test "is_log_enabled-modules-default" (function()
  local module_name = "module_name"
  local levels_config = tset(LOG_LEVEL) -- Enable all levels
  levels_config[LOG_LEVEL.ERROR] = false

  local modules_config = nil -- Default
  for _, log_level in pairs(LOG_LEVEL) do
    check_is_log_enabled(
        levels_config,
        modules_config,
        module_name,
        log_level,
        levels_config[log_level]
      )
  end
end)

test "is_log_enabled-modules-false" (function()
  local module_name = "module_name"
  local other_module_name = "other_module_name"
  local levels_config = tset(LOG_LEVEL) -- Enable all levels
  local modules_config = { [module_name] = false }

  for _, log_level in pairs(LOG_LEVEL) do
    check_is_log_enabled(
        levels_config,
        modules_config,
        module_name,
        log_level,
        false
      )

    check_is_log_enabled( -- To ensure defaults are the same
        levels_config,
        modules_config,
        other_module_name,
        log_level,
        true
      )
  end
end)

test "is_log_enabled-modules-true" (function()
  local module_name = "module_name"
  local other_module_name = "other_module_name"
  local levels_config = tset(LOG_LEVEL) -- Enable all levels
  local modules_config = { [module_name] = true }

  for _, log_level in pairs(LOG_LEVEL) do
    check_is_log_enabled(
        levels_config,
        modules_config,
        module_name,
        log_level,
        true
      )

    check_is_log_enabled( -- To ensure defaults are the same
        levels_config,
        modules_config,
        other_module_name,
        log_level,
        true
      )
  end
end)

test "is_log_enabled-modules-empty" (function()
  local module_name = "module_name"
  local other_module_name = "other_module_name"
  local levels_config = tset(LOG_LEVEL) -- Enable all levels
  local modules_config = { [module_name] = { } }

  for _, log_level in pairs(LOG_LEVEL) do
    check_is_log_enabled(
        levels_config,
        modules_config,
        module_name,
        log_level,
        false
      )

    check_is_log_enabled( -- To ensure defaults are the same
        levels_config,
        modules_config,
        other_module_name,
        log_level,
        true
      )
  end
end)

test "is_log_enabled-modules-levels" (function()
  local module_config = { [LOG_LEVEL.LOG] = true, [LOG_LEVEL.SPAM] = false }
  local module_name = "module_name"
  local other_module_name = "other_module_name"
  local levels_config = tset(LOG_LEVEL) -- Enable all levels
  local modules_config = { [module_name] = module_config }

  for _, log_level in pairs(LOG_LEVEL) do
    local actually_enabled = module_config[log_level]
    if actually_enabled == nil then
      actually_enabled = false
    end

    check_is_log_enabled(
        levels_config,
        modules_config,
        module_name,
        log_level,
        actually_enabled
      )

    check_is_log_enabled( -- To ensure defaults are the same
        levels_config,
        modules_config,
        other_module_name,
        log_level,
        true
      )
  end
end)

--------------------------------------------------------------------------------

test:tests_for "make_loggers"

--------------------------------------------------------------------------------

test "make_loggers-complex" (function()
  local concatter = make_test_concatter()

  local module_name = "module_name"
  local module_prefix = "MOD"
  local logging_system_id = "{logger_id} "

  local levels_config = tset(LOG_LEVEL) -- Enable all levels
  local modules_config = nil -- Default

  local common_logging_config = make_common_logging_config(
      levels_config,
      modules_config
    )

  local logging_system = make_logging_system(
      logging_system_id,
      concatter.cat,
      common_logging_config
    )

  local loggers_info =
  {
    { suffix = " ", level = LOG_LEVEL.LOG   };
    { suffix = "*", level = LOG_LEVEL.DEBUG };
    { suffix = "#", level = LOG_LEVEL.SPAM  };
    { suffix = "!", level = LOG_LEVEL.ERROR };
  }

  local loggers =
  {
    make_loggers(module_name, module_prefix, loggers_info, logging_system)
  }

  ensure_equals("number of loggers", #loggers, #loggers_info)

  -- Check default values
  for i = 1, #loggers_info do
    local logger_info = loggers_info[i]
    ensure_equals(
        "module logging on",
        common_logging_config:is_log_enabled(module_name, logger_info.level),
        true
      )
  end

  for i = 1, #loggers_info do
    local logger_info = loggers_info[i]
    local logger = loggers[i]

    local log_level = logger_info.level
    local module_suffix = module_prefix .. logger_info.suffix

    common_logging_config:set_log_enabled(module_name, log_level, true)

    check_logger(
        concatter, logger, check_date_str,
        logging_system_id, module_suffix,
        42, "embedded\0zero", nil, true, nil
      )

    common_logging_config:set_log_enabled(module_name, log_level, false)

    logger(42, "embedded\0zero", nil, true, nil)
    ensure_equals("logging is disabled", next(concatter.buf()), nil)
  end
end)
