local test_env = {}

local lfs = require("lfs")
local versions = require("spec.util.versions")

local help_message = [[
LuaRocks test-suite

INFORMATION
   New test-suite for LuaRocks project, using unit testing framework Busted.
REQUIREMENTS
   Be sure sshd is running on your system, or use '--exclude-tags=ssh',
   to not execute tests which require sshd.
USAGE
   busted [-Xhelper <arguments>]
ARGUMENTS
   env=<type>             Set type of environment to use ("minimal" or "full",
                          default: "minimal").
   noreset                Don't reset environment after each test
   clean                  Remove existing testing environment.
   ci                     Add if running on Unix CI.
   ci-windows             Add if running on Windows CI.
   os=<type>              Set OS ("linux", "osx", or "windows").
   lua_dir=<path>         Path of Lua installation (default "/usr/local")
   lua=<lua>              Name of the interpreter, may be full path (default "lua")
]]

local function help()
   print(help_message)
   os.exit(1)
end

local function title(str)
   print()
   print(("-"):rep(#str))
   print(str)
   print(("-"):rep(#str))
end

local dir_sep = package.config:sub(1, 1)
local function P(p)
   return (p:gsub("/", dir_sep))
end

local function dir_path(...)
   return P((table.concat({ ... }, "/"):gsub("\\", "/"):gsub("/+", "/")))
end

local function C(...)
   return table.concat({...}, " ")
end

--- Quote argument for shell processing. Fixes paths on Windows.
-- Adds double quotes and escapes. Based on function in fs/win32.lua.
-- @param arg string: Unquoted argument.
-- @return string: Quoted argument.
local function Q(arg)
   if test_env.TEST_TARGET_OS == "windows" then
      local drive_letter = "[%.a-zA-Z]?:?[\\/]"
      -- Quote DIR for Windows
      if arg:match("^"..drive_letter)  then
         arg = P(arg)
      end

      if arg == "\\" then
         return '\\' -- CHDIR needs special handling for root dir
      end

      return '"' .. arg .. '"'
   else
      return "'" .. arg:gsub("'", "'\\''") .. "'"
   end
end

local function V(str)
   return (str:gsub("${([^}]-)}", function(name)
      name = name:lower()
      local prefix, suffix = name:match("^(.*)_(.)$")
      if suffix then
         name = prefix
         local d = assert(versions[name])
         local v, r = d:match("^([^-]*)%-(%d*)$")
         if suffix == "d" then
            return d
         elseif suffix == "v" then
            return v
         elseif suffix == "r" then
            return r
         else
            print("Test error: invalid suffix " .. suffix .. " in variable " .. name)
            os.exit(1)
         end
      else
         if not versions[name] then
            print("Test error: no version definition for " .. name)
            os.exit(1)
         end
         return versions[name]
      end
   end))
end

local function tool(name)
   if test_env.TEST_TARGET_OS == "windows" then
      return Q(dir_path(test_env.testing_paths.win_tools, name .. ".exe"))
   else
      return name
   end
end

local os_remove = os.remove
os.remove = function(f) -- luacheck: ignore
   return os_remove(V(f))
end

local os_rename = os.rename
os.rename = function(a, b) -- luacheck: ignore
   return os_rename(V(a), V(b))
end

-- Monkeypatch incorrect tmpname's on some Lua distributions for Windows
local os_tmpname = os.tmpname
os.tmpname = function() -- luacheck:ignore
   local name = os_tmpname()
   if name:sub(1, 1) == '\\' then
      name = os.getenv "TEMP"..name
   end
   return name
end

local lfs_chdir = lfs.chdir
lfs.chdir = function(d) -- luacheck: ignore
   return lfs_chdir(V(d))
end

local lfs_attributes = lfs.attributes
lfs.attributes = function(f, ...) -- luacheck: ignore
   return lfs_attributes(V(f), ...)
end

local function exists(path)
   return lfs.attributes(path, "mode") ~= nil
end

function test_env.file_if_exists(path)
   return lfs.attributes(path, "mode") and path
end

function test_env.quiet(command)
   if not test_env.VERBOSE then
      if test_env.TEST_TARGET_OS == "windows" then
         return command .. " 1> NUL 2> NUL"
      else
         return command .. " 1> /dev/null 2> /dev/null"
      end
   else
      return command
   end
end

function test_env.copy(source, destination)
   source = V(source)
   destination = V(destination)

   local r_source, r_destination, err
   r_source, err = io.open(source, "r")
   if err then
      print(debug.traceback())
      os.exit(1)
   end

   r_destination, err = io.open(destination, "w")
   if err then
      print(debug.traceback())
      os.exit(1)
   end

   while true do
      local block = r_source:read(8192)
      if not block then break end
      r_destination:write(block)
   end

   r_source:close()
   r_destination:close()
end

function test_env.get_tmp_path()
   local path = os.tmpname()
   if test_env.TEST_TARGET_OS == "windows" and not path:find(":") then
      path = dir_path(os.getenv("TEMP"), path)
   end
   os.remove(path)
   return path
end

--- Helper function that runs the given function inside
-- a temporary directory, isolating it
-- @param f function: the function to be run
function test_env.run_in_tmp(f, finally)
   local olddir = lfs.currentdir()
   local tmpdir = test_env.get_tmp_path()
   lfs.mkdir(tmpdir)
   lfs.chdir(tmpdir)

   if not finally then
      error("run_in_tmp needs a finally argument")
   end

   -- for unit tests, so that current dir known by luarocks.fs (when running with non-lfs)
   -- is synchronized with actual lfs (system) current dir
   local fs = require("luarocks.fs")
   if not fs.change_dir then
      local cfg = require("luarocks.core.cfg")
      cfg.init()
      fs.init()
   end
   fs.change_dir(tmpdir)

   local lr_config = test_env.env_variables.LUAROCKS_CONFIG

   test_env.copy(lr_config, lr_config .. ".bak")

   finally(function()
      test_env.copy(lr_config .. ".bak", lr_config)
      lfs.chdir(olddir)
      lfs.rmdir(tmpdir)
      fs.change_dir(olddir)
   end)

   f(tmpdir)
end

--- Helper function for execute_bool and execute_output
-- @param command string: command to execute
-- @param print_command boolean: print command if 'true'
-- @param env_variables table: table of environment variables to export {FOO="bar", BAR="foo"}
-- @return final_command string: concatenated command to execution
function test_env.execute_helper(command, print_command, env_variables)
   local final_command = ""

   if print_command then
      print("[EXECUTING]: " .. command)
   end

   local unset_variables = {
      "LUA_PATH",
      "LUA_CPATH",
      "LUA_PATH_5_2",
      "LUA_CPATH_5_2",
      "LUA_PATH_5_3",
      "LUA_CPATH_5_3",
      "LUA_PATH_5_4",
      "LUA_CPATH_5_4",
      "LUA_PATH_5_5",
      "LUA_CPATH_5_5",
      "LUAROCKS_SYSCONFDIR",
   }

   if env_variables then
      if test_env.TEST_TARGET_OS == "windows" then
         for _, k in ipairs(unset_variables) do
            final_command = final_command .. "set " .. k .. "=&"
         end
         for k,v in pairs(env_variables) do
            final_command = final_command .. "set " .. k .. "=" .. v .. "&"
         end
         final_command = final_command:sub(1, -2) .. "&"
      else
         for _, k in ipairs(unset_variables) do
            final_command = final_command .. "unset " .. k .. "; "
         end
         final_command = final_command .. "export "
         for k,v in pairs(env_variables) do
            final_command = final_command .. k .. "='" .. v .. "' "
         end
            -- remove last space and add ';' to separate exporting variables from command
            final_command = final_command:sub(1, -2) .. "; "
      end
   end

   final_command = final_command .. command .. " 2>&1"

   return final_command
end

function test_env.execute(cmd)
   local ok = os.execute(cmd)
   return (ok == true or ok == 0) -- normalize Lua 5.1 output to boolean
end

--- Execute command and returns true/false
-- @return true/false boolean: status of the command execution
local function execute_bool(command, print_command, env_variables)
   command = test_env.execute_helper(command, print_command, env_variables)

   local redirect_filename
   local redirect = ""
   if print_command ~= nil then
      redirect_filename = dir_path(test_env.testing_paths.luarocks_tmp, "output.txt")
      redirect = " > " .. redirect_filename
      os.remove(redirect_filename)
   end
   local ok = test_env.execute(command .. redirect)
   if redirect ~= "" then
      if not ok or test_env.VERBOSE then
         local fd = io.open(redirect_filename, "r")
         if fd then
            print(fd:read("*a"))
            fd:close()
         end
      end
      os.remove(redirect_filename)
   end
   return ok
end

--- Execute command and returns output of command
-- @return output string: output the command execution
local function execute_output(command, print_command, env_variables)
   command = test_env.execute_helper(command, print_command, env_variables)

   local file = assert(io.popen(command))
   local output = file:read('*all')
   file:close()
   return (output:gsub("\r\n", "\n"):gsub("\n$", "")) -- remove final newline
end

--- Set test_env.LUA_V or test_env.LUAJIT_V based
-- on version of Lua used to run this script.
function test_env.set_lua_version()
   if _G.jit then
      test_env.LUAJIT_V = _G.jit.version:match("(2%.%d)%.%d")
      test_env.lua_version = "5.1"
   else
      test_env.LUA_V = _VERSION:match("5%.%d")
      test_env.lua_version = test_env.LUA_V
   end
end

--- Set all arguments from input into global variables
function test_env.set_args()
   -- if at least Lua/LuaJIT version argument was found on input start to parse other arguments to env. variables
   test_env.TYPE_TEST_ENV = "minimal"
   test_env.RESET_ENV = true

   for _, argument in ipairs(arg) do
      if argument:find("^env=") then
         test_env.TYPE_TEST_ENV = argument:match("^env=(.*)$")
      elseif argument == "noreset" then
         test_env.RESET_ENV = false
      elseif argument == "clean" then
         test_env.TEST_ENV_CLEAN = true
      elseif argument == "verbose" then
         test_env.VERBOSE = true
      elseif argument == "ci" then
         test_env.CI = true
      elseif argument == "ci-windows" then
         test_env.CI_WINDOWS = true
      elseif argument:find("^os=") then
         test_env.TEST_TARGET_OS = argument:match("^os=(.*)$")
      elseif argument == "mingw" then
         test_env.MINGW = true
      elseif argument == "vs" then
         test_env.MINGW = false
      elseif argument:find("^lua_dir=") then
         test_env.LUA_DIR = argument:match("^lua_dir=(.*)$")
      elseif argument:find("^lua=") then
         test_env.LUA = argument:match("^lua=(.*)$")
      else
         help()
      end
   end

   if not test_env.TEST_TARGET_OS then
      title("OS CHECK")

      if dir_sep == "\\" then
         test_env.TEST_TARGET_OS = "windows"
         if test_env.CI_WINDOWS then
            if test_env.MINGW then
               test_env.OPENSSL_INCDIR = "c:\\msys64\\ucrt64\\include"
               test_env.OPENSSL_LIBDIR = "c:\\msys64\\ucrt64\\lib"

               test_env.ZLIB_INCDIR = test_env.OPENSSL_INCDIR
               test_env.ZLIB_LIBDIR = test_env.OPENSSL_LIBDIR

               test_env.BZ2_INCDIR = test_env.OPENSSL_INCDIR
               test_env.BZ2_LIBDIR = test_env.OPENSSL_LIBDIR
            else
               test_env.OPENSSL_INCDIR = "c:\\external\\include"
               test_env.OPENSSL_LIBDIR = "c:\\external\\lib"
            end
         end
      else
         local system = execute_output("uname -s")
         if system == "Linux" then
            test_env.TEST_TARGET_OS = "linux"
            if test_env.CI then
               test_env.OPENSSL_INCDIR = "/usr/include"
               test_env.OPENSSL_LIBDIR = "/usr/lib/x86_64-linux-gnu"
            end
         elseif system == "Darwin" then
            test_env.TEST_TARGET_OS = "osx"
            if test_env.CI then
               if exists("/opt/homebrew/opt/openssl@3/include") then
                  test_env.OPENSSL_INCDIR = "/opt/homebrew/opt/openssl@3/include"
                  test_env.OPENSSL_LIBDIR = "/opt/homebrew/opt/openssl@3/lib"
               elseif exists("/opt/homebrew/opt/openssl@1.1/include") then
                  test_env.OPENSSL_INCDIR = "/opt/homebrew/opt/openssl@1.1/include"
                  test_env.OPENSSL_LIBDIR = "/opt/homebrew/opt/openssl@1.1/lib"
               elseif exists("/opt/homebrew/opt/openssl/include") then
                  test_env.OPENSSL_INCDIR = "/opt/homebrew/opt/openssl/include"
                  test_env.OPENSSL_LIBDIR = "/opt/homebrew/opt/openssl/lib"
               else
                  test_env.OPENSSL_INCDIR = "/usr/local/opt/openssl/include"
                  test_env.OPENSSL_LIBDIR = "/usr/local/opt/openssl/lib"
               end
            end
         end
      end
      print(test_env.TEST_TARGET_OS)
   end

   if test_env.TEST_TARGET_OS == "windows" then
      test_env.lib_extension = "dll"
   else
      test_env.lib_extension = "so"
   end

   test_env.openssl_dirs = ""
   if test_env.OPENSSL_INCDIR then
      test_env.openssl_dirs = C("OPENSSL_INCDIR=" .. test_env.OPENSSL_INCDIR,
                                "OPENSSL_LIBDIR=" .. test_env.OPENSSL_LIBDIR)
   end

   return true
end

function test_env.copy_dir(source_path, target_path)
   source_path = V(source_path)
   target_path = V(target_path)

   local flag = test_env.TEST_TARGET_OS == "windows" and "-R" or "-a"
   os.execute(C(tool("cp"), flag, dir_path(source_path, "."), target_path))
end

--- Remove directory recursively
-- @param path string: directory path to delete
function test_env.remove_dir(path)
   path = V(path)

   if exists(path) then
      for file in lfs.dir(path) do
         if file ~= "." and file ~= ".." then
            local full_path = dir_path(path, file)

            if lfs.attributes(full_path, "mode") == "directory" then
               test_env.remove_dir(full_path)
            else
               os.remove(full_path)
            end
         end
      end
   end
   lfs.rmdir(path)
end

--- Remove subdirectories of a directory that match a pattern
-- @param path string: path to directory
-- @param pattern string: pattern matching basenames of subdirectories to be removed
function test_env.remove_subdirs(path, pattern)
   path = V(path)

   if exists(path) then
      for file in lfs.dir(path) do
         if file ~= "." and file ~= ".." then
            local full_path = dir_path(path, file)

            if lfs.attributes(full_path, "mode") == "directory" and file:find(pattern) then
               test_env.remove_dir(full_path)
            end
         end
      end
   end
end

--- Remove files matching a pattern
-- @param path string: directory where to delete files
-- @param pattern string: pattern matching basenames of files to be deleted
-- @return result_check boolean: true if one or more files deleted
function test_env.remove_files(path, pattern)
   path = V(path)

   local result_check = false
   if exists(path) then
      for file in lfs.dir(path) do
         if file ~= "." and file ~= ".." then
            if file:find(pattern) then
               if os.remove(dir_path(path, file)) then
                  result_check = true
               end
            end
         end
      end
   end
   return result_check
end


--- Function for downloading rocks and rockspecs
-- @param urls table: array of full names of rocks/rockspecs to download
-- @param save_path string: path to directory, where to download rocks/rockspecs
-- @return make_manifest boolean: true if new rocks downloaded
local function download_rocks(urls, save_path)
   local luarocks_repo = "https://luarocks.org/"

   local to_download = {}
   local fixtures = {}
   for _, url in ipairs(urls) do
      url = V(url)

      if url:match("^spec/fixtures") then
         table.insert(fixtures, P(url:gsub("^spec/fixtures", test_env.testing_paths.fixtures_dir)))
      else
         -- check if already downloaded
         if not exists(dir_path(save_path, url)) then
            table.insert(to_download, ((luarocks_repo .. url):gsub("org//", "org/")))
         end
      end
   end

   if #fixtures > 0 then
      os.execute(C(tool("cp"), table.concat(fixtures, " "), save_path))
   end

   if #to_download > 0 then
      local ok = execute_bool(C(tool("wget"), "--no-check-certificate -cP", save_path, table.concat(to_download, " ")))
      if not ok then
         os.exit(1)
      end
   end

   return (#fixtures > 0) or (#to_download > 0)
end

--- Create a file containing a string.
-- @param pathname string: path to file.
-- @param str string: content of the file.
function test_env.write_file(pathname, str, finally)
   pathname = V(pathname)

   local file = assert(io.open(pathname, "wb"))
   file:write(str)
   file:close()
   if finally then
      finally(function()
         os.remove(pathname)
      end)
   end
end

--- Create environment variables needed for tests
-- @param testing_paths table: table with paths to testing directory
-- @return env_variables table: table with created environment variables
local function create_env(testing_paths)
   local lua_v = _VERSION:gsub("Lua ", "")
   local testrun_dir = test_env.testing_paths.testrun_dir
   local lrprefix = testing_paths.testing_lrprefix
   local tree = testing_paths.testing_tree
   local sys_tree = testing_paths.testing_sys_tree
   local deps_tree = testing_paths.testing_deps_tree

   if test_env.LUAJIT_V then
      lua_v="5.1"
   end

   local env_variables = {}
   env_variables.GNUPGHOME = testing_paths.gpg_dir
   env_variables.LUA_VERSION = lua_v
   env_variables.LUAROCKS_CONFIG = dir_path(testrun_dir, "testing_config.lua")

   local lua_path = {}
   if test_env.TEST_TARGET_OS == "windows" then
      table.insert(lua_path, dir_path(lrprefix, "lua", "?.lua"))
   else
      table.insert(lua_path, dir_path(lrprefix, "share", "lua", lua_v, "?.lua"))
   end
   table.insert(lua_path, dir_path(tree,      "share", "lua", lua_v, "?.lua"))
   table.insert(lua_path, dir_path(tree,      "share", "lua", lua_v, "?", "init.lua"))
   table.insert(lua_path, dir_path(sys_tree,  "share", "lua", lua_v, "?.lua"))
   table.insert(lua_path, dir_path(sys_tree,  "share", "lua", lua_v, "?", "init.lua"))
   table.insert(lua_path, dir_path(deps_tree, "share", "lua", lua_v, "?.lua"))
   table.insert(lua_path, dir_path(deps_tree, "share", "lua", lua_v, "?", "init.lua"))
   table.insert(lua_path, dir_path(testing_paths.src_dir, "?.lua"))
   env_variables.LUA_PATH = table.concat(lua_path, ";") .. ";"

   local lua_cpath = {}
   local lib_pattern = "?." .. test_env.lib_extension
   table.insert(lua_cpath, dir_path(tree,      "lib", "lua", lua_v, lib_pattern))
   table.insert(lua_cpath, dir_path(sys_tree,  "lib", "lua", lua_v, lib_pattern))
   table.insert(lua_cpath, dir_path(deps_tree, "lib", "lua", lua_v, lib_pattern))
   env_variables.LUA_CPATH = table.concat(lua_cpath, ";") .. ";"

   local path = { os.getenv("PATH") }
   table.insert(path, dir_path(tree, "bin"))
   table.insert(path, dir_path(sys_tree, "bin"))
   table.insert(path, dir_path(deps_tree, "bin"))
   env_variables.PATH = table.concat(path, test_env.TARGET_OS == "windows" and ";" or ":")

   return env_variables
end

local function make_run_function(cmd_name, exec_function, with_coverage, do_print)
   local cmd_prefix = Q(test_env.testing_paths.lua)
   local testrun_dir = test_env.testing_paths.testrun_dir

   if with_coverage then
      cmd_prefix = C(cmd_prefix, "-e", "\"require('luacov.runner')([[" .. testrun_dir .. "/luacov.config]])\"")
   end

   if cmd_name then
      cmd_prefix = C(cmd_prefix, dir_path(test_env.testing_paths.src_dir, "bin", cmd_name))
   end

   cmd_prefix = P(cmd_prefix)

   return function(cmd, new_vars)
      cmd = V(cmd)
      local temp_vars = {}
      for k, v in pairs(test_env.env_variables) do
         temp_vars[k] = v
      end
      if new_vars then
         for k, v in pairs(new_vars) do
            temp_vars[k] = v
         end
      end
      return exec_function(C(cmd_prefix, cmd), do_print, temp_vars)
   end
end

local function make_run_functions()
   local fns = {}

   local cmds = {
      ["lua"] = nil,
      ["luarocks"] = "luarocks",
      ["luarocks_admin"] = "luarocks-admin",
   }

   for _, name in ipairs({"lua", "luarocks", "luarocks_admin"}) do
      fns[name]                     = make_run_function(cmds[name], execute_output, true, true)
      fns[name .. "_bool"]          = make_run_function(cmds[name], execute_bool,   true, true)
      fns[name .. "_nocov"]         = make_run_function(cmds[name], execute_bool,   false, true)
      fns[name .. "_noprint_nocov"] = make_run_function(cmds[name], execute_bool,   false, false)
   end

   return fns
end

local function move_file(src, dst)
   local ok = execute_bool(C(tool("mv"), P(src), P(dst)))
   if not ok then
      print(debug.traceback())
      os.exit(1)
   end
end

--- Rebuild environment.
-- Remove old installed rocks and install new ones,
-- updating manifests and tree copies.
local function build_environment(rocks, env_variables)
   title("BUILDING ENVIRONMENT")
   local testing_paths = test_env.testing_paths
   test_env.remove_dir(testing_paths.testing_tree)
   test_env.remove_dir(testing_paths.testing_sys_tree)

   lfs.mkdir(testing_paths.testing_tree)
   lfs.mkdir(testing_paths.testing_sys_tree)
   lfs.mkdir(testing_paths.testing_deps_tree)

   test_env.run.luarocks_admin_nocov(C("make_manifest", Q(testing_paths.testing_server)))
   test_env.run.luarocks_admin_nocov(C("make_manifest", Q(testing_paths.testing_cache)))

   if test_env.MSVCRT then
      test_env.run.luarocks_nocov(C("config", "variables.MSVCRT", Q(test_env.MSVCRT), Q("--tree=" .. testing_paths.testing_cache)))
   end

   for _, rock in ipairs(rocks) do
      local only_server = "--only-server=" .. testing_paths.testing_cache
      local tree = "--tree=" .. testing_paths.testing_deps_tree
      if not test_env.run.luarocks_nocov(test_env.quiet(C("install", only_server, tree, Q(rock)), env_variables)) then
         assert(test_env.run.luarocks_nocov(C("build", tree, Q(rock)), env_variables))
         assert(test_env.run.luarocks_nocov(C("pack", tree, Q(rock)), env_variables))
         move_file(rock .. "-*.rock", testing_paths.testing_cache)
      end
   end
end

local function find_lua()
   -- (1) LUA is a full path
   if test_env.LUA and test_env.LUA:match("[/\\]") then

      local lua_bindir = test_env.LUA:match("^(.-)[/\\][^/\\]*$")
      local luadir = test_env.LUA_DIR or lua_bindir:gsub("[/\\]bin$")
      local lua = test_env.LUA

      return lua_bindir, luadir, lua
   end

   -- (2) LUA is just the interpreter name
   local lua_exe = test_env.LUA
                   or ((test_env.TEST_TARGET_OS == "windows") and "lua.exe")
                   or "lua"

   -- (2.1) LUA_DIR was given
   if test_env.LUA_DIR then

      local luadir = test_env.LUA_DIR
      local lua_bindir = exists(dir_path(luadir, "bin"))
                   and dir_path(luadir, "bin")
                   or luadir
      local lua = dir_path(lua_bindir, lua_exe)

      return lua_bindir, luadir, lua
   end

   -- (2.2) LUA_DIR was not given, try some default paths
   local try_dirs = (test_env.TEST_TARGET_OS == "windows")
                    and { os.getenv("ProgramFiles(x86)").."\\LuaRocks" }
                    or  { "/usr/local", "/usr" }

   for _, luadir in ipairs(try_dirs) do
      for _, lua_bindir in ipairs({ luadir, dir_path(luadir, "bin") }) do
         local lua = dir_path(lua_bindir, lua_exe)
         if exists(lua) then
            return lua_bindir, luadir, lua
         end
      end
   end
end

local function create_testing_paths(suffix)
   local paths = {}

   paths.lua_bindir, paths.luadir, paths.lua = find_lua()
   if (not paths.lua) or (not exists(paths.lua)) then
      error("Lua interpreter not found! Run `busted -Xhelper help` for options")
   end

   local base_dir = lfs.currentdir()
   paths.src_dir           = dir_path(base_dir, "src")
   paths.spec_dir          = dir_path(base_dir, "spec")
   paths.util_dir          = dir_path(base_dir, "spec", "util")
   paths.fixtures_dir      = dir_path(base_dir, "spec", "fixtures")
   paths.fixtures_repo_dir = dir_path(base_dir, "spec", "fixtures", "a_repo")
   paths.gpg_dir           = dir_path(base_dir, "spec", "fixtures", "gpg")

   local testrun_dir = dir_path(base_dir, "testrun")
   paths.testrun_dir           = testrun_dir
   paths.testing_lrprefix      = dir_path(testrun_dir, "testing_lrprefix-" .. suffix)
   paths.testing_tree          = dir_path(testrun_dir, "testing-" .. suffix)
   paths.testing_sys_tree      = dir_path(testrun_dir, "testing_sys-" .. suffix)
   paths.testing_deps_tree     = dir_path(testrun_dir, "testing_deps-" .. suffix)
   paths.testing_cache         = dir_path(testrun_dir, "testing_cache-" .. suffix)
   paths.testing_server        = dir_path(testrun_dir, "testing_server-" .. suffix)

   local rocks_v = "rocks-" .. test_env.lua_version
   paths.testing_rocks      = dir_path(paths.testing_tree,      "lib", "luarocks", rocks_v)
   paths.testing_sys_rocks  = dir_path(paths.testing_sys_tree,  "lib", "luarocks", rocks_v)
   paths.testing_deps_rocks = dir_path(paths.testing_deps_tree, "lib", "luarocks", rocks_v)

   if test_env.TEST_TARGET_OS == "windows" then
      paths.luarocks_tmp = os.getenv("TEMP")
   else
      paths.luarocks_tmp = "/tmp/luarocks_testing"
   end

   if test_env.TEST_TARGET_OS == "windows" then
      paths.win_tools = dir_path(base_dir, "win32", "tools")
   end

   return paths
end

--- Helper function to unload luarocks modules from global table package.loaded
-- Needed to load our local (testing) version of LuaRocks
function test_env.unload_luarocks()
   for modname, _ in pairs(package.loaded) do
      if modname:match("^luarocks%.") then
         package.loaded[modname] = nil
      end
   end
   local src_pattern = dir_path(test_env.testing_paths.src_dir, "?.lua")
   if not package.path:find(src_pattern, 1, true) then
      package.path = src_pattern .. ";" .. package.path
   end
end

--- Gets the C Run Time of the Lua interpreter built by mingw-w64 on ci-windows.
-- Returns the MSVCRT on success
-- Returns nil on failure
local function get_MSVCRT(variables)
   if not (test_env.CI_WINDOWS and test_env.MINGW) then return nil end
   local pe_parser_path = dir_path(test_env.testing_paths.src_dir, '..', 'win32', 'pe-parser.lua')
   local print_arch_script = "\"" ..
                             "local pe = assert(loadfile([[" .. pe_parser_path .. "]]))();" ..
                             "local rt, _ = pe.msvcrt([[" .. test_env.testing_paths.lua .. "]]);" ..
                             "print(rt or 'nil');" ..
                             "\""
   local cmd = C(test_env.testing_paths.lua, "-e", print_arch_script)
   local output = execute_output(cmd, false, variables)
   if output == 'nil' then
      output = nil
   end
   return output
end

local function get_luarocks_platform(variables)
   local print_arch_script = "\"" ..
                             "cfg = require('luarocks.core.cfg');" ..
                             "cfg.init();" ..
                             "print(cfg.arch)" ..
                             "\""
   local cmd = C(test_env.testing_paths.lua, "-e", print_arch_script)
   return execute_output(cmd, false, variables)
end

--- Test if required rock is installed and if not, install it.
-- Return `true` if the rock is already installed or has been installed successfully,
-- `false` if installation failed.
function test_env.need_rock(rock)
   rock = V(rock)

   print("Check if " .. rock .. " is installed")
   if test_env.run.luarocks_noprint_nocov(test_env.quiet("show " .. rock)) then
      return true
   else
      local ok = test_env.run.luarocks_noprint_nocov(test_env.quiet("install " .. rock))
      if not ok then
         print("WARNING: failed installing " .. rock)
      end
      return ok
   end
end

--- For each key-value pair in replacements table
-- replace %{key} in given string with value.
local function substitute(str, replacements)
   return (str:gsub("%%%b{}", function(marker)
      local r = replacements[marker:sub(3, -2)]
      if r then
         r = r:gsub("\\", "\\\\")
      end
      return r
   end))
end


--- Create configs for luacov and several versions of Luarocks
-- configs needed for some tests.
local function create_configs()
   local testrun_dir = test_env.testing_paths.testrun_dir

   -- testing_config.lua
   -- testing_config_show_downloads.lua
   -- testing_config_no_downloader.lua
   local config_content = substitute([[
      rocks_trees = {
         { name = "user", root = "%{testing_tree}" },
         { name = "deps", root = "%{testing_deps_tree}" },
         { name = "system", root = "%{testing_sys_tree}" },
      }
      rocks_servers = {
         "%{testing_server}"
      }
      local_cache = "%{testing_cache}"
      upload_server = "testing"
      upload_user = "%{user}"
      upload_servers = {
         testing = {
            rsync = "localhost/tmp/luarocks_testing",
         },
      }
   ]], {
      user = "testuser",
      testing_sys_tree = test_env.testing_paths.testing_sys_tree,
      testing_deps_tree = test_env.testing_paths.testing_deps_tree,
      testing_tree = test_env.testing_paths.testing_tree,
      testing_server = test_env.testing_paths.testing_server,
      testing_cache = test_env.testing_paths.testing_cache
   })

   test_env.write_file(dir_path(testrun_dir, "testing_config.lua"), config_content .. " \nweb_browser = \"true\"")
   test_env.write_file(dir_path(testrun_dir, "testing_config_show_downloads.lua"), config_content
                  .. "show_downloads = true \n rocks_servers={\"http://luarocks.org/repositories/rocks\"}")
   test_env.write_file(dir_path(testrun_dir, "testing_config_no_downloader.lua"), config_content
                  .. "variables = { WGET = 'invalid', CURL = 'invalid' }")

   -- testing_config_sftp.lua
   config_content = substitute([[
      rocks_trees = {
         "%{testing_tree}",
         "%{testing_deps_tree}",
         "%{testing_sys_tree}",
      }
      local_cache = "%{testing_cache}"
      upload_server = "testing"
      upload_user = "%{user}"
      upload_servers = {
         testing = {
            sftp = "localhost/tmp/luarocks_testing",
         },
      }
   ]], {
      user = "testuser",
      testing_sys_tree = test_env.testing_paths.testing_sys_tree,
      testing_deps_tree = test_env.testing_paths.testing_deps_tree,
      testing_tree = test_env.testing_paths.testing_tree,
      testing_cache = test_env.testing_paths.testing_cache
   })

   test_env.write_file(dir_path(testrun_dir, "testing_config_sftp.lua"), config_content)

   -- luacov.config
   config_content = substitute([[
      return {
         statsfile = "%{statsfile}",
         reportfile = "%{reportfile}",
         exclude = {
            "src%/luarocks%/vendor.+$",
         },
         modules = {
            ["luarocks"] = "%{luarocks_path}",
            ["luarocks-admin"] = "%{luarocks_admin_path}",
            ["luarocks.*"] = "src",
            ["luarocks.*.*"] = "src",
            ["luarocks.*.*.*"] = "src"
         }
      }
   ]], {
      statsfile = dir_path(testrun_dir, "luacov.stats.out"),
      reportfile = dir_path(testrun_dir, "luacov.report.out"),
      luarocks_path = dir_path("src", "bin", "luarocks"),
      luarocks_admin_path = dir_path("src", "bin", "luarocks-admin"),
   })

   test_env.write_file(dir_path(testrun_dir, "luacov.config"), config_content)

   config_content = [[
      -- Config file of mock LuaRocks.org site for tests
      upload = {
         server = "http://localhost:8080",
         tool_version = "1.0.0",
         api_version = "1",
      }
   ]]
   test_env.write_file(dir_path(testrun_dir, "luarocks_site.lua"), config_content)
end

--- Remove testing directories.
local function clean()
   local testrun_dir = test_env.testing_paths.testrun_dir

   print("Cleaning testing directory...")
   test_env.remove_dir(test_env.testing_paths.luarocks_tmp)
   test_env.remove_subdirs(testrun_dir, "testing[_%-]")
   test_env.remove_files(testrun_dir, "testing_")
   test_env.remove_files(testrun_dir, "luacov")
   test_env.remove_files(testrun_dir, "upload_config")
   test_env.remove_files(testrun_dir, "luarocks_site")
   print("Cleaning done!")
end

--- Setup current checkout of luarocks to work with testing prefix.
local function setup_luarocks()
   local testing_paths = test_env.testing_paths
   title("Setting up LuaRocks")

   local lines = {
      "return {",
      ("SYSCONFDIR = %q,"):format(dir_path(testing_paths.testing_lrprefix, "etc/luarocks")),
      ("LUA_DIR = %q,"):format(testing_paths.luadir),
      ("LUA_BINDIR = %q,"):format(testing_paths.lua_bindir),
      ("LUA = %q,"):format(testing_paths.lua),
   }

   if test_env.TEST_TARGET_OS == "windows" then
      if test_env.MINGW then
         table.insert(lines, [[SYSTEM = "mingw",]])
      else
         table.insert(lines, [[SYSTEM = "windows",]])
      end
      table.insert(lines, ("WIN_TOOLS = %q,"):format(testing_paths.win_tools))
   end

   table.insert(lines, "}")

   test_env.write_file("src/luarocks/core/hardcoded.lua", table.concat(lines, "\n") .. "\n")

   print("LuaRocks set up correctly!")
end

local function mock_api_call(path)
   return test_env.execute(C(tool("wget"), "--timeout=0.1 --quiet --tries=10 http://localhost:8080" .. path))
end

function test_env.mock_server_init()
   if not test_env.mock_prepared then
      error("need to setup_specs with with_mock set to true")
   end

   local testing_paths = test_env.testing_paths
   assert(test_env.need_rock("restserver-xavante"))

   local lua = Q(testing_paths.lua)
   local mock_server = Q(dir_path(testing_paths.util_dir, "mock-server.lua"))
   local fixtures_dir = Q(testing_paths.fixtures_dir)

   local cmd = C(lua, mock_server, fixtures_dir)

   local bg_cmd = test_env.TEST_TARGET_OS == "windows"
                  and C("start", "/b", "\"\"", cmd)
                  or  C(cmd, "&")

   os.execute(test_env.execute_helper(bg_cmd, true, test_env.env_variables))

   for _ = 1, 100 do
      if mock_api_call("/api/tool_version") then
         break
      end
      os.execute(test_env.TEST_TARGET_OS == "windows"
                 and "ping 192.0.2.0 -n 1 -w 250 > NUL"
                 or  "sleep 0.1")
   end

end

function test_env.mock_server_done()
   mock_api_call("/shutdown")
end

local function find_binary_rock(src_rock, dirname)
   local patt = src_rock:gsub("([.-])", "%%%1"):gsub("src", ".*[^s][^r][^c]")
   for name in lfs.dir(dirname) do
      if name:match(patt) then
         return true
      end
   end
   return false
end

local function prepare_mock_server_binary_rocks()
   if test_env.mock_prepared then
      return
   end

   local testing_paths = test_env.testing_paths

   local rocks = {
      -- rocks needed for mock-server
      "luasocket-${LUASOCKET}.src.rock",
      "coxpcall-1.16.0-1.src.rock",
      "binaryheap-${BINARYHEAP}.src.rock",
      "timerwheel-${TIMERWHEEL}.src.rock",
      "copas-${COPAS}.src.rock",
      "luafilesystem-${LUAFILESYSTEM}.src.rock",
      "xavante-2.4.0-1.src.rock",
      "wsapi-1.6.1-1.src.rock",
      "rings-1.3.0-1.src.rock",
      "wsapi-xavante-1.6.1-1.src.rock",
      "dkjson-${DKJSON}.src.rock",
      "restserver-0.1-1.src.rock",
      "restserver-xavante-0.2-1.src.rock",
   }
   local make_manifest = download_rocks(rocks, testing_paths.testing_server)
   for _, rock in ipairs(rocks) do
      rock = V(rock)
      local rockname = rock:gsub("%-[^-]+%-%d+%.[%a.]+$", "")
      if not find_binary_rock(rock, testing_paths.testing_server) then
         local rockpath = dir_path(testing_paths.testing_server, rock)
         local tree = "--tree=" .. testing_paths.testing_cache

         test_env.run.luarocks_nocov(C("build", Q(rockpath), tree))
         test_env.run.luarocks_nocov(C("pack", rockname, tree))

         move_file(rockname .. "-*.rock", testing_paths.testing_server)
         make_manifest = true
      end
   end
   if make_manifest then
      test_env.run.luarocks_admin_nocov(C("make_manifest", Q(testing_paths.testing_server)))
   end

   test_env.mock_prepared = true
end

---
-- Main function to create config files and testing environment
function test_env.main()
   local testing_paths = test_env.testing_paths
   local testrun_dir = test_env.testing_paths.testrun_dir

   if test_env.TEST_ENV_CLEAN then
      clean()
   end

   lfs.mkdir(testrun_dir)
   test_env.write_file(dir_path(testrun_dir, ".luarocks-no-project"), "")
   lfs.mkdir(testing_paths.testing_cache)
   lfs.mkdir(testing_paths.luarocks_tmp)

   create_configs()

   setup_luarocks()

   -- Preparation of rocks for building environment
   local rocks = {} -- names of rocks, required for building environment
   local urls = {}  -- names of rock and rockspec files to be downloaded

   local env_vars = {
      LUAROCKS_CONFIG = dir_path(testrun_dir, "testing_config.lua")
   }

   if test_env.TYPE_TEST_ENV == "full" then
      table.insert(urls, "/luafilesystem-${LUAFILESYSTEM}.src.rock")
      table.insert(urls, "/luasocket-${LUASOCKET}.src.rock")
      table.insert(urls, "/luasec-${LUASEC}.src.rock")
      table.insert(urls, "/md5-1.2-1.src.rock")
      table.insert(urls, "/manifests/hisham/lua-zlib-1.2-0.src.rock")
      table.insert(urls, "/manifests/hisham/lua-bz2-0.2.1.1-1.src.rock")
      rocks = {"luafilesystem", "luasocket", "luasec", "md5", "lua-zlib", "lua-bz2"}
      if test_env.TEST_TARGET_OS ~= "windows" then
         if test_env.lua_version == "5.1" then
            table.insert(urls, "/bit32-${BIT32}.src.rock")
            table.insert(rocks, "bit32")
         end
         table.insert(urls, "/luaposix-${LUAPOSIX}.src.rock")
         table.insert(rocks, "luaposix")
      end
      assert(test_env.run.luarocks_nocov(C("config", "variables.OPENSSL_INCDIR", Q(test_env.OPENSSL_INCDIR)), env_vars))
      assert(test_env.run.luarocks_nocov(C("config", "variables.OPENSSL_LIBDIR", Q(test_env.OPENSSL_LIBDIR)), env_vars))
      if test_env.CI_WINDOWS and test_env.MINGW then
         assert(test_env.run.luarocks_nocov(C("config", "variables.ZLIB_INCDIR", Q(test_env.ZLIB_INCDIR)), env_vars))
         assert(test_env.run.luarocks_nocov(C("config", "variables.ZLIB_LIBDIR", Q(test_env.ZLIB_LIBDIR)), env_vars))
         assert(test_env.run.luarocks_nocov(C("config", "variables.BZ2_INCDIR", Q(test_env.BZ2_INCDIR)), env_vars))
         assert(test_env.run.luarocks_nocov(C("config", "variables.BZ2_LIBDIR", Q(test_env.BZ2_LIBDIR)), env_vars))
      end
   end

   -- luacov is needed for both minimal or full environment
   table.insert(urls, "/luacov-${LUACOV}.src.rock")
   table.insert(urls, "/cluacov-${CLUACOV}.src.rock")
   table.insert(rocks, "luacov")
   table.insert(rocks, "cluacov")

   -- compat53 is needed
   table.insert(urls, "/compat53-${COMPAT53}.src.rock")
   table.insert(rocks, "compat53")

   -- Download rocks needed for LuaRocks testing environment
   lfs.mkdir(testing_paths.testing_server)
   download_rocks(urls, testing_paths.testing_server)

   build_environment(rocks, env_vars)
end

--- Function for initial setup of environment and variables
function test_env.setup_specs(extra_rocks, use_mock)
   test_env.unload_luarocks()

   local testrun_dir = test_env.testing_paths.testrun_dir
   local variables = test_env.env_variables

   -- if global variable about successful creation of testing environment doesn't exist, build environment
   if not test_env.setup_done then
      if test_env.CI then
         if not exists(os.getenv("HOME"), ".ssh/id_rsa.pub") then
            execute_bool("ssh-keygen -t rsa -P \"\" -f ~/.ssh/id_rsa")
            execute_bool("cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys")
            execute_bool("chmod og-wx ~/.ssh/authorized_keys")
            execute_bool("ssh-keyscan localhost >> ~/.ssh/known_hosts")
         end
      end

      test_env.main()

      -- preload before meddling with package.path
      require("spec.util.git_repo")
      require("spec.util.quick")

      package.path = variables.LUA_PATH
      package.cpath = variables.LUA_CPATH

      test_env.MSVCRT = get_MSVCRT(test_env.env_variables)
      test_env.platform = get_luarocks_platform(test_env.env_variables)
      test_env.wrapper_extension = test_env.TEST_TARGET_OS == "windows" and ".bat" or ""
      test_env.setup_done = true
      title("RUNNING TESTS")
   end

   if use_mock == "mock" then
      prepare_mock_server_binary_rocks()
   end

   if extra_rocks then
      local make_manifest = download_rocks(extra_rocks, test_env.testing_paths.testing_server)
      if make_manifest then
         test_env.run.luarocks_admin_nocov("make_manifest " .. test_env.testing_paths.testing_server)
      end
   end

   if test_env.RESET_ENV then
      test_env.remove_dir(test_env.testing_paths.testing_tree)
      test_env.remove_dir(test_env.testing_paths.testing_sys_tree)
   end

   lfs.chdir(testrun_dir)
end

test_env.set_lua_version()
test_env.set_args()
test_env.testing_paths = create_testing_paths(test_env.LUA_V or test_env.LUAJIT_V)
test_env.env_variables = create_env(test_env.testing_paths)
test_env.run = make_run_functions()
test_env.exists = exists
test_env.V = V
test_env.Q = Q
test_env.P = P
test_env.MSVCRT = get_MSVCRT(test_env.env_variables)
test_env.platform = get_luarocks_platform(test_env.env_variables)

return test_env
