#include <test_helper.h>

// Note: A lot of these tests make specific assumptions about the timing of
// various operations.  If the tests fail intermittently, some parameters may
// need to be adjusted.

const uint8_t pipe_id = 0x82;

#ifdef USE_TEST_DEVICE_A
static void check_error_for_cancelled_transfer(const libusbp::error & error)
{
    if (!error)
    {
        // The transfer actually completed successfully.
        return;
    }

    if (error.has_code(LIBUSBP_ERROR_CANCELLED))
    {
        // This is expected.
    }
    else
    {
        // Some other error happened that we didn't expect.
        throw error;
    }
}

static void clean_up_async_in_pipe(libusbp::async_in_pipe & pipe)
{
    pipe.cancel_transfers();
    test_timeout timeout(500);
    while(pipe.has_pending_transfers())
    {
        pipe.handle_events();
        libusbp::error transfer_error;
        while(pipe.handle_finished_transfer(NULL, NULL, &transfer_error))
        {
            check_error_for_cancelled_transfer(transfer_error);
        }
        timeout.check();
        sleep_quick();
    }
}

static void clean_up_async_in_pipe_and_expect_a_success(libusbp::async_in_pipe & pipe)
{
    pipe.cancel_transfers();

    test_timeout timeout(500);
    uint32_t success_count = 0;
    while(pipe.has_pending_transfers())
    {
        pipe.handle_events();
        libusbp::error transfer_error;
        uint8_t buffer[64] = {0};
        size_t transferred;
        while(pipe.handle_finished_transfer(buffer, &transferred, &transfer_error))
        {
            check_error_for_cancelled_transfer(transfer_error);
            if (!transfer_error)
            {
                REQUIRE(buffer[4] == 0xAB);
                REQUIRE(transferred == 5);
                success_count++;
            }
        }
        timeout.check();
        sleep_quick();
    }
    REQUIRE(success_count > 0);
}
#endif

TEST_CASE("async_in_pipe traits")
{
    libusbp::async_in_pipe pipe, pipe2;

    SECTION("is not copy-constructible")
    {
        REQUIRE(std::is_copy_constructible<libusbp::async_in_pipe>::value == false);

        // Should not compile:
        // libusbp::async_in_pipe pipe3(pipe);
    }

    SECTION("is not copy-assignable")
    {
        REQUIRE(std::is_copy_assignable<libusbp::async_in_pipe>::value == false);

        // Should not compile:
        // pipe2 = pipe;
    }
}

#ifdef USE_TEST_DEVICE_A

TEST_CASE("async_in_pipe basic properties")
{
    libusbp::device device = find_test_device_a();
    libusbp::generic_interface gi(device, 0, true);
    libusbp::generic_handle handle(gi);
    libusbp::async_in_pipe pipe = handle.open_async_in_pipe(pipe_id);

    SECTION("is present")
    {
        REQUIRE(pipe);
    }

    SECTION("is movable")
    {
        libusbp::async_in_pipe pipe2 = std::move(pipe);
        REQUIRE(pipe2);
        REQUIRE_FALSE(pipe);
    }

    SECTION("is move-assignable")
    {
        libusbp::async_in_pipe pipe2;
        pipe2 = std::move(pipe);
        REQUIRE(pipe2);
        REQUIRE_FALSE(pipe);
    }
}

TEST_CASE("null async_in_pipe")
{
    libusbp::async_in_pipe pipe;
    std::string expected_message = "Pipe argument is null.";

    SECTION("is not present")
    {
        CHECK_FALSE(pipe);
    }

    SECTION("cannot allocate transfers")
    {
        try
        {
            pipe.allocate_transfers(4, 5);
            REQUIRE(0);
        }
        catch(const libusbp::error & error)
        {
            REQUIRE(error.message() == expected_message);
        }
    }

    SECTION("cannot start endless transfers")
    {
        try
        {
            pipe.start_endless_transfers();
            REQUIRE(0);
        }
        catch(const libusbp::error & error)
        {
            REQUIRE(error.message() == expected_message);
        }
    }

    SECTION("cannot handle events")
    {
        try
        {
            pipe.handle_events();
            REQUIRE(0);
        }
        catch(const libusbp::error & error)
        {
            REQUIRE(error.message() == expected_message);
        }
    }

    SECTION("cannot say if it has pending transfers")
    {
        // Test it this way to make sure the C++ wrapper throws an exception
        // instead of ignoring it.
        try
        {
            pipe.has_pending_transfers();
            REQUIRE(0);
        }
        catch(const libusbp::error & error)
        {
            REQUIRE(error.message() == expected_message);
        }

        // Also test it this way so we can check the "result" output parameter
        // in the C API.
        bool result = true;
        libusbp::error error(libusbp_async_in_pipe_has_pending_transfers(NULL, &result));
        REQUIRE(error.message() == expected_message);
        REQUIRE_FALSE(result);
    }

    SECTION("cannot handle a finished transfer")
    {
        uint8_t buffer[] = "hi there";
        size_t transferred = 10;
        libusbp::error error = get_some_error();
        try
        {
            pipe.handle_finished_transfer(buffer, &transferred, &error);
        }
        catch(const libusbp::error & error)
        {
            REQUIRE(error.message() == expected_message);
        }
        CHECK(buffer[0] == 'h');
        CHECK(transferred == 0);
        CHECK_FALSE(error);
    }

    SECTION("cannot cancel all transfers")
    {
        try
        {
            pipe.cancel_transfers();
            REQUIRE(0);
        }
        catch(const libusbp::error & error)
        {
            REQUIRE(error.message() == expected_message);
        }
    }
}

TEST_CASE("async_in_pipe parameter validation and state checks")
{
    libusbp::device device = find_test_device_a();
    libusbp::generic_interface gi(device, 0, true);
    libusbp::generic_handle handle(gi);
    libusbp::async_in_pipe pipe = handle.open_async_in_pipe(0x82);

    SECTION("allocate_transfers")
    {
        SECTION("cannot be called twice on the same pipe")
        {
            pipe.allocate_transfers(1, 64);
            try
            {
                pipe.allocate_transfers(1, 64);
            }
            catch(const libusbp::error & error)
            {
                REQUIRE(error.message() == "Transfers were already allocated for this pipe.");
            }
        }

        SECTION("does not allow transfer_count to be 0")
        {
            // Set the size to 0 also so we can test that the count is checked
            // before the size (it's nice if the little details stay consistent
            // over time and across platforms).
            try
            {
                pipe.allocate_transfers(0, 0);
            }
            catch(const libusbp::error & error)
            {
                REQUIRE(error.message() == "Transfer count cannot be zero.");
            }
        }

        SECTION("does not allow transfer_size to be 0")
        {
            try
            {
                pipe.allocate_transfers(64, 0);
            }
            catch(const libusbp::error & error)
            {
                REQUIRE(error.message() == "Transfer size cannot be zero.");
            }
        }

        SECTION("rejects transfer sizes too large for the underlying APIs")
        {
            #ifdef _WIN32
            // On Windows, URB buffer sizes are represented by ULONGs.
            if (SIZE_MAX <= ULONG_MAX) { return; }
            size_t too_large_size = (size_t)ULONG_MAX + 1;
            #endif

            #ifdef __linux__
            // On Linux, URB buffer sizes are represented by ints.
            if (SIZE_MAX <= INT_MAX) { return; }
            size_t too_large_size = (size_t)INT_MAX + 1;
            #endif

            #ifdef __APPLE__
            size_t too_large_size = (size_t)UINT32_MAX + 1;
            #endif

            try
            {
                pipe.allocate_transfers(1, too_large_size);
                REQUIRE(0);
            }
            catch(const libusbp::error & error)
            {
                REQUIRE(error.message() ==
                    "Failed to allocate transfers for asynchronous IN pipe.  "
                    "Transfer size is too large.");
            }
        }
    }

    SECTION("start_endless_transfers")
    {
        SECTION("complains if transfers were not allocated")
        {
            try
            {
                pipe.start_endless_transfers();
            }
            catch(const libusbp::error & error)
            {
                REQUIRE(error.message() == "Pipe transfers have not been allocated yet.");
            }
        }
    }

    SECTION("has_pending_transfers")
    {
        SECTION("works even if transfers were not allocated")
        {
            REQUIRE_FALSE(pipe.has_pending_transfers());
        }

        SECTION("complains if the output pointer is NULL")
        {
            libusbp::error error(libusbp_async_in_pipe_has_pending_transfers(
                    pipe.pointer_get(), NULL));
            REQUIRE(error.message() == "Boolean output pointer is null.");
        }
    }
}

TEST_CASE("async_in_pipe for an interrupt endpoint")
{
    // If it works for an IN endpoint, it should work for a bulk endpoint too
    // because the underlying APIs that libusbp uses allow us to treat those
    // types of endpoints the same.

    libusbp::device device = find_test_device_a();
    libusbp::generic_interface gi(device, 0, true);
    libusbp::generic_handle handle(gi);
    libusbp::async_in_pipe pipe = handle.open_async_in_pipe(0x82);
    const size_t transfer_size = 5;

    test_timeout timeout(500);

    SECTION("can do continuous transfers and then cancel them")
    {
        pipe.allocate_transfers(5, transfer_size);
        pipe.start_endless_transfers();

        size_t finish_count = 0;
        while(finish_count < 12)
        {
            // Don't use REQUIRE or CATCH here because then the number of
            // assertions printed at the end of the tests will be unpredictable.
            if (!pipe.has_pending_transfers()) { throw "No pending transfers."; }

            uint8_t buffer[transfer_size] = {0};
            size_t transferred;
            libusbp::error transfer_error;
            if (pipe.handle_finished_transfer(buffer, &transferred, &transfer_error))
            {
                if (transfer_error) { throw transfer_error; }

                REQUIRE(buffer[4] == 0xAB);
                finish_count++;
            }

            pipe.handle_events();
            timeout.check();
            sleep_quick();
        }

        REQUIRE(pipe.has_pending_transfers());

        clean_up_async_in_pipe(pipe);
    }

    SECTION("cancelling a transfer before it is completed")
    {
        // Event order tested here: submit, cancel, "reap"

        // Make a lot of transfers and cancel then immediately to make sure at
        // least one was cancelled before it was completed.

        #ifdef __linux__

        // On a normal Linux machine it takes about 1 ms per transfer to cancel
        // transfers.  (Maybe that is because we are using an endpoint with a 1
        // ms polling interval.)  On a VirtualBox machine running on a Windows
        // guest, it takes about much longer: 20 ms per transfer.  To work
        // around this slowness, we only allocate 20 transfers at a time in
        // Linux.

        // Maybe later we should we provide a workaround that lets
        // people close the generic_handle and all its pipes at the same time,
        // thus saving their users from this painful wait, without causing
        // memory leaks.  If the generic handle fd gets closed and replaced with
        // -1, then it would be safe to just free all the resources for the URBs
        // without actually cancelling them.

        // We would like to make the transfer count larger (500) to make this
        // test more resilient to lag caused by the operating system, but that
        // slows down the development process too much.
        const size_t transfer_count = 20;

        #else

        const size_t transfer_count = 500;

        #endif

        pipe.allocate_transfers(transfer_count, transfer_size);
        pipe.start_endless_transfers();

        //printf("Cancelling %d transfers\n", (int)transfer_count);
        test_timeout cancel_timer(10000);
        pipe.cancel_transfers();
        //printf("Done cancelling, took %d ms\n", cancel_timer.get_milliseconds());

        size_t cancel_count = 0;
        test_timeout timeout(10000);
        while(pipe.has_pending_transfers())
        {
            pipe.handle_events();
            libusbp::error transfer_error;
            while(pipe.handle_finished_transfer(NULL, NULL, &transfer_error))
            {
                check_error_for_cancelled_transfer(transfer_error);

                if (transfer_error.has_code(LIBUSBP_ERROR_CANCELLED))
                {
                    cancel_count++;
                }
            }
            timeout.check();
            sleep_quick();
        }

        // Make sure at least one error from the cancelled transfers had the
        // right error code.
        //printf("Cancel count: %d\n", (int)cancel_count);
        REQUIRE(cancel_count > 5);
    }

    SECTION("cancelling a transfer after it is completed but before the libusbp knows")
    {
        // Event order tested here: submit, complete, cancel, "reap"

        pipe.allocate_transfers(1, transfer_size);
        pipe.start_endless_transfers();
        sleep_ms(30);
        clean_up_async_in_pipe_and_expect_a_success(pipe);
    }

    SECTION("cancelling a transfer after it is completed and the event is handled")
    {
        // Event order tested here: submit, complete, "reap", cancel

        pipe.allocate_transfers(1, transfer_size);
        pipe.start_endless_transfers();
        sleep_ms(30);
        pipe.handle_events();
        clean_up_async_in_pipe_and_expect_a_success(pipe);
    }

    SECTION("cancelling a partially completed transfer")
    {
        // Note: This test seems to always fail on Windows Vista and Windows 7
        // because WinUSB reports that 0 bytes have been transferred instead of
        // 10.  It might be that older versions of WinUSB or of the USB stack
        // didn't support returning data from a cancelled, partially completed
        // transfer.

        // Assumption: there will be two packets queued up by Test Device A in
        // its ping-pong buffers.  So when we tell it to pause the ADC for 100
        // ms, a three-packet transfer will quickly receive those two packets
        // and then keep waiting for more.

        // Previous versions of macOS returned one packet instead of two,
        // but this is no longer true on Darwin Kernel 22.1.0 (Oct 2022).

        // Pause the ADC for 100 ms.
        handle.control_transfer(0x40, 0xA0, 100, 0);

        pipe.allocate_transfers(1, transfer_size * 3);
        pipe.start_endless_transfers();
        sleep_ms(20);

        pipe.cancel_transfers();
        test_timeout timeout(500);
        while(pipe.has_pending_transfers())
        {
            pipe.handle_events();
            libusbp::error transfer_error;
            uint8_t buffer[transfer_size * 3];
            size_t transferred;
            while(pipe.handle_finished_transfer(buffer, &transferred, &transfer_error))
            {
                CHECK(transfer_error);
                check_error_for_cancelled_transfer(transfer_error);

                #if defined(VBOX_LINUX_ON_WINDOWS)
                CHECK(transferred == 0);
                #else
                CHECK(transferred == transfer_size * 2);
                CHECK(buffer[4] == 0xAB);
                CHECK(buffer[9] == 0xAB);
                #endif
            }
            timeout.check();
            sleep_quick();
        }

        // Unpause the ADC.
        handle.control_transfer(0x40, 0xA0, 0, 0);
    }

    SECTION("cancelling transfers twice is okay")
    {
        pipe.allocate_transfers(1, transfer_size);
        pipe.start_endless_transfers();
        pipe.cancel_transfers();
        clean_up_async_in_pipe(pipe);
    }

    SECTION("might respect the time out from set_timeout")
    {
        #ifdef __linux__
        // On Linux, asynchronous requests cannot have a timeout, so this test
        // will not pass.  If we want to implement a timeout feature, we would
        // need to make some kind of timer like libusb does, and cancel the
        // requests when the time is up.  It's probably fine for the user to
        // just implement a timeout themselves using a separate time library.
        return;
        #endif

        #ifdef __APPLE__
        // On Mac OS X, interrupt endpoints cannot have a timeout, and our
        // library simply doesn't use a timeout (we use ReadPipeAsync instead of
        // ReadPipeAsyncTO).
        return;
        #endif

        // Pause the ADC for 200 ms.
        handle.control_transfer(0x40, 0xA0, 200, 0);

        handle.set_timeout(pipe_id, 10);
        pipe.allocate_transfers(5, transfer_size);
        pipe.start_endless_transfers();

        uint32_t timeout_count = 0;
        uint32_t success_count = 0;
        test_timeout timeout(500);
        while(timeout_count < 8)
        {
            pipe.handle_events();
            libusbp::error transfer_error;
            while(pipe.handle_finished_transfer(NULL, NULL, &transfer_error))
            {
                // We expect there to be 0 to 2 successes, and the rest of the
                // transfers will be timeouts.
                if (transfer_error == false)
                {
                    // Successful transfer.
                    success_count++;

                    if (timeout_count != 0)
                    {
                        throw "Got a success after a timeout.";
                    }
                    if (success_count > 2)
                    {
                        throw "Got too many successes.";
                    }
                }
                else if (transfer_error.has_code(LIBUSBP_ERROR_TIMEOUT))
                {
                    // Timeout.

                    const char * expected =
                        "Asynchronous IN transfer failed.  "
                        "The operation timed out.  "
                        #ifdef _WIN32
                        "Windows error code 0x79."
                        #endif
                        ;
                    REQUIRE(transfer_error.message() == expected);
                    timeout_count++;
                }
                else
                {
                    // Unexpected error.
                    throw transfer_error;
                }
            }
            timeout.check();
            sleep_quick();
        }

        // Clean up the pipe, while accepting that there might be more transfers
        // finishing with a cancellation error.
        pipe.cancel_transfers();
        test_timeout timeout_cleanup(500);
        while(pipe.has_pending_transfers())
        {
            pipe.handle_events();
            libusbp::error transfer_error;
            while(pipe.handle_finished_transfer(NULL, NULL, &transfer_error))
            {
                if (transfer_error
                    && !transfer_error.has_code(LIBUSBP_ERROR_CANCELLED)
                    && !transfer_error.has_code(LIBUSBP_ERROR_TIMEOUT))
                {
                    throw transfer_error;
                }
            }
            timeout_cleanup.check();
            sleep_quick();
        }

        // Unpause the ADC.
        handle.control_transfer(0x40, 0xA0, 0, 0);
    }

    SECTION("overflow")
    {
        #ifdef VBOX_LINUX_ON_WINDOWS
        // This test fails and then puts the USB device into a weird state
        // if run on Linux inside VirtualBox on a Windows host.
        std::cerr << "Skipping asynchronous IN pipe overflow test.\n";
        return;
        #endif

        pipe.allocate_transfers(1, transfer_size + 1);
        pipe.start_endless_transfers();
        sleep_ms(10);
        pipe.handle_events();

	uint8_t buffer[transfer_size] = {0};
	size_t transferred;
	libusbp::error transfer_error;
	bool finished = pipe.handle_finished_transfer(buffer,
            &transferred, &transfer_error);
        REQUIRE(finished);

        std::string expected_message;
        size_t expected_transferred;
        #ifdef _WIN32
        // This request is an error in WinUSB since we are using RAW_IO.  The
        // error is detected before any data is transferred.
        expected_transferred = 0;
        expected_message = "Asynchronous IN transfer failed.  "
            "Incorrect function.  Windows error code 0x1.";
        #elif defined(__linux__)
        // This request results in an error in Linux after some data is transferred.
        // On some older Linux systems, expected_transferred was transfer_size + 1.
        // With Linux 5.15 on a Raspberry Pi 4, expected_transferred is transfer_size.
        expected_transferred = transfer_size;
        expected_message = "Asynchronous IN transfer failed.  "
            "The transfer overflowed.  Error code 75.";
        #elif defined(__APPLE__)
        // On Mac OS X, this results in an error after some data is transferred.
        expected_transferred = transfer_size;
        expected_message = "Asynchronous IN transfer failed.  "
            "The transfer overflowed.  Error code 0xe00002e8.";
        #else
        REQUIRE(0);
        #endif

        CHECK(transferred == expected_transferred);
        CHECK(transfer_error.message() == expected_message);

        // We can't just call clean_up_async_in_pipe because we might be running
        // on a slower computer and the next transfer has actually already been
        // queued.
        pipe.cancel_transfers();
        test_timeout timeout(500);
        while(pipe.has_pending_transfers())
        {
            pipe.handle_events();
            libusbp::error transfer_error;
            while(pipe.handle_finished_transfer(NULL, NULL, &transfer_error))
            {
                if (!transfer_error)
                {
                    #if defined(__linux__)
                    // Overflowing is always an error, none of the transfers
                    // should have succeeded.
                    throw "Expected to get a transfer error.";
                    #endif
                }
                else if (transfer_error.message() == expected_message)
                {
                    // This is fine; the error indicates an overflow.
                }
                #ifdef __APPLE__
                else if (transfer_error.has_code(LIBUSBP_ERROR_STALL))
                {
                    // Mac OS X considers the pipe to be "stalled" after an
                    // overflow happens and we cannot actually submit any more
                    // transfers.
                }
                #endif
                else if (transfer_error.has_code(LIBUSBP_ERROR_CANCELLED))
                {
                    // This is fine; the transfer was cancelled.
                }
                else
                {
                    // Unexpected error.
                    throw transfer_error;
                }
            }
            timeout.check();
            sleep_quick();
        }

        clean_up_async_in_pipe(pipe);
    }

    #ifdef __linux__
    SECTION("does not prevent the creation of another generic_interface object")
    {
        // It seems that the "usbfs" driver gets attached to the device
        // while we are doing asynchronous transfers.  This tests that our
        // code is OK with that and we can still open up other handles
        // to the device, at least in Linux.

        pipe.allocate_transfers(1, transfer_size);
        pipe.start_endless_transfers();
        libusbp::generic_interface gi2(device, 0, true);
        libusbp::generic_interface gi3(device, 0, true);
        libusbp::generic_handle handle2(gi3);
        handle2.control_transfer(0x40, 0x90, 1, 0);
        clean_up_async_in_pipe(pipe);
    }
    #endif
}

#endif
