#!/usr/bin/env lua

local lfs = require("lfs")
local diff = require("diff")
local alt_getopt = require("alt_getopt")
local tests_failed = 0
local tests_passed = 0

local function is_directory(path)
  return lfs.attributes(path, "mode") == "directory"
end

local function do_matching_tests(path, patt, fun)
  local patt = patt or "."
  local result = {}
  for f in lfs.dir(path) do
    local fpath = path .. "/" .. f
    if f ~= "." and f ~= ".." then
      if is_directory(fpath) then
        do_matching_tests(fpath, patt, fun)
      elseif fpath:match(patt) and fpath:match("%.test$") then
        local fh = io.open(fpath, "r")
        local contents = fh:read("*all"):gsub("\r","")
        local cmd, inp, out = contents:match("^([^\n]*)\n<<<[ \t]*\n(.-\n)>>>[ \t]*\n(.*)$")
        assert(cmd ~= nil, "Command not found in " .. f)
        if cmdname then
          cmd = cmd:gsub("^%S+",cmdname)
        end
        if testopts then
          cmd = cmd:gsub("%-.*$", testopts)
        end
        fun({ name = f:match("^(.*)%.test$"), path = fpath,
              command = cmd, input = inp or "", output = out or ""})
        fh:close()
      end
    end
  end
end

local format = string.format

local function ansicolor(s)
  return string.char(27) .. '[' .. tostring(s) .. 'm'
end

local function expectedcolor(s)
  return ansicolor(41) .. ansicolor(37) .. s .. ansicolor(0)
end

local function actualcolor(s)
  return ansicolor(42) .. s .. ansicolor(0)
end

local function bothcolor(s)
  return ansicolor(36) .. s .. ansicolor(0)
end

local function passcolor(s)
  return ansicolor(33) .. s .. ansicolor(0)
end

local function failcolor(s)
  return ansicolor(31) .. s .. ansicolor(0)
end

local function show_diff(expected, actual)
  io.write(expectedcolor("expected") .. actualcolor("actual") .. "\n")
  local tokenpattern = "[%s]"
  local difftoks = diff.diff(expected, actual, tokenpattern)
  for _,l in ipairs(difftoks) do
    local text, status = l[1], l[2]
    if status == "in" then
      io.write(actualcolor(text))
    elseif status == "out" then
      io.write(expectedcolor(text))
    else
      io.write(bothcolor(text))
    end
  end
end

-- runs cmd on file containing inp and returns result
local function pipe(cmd, inp)
  local tmp = os.tmpname()
  local tmph = io.open(tmp, "w")
  tmph:write(inp)
  tmph:close()
  local outh = io.popen(cmd .. " " .. tmp,"r")
  local result = outh:read("*all")
  outh:close()
  os.remove(tmp)
  return result
end

local function normalize_output(s)
  return pipe("tidy --show-body-only yes --show-warnings no -utf8 -quiet", s)
end

local function run_test(test)
  local expected = test.output
  local actual = pipe(test.command, test.input)
  if normalize then
    actual = normalize_output(actual)
    expected = normalize_output(expected)
  end
  if actual == expected then
    tests_passed = tests_passed + 1
    io.write(passcolor("[OK]") .. "     " .. test.path .. "\n")
  else
    tests_failed = tests_failed + 1
    io.write(failcolor("[FAILED]") .. " " .. test.path .. "\n")
    local worddiff = false
    show_diff(expected, actual)
  end
end

-- main program

local version = [[
shtest 0.1
Copyright (C) 2009-2011 John MacFarlane
]]

local usage = [[
Usage: shtest [options] [pattern] - run shell tests

Options:
  --dir,-d PATH      Directory containing .test files (default 'tests')
  --prog,-p CMD      Program to run for tests
  --opts,-o OPTS     Command-line options to use in tests
  --tidy,-t          Normalize output using html tidy
  --version,-V       Version information
  --help,-h          This message
]]

local long_opts = {
  dir = "d",
  prog = "p",
  opts = "o",
  tidy = "t",
  version = "V",
  help = "h"
}

local short_opts = "d:p:o:tVh"
local optarg,optind = alt_getopt.get_opts(arg, short_opts, long_opts)

if optarg.h then
  io.write(usage)
  os.exit(0)
end

if optarg.V then
  io.write(version)
  os.exit(0)
end

local testdir = optarg.d or "tests"
local pattern = arg[optind]
cmdname = optarg.p or "bin/lunamark"
testopts = optarg.o
normalize = optarg.t

do_matching_tests(testdir, pattern, run_test)
io.write(string.format("Passed: %d\nFailed: %d\n", tests_passed, tests_failed))
os.exit(tests_failed)
