local h = require('helpdb')

local mock_plugin_db = {
    hascommands={
        boxbinders={description="Box your binders.", help=[[Box your binders.
        This command will help you box your binders.]]},
        bindboxers={description="Bind your boxers.", help=[[Bind your boxers.
        This command will help you bind your boxers.]]}
    },
    samename={
        samename={description="Samename.", help=[[Samename.
        This command has the same name as its host plugin.]]}
    },
    nocommand={},
    nodocs_hascommands={
        nodoc_command={description="cpp description.", help=[[cpp description.
        Rest of help.]]}
    },
    nodocs_samename={
        nodocs_samename={description="Nodocs samename.", help=[[Nodocs samename.
        This command has the same name as its host plugin but no rst docs.]]}
    },
    nodocs_nocommand={},
}

local mock_command_db = {}
for k,v in pairs(mock_plugin_db) do
    for c,d in pairs(v) do
        mock_command_db[c] = d
    end
end

local mock_script_db = {
    basic=true,
    ['subdir/scriptname']=true,
    inscript_docs=true,
    inscript_short_only=true,
    nodocs_script=true,
    dev_script=true,
}

local files = {
    ['hack/docs/docs/Tags.txt']=[[
* fort: Tools that are useful while in fort mode.

* armok: Tools that give you complete control over an aspect of the game or provide access to information that the game intentionally keeps hidden.

* map: Tools that interact with the game map.

* units: Tools that interact with units.

* dev: Dev tools.

* nomembers: Nothing is tagged with this.
    ]],
    ['hack/docs/docs/tools/hascommands.txt']=[[
hascommands
===========

Tags: fort | armok | units

  Documented a plugin that has commands.

Command: "boxbinders"

  Documented boxbinders.

Command: "bindboxers"

  Documented bindboxers.

Documented full help.
    ]],
    ['hack/docs/docs/tools/samename.txt']=[[
samename
========

Tags: fort | armok | units

Command: "samename"

  Documented samename.

Documented full help.
    ]],
    ['hack/docs/docs/tools/nocommand.txt']=[[
nocommand
=========

Tags: fort | armok | units

  Documented nocommand.

Documented full help.
    ]],
    ['hack/docs/docs/tools/basic.txt']=[[
basic
=====

Tags: map

Command: "basic"

  Documented basic.

Documented full help.
    ]],
    ['hack/docs/docs/tools/subdir/scriptname.txt']=[[
subdir/scriptname
=================

Tags: map

Command: "subdir/scriptname"

  Documented subdir/scriptname.

Documented full help.
    ]],
    ['hack/docs/docs/tools/dev_script.txt']=[[
dev_script
==========

Tags: dev

Command: "dev_script"

  Short desc.

Full help.
]====]
script contents
    ]],
    ['scripts/scriptpath/basic.lua']=[[
-- in-file short description for basic
-- [====[
basic
=====

Tags: map

Command: "basic"

  in-file basic.

Documented full help.
]====]
script contents
    ]],
    ['scripts/scriptpath/subdir/scriptname.lua']=[[
-- in-file short description for scriptname
-- [====[
subdir/scriptname
=================

Tags: map

Command: "subdir/scriptname"

  in-file scriptname.

Documented full help.
]====]
script contents
    ]],
    ['scripts/scriptpath/inscript_docs.lua']=[[
-- in-file short description for inscript_docs
-- [====[
inscript_docs
=============

Tags: map | badtag

Command: "inscript_docs"

  in-file inscript_docs.

Documented full help.
]====]
script contents
    ]],
    ['scripts/scriptpath/nodocs_script.lua']=[[
    script contents
    ]],
    ['scripts/scriptpath/inscript_short_only.lua']=[[
    -- inscript short desc.

    script contents
    ]],
    ['other/scriptpath/basic.lua']=[[
-- in-file short description for basic (other)
-- [====[
basic
=====

Tags: map

Command: "basic"

  in-file basic (other).

Documented full help.
]====]
script contents
    ]],
    ['other/scriptpath/subdir/scriptname.lua']=[[
-- in-file short description for scriptname (other)
-- [====[
subdir/scriptname
=================

Tags: map

Command: "subdir/scriptname"

  in-file scriptname (other).

Documented full help.
]====]
script contents
    ]],
    ['other/scriptpath/inscript_docs.lua']=[[
-- in-file short description for inscript_docs (other)
-- [====[
inscript_docs
=============

Tags: map

Command: "inscript_docs"

  in-file inscript_docs (other).

Documented full help.
]====]
script contents
    ]],
    ['other/scriptpath/dev_script.lua']=[[
script contents
    ]],
}

local function mock_getCommandHelp(command)
    if mock_command_db[command] then
        return mock_command_db[command].help
    end
end

local function mock_listPlugins()
    local list = {}
    for k in pairs(mock_plugin_db) do
        table.insert(list, k)
    end
    return list
end

local function mock_listCommands(plugin)
    local list = {}
    for k,v in pairs(mock_plugin_db) do
        if k == plugin then
            for c in pairs(v) do
                table.insert(list, c)
            end
            break
        end
    end
    return list
end

local function mock_getCommandDescription(command)
    if mock_command_db[command] then
        return mock_command_db[command].description
    end
end

local function mock_getScriptPaths()
    return {'scripts/scriptpath', 'other/scriptpath'}
end

local function mock_mtime(path)
    if files[path] then return 1 end
    return -1
end

local function mock_listdir_recursive(script_path)
    local list = {}
    for s in pairs(mock_script_db) do
        table.insert(list, {isdir=false, path=s..'.lua'})
    end
    return list
end

local function mock_getTickCount()
    return 100000
end

local function mock_pcall(fn, fname)
    if fn ~= io.lines then error('unexpected fn for pcall') end
    if not files[fname] then
        return false
    end
    return true, files[fname]:gmatch('([^\n]*)\n?')
end

config.wrapper = function(test_fn)
    mock.patch({
        {h.dfhack.internal, 'getCommandHelp', mock_getCommandHelp},
        {h.dfhack.internal, 'listPlugins', mock_listPlugins},
        {h.dfhack.internal, 'listCommands', mock_listCommands},
        {h.dfhack.internal, 'getCommandDescription', mock_getCommandDescription},
        {h.dfhack.internal, 'getScriptPaths', mock_getScriptPaths},
        {h.dfhack.filesystem, 'mtime', mock_mtime},
        {h.dfhack.filesystem, 'listdir_recursive', mock_listdir_recursive},
        {h.dfhack, 'getTickCount', mock_getTickCount},
        {h, 'pcall', mock_pcall},
    }, test_fn)
end

function test.is_entry()
    expect.true_(h.is_entry('ls'),
        'builtin commands get an entry')
    expect.true_(h.is_entry('hascommands'),
        'plugins whose names do not match their commands get an entry')
    expect.true_(h.is_entry('boxbinders'),
        'commands whose name does not match the host plugin get an entry')
    expect.true_(h.is_entry('samename'),
        'plugins that have a command with the same name get one entry')
    expect.true_(h.is_entry('nocommand'),
        'plugins that do not have commands get an entry')
    expect.true_(h.is_entry('basic'),
        'scripts in the script path get an entry')
    expect.true_(h.is_entry('subdir/scriptname'),
        'scripts in subdirs of a script path get an entry')

    expect.true_(h.is_entry('nodocs_hascommands'),
        'plugins whose names do not match their commands get an entry')
    expect.true_(h.is_entry('nodoc_command'),
        'commands whose name does not match the host plugin get an entry')
    expect.true_(h.is_entry('nodocs_samename'),
        'plugins that have a command with the same name get one entry')
    expect.true_(h.is_entry('nodocs_nocommand'),
        'plugins that do not have commands get an entry')
    expect.true_(h.is_entry('inscript_docs'),
        'scripts in the script path get an entry')
    expect.true_(h.is_entry('nodocs_script'),
        'scripts in the script path get an entry')
    expect.true_(h.is_entry('inscript_short_only'),
        'scripts in the script path get an entry')

    expect.false_(h.is_entry(nil),
        'nil is not an entry')
    expect.false_(h.is_entry(''),
        'blank is not an entry')
    expect.false_(h.is_entry('notanentryname'),
        'strings that are neither plugin names nor command names do not get an entry')

    expect.true_(h.is_entry({'hascommands', 'boxbinders', 'nocommand'}),
        'list of valid entries')
    expect.false_(h.is_entry({'hascommands', 'notanentryname'}),
        'list contains an element that is not an entry')
end

function test.get_entry_types()
    expect.table_eq({builtin=true, command=true}, h.get_entry_types('ls'))

    expect.table_eq({plugin=true}, h.get_entry_types('hascommands'))
    expect.table_eq({command=true}, h.get_entry_types('boxbinders'))
    expect.table_eq({plugin=true, command=true}, h.get_entry_types('samename'))
    expect.table_eq({plugin=true}, h.get_entry_types('nocommand'))
    expect.table_eq({command=true}, h.get_entry_types('basic'))
    expect.table_eq({command=true}, h.get_entry_types('subdir/scriptname'))

    expect.table_eq({plugin=true}, h.get_entry_types('nodocs_hascommands'))
    expect.table_eq({command=true}, h.get_entry_types('nodoc_command'))
    expect.table_eq({plugin=true, command=true}, h.get_entry_types('nodocs_samename'))
    expect.table_eq({plugin=true}, h.get_entry_types('nodocs_nocommand'))
    expect.table_eq({command=true}, h.get_entry_types('nodocs_script'))
    expect.table_eq({command=true}, h.get_entry_types('inscript_docs'))
    expect.table_eq({command=true}, h.get_entry_types('inscript_short_only'))

    expect.error_match('entry not found', function()
        h.get_entry_types('notanentry') end)
end

function test.get_entry_short_help()
    expect.eq('No help available.', h.get_entry_short_help('ls'),
        'no docs for builtin fn result in default short description')

    expect.eq('Documented a plugin that has commands.', h.get_entry_short_help('hascommands'))
    expect.eq('Box your binders.', h.get_entry_short_help('boxbinders'),
        'should get short help from command description')
    expect.eq('Samename.', h.get_entry_short_help('samename'),
        'should get short help from command description')
    expect.eq('Documented nocommand.', h.get_entry_short_help('nocommand'))
    expect.eq('Documented basic.', h.get_entry_short_help('basic'))
    expect.eq('Documented subdir/scriptname.', h.get_entry_short_help('subdir/scriptname'))

    expect.eq('No help available.', h.get_entry_short_help('nodocs_hascommands'))
    expect.eq('cpp description.', h.get_entry_short_help('nodoc_command'),
        'should get short help from command description')
    expect.eq('Nodocs samename.', h.get_entry_short_help('nodocs_samename'),
        'should get short help from command description')
    expect.eq('No help available.', h.get_entry_short_help('nodocs_nocommand'))
    expect.eq('in-file short description for inscript_docs.',
        h.get_entry_short_help('inscript_docs'),
        'should get short help from header comment')
    expect.eq('No help available.',
        h.get_entry_short_help('nodocs_script'))
    expect.eq('inscript short desc.',
        h.get_entry_short_help('inscript_short_only'),
        'should get short help from header comment')
end

function test.get_entry_long_help()
    local expected = [[
basic
=====

Tags: map

Command:
"basic"

  Documented
  basic.

Documented
full help.
    ]]
    expect.eq(expected, h.get_entry_long_help('basic', 13))

    -- long help for plugins/commands that have doc files should match the
    -- contents of those files exactly (test data is already wrapped)
    expect.eq(files['hack/docs/docs/tools/hascommands.txt'],
        h.get_entry_long_help('hascommands'))
    expect.eq(files['hack/docs/docs/tools/hascommands.txt'],
        h.get_entry_long_help('boxbinders'))
    expect.eq(files['hack/docs/docs/tools/samename.txt'],
        h.get_entry_long_help('samename'))
    expect.eq(files['hack/docs/docs/tools/nocommand.txt'],
        h.get_entry_long_help('nocommand'))
    expect.eq(files['hack/docs/docs/tools/basic.txt'],
        h.get_entry_long_help('basic'))
    expect.eq(files['hack/docs/docs/tools/subdir/scriptname.txt'],
        h.get_entry_long_help('subdir/scriptname'))

    -- plugins/commands that have no doc files get the default template
    expect.eq([[ls
==

No help available.
]], h.get_entry_long_help('ls'))
    expect.eq([[nodocs_hascommands
==================

No help available.
]], h.get_entry_long_help('nodocs_hascommands'))
    expect.eq([[nodocs_hascommands
==================

No help available.
]], h.get_entry_long_help('nodoc_command'))
    expect.eq([[Nodocs samename.
        This command has the same name as its host plugin but no rst docs.]], h.get_entry_long_help('nodocs_samename'))
    expect.eq([[nodocs_nocommand
================

No help available.
]], h.get_entry_long_help('nodocs_nocommand'))
    expect.eq([[nodocs_script
=============

No help available.
]], h.get_entry_long_help('nodocs_script'))
    expect.eq([[inscript_short_only
===================

No help available.
]], h.get_entry_long_help('inscript_short_only'))

    -- scripts that have no doc files get the docs from the script lua source
    expect.eq([[inscript_docs
=============

Tags: map | badtag

Command: "inscript_docs"

  in-file inscript_docs.

Documented full help.]], h.get_entry_long_help('inscript_docs'))
end

function test.get_entry_tags()
    expect.table_eq({fort=true, armok=true, units=true},
        h.get_entry_tags('hascommands'))
    expect.table_eq({fort=true, armok=true, units=true},
        h.get_entry_tags('samename'))
    expect.table_eq({fort=true, armok=true, units=true},
        h.get_entry_tags('nocommand'))
    expect.table_eq({map=true}, h.get_entry_tags('basic'))
    expect.table_eq({map=true}, h.get_entry_tags('inscript_docs'),
        'bad tags should get filtered out')
end

function test.is_tag()
    -- see tags defined in the Tags.txt files entry above
    expect.true_(h.is_tag('map'))
    expect.true_(h.is_tag({'map', 'armok'}))

    expect.false_(h.is_tag(nil))
    expect.false_(h.is_tag(''))
    expect.false_(h.is_tag('not_tag'))
    expect.false_(h.is_tag({'map', 'not_tag', 'armok'}))
end

function test.get_tags()
    expect.table_eq({'armok', 'dev', 'fort', 'map', 'nomembers', 'units'},
        h.get_tags())
end

function test.get_tag_data()
    local tag_data = h.get_tag_data('armok')
    table.sort(tag_data)
    expect.table_eq({description='Tools that give you complete control over an aspect of the game or provide access to information that the game intentionally keeps hidden.',
        'bindboxers', 'boxbinders', 'hascommands', 'nocommand', 'samename'},
        tag_data,
        'multi-line descriptions should get joined into a single line.')

    tag_data = h.get_tag_data('fort')
    table.sort(tag_data)
    expect.table_eq({description='Tools that are useful while in fort mode.',
        'bindboxers', 'boxbinders', 'hascommands', 'nocommand', 'samename'},
        tag_data)

    tag_data = h.get_tag_data('units')
    table.sort(tag_data)
    expect.table_eq({description='Tools that interact with units.',
        'bindboxers', 'boxbinders', 'hascommands', 'nocommand', 'samename'},
        tag_data)

    tag_data = h.get_tag_data('map')
    table.sort(tag_data)
    expect.table_eq({description='Tools that interact with the game map.',
        'basic', 'inscript_docs', 'subdir/scriptname'},
        tag_data)

    expect.table_eq({description='Nothing is tagged with this.'}, h.get_tag_data('nomembers'))

    expect.error_match('tag not found',
        function() h.get_tag_data('notatag') end)
end

function test.search_entries()
    -- all entries, in alphabetical order by last path component
    local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders',
        'clear', 'cls', 'dev_script', 'die', 'dir', 'disable', 'devel/dump-rpc',
        'enable', 'fpause', 'hascommands', 'help', 'hide', 'inscript_docs',
        'inscript_short_only', 'keybinding', 'kill-lua', 'load', 'ls', 'man',
        'nocommand', 'nodoc_command', 'nodocs_hascommands', 'nodocs_nocommand',
        'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename',
        'script', 'subdir/scriptname', 'sc-script', 'show', 'tags', 'type',
        'unload'}
    table.sort(expected, h.sort_by_basename)
    expect.table_eq(expected, h.search_entries())
    expect.table_eq(expected, h.search_entries({}))
    expect.table_eq(expected, h.search_entries(nil, {}))
    expect.table_eq(expected, h.search_entries({}, {}))

    expected = {'inscript_docs', 'subdir/scriptname'}
    expect.table_eq(expected, h.search_entries({tag='map', str='script'}))

    expected = {'script', 'sc-script'}
    table.sort(expected, h.sort_by_basename)
    expect.table_eq(expected, h.search_entries({str='script',
                                                entry_type='builtin'}))

    expected = {'dev_script', 'inscript_docs', 'inscript_short_only',
                'nodocs_script', 'subdir/scriptname'}
    expect.table_eq(expected, h.search_entries({str='script'},
                                               {entry_type='builtin'}))

    expected = {'bindboxers', 'boxbinders'}
    expect.table_eq(expected, h.search_entries({str='box'}))

    expected = {'bindboxers', 'boxbinders', 'inscript_docs',
                'inscript_short_only', 'nodocs_script', 'subdir/scriptname'}
    expect.table_eq(expected, h.search_entries({{str='script'}, {str='box'}},
                                               {{entry_type='builtin'},
                                                {tag='dev'}}),
                    'multiple filters for include and exclude')
end

function test.get_commands()
    local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders',
        'clear', 'cls', 'dev_script', 'die', 'dir', 'disable', 'devel/dump-rpc',
        'enable', 'fpause', 'help', 'hide', 'inscript_docs', 'inscript_short_only',
        'keybinding', 'kill-lua', 'load', 'ls', 'man', 'nodoc_command',
        'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename',
        'script', 'subdir/scriptname', 'sc-script', 'show', 'tags', 'type',
        'unload'}
    table.sort(expected, h.sort_by_basename)
    expect.table_eq(expected, h.get_commands())
end

function test.is_builtin()
    expect.true_(h.is_builtin('ls'))
    expect.false_(h.is_builtin('basic'))
    expect.false_(h.is_builtin('notanentry'))
end

function test.help()
    expect.printerr_match('No help entry found', function() h.help('blarg') end)

    local mock_print = mock.func()
    mock.patch(h, 'print', mock_print, function()
        h.help('nocommand')
        expect.eq(1, mock_print.call_count)
        expect.eq(files['hack/docs/docs/tools/nocommand.txt'],
            mock_print.call_args[1][1])
    end)
end

function test.tags()
    local mock_print = mock.func()
    mock.patch(h, 'print', mock_print, function()
        h.tags()
        expect.eq(8, mock_print.call_count)
        expect.eq('armok                Tools that give you complete control over an aspect of the',
            mock_print.call_args[1][1])
        expect.eq('                      game or provide access to information that the game',
            mock_print.call_args[2][1])
        expect.eq('                      intentionally keeps hidden.',
            mock_print.call_args[3][1])
        expect.eq('dev                  Dev tools.',
            mock_print.call_args[4][1])
        expect.eq('fort                 Tools that are useful while in fort mode.',
            mock_print.call_args[5][1])
        expect.eq('map                  Tools that interact with the game map.',
            mock_print.call_args[6][1])
        expect.eq('nomembers            Nothing is tagged with this.',
            mock_print.call_args[7][1])
        expect.eq('units                Tools that interact with units.',
            mock_print.call_args[8][1])
    end)
end

function test.tags_tag()
    local mock_print = mock.func()
    mock.patch(h, 'print', mock_print, function()
        h.tags('armok')
        expect.eq(3, mock_print.call_count)
        expect.eq('bindboxers           Bind your boxers.',
            mock_print.call_args[1][1])
        expect.eq('boxbinders           Box your binders.',
            mock_print.call_args[2][1])
        expect.eq('samename             Samename.',
            mock_print.call_args[3][1])
    end)
end

function test.ls()
    local mock_print = mock.func()
    mock.patch(h, 'print', mock_print, function()
        h.ls('doc') -- interpreted as a string
        expect.eq(5, mock_print.call_count)
        expect.eq('inscript_docs        in-file short description for inscript_docs.',
            mock_print.call_args[1][1])
        expect.eq('                      tags: map', mock_print.call_args[2][1])
        expect.eq('nodoc_command        cpp description.',
            mock_print.call_args[3][1])
        expect.eq('nodocs_samename      Nodocs samename.',
            mock_print.call_args[4][1])
        expect.eq('nodocs_script        No help available.',
            mock_print.call_args[5][1])
    end)

    mock_print = mock.func()
    mock.patch(h, 'print', mock_print, function()
        h.ls('armok') -- interpreted as a tag
        expect.eq(6, mock_print.call_count)
        expect.eq('bindboxers           Bind your boxers.',
            mock_print.call_args[1][1])
        expect.eq('                      tags: armok, fort, units',
            mock_print.call_args[2][1])
        expect.eq('boxbinders           Box your binders.',
            mock_print.call_args[3][1])
        expect.eq('                      tags: armok, fort, units',
            mock_print.call_args[4][1])
        expect.eq('samename             Samename.',
            mock_print.call_args[5][1])
        expect.eq('                      tags: armok, fort, units',
            mock_print.call_args[6][1])
    end)

    mock_print = mock.func()
    mock.patch(h, 'print', mock_print, function()
        h.ls('not a match')
        expect.eq(1, mock_print.call_count)
        expect.eq('No matches.', mock_print.call_args[1][1])
    end)

    -- test skipping tags and excluding strings
    mock_print = mock.func()
    mock.patch(h, 'print', mock_print, function()
        h.ls('armok', true, false, 'boxer,binder')
        expect.eq(1, mock_print.call_count)
        expect.eq('samename             Samename.', mock_print.call_args[1][1])
    end)

    -- test excluding dev scripts
    mock_print = mock.func()
    mock.patch(h, 'print', mock_print, function()
        h.ls('_script', true, false, 'inscript,nodocs')
        expect.eq(1, mock_print.call_count)
        expect.eq('No matches.', mock_print.call_args[1][1])
    end)

    -- test including dev scripts
    mock_print = mock.func()
    mock.patch(h, 'print', mock_print, function()
        h.ls('_script', true, true, 'inscript,nodocs')
        expect.eq(1, mock_print.call_count)
        expect.eq('dev_script           Short desc.',
            mock_print.call_args[1][1])
    end)
end