/*
   Copyright (C) 2019 Red Hat, Inc.

   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, see <http://www.gnu.org/licenses/>.
*/

/* Mock some code in usb-backend.c
 *
 * We include directly the source we want to modify, this allows to:
 * - access static functions;
 * - access private declarations;
 * - mock (replace) some external function calls.
 * All without changing a single line of code, adding files or similar.
 * This works as the linker prefer object symbols to library symbols;
 * as the symbols here will be in an object and the other version is
 * in a library this will work.
 *
 * The defines before the include allows to replace the external
 * function we need to replace. You can always declare the original
 * function and wrap it in the mock function if needed.
 */
#define spice_usbredir_write mock_spice_usbredir_write
#define spice_channel_get_state mock_spice_channel_get_state
#include "../src/usb-backend.c"

#include "usb-device-cd.h"

static SpiceUsbDevice *device = NULL;

/* simple usb manager hotplug callback emulation. */
static void
test_hotplug_callback(void *user_data, SpiceUsbDevice *dev, gboolean added)
{
    // ignore not emulated devices
    const UsbDeviceInformation *info = spice_usb_backend_device_get_info(dev);
    if (info->bus != BUS_NUMBER_FOR_EMULATED_USB) {
        return;
    }

    if (added) {
        g_assert_null(device);
        device = spice_usb_backend_device_ref(dev);
    } else {
        g_assert_nonnull(device);
        g_assert(device == dev);
        spice_usb_backend_device_unref(dev);
        device = NULL;
    }
}

static void multiple(const void *param)
{
    guint limit = GPOINTER_TO_UINT(param);
    CdEmulationParams params = { "test-cd-emu.iso", 1 };
    GError *err = NULL;
    SpiceUsbBackend * be = spice_usb_backend_new(&err);
    g_assert_nonnull(be);
    g_assert_null(err);
    spice_usb_backend_register_hotplug(be, NULL, test_hotplug_callback, &err);
    g_assert_null(err);
    for (int i = 0; i < limit; i++) {
        // allocate a CD emulation device
        g_assert_true(create_emulated_cd(be, &params, &err));
        g_assert_null(err);
        g_assert_nonnull(device);

        // emulate automatic CD ejection, this should release the
        // object
        spice_usb_backend_device_eject(be, device);
        g_assert_null(device);
    }
    spice_usb_backend_deregister_hotplug(be);
    spice_usb_backend_delete(be);
}

static unsigned int messages_sent = 0;
static unsigned int hellos_sent = 0;
static SpiceUsbBackendChannel *usb_ch;

int
mock_spice_usbredir_write(SpiceUsbredirChannel *channel, uint8_t *data, int count)
{
    messages_sent++;
    g_assert_cmpint(count, >=, 4);
    const uint32_t type = data[0] + data[1] * 0x100u + data[2] * 0x10000u + data[3] * 0x1000000u;
    if (type == usb_redir_hello) {
        hellos_sent++;
    }

    // return we handled the data
    spice_usb_backend_return_write_data(usb_ch, data);
    return count;
}

// channel state to return from Mock function
static enum spice_channel_state ch_state = SPICE_CHANNEL_STATE_UNCONNECTED;

enum spice_channel_state
mock_spice_channel_get_state(SpiceChannel *channel)
{
    return ch_state;
}

// number of GObjects allocated we expect will be freed
static unsigned gobjects_allocated = 0;
static void decrement_allocated(gpointer data G_GNUC_UNUSED, GObject *old_gobject G_GNUC_UNUSED)
{
    g_assert_cmpint(gobjects_allocated, !=, 0);
    gobjects_allocated--;
}

#define DATA_START \
    do { static const uint8_t data[] = {
#define DATA_SEND \
        }; \
        spice_usb_backend_read_guest_data(usb_ch, (uint8_t*)data, G_N_ELEMENTS(data)); \
    } while(0)

static void
device_iteration(const int loop, const bool attach_on_connect)
{
    GError *err = NULL;
    unsigned int hellos_expected, messages_expected;

    hellos_expected = hellos_sent;
    messages_expected = messages_sent;

    if (ch_state == SPICE_CHANNEL_STATE_UNCONNECTED) {
        ch_state = SPICE_CHANNEL_STATE_CONNECTING;
    }
    if (attach_on_connect) {
        g_assert_true(spice_usb_backend_channel_attach(usb_ch, device, &err));
        g_assert_null(err);
        if (ch_state == SPICE_CHANNEL_STATE_READY) {
            hellos_expected = MIN(hellos_expected + 1, 1);
            messages_expected++;
        } else {
            g_assert_cmpint(messages_sent, ==, messages_expected);
        }
    }
    g_assert_cmpint(hellos_sent, ==, hellos_expected);
    g_assert_cmpint(messages_sent, >=, messages_expected);

    // try to get initial data
    if (ch_state == SPICE_CHANNEL_STATE_CONNECTING) {
        ch_state = SPICE_CHANNEL_STATE_READY;
        spice_usb_backend_channel_flush_writes(usb_ch);
        hellos_expected = MIN(hellos_expected + 1, 1);
        messages_expected++;
    }

    // we should get an hello (only one!)
    g_assert_cmpint(hellos_sent, ==, hellos_expected);
    g_assert_cmpint(messages_sent, >=, messages_expected);

    spice_usb_backend_channel_flush_writes(usb_ch);
    g_assert_cmpint(hellos_sent, ==, hellos_expected);
    g_assert_cmpint(messages_sent, >=, messages_expected);

    // send hello reply
    if (loop == 0) {
        DATA_START
            0x00,0x00,0x00,0x00,0x44,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //000 ....D.......
            0x71,0x65,0x6d,0x75,0x20,0x75,0x73,0x62,0x2d,0x72,0x65,0x64, //00c qemu usb-red
            0x69,0x72,0x20,0x67,0x75,0x65,0x73,0x74,0x20,0x33,0x2e,0x30, //018 ir guest 3.0
            0x2e,0x31,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //024 .1..........
            0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //030 ............
            0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //03c ............
            0x00,0x00,0x00,0x00,0xff,0x00,0x00,0x00,                     //048 ........
        DATA_SEND;
    }

    if (!attach_on_connect) {
        g_assert_true(spice_usb_backend_channel_attach(usb_ch, device, &err));
        g_assert_null(err);
    }
    g_assert_cmpint(hellos_sent, ==, 1);
    g_assert_cmpint(messages_sent, >, 1);

    spice_usb_backend_channel_detach(usb_ch);
}

static void attach(const void *param)
{
    const bool attach_on_connect = !!(GPOINTER_TO_UINT(param) & 1);
    const bool libusb_enabled = !!(GPOINTER_TO_UINT(param) & 2);

    hellos_sent = 0;
    messages_sent = 0;
    ch_state = SPICE_CHANNEL_STATE_UNCONNECTED;

    SpiceSession *session = spice_session_new();
    g_assert_nonnull(session);
    g_object_weak_ref(G_OBJECT(session), decrement_allocated, NULL);
    SpiceChannel *ch = spice_channel_new(session, SPICE_CHANNEL_USBREDIR, 0);
    g_assert_nonnull(ch);
    g_object_weak_ref(G_OBJECT(ch), decrement_allocated, NULL);
    gobjects_allocated = 2;

    /*
     * real test, allocate a channel usbredir, emulate device
     * initialization.
     * Filter some call.
     * Start sequence:
     * - spice_usb_backend_new
     * - spice_usb_backend_register_hotplug
     * - spice_usb_backend_create_emulated_device
     * - spice_usb_backend_channel_new
     * - spice_usb_backend_channel_attach (if redir on connect)
     * - spice_usb_backend_channel_flush_writes
     * - spice_usbredir_write_callback
     * - spice_usb_backend_return_write_data
     * - spice_usb_backend_read_guest_data
     * - spice_usb_backend_channel_attach (if not redir on connect)
     */
    GError *err = NULL;
    SpiceUsbBackend * be = spice_usb_backend_new(&err);
    g_assert_nonnull(be);
    g_assert_null(err);
    spice_usb_backend_register_hotplug(be, NULL, test_hotplug_callback, &err);
    g_assert_null(err);

    CdEmulationParams params = { "test-cd-emu.iso", 1 };
    g_assert_true(create_emulated_cd(be, &params, &err));
    g_assert_null(err);
    g_assert_nonnull(device);
    g_assert_false(device->edev_configured);

    void *libusb_context_saved = be->libusb_context;
    if (!libusb_enabled) {
        spice_usb_backend_deregister_hotplug(be);
        be->libusb_context = NULL;
    }
    usb_ch = spice_usb_backend_channel_new(be, SPICE_USBREDIR_CHANNEL(ch));
    if (!libusb_enabled) {
        be->libusb_context = libusb_context_saved;
        spice_usb_backend_register_hotplug(be, NULL, test_hotplug_callback, &err);
        g_assert_null(err);
    }
    g_assert_nonnull(usb_ch);

    for (int loop = 0; loop < 2; loop++) {
        device_iteration(loop, attach_on_connect);
    }

/*

> to server/guest
< to client from server/guest

> usb_redir_interface_info,
> usb_redir_ep_info,
> usb_redir_device_connect

< usb_redir_reset
< usb_redir_control_packet

> usb_redir_control_packet

*/

    // cleanup
    spice_usb_backend_device_unref(device);
    device = NULL;
    spice_usb_backend_channel_delete(usb_ch);
    usb_ch = NULL;
    spice_usb_backend_deregister_hotplug(be);
    spice_usb_backend_delete(be);

    // this it's the correct sequence to free session!
    // g_object_unref is not enough, causing wrong reference countings
    spice_session_disconnect(session);
    g_object_unref(session);
    while (g_main_context_iteration(NULL, FALSE)) {
        continue;
    }

    g_assert_cmpint(gobjects_allocated, ==, 0);
}

#define TEST_CD_ISO_FILE "test-cd-emu.iso"

static void
write_test_iso(void)
{
    uint8_t sector[2048];
    FILE *f = fopen(TEST_CD_ISO_FILE, "wb");
    g_assert_nonnull(f);
    memset(sector, 0, sizeof(sector));
    strcpy((char*) sector, "sector 0");
    fwrite(sector, sizeof(sector), 1, f);
    fclose(f);
}

int main(int argc, char* argv[])
{
    write_test_iso();

    g_test_init(&argc, &argv, NULL);

    g_test_add_data_func("/cd-emu/simple", GUINT_TO_POINTER(1), multiple);
    g_test_add_data_func("/cd-emu/multiple", GUINT_TO_POINTER(128), multiple);
#define ATTACH_PARAM(auto_attach, libusb) \
    GUINT_TO_POINTER(!!(auto_attach) + 2 * !!(libusb))
    g_test_add_data_func("/cd-emu/attach_no_auto", ATTACH_PARAM(0, 1), attach);
    g_test_add_data_func("/cd-emu/attach_auto", ATTACH_PARAM(1, 1), attach);
    g_test_add_data_func("/cd-emu/attach_no_auto_no_libusb", ATTACH_PARAM(0, 0), attach);
    g_test_add_data_func("/cd-emu/attach_auto_no_libusb", ATTACH_PARAM(1, 0), attach);

    int ret =  g_test_run();

    unlink(TEST_CD_ISO_FILE);
    return ret;
}
