--------------------------------------------------------------------------------
-- 0020-args.lua: tests for various utilities related to function arguments
-- This file is a part of lua-nucleo library
-- Copyright (c) lua-nucleo authors (see file `COPYRIGHT` for the license)
--------------------------------------------------------------------------------

local unpack = unpack or table.unpack

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

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

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

local nargs,
      pack,
      eat_true,
      amap,
      arguments,
      method_arguments,
      optional_arguments,
      args_exports
      = import 'lua-nucleo/args.lua'
      {
        'nargs',
        'pack',
        'eat_true',
        'amap',
        'arguments',
        'method_arguments',
        'optional_arguments'
      }

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

local test = make_suite("args", args_exports)

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

test:test_for "nargs" (function()
  local t1 = "a"
  local t2 = "b"
  local t3 = "c"
  ensure_equals("args number", nargs(t1, t2, t3),3)
  ensure_equals("args number", nargs(t1, nil, t3),3)
  ensure_equals("args number", table.concat({nargs(t1, t2, t3)}), "3abc")
end)

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

test:test_for "pack" (function()
  local t1 = "a"
  local t2 = "b"
  local t3 = "c"
  ensure_equals("args number", pack(t1, t2, t3), 3)
  ensure_equals("args number", pack(t1, nil, t3), 3)
  local num, tbl = pack(t1, t2, t3)
  ensure_equals("args", table.concat(tbl), "abc")
end)

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

test:test_for "eat_true" (function()
  ensure_returns("no args", 0, { }, eat_true(true))
  ensure_returns("one arg", 1, { 42 }, eat_true(true, 42))
  ensure_returns(
      "nils",
      4, { nil, nil, "embedded\0zero", nil },
      eat_true(true, nil, nil, "embedded\0zero", nil)
    )

  ensure_fails_with_substring(
      "no args at all",
      function() eat_true() end,
      "can't eat true, got nil"
    )

  ensure_fails_with_substring(
      "false",
      function() eat_true(false) end,
      "can't eat true, got false"
    )

  ensure_fails_with_substring(
      "nil",
      function() eat_true(nil) end,
      "can't eat true, got nil"
    )

  ensure_fails_with_substring(
      "data",
      function() eat_true(42) end,
      "can't eat true, got 42"
    )

  ensure_fails_with_substring(
      "data, string",
      function() eat_true(42, "MYERROR") end,
      "can't eat true, got 42"
    )

  ensure_fails_with_substring(
      "error message",
      function() eat_true(nil, "MYERROR") end,
      "can't eat true:\nMYERROR"
    )

  ensure_fails_with_substring(
      "nil, true",
      function() eat_true(nil, true) end,
      "can't eat true, got nil"
    )
end)

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

do
  local check_arguments = function(arguments_fn, name, self, ...)
    -- Note self is ignored
    -- TODO: Use name in error message
    arguments_fn(self, ...)
  end

  local check_arguments_fail = function(arguments_fn, name, expected_msg, self, ...)
    -- Note self is ignored
    local n, args = pack(...)
    ensure_fails_with_substring(
        name,
        function() arguments_fn(self, unpack(args, 1, n)) end,
        expected_msg
      )
  end

  -- TODO: Generalize copy-paste with below
  local run_arguments_tests = function(arguments, check_arguments, check_arguments_fail)
    local self = {} -- Note only good self is tested

         check_arguments(arguments, "empty", self)
    check_arguments_fail(arguments, "bad type dangling", "arguments: bad call, dangling argument detected", self, "garbage")
    check_arguments_fail(arguments, "bad type", "argument #1: bad expected type `garbage'", self, "garbage", nil)
    check_arguments_fail(arguments, "bad type: false dangling", "arguments: bad call, dangling argument detected", self, false)
    check_arguments_fail(arguments, "bad type: false", "argument #1: bad expected type `false'", self, false, nil)
    check_arguments_fail(arguments, "bad type with value", "argument #1: bad expected type `garbage'", self, "garbage", "value")
         check_arguments(arguments, "nil", self, "nil", nil)
    check_arguments_fail(arguments, "bad type tail dangling", "arguments: bad call, dangling argument detected", self, "nil", nil, "tail garbage")
    check_arguments_fail(arguments, "bad type tail", "argument #2: bad expected type `tail garbage'", self, "nil", nil, "tail garbage", nil)
         check_arguments(arguments, "boolean", self, "boolean", false)
         check_arguments(arguments, "many args", self, "boolean", false, "nil", nil, "number", 42)
    check_arguments_fail(arguments, "bad in the middle", "argument #2: expected `nil', got `number'", self, "boolean", false, "nil", 42, "number", 42)
    check_arguments_fail(arguments, "nil at the end", "argument #2: expected `table', got `nil'", self, "boolean", false, "table", nil)
    check_arguments_fail(arguments, "false at the end fails", "argument #2: expected `table', got `boolean'", self, "boolean", false, "table", false)
    check_arguments_fail(arguments, "bad at the end", "argument #3: expected `number', got `table'", self, "boolean", false, "nil", nil, "number", {})

    do
      local called = false
      local fn = function() called = true end

      check_arguments(arguments, "function", self, "function", fn)
      ensure_equals("function should be not called", called, false)
    end

    do
      local called = false
      local fn = function() called = true end

      check_arguments_fail(arguments, "bad function", "argument #1: expected `number', got `function'", self, "number", fn)
      ensure_equals("function as bad argument should be not called", called, false)
    end

    check_arguments_fail(arguments, "first extra nil", "arguments: bad call, dangling argument detected", self, nil)
    check_arguments_fail(arguments, "second extra nil", "arguments: bad call, dangling argument detected", self, "number", 42, nil)
    check_arguments_fail(arguments, "nil, second extra nil", "arguments: bad call, dangling argument detected", self, "nil", nil, nil)
  end

  test:test_for "arguments" (function()
    run_arguments_tests(
        function(self, ...) return arguments(...) end, -- Filter out self
        check_arguments,
        check_arguments_fail
      )
  end)

  test:test_for "method_arguments" (function()
    run_arguments_tests(
        method_arguments,
        check_arguments,
        check_arguments_fail
      )

    -- Additional tests for bad self

    check_arguments_fail(method_arguments, "missing self", "bad self %(got `nil'%); use `:'")
    check_arguments_fail(method_arguments, "missing self, have args", "bad self %(got `string'%); use `:'", "number", 42)
    check_arguments_fail(method_arguments, "missing self, have args", "bad self %(got `nil'%); use `:'", nil, "number", 42)
  end)
end

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

do
  local check_arguments = function(arguments_fn, name, self, ...)
    -- Note self is ignored
    -- TODO: Use name in error message
    arguments_fn(self, ...)
  end

  local check_arguments_fail = function(arguments_fn, name, expected_msg, self, ...)
    -- Note self is ignored
    local n, args = pack(...)
    ensure_fails_with_substring(
        name,
        function() arguments_fn(self, unpack(args, 1, n)) end,
        expected_msg
      )
  end

  -- TODO: Generalize copy-paste with above
  local run_optional_arguments_tests = function(arguments, check_arguments, check_arguments_fail)
    local self = {} -- Note only good self is tested

         check_arguments(arguments, "empty", self)
    check_arguments_fail(arguments, "bad type dangling", "arguments: bad call, dangling argument detected", self, "garbage")
    check_arguments_fail(arguments, "bad type", "argument #1: bad expected type `garbage'", self, "garbage", nil)
    check_arguments_fail(arguments, "bad type: false dangling", "arguments: bad call, dangling argument detected", self, false)
    check_arguments_fail(arguments, "bad type: false", "argument #1: bad expected type `false'", self, false, nil)
    check_arguments_fail(arguments, "bad type with value", "argument #1: bad expected type `garbage'", self, "garbage", "value")
         check_arguments(arguments, "nil", self, "nil", nil)
    check_arguments_fail(arguments, "bad type tail dangling", "arguments: bad call, dangling argument detected", self, "nil", nil, "tail garbage")
    check_arguments_fail(arguments, "bad type tail", "argument #2: bad expected type `tail garbage'", self, "nil", nil, "tail garbage", nil)
         check_arguments(arguments, "boolean", self, "boolean", false)
         check_arguments(arguments, "many args", self, "boolean", false, "nil", nil, "number", 42)
    check_arguments_fail(arguments, "bad in the middle", "argument #2: expected `nil', got `number'", self, "boolean", false, "nil", 42, "number", 42)
         check_arguments(arguments, "nil does not fail", self, "boolean", false, "table", nil)
    check_arguments_fail(arguments, "false at the end fails", "argument #2: expected `table', got `boolean'", self, "boolean", false, "table", false)
    check_arguments_fail(arguments, "bad at the end", "argument #3: expected `number', got `table'", self, "boolean", false, "nil", nil, "number", {})

    do
      local called = false
      local fn = function() called = true end

      check_arguments(arguments, "function", self, "function", fn)
      ensure_equals("function should be not called", called, false)
    end

    do
      local called = false
      local fn = function() called = true end

      check_arguments_fail(arguments, "bad function", "argument #1: expected `number', got `function'", self, "number", fn)
      ensure_equals("function as bad argument should be not called", called, false)
    end

    check_arguments_fail(arguments, "first extra nil", "arguments: bad call, dangling argument detected", self, nil)
    check_arguments_fail(arguments, "second extra nil", "arguments: bad call, dangling argument detected", self, "number", 42, nil)
    check_arguments_fail(arguments, "nil, second extra nil", "arguments: bad call, dangling argument detected", self, "nil", nil, nil)
  end

  test:test_for "optional_arguments" (function()
    run_optional_arguments_tests(
        function(self, ...) return optional_arguments(...) end, -- Filter out self
        check_arguments,
        check_arguments_fail
      )
  end)
end

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

test:UNTESTED 'amap'
test:UNTESTED 'check_types'
