/*
 * Copyright 2019 Nikolay Sivov for CodeWeavers
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include <stdarg.h>
#include <stdio.h>
#include <string.h>

#include "windef.h"
#include "winbase.h"

#include "initguid.h"
#include "dbgeng.h"

#include "wine/test.h"

static void test_engine_options(void)
{
    IDebugControl *control;
    ULONG options;
    HRESULT hr;

    hr = DebugCreate(&IID_IDebugControl, (void **)&control);
    ok(hr == S_OK, "Failed to create engine object, hr %#x.\n", hr);

    options = 0xf;
    hr = control->lpVtbl->GetEngineOptions(control, &options);
    ok(hr == S_OK, "Failed to get engine options, hr %#x.\n", hr);
    ok(options == 0, "Unexpected options %#x.\n", options);

    hr = control->lpVtbl->AddEngineOptions(control, DEBUG_ENGOPT_INITIAL_BREAK);
    ok(hr == S_OK, "Failed to add engine options, hr %#x.\n", hr);

    options = 0;
    hr = control->lpVtbl->GetEngineOptions(control, &options);
    ok(hr == S_OK, "Failed to get engine options, hr %#x.\n", hr);
    ok(options == DEBUG_ENGOPT_INITIAL_BREAK, "Unexpected options %#x.\n", options);

    hr = control->lpVtbl->AddEngineOptions(control, 0x00800000);
    ok(hr == E_INVALIDARG, "Unexpected hr %#x.\n", hr);

    options = 0;
    hr = control->lpVtbl->GetEngineOptions(control, &options);
    ok(hr == S_OK, "Failed to get engine options, hr %#x.\n", hr);
    ok(options == DEBUG_ENGOPT_INITIAL_BREAK, "Unexpected options %#x.\n", options);

    hr = control->lpVtbl->RemoveEngineOptions(control, 0x00800000);
    ok(hr == S_OK, "Failed to remove options, hr %#x.\n", hr);

    hr = control->lpVtbl->AddEngineOptions(control, DEBUG_ENGOPT_IGNORE_DBGHELP_VERSION);
    ok(hr == S_OK, "Failed to add engine options, hr %#x.\n", hr);

    options = 0;
    hr = control->lpVtbl->GetEngineOptions(control, &options);
    ok(hr == S_OK, "Failed to get engine options, hr %#x.\n", hr);
    ok(options == (DEBUG_ENGOPT_INITIAL_BREAK | DEBUG_ENGOPT_IGNORE_DBGHELP_VERSION),
            "Unexpected options %#x.\n", options);

    hr = control->lpVtbl->RemoveEngineOptions(control, DEBUG_ENGOPT_INITIAL_BREAK);
    ok(hr == S_OK, "Failed to remove options, hr %#x.\n", hr);

    options = 0;
    hr = control->lpVtbl->GetEngineOptions(control, &options);
    ok(hr == S_OK, "Failed to get engine options, hr %#x.\n", hr);
    ok(options == DEBUG_ENGOPT_IGNORE_DBGHELP_VERSION, "Unexpected options %#x.\n", options);

    hr = control->lpVtbl->SetEngineOptions(control, DEBUG_ENGOPT_INITIAL_BREAK);
    ok(hr == S_OK, "Failed to set options, hr %#x.\n", hr);

    options = 0;
    hr = control->lpVtbl->GetEngineOptions(control, &options);
    ok(hr == S_OK, "Failed to get engine options, hr %#x.\n", hr);
    ok(options == DEBUG_ENGOPT_INITIAL_BREAK, "Unexpected options %#x.\n", options);

    hr = control->lpVtbl->SetEngineOptions(control, 0x00800000);
    ok(hr == E_INVALIDARG, "Unexpected hr %#x.\n", hr);

    hr = control->lpVtbl->SetEngineOptions(control, 0x00800000 | DEBUG_ENGOPT_IGNORE_DBGHELP_VERSION);
    ok(hr == E_INVALIDARG, "Unexpected hr %#x.\n", hr);

    options = 0;
    hr = control->lpVtbl->GetEngineOptions(control, &options);
    ok(hr == S_OK, "Failed to get engine options, hr %#x.\n", hr);
    ok(options == DEBUG_ENGOPT_INITIAL_BREAK, "Unexpected options %#x.\n", options);

    control->lpVtbl->Release(control);
}

static HRESULT WINAPI event_callbacks_QueryInterface(IDebugEventCallbacks *iface, REFIID riid, void **out)
{
    if (IsEqualIID(riid, &IID_IDebugEventCallbacks) ||
            IsEqualIID(riid, &IID_IUnknown))
    {
        *out = iface;
        iface->lpVtbl->AddRef(iface);
        return S_OK;
    }

    *out = NULL;
    return E_NOINTERFACE;
}

static ULONG WINAPI event_callbacks_AddRef(IDebugEventCallbacks *iface)
{
    return 2;
}

static ULONG WINAPI event_callbacks_Release(IDebugEventCallbacks *iface)
{
    return 1;
}

static HRESULT WINAPI event_callbacks_GetInterestMask(IDebugEventCallbacks *iface, ULONG *mask)
{
    *mask = ~0u;
    return S_OK;
}

static HRESULT WINAPI event_callbacks_Breakpoint(IDebugEventCallbacks *iface, PDEBUG_BREAKPOINT breakpoint)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI event_callbacks_Exception(IDebugEventCallbacks *iface, EXCEPTION_RECORD64 *exception,
        ULONG first_chance)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI event_callbacks_CreateThread(IDebugEventCallbacks *iface, ULONG64 handle, ULONG64 data_offset,
        ULONG64 start_offset)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI event_callbacks_ExitThread(IDebugEventCallbacks *iface, ULONG exit_code)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI event_callbacks_CreateProcess(IDebugEventCallbacks *iface, ULONG64 image_handle, ULONG64 handle,
        ULONG64 base_offset, ULONG module_size, const char *module_name, const char *image_name, ULONG checksum,
        ULONG timedatestamp, ULONG64 initial_thread_handle, ULONG64 thread_data_offset, ULONG64 start_offset)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI event_callbacks_ExitProcess(IDebugEventCallbacks *iface, ULONG exit_code)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI event_callbacks_LoadModule(IDebugEventCallbacks *iface, ULONG64 image_handle,
        ULONG64 base_offset, ULONG module_size, const char *module_name, const char *image_name, ULONG checksum,
        ULONG timedatestamp)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI event_callbacks_UnloadModule(IDebugEventCallbacks *iface, const char *image_basename,
        ULONG64 base_offset)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI event_callbacks_SystemError(IDebugEventCallbacks *iface, ULONG error, ULONG level)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI event_callbacks_SessionStatus(IDebugEventCallbacks *iface, ULONG status)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI event_callbacks_ChangeDebuggeeState(IDebugEventCallbacks *iface, ULONG flags, ULONG64 argument)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI event_callbacks_ChangeEngineState(IDebugEventCallbacks *iface, ULONG flags, ULONG64 argument)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI event_callbacks_ChangeSymbolState(IDebugEventCallbacks *iface, ULONG flags, ULONG64 argument)

{
    return E_NOTIMPL;
}

static const IDebugEventCallbacksVtbl event_callbacks_vtbl =
{
    event_callbacks_QueryInterface,
    event_callbacks_AddRef,
    event_callbacks_Release,
    event_callbacks_GetInterestMask,
    event_callbacks_Breakpoint,
    event_callbacks_Exception,
    event_callbacks_CreateThread,
    event_callbacks_ExitThread,
    event_callbacks_CreateProcess,
    event_callbacks_ExitProcess,
    event_callbacks_LoadModule,
    event_callbacks_UnloadModule,
    event_callbacks_SystemError,
    event_callbacks_SessionStatus,
    event_callbacks_ChangeDebuggeeState,
    event_callbacks_ChangeEngineState,
    event_callbacks_ChangeSymbolState,
};

static BOOL create_target_process(const char *event_name, PROCESS_INFORMATION *info)
{
    static const char *event_target_ready_name = "dbgeng_test_target_ready_event";
    char path_name[MAX_PATH];
    STARTUPINFOA startup;
    HANDLE ready_event;
    char **argv;
    BOOL ret;

    ready_event = CreateEventA(NULL, FALSE, FALSE, event_target_ready_name);
    ok(ready_event != NULL, "Failed to create event.\n");

    winetest_get_mainargs(&argv);
    memset(&startup, 0, sizeof(startup));
    startup.cb = sizeof(startup);
    sprintf(path_name, "%s dbgeng target %s %s", argv[0], event_name, event_target_ready_name);
    ret = CreateProcessA(NULL, path_name, NULL, NULL, FALSE, 0, NULL, NULL, &startup, info);
    if (ret)
    {
        WaitForSingleObject(ready_event, INFINITE);
    }

    CloseHandle(ready_event);

    return ret;
}

static void test_attach(void)
{
    static const char *event_name = "dbgeng_test_event";
    IDebugEventCallbacks event_callbacks = { &event_callbacks_vtbl };
    PROCESS_INFORMATION info;
    IDebugControl *control;
    IDebugClient *client;
    BOOL is_debugged;
    HANDLE event;
    HRESULT hr;
    BOOL ret;

    hr = DebugCreate(&IID_IDebugClient, (void **)&client);
    ok(hr == S_OK, "Failed to create engine object, hr %#x.\n", hr);

    hr = client->lpVtbl->QueryInterface(client, &IID_IDebugControl, (void **)&control);
    ok(hr == S_OK, "Failed to get interface pointer, hr %#x.\n", hr);

    hr = client->lpVtbl->SetEventCallbacks(client, &event_callbacks);
    ok(hr == S_OK, "Failed to set event callbacks, hr %#x.\n", hr);

    event = CreateEventA(NULL, FALSE, FALSE, event_name);
    ok(event != NULL, "Failed to create event.\n");

    ret = create_target_process(event_name, &info);
    ok(ret, "Failed to create target process.\n");

    is_debugged = TRUE;
    CheckRemoteDebuggerPresent(info.hProcess, &is_debugged);
    ok(!is_debugged, "Unexpected mode.\n");

    /* Non-invasive mode. */
    hr = client->lpVtbl->AttachProcess(client, 0, info.dwProcessId, DEBUG_ATTACH_NONINVASIVE);
    ok(hr == S_OK, "Failed to attach to process, hr %#x.\n", hr);

    is_debugged = TRUE;
    ret = CheckRemoteDebuggerPresent(info.hProcess, &is_debugged);
    ok(ret, "Failed to check target status.\n");
    ok(!is_debugged, "Unexpected mode.\n");

    hr = control->lpVtbl->WaitForEvent(control, 0, INFINITE);
    ok(hr == S_OK, "Waiting for event failed, hr %#x.\n", hr);

    is_debugged = TRUE;
    ret = CheckRemoteDebuggerPresent(info.hProcess, &is_debugged);
    ok(ret, "Failed to check target status.\n");
    ok(!is_debugged, "Unexpected mode.\n");

    hr = client->lpVtbl->DetachProcesses(client);
    ok(hr == S_OK, "Failed to detach, hr %#x.\n", hr);

    hr = client->lpVtbl->EndSession(client, DEBUG_END_ACTIVE_DETACH);
todo_wine
    ok(hr == S_OK, "Failed to end session, hr %#x.\n", hr);

    SetEvent(event);

    winetest_wait_child_process(info.hProcess);

    CloseHandle(info.hProcess);
    CloseHandle(info.hThread);

    CloseHandle(event);

    client->lpVtbl->Release(client);
    control->lpVtbl->Release(control);
}

static void test_module_information(void)
{
    static const char *event_name = "dbgeng_test_event";
    unsigned int loaded, unloaded, index, length;
    DEBUG_MODULE_PARAMETERS params[2];
    IDebugDataSpaces *dataspaces;
    PROCESS_INFORMATION info;
    IDebugSymbols2 *symbols;
    IDebugControl *control;
    ULONG64 bases[2], base;
    char buffer[MAX_PATH];
    IDebugClient *client;
    HANDLE event;
    HRESULT hr;
    BOOL ret;

    hr = DebugCreate(&IID_IDebugClient, (void **)&client);
    ok(hr == S_OK, "Failed to create engine object, hr %#x.\n", hr);

    hr = client->lpVtbl->QueryInterface(client, &IID_IDebugControl, (void **)&control);
    ok(hr == S_OK, "Failed to get interface pointer, hr %#x.\n", hr);

    hr = client->lpVtbl->QueryInterface(client, &IID_IDebugSymbols2, (void **)&symbols);
    ok(hr == S_OK, "Failed to get interface pointer, hr %#x.\n", hr);

    hr = client->lpVtbl->QueryInterface(client, &IID_IDebugDataSpaces, (void **)&dataspaces);
    ok(hr == S_OK, "Failed to get interface pointer, hr %#x.\n", hr);

    hr = control->lpVtbl->IsPointer64Bit(control);
    ok(hr == E_UNEXPECTED, "Unexpected hr %#x.\n", hr);

    event = CreateEventA(NULL, FALSE, FALSE, event_name);
    ok(event != NULL, "Failed to create event.\n");

    ret = create_target_process(event_name, &info);
    ok(ret, "Failed to create target process.\n");

    hr = control->lpVtbl->SetEngineOptions(control, DEBUG_ENGOPT_INITIAL_BREAK);
    ok(hr == S_OK, "Failed to set engine options, hr %#x.\n", hr);

    hr = client->lpVtbl->AttachProcess(client, 0, info.dwProcessId, DEBUG_ATTACH_NONINVASIVE);
    ok(hr == S_OK, "Failed to attach to process, hr %#x.\n", hr);

    hr = control->lpVtbl->IsPointer64Bit(control);
    ok(hr == E_UNEXPECTED, "Unexpected hr %#x.\n", hr);

    hr = control->lpVtbl->WaitForEvent(control, 0, INFINITE);
    ok(hr == S_OK, "Waiting for event failed, hr %#x.\n", hr);

    hr = control->lpVtbl->IsPointer64Bit(control);
    ok(SUCCEEDED(hr), "Failed to get pointer length, hr %#x.\n", hr);

    /* Number of modules. */
    hr = symbols->lpVtbl->GetNumberModules(symbols, &loaded, &unloaded);
    ok(hr == S_OK, "Unexpected hr %#x.\n", hr);
    ok(loaded > 0, "Unexpected module count %u.\n", loaded);

    /* Module base. */
    hr = symbols->lpVtbl->GetModuleByIndex(symbols, loaded, &base);
    ok(FAILED(hr), "Unexpected hr %#x.\n", hr);

    base = 0;
    hr = symbols->lpVtbl->GetModuleByIndex(symbols, 0, &base);
    ok(hr == S_OK, "Unexpected hr %#x.\n", hr);
    ok(!!base, "Unexpected module base.\n");

    hr = symbols->lpVtbl->GetModuleByOffset(symbols, 0, 0, &index, &base);
    ok(FAILED(hr), "Unexpected hr %#x.\n", hr);

    hr = symbols->lpVtbl->GetModuleByOffset(symbols, base, 0, &index, &base);
    ok(hr == S_OK, "Failed to get module, hr %#x.\n", hr);

    hr = symbols->lpVtbl->GetModuleByOffset(symbols, base, 0, NULL, NULL);
    ok(hr == S_OK, "Failed to get module, hr %#x.\n", hr);

    hr = symbols->lpVtbl->GetModuleByOffset(symbols, base + 1, 0, NULL, NULL);
    ok(hr == S_OK, "Failed to get module, hr %#x.\n", hr);

    hr = symbols->lpVtbl->GetModuleByOffset(symbols, base, loaded, NULL, NULL);
    ok(FAILED(hr), "Unexpected hr %#x.\n", hr);

    /* Parameters. */
    base = 0;
    hr = symbols->lpVtbl->GetModuleByIndex(symbols, 0, &base);
    ok(hr == S_OK, "Unexpected hr %#x.\n", hr);
    ok(!!base, "Unexpected module base.\n");

    hr = symbols->lpVtbl->GetModuleParameters(symbols, 1, NULL, 0, params);
    ok(hr == S_OK, "Failed to get module parameters, hr %#x.\n", hr);
    ok(params[0].Base == base, "Unexpected module base.\n");

    hr = symbols->lpVtbl->GetModuleParameters(symbols, 1, &base, 100, params);
    ok(hr == S_OK, "Failed to get module parameters, hr %#x.\n", hr);
    ok(params[0].Base == base, "Unexpected module base.\n");

    bases[0] = base + 1;
    bases[1] = base;
    hr = symbols->lpVtbl->GetModuleParameters(symbols, 2, bases, 0, params);
    ok(hr == S_OK || broken(hr == E_NOINTERFACE) /* XP */, "Failed to get module parameters, hr %#x.\n", hr);
    ok(params[0].Base == DEBUG_INVALID_OFFSET, "Unexpected module base.\n");
    ok(params[0].Size == 0, "Unexpected module size.\n");
    ok(params[1].Base == base, "Unexpected module base.\n");
    ok(params[1].Size != 0, "Unexpected module size.\n");

    hr = symbols->lpVtbl->GetModuleParameters(symbols, 1, bases, 0, params);
    ok(hr == S_OK || broken(hr == E_NOINTERFACE) /* XP */, "Failed to get module parameters, hr %#x.\n", hr);

    hr = symbols->lpVtbl->GetModuleParameters(symbols, 1, bases, loaded, params);
    ok(hr == S_OK || broken(hr == E_NOINTERFACE) /* XP */, "Failed to get module parameters, hr %#x.\n", hr);

    hr = symbols->lpVtbl->GetModuleParameters(symbols, 1, NULL, loaded, params);
    ok(FAILED(hr), "Unexpected hr %#x.\n", hr);

    /* Image name. */
    hr = symbols->lpVtbl->GetModuleNameString(symbols, DEBUG_MODNAME_IMAGE, 0, 0, buffer, sizeof(buffer), &length);
    ok(hr == S_OK, "Failed to get image name, hr %#x.\n", hr);
    ok(strlen(buffer) + 1 == length, "Unexpected length.\n");

    hr = symbols->lpVtbl->GetModuleNameString(symbols, DEBUG_MODNAME_IMAGE, 0, 0, NULL, sizeof(buffer), &length);
    ok(hr == S_OK, "Failed to get image name, hr %#x.\n", hr);
    ok(length > 0, "Unexpected length.\n");

    hr = symbols->lpVtbl->GetModuleNameString(symbols, DEBUG_MODNAME_IMAGE, DEBUG_ANY_ID, base, buffer, sizeof(buffer),
            &length);
    ok(hr == S_OK, "Failed to get image name, hr %#x.\n", hr);
    ok(strlen(buffer) + 1 == length, "Unexpected length.\n");

    hr = symbols->lpVtbl->GetModuleNameString(symbols, DEBUG_MODNAME_IMAGE, 0, 0, buffer, length - 1, &length);
    ok(hr == S_FALSE, "Failed to get image name, hr %#x.\n", hr);
    ok(strlen(buffer) + 2 == length, "Unexpected length %u.\n", length);

    hr = symbols->lpVtbl->GetModuleNameString(symbols, DEBUG_MODNAME_IMAGE, 0, 0, NULL, length - 1, NULL);
    ok(hr == S_FALSE, "Failed to get image name, hr %#x.\n", hr);

    /* Read memory. */
    base = 0;
    hr = symbols->lpVtbl->GetModuleByIndex(symbols, 0, &base);
    ok(hr == S_OK, "Unexpected hr %#x.\n", hr);
    ok(!!base, "Unexpected module base.\n");

    hr = dataspaces->lpVtbl->ReadVirtual(dataspaces, base, buffer, sizeof(buffer), &length);
    ok(hr == S_OK, "Failed to read process memory, hr %#x.\n", hr);
    ok(length == sizeof(buffer), "Unexpected length %u.\n", length);
    ok(buffer[0] == 'M' && buffer[1] == 'Z', "Unexpected contents.\n");

    memset(buffer, 0, sizeof(buffer));
    hr = dataspaces->lpVtbl->ReadVirtual(dataspaces, base, buffer, sizeof(buffer), NULL);
    ok(hr == S_OK, "Failed to read process memory, hr %#x.\n", hr);
    ok(buffer[0] == 'M' && buffer[1] == 'Z', "Unexpected contents.\n");

    hr = client->lpVtbl->DetachProcesses(client);
    ok(hr == S_OK, "Failed to detach, hr %#x.\n", hr);

    SetEvent(event);
    winetest_wait_child_process(info.hProcess);

    CloseHandle(info.hProcess);
    CloseHandle(info.hThread);
    CloseHandle(event);

    client->lpVtbl->Release(client);
    control->lpVtbl->Release(control);
    symbols->lpVtbl->Release(symbols);
    dataspaces->lpVtbl->Release(dataspaces);
}

static void target_proc(const char *event_name, const char *event_ready_name)
{
    HANDLE terminate_event, ready_event;

    terminate_event = OpenEventA(SYNCHRONIZE, FALSE, event_name);
    ok(terminate_event != NULL, "Failed to open event handle.\n");

    ready_event = OpenEventA(EVENT_MODIFY_STATE, FALSE, event_ready_name);
    ok(ready_event != NULL, "Failed to open event handle.\n");

    SetEvent(ready_event);

    for (;;)
    {
        if (WaitForSingleObject(terminate_event, 100) == WAIT_OBJECT_0)
            break;
    }

    CloseHandle(terminate_event);
    CloseHandle(ready_event);
}

START_TEST(dbgeng)
{
    char **argv;
    int argc;

    argc = winetest_get_mainargs(&argv);

    if (argc > 4 && !strcmp(argv[2], "target"))
    {
        target_proc(argv[3], argv[4]);
        return;
    }

    test_engine_options();
    test_attach();
    test_module_information();
}