// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QtTest/QtTest>

#include <QDebug>
#include <QVariant>
#include <QList>
#include <QLoggingCategory>

#include "../../shared/bttestutil_p.h"
#include <private/qtbluetoothglobal_p.h>
#include <qbluetoothaddress.h>
#include <qbluetoothdevicediscoveryagent.h>
#include <qbluetoothlocaldevice.h>

#if QT_CONFIG(permissions)
#include <QtCore/qcoreapplication.h>
#include <QtCore/qpermissions.h>
#include <QtCore/qnamespace.h>
#endif // permissions

#ifdef Q_OS_ANDROID
#include <QtCore/qjnienvironment.h>
#endif

#ifdef Q_OS_WIN
#include <objbase.h> // for CoInitializeEx() and friends
#endif

#include <memory>

using namespace Qt::StringLiterals;

QT_USE_NAMESPACE

/*
 * Some parts of this test require a remote and discoverable Bluetooth
 * device. Setting the BT_TEST_DEVICE environment variable will
 * set the test up to fail if it cannot find a remote device.
 * BT_TEST_DEVICE should contain the address of the device the
 * test expects to find. Ensure that the device is running
 * in discovery mode.
 **/

// Maximum time to for bluetooth device scan
const int MaxScanTime = 5 * 60 * 1000;  // 5 minutes in ms

//Bluez needs at least 10s for a device discovery to be cancelled
const int MaxWaitForCancelTime = 15 * 1000;  // 15 seconds in ms

#ifdef Q_OS_ANDROID
// Android is sometimes unable to cancel immediately
const int WaitBeforeStopTime = 200;
extern void testEnumToString(std::function<void(const char*, int)> callback);
extern void parseScanRecord(QBluetoothDeviceInfo &, jbyteArray);
#endif

class tst_QBluetoothDeviceDiscoveryAgent : public QObject
{
    Q_OBJECT

public:
    tst_QBluetoothDeviceDiscoveryAgent();
    ~tst_QBluetoothDeviceDiscoveryAgent();

public slots:
    void deviceDiscoveryDebug(const QBluetoothDeviceInfo &info);
    void finished();

private slots:
    void initTestCase();

    void tst_invalidBtAddress();

    void tst_startStopDeviceDiscoveries();

    void tst_deviceDiscovery();

    void tst_discoveryTimeout();

    void tst_discoveryMethods();

    void tst_majorMinorMapping();

    void tst_androidScanRecordParsing_data();
    void tst_androidScanRecordParsing();

    void tst_windowsDiscoveryInThread_data();
    void tst_windowsDiscoveryInThread();
private:
    qsizetype noOfLocalDevices;
    using DiscoveryAgentPtr = std::unique_ptr<QBluetoothDeviceDiscoveryAgent>;
#if QT_CONFIG(permissions)
    Qt::PermissionStatus permissionStatus = Qt::PermissionStatus::Undetermined;
#endif
};

tst_QBluetoothDeviceDiscoveryAgent::tst_QBluetoothDeviceDiscoveryAgent()
{
    QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true"));
    qRegisterMetaType<QBluetoothDeviceDiscoveryAgent::Error>();
#if QT_CONFIG(permissions)
    permissionStatus = qApp->checkPermission(QBluetoothPermission{});

    const bool ciRun = qEnvironmentVariable("QTEST_ENVIRONMENT").split(' ').contains("ci");
    if (!ciRun && permissionStatus == Qt::PermissionStatus::Undetermined) {
        QTestEventLoop loop;
        qApp->requestPermission(QBluetoothPermission{},
                                &loop, [this, &loop](const QPermission &permission) {
            permissionStatus = permission.status();
            loop.exitLoop();
        });
        if (permissionStatus == Qt::PermissionStatus::Undetermined)
            loop.enterLoopMSecs(30000);
    }
#endif // QT_CONFIG(permissions)
}

tst_QBluetoothDeviceDiscoveryAgent::~tst_QBluetoothDeviceDiscoveryAgent()
{
}

void tst_QBluetoothDeviceDiscoveryAgent::initTestCase()
{
    qRegisterMetaType<QBluetoothDeviceInfo>();

    noOfLocalDevices = QBluetoothLocalDevice::allDevices().size();

    if (!noOfLocalDevices)
        return;

    // turn on BT in case it is not on
    QBluetoothLocalDevice *device = new QBluetoothLocalDevice();
    if (device->hostMode() == QBluetoothLocalDevice::HostPoweredOff) {
        QSignalSpy hostModeSpy(device, SIGNAL(hostModeStateChanged(QBluetoothLocalDevice::HostMode)));
        QVERIFY(hostModeSpy.isEmpty());
        device->powerOn();
        int connectTime = 5000;  // ms
        while (hostModeSpy.isEmpty() && connectTime > 0) {
            QTest::qWait(500);
            connectTime -= 500;
        }
        QVERIFY(!hostModeSpy.isEmpty());
    }
    QBluetoothLocalDevice::HostMode hostMode= device->hostMode();
    QVERIFY(hostMode == QBluetoothLocalDevice::HostConnectable
         || hostMode == QBluetoothLocalDevice::HostDiscoverable
         || hostMode == QBluetoothLocalDevice::HostDiscoverableLimitedInquiry);
    delete device;
}

void tst_QBluetoothDeviceDiscoveryAgent::tst_invalidBtAddress()
{
    DiscoveryAgentPtr discoveryAgent(new QBluetoothDeviceDiscoveryAgent(
                QBluetoothAddress(QStringLiteral("11:11:11:11:11:11"))));

    QCOMPARE(discoveryAgent->error(), QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError);
    discoveryAgent->start();
    QCOMPARE(discoveryAgent->isActive(), false);

#if QT_CONFIG(permissions)
    if (permissionStatus != Qt::PermissionStatus::Granted)
        return;
#endif

    discoveryAgent.reset(new QBluetoothDeviceDiscoveryAgent(QBluetoothAddress()));
    QCOMPARE(discoveryAgent->error(), QBluetoothDeviceDiscoveryAgent::NoError);
    if (!QBluetoothLocalDevice::allDevices().isEmpty()) {
        discoveryAgent->start();
        QCOMPARE(discoveryAgent->isActive(), true);
    }
}

void tst_QBluetoothDeviceDiscoveryAgent::deviceDiscoveryDebug(const QBluetoothDeviceInfo &info)
{
    qDebug() << "Discovered device:" << info.address().toString() << info.name();
}

void tst_QBluetoothDeviceDiscoveryAgent::tst_startStopDeviceDiscoveries()
{
    QBluetoothDeviceDiscoveryAgent discoveryAgent;

    QVERIFY(discoveryAgent.error() == discoveryAgent.NoError);
    QVERIFY(discoveryAgent.errorString().isEmpty());
    QVERIFY(!discoveryAgent.isActive());
    QVERIFY(discoveryAgent.discoveredDevices().isEmpty());

    QSignalSpy finishedSpy(&discoveryAgent, SIGNAL(finished()));
    QSignalSpy cancelSpy(&discoveryAgent, SIGNAL(canceled()));
    QSignalSpy errorSpy(&discoveryAgent,
                        SIGNAL(errorOccurred(QBluetoothDeviceDiscoveryAgent::Error)));

    // Starting case 1: start-stop, expecting cancel signal
    // we should have no errors at this point.
    QVERIFY(errorSpy.isEmpty());

    discoveryAgent.start();
#if QT_CONFIG(permissions)
    if (permissionStatus != Qt::PermissionStatus::Granted) {
        // If bluetooth is OFF, the permission does not get checked (e.g. on Darwin),
        // but not to depend on the order in which errors generated, we
        // do not compare error value with MissionPermissionsError here.
        return;
    }
#endif

    if (errorSpy.isEmpty()) {
        QVERIFY(discoveryAgent.isActive());
        QCOMPARE(discoveryAgent.errorString(), QString());
        QCOMPARE(discoveryAgent.error(), QBluetoothDeviceDiscoveryAgent::NoError);
    } else {
        QCOMPARE(noOfLocalDevices, 0);
        QVERIFY(!discoveryAgent.isActive());
        QVERIFY(!discoveryAgent.errorString().isEmpty());
        QVERIFY(discoveryAgent.error() != QBluetoothDeviceDiscoveryAgent::NoError);
        QSKIP("No local Bluetooth device available. Skipping remaining part of test.");
    }
    // cancel current request.
#ifdef Q_OS_ANDROID
    // Android sometimes can't cancel immediately (~on the same millisecond),
    // but instead a "pending cancel" happens, which means the discovery will be
    // canceled at a later point in time. When this happens the Android backend
    // also emits an immediate errorOccurred(). While this seems to be as intended,
    // as a result many parts of this test function may fail (not always).
    //
    // In this test function we wait some milliseconds between start() and stop() to
    // bypass this behavior difference. This is to avoid complex iffery (Android itself
    // can behave differently every time) and ifdeffery (Q_OS_ANDROID) in the test
    QTest::qWait(WaitBeforeStopTime);
#endif
    discoveryAgent.stop();

    // Wait for up to MaxWaitForCancelTime for the cancel to finish
    QTRY_VERIFY_WITH_TIMEOUT(!cancelSpy.isEmpty(), MaxWaitForCancelTime);

    // we should not be active anymore
    QVERIFY(!discoveryAgent.isActive());
    QVERIFY(errorSpy.isEmpty());
    QCOMPARE(cancelSpy.size(), 1);
    cancelSpy.clear();
    // Starting case 2: start-start-stop, expecting cancel signal
    discoveryAgent.start();
    // we should be active now
    QVERIFY(discoveryAgent.isActive());
    QVERIFY(errorSpy.isEmpty());
    // start again. should this be error?
    discoveryAgent.start();
    QVERIFY(discoveryAgent.isActive());
    QVERIFY(errorSpy.isEmpty());
    // stop
#ifdef Q_OS_ANDROID
    QTest::qWait(WaitBeforeStopTime);
#endif
    discoveryAgent.stop();

    // Wait for up to MaxWaitForCancelTime for the cancel to finish
    QTRY_VERIFY_WITH_TIMEOUT(!cancelSpy.isEmpty(), MaxWaitForCancelTime);

    // we should not be active anymore
    QVERIFY(!discoveryAgent.isActive());
    QVERIFY(errorSpy.isEmpty());

    QCOMPARE(cancelSpy.size(), 1);
    cancelSpy.clear();

    //  Starting case 3: stop
    discoveryAgent.stop();
    QVERIFY(!discoveryAgent.isActive());
    QVERIFY(errorSpy.isEmpty());

    // Don't expect finished signal and no error
    QVERIFY(finishedSpy.isEmpty());
    QVERIFY(discoveryAgent.error() == discoveryAgent.NoError);
    QVERIFY(discoveryAgent.errorString().isEmpty());

    /*
        Starting case 4: start-stop-start-stop:
        We are testing that two subsequent stop() calls reduce total number
        of cancel() signals to 1 if the true cancellation requires
        asynchronous function calls (signal consolidation); otherwise we
        expect 2x cancel() signal.

        Examples are:
            - Bluez4 (event loop needs to run for cancel)
            - Bluez5 (no event loop required)
    */

    bool immediateSignal = false;
    discoveryAgent.start();
    QVERIFY(discoveryAgent.isActive());
    QVERIFY(errorSpy.isEmpty());
    // cancel current request.
#ifdef Q_OS_ANDROID
    QTest::qWait(WaitBeforeStopTime);
#endif
    discoveryAgent.stop();
    //should only have triggered cancel() if stop didn't involve the event loop
    if (cancelSpy.size() == 1) immediateSignal = true;

    // start a new one
    discoveryAgent.start();
    // we should be active now
    QVERIFY(discoveryAgent.isActive());
    QVERIFY(errorSpy.isEmpty());
    // stop
#ifdef Q_OS_ANDROID
    QTest::qWait(WaitBeforeStopTime);
#endif
    discoveryAgent.stop();
    if (immediateSignal)
        QCOMPARE(cancelSpy.size(), 2);

    // Wait for up to MaxWaitForCancelTime for the cancel to finish
    QTRY_VERIFY_WITH_TIMEOUT(!cancelSpy.isEmpty(), MaxWaitForCancelTime);

    // we should not be active anymore
    QVERIFY(!discoveryAgent.isActive());
    QVERIFY(errorSpy.isEmpty());

    // should only have 1 cancel
    if (immediateSignal)
        QCOMPARE(cancelSpy.size(), 2);
    else
        QCOMPARE(cancelSpy.size(), 1);
    cancelSpy.clear();

    // Starting case 5: start-stop-start: expecting finished signal & no cancel
    discoveryAgent.start();
    QVERIFY(discoveryAgent.isActive());
    QVERIFY(errorSpy.isEmpty());
    // cancel current request.
#ifdef Q_OS_ANDROID
    QTest::qWait(WaitBeforeStopTime);
#endif
    discoveryAgent.stop();
    // start a new one
    discoveryAgent.start();
    // we should be active now
    QVERIFY(discoveryAgent.isActive());
    QVERIFY(errorSpy.isEmpty());

    // Wait for up to MaxScanTime for the scan to finish
    QTRY_VERIFY_WITH_TIMEOUT(!finishedSpy.isEmpty(), MaxScanTime);

    // we should not be active anymore
    QVERIFY(!discoveryAgent.isActive());
    QVERIFY(errorSpy.isEmpty());
    // should only have 1 cancel
    QCOMPARE(finishedSpy.size(), 1);

    // On OS X, stop is synchronous (signal will be emitted immediately).
    if (!immediateSignal)
        QVERIFY(cancelSpy.isEmpty());
}

void tst_QBluetoothDeviceDiscoveryAgent::finished()
{
    qDebug() << "Finished called";
}

void tst_QBluetoothDeviceDiscoveryAgent::tst_deviceDiscovery()
{
    {
        //Run test in case of multiple Bluetooth adapters
        QBluetoothLocalDevice localDevice;
        //We will use default adapter if there is no other adapter
        QBluetoothAddress address = localDevice.address();

        QBluetoothDeviceDiscoveryAgent discoveryAgent(address);
        QVERIFY(discoveryAgent.error() == discoveryAgent.NoError);
        QVERIFY(discoveryAgent.errorString().isEmpty());
        QVERIFY(!discoveryAgent.isActive());

        QVERIFY(discoveryAgent.discoveredDevices().isEmpty());

        QSignalSpy finishedSpy(&discoveryAgent, SIGNAL(finished()));
        QSignalSpy errorSpy(&discoveryAgent,
                            SIGNAL(errorOccurred(QBluetoothDeviceDiscoveryAgent::Error)));
        QSignalSpy discoveredSpy(&discoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)));
//        connect(&discoveryAgent, SIGNAL(finished()), this, SLOT(finished()));
//        connect(&discoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)),
//                this, SLOT(deviceDiscoveryDebug(QBluetoothDeviceInfo)));

        discoveryAgent.start();

        if (!errorSpy.isEmpty()) {
#if QT_CONFIG(permissions)
            if (permissionStatus == Qt::PermissionStatus::Granted)
#endif

            QCOMPARE(noOfLocalDevices, 0);
            QVERIFY(!discoveryAgent.isActive());
            QSKIP("No local Bluetooth device available. Skipping remaining part of test.");
        }

        QVERIFY(discoveryAgent.isActive());

        // Wait for up to MaxScanTime for the scan to finish
        int scanTime = MaxScanTime;
        while (finishedSpy.isEmpty() && scanTime > 0) {
            QTest::qWait(15000);
            scanTime -= 15000;
        }

        // verify that we are finished
        QVERIFY(!discoveryAgent.isActive());
        // stop
        discoveryAgent.stop();
        QVERIFY(!discoveryAgent.isActive());
        qDebug() << "Scan time left:" << scanTime;
        // Expect finished signal with no error
        QVERIFY(finishedSpy.size() == 1);
        QVERIFY(errorSpy.isEmpty());
        QVERIFY(discoveryAgent.error() == discoveryAgent.NoError);
        QVERIFY(discoveryAgent.errorString().isEmpty());

        // verify that the list is as big as the signals received.
        // discoveredSpy might have more events as some devices are found multiple times,
        // leading to messages like
        // "Almost Duplicate  "88:C6:26:F5:3E:E2" "88-C6-26-F5-3E-E2" - replacing in place"
        QVERIFY(discoveredSpy.size() >= discoveryAgent.discoveredDevices().size());
        // verify that there really was some devices in the array

        const QString remote = qEnvironmentVariable("BT_TEST_DEVICE");
        QBluetoothAddress remoteDevice;
        if (!remote.isEmpty()) {
            remoteDevice = QBluetoothAddress(remote);
            QVERIFY2(!remote.isNull(), "Expecting valid Bluetooth address to be passed via BT_TEST_DEVICE");
        } else {
            qWarning() << "Not using a remote device for testing. Set BT_TEST_DEVICE env to run extended tests involving a remote device";
        }

        if (!remoteDevice.isNull())
            QVERIFY(!discoveredSpy.isEmpty());
        // All returned QBluetoothDeviceInfo should be valid.
        while (!discoveredSpy.isEmpty()) {
            const QBluetoothDeviceInfo info =
                qvariant_cast<QBluetoothDeviceInfo>(discoveredSpy.takeFirst().at(0));
            QVERIFY(info.isValid());
        }
    }
}


void tst_QBluetoothDeviceDiscoveryAgent::tst_discoveryTimeout()
{
    QBluetoothDeviceDiscoveryAgent agent;

    // check default values
#if defined(Q_OS_MACOS) || defined(Q_OS_IOS) || defined(Q_OS_ANDROID) || QT_CONFIG(winrt_bt) \
    || QT_CONFIG(bluez)
    QCOMPARE(agent.lowEnergyDiscoveryTimeout(), 40000);
    agent.setLowEnergyDiscoveryTimeout(-1); // negative ignored
    QCOMPARE(agent.lowEnergyDiscoveryTimeout(), 40000);
    agent.setLowEnergyDiscoveryTimeout(20000);
    QCOMPARE(agent.lowEnergyDiscoveryTimeout(), 20000);
#else
    QCOMPARE(agent.lowEnergyDiscoveryTimeout(), -1);
    agent.setLowEnergyDiscoveryTimeout(20000); // feature not supported -> ignored
    QCOMPARE(agent.lowEnergyDiscoveryTimeout(), -1);
#endif
}

void tst_QBluetoothDeviceDiscoveryAgent::tst_discoveryMethods()
{
    const QBluetoothLocalDevice localDevice;
    if (localDevice.allDevices().size() != 1) {
        // On iOS it returns 0 but we still have working BT.
#ifndef Q_OS_IOS
        QSKIP("This test expects exactly one local device working");
#endif
    }

    const QBluetoothDeviceDiscoveryAgent::DiscoveryMethods
        supportedMethods = QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods();

    QVERIFY(supportedMethods != QBluetoothDeviceDiscoveryAgent::NoMethod);

    QBluetoothDeviceDiscoveryAgent::DiscoveryMethod
        unsupportedMethods = QBluetoothDeviceDiscoveryAgent::NoMethod;
    QBluetoothDeviceInfo::CoreConfiguration
        expectedConfiguration = QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration;

    if (supportedMethods == QBluetoothDeviceDiscoveryAgent::ClassicMethod) {
        unsupportedMethods = QBluetoothDeviceDiscoveryAgent::LowEnergyMethod;
        expectedConfiguration = QBluetoothDeviceInfo::BaseRateCoreConfiguration;
    } else if (supportedMethods == QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) {
        unsupportedMethods = QBluetoothDeviceDiscoveryAgent::ClassicMethod;
        expectedConfiguration = QBluetoothDeviceInfo::LowEnergyCoreConfiguration;
    }

    QBluetoothDeviceDiscoveryAgent agent;
    QSignalSpy finishedSpy(&agent, SIGNAL(finished()));
    QSignalSpy errorSpy(&agent, SIGNAL(errorOccurred(QBluetoothDeviceDiscoveryAgent::Error)));
    QSignalSpy discoveredSpy(&agent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)));

    // NoMethod - should just immediately return:
    agent.start(QBluetoothDeviceDiscoveryAgent::NoMethod);
    QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::NoError);
    QVERIFY(!agent.isActive());
    QCOMPARE(finishedSpy.size(), 0);
    QCOMPARE(errorSpy.size(), 0);
    QCOMPARE(discoveredSpy.size(), 0);

    if (unsupportedMethods != QBluetoothDeviceDiscoveryAgent::NoMethod) {
        agent.start(unsupportedMethods);
        QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod);
        QVERIFY(!agent.isActive());
        QVERIFY(finishedSpy.isEmpty());
        QCOMPARE(errorSpy.size(), 1);
        errorSpy.clear();
        QVERIFY(discoveredSpy.isEmpty());
    }

    // Start discovery, probably both Classic and LE methods:
    agent.start(supportedMethods);
#if QT_CONFIG(permissions)
    if (permissionStatus != Qt::PermissionStatus::Granted) {
        QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::MissingPermissionsError);
        QSKIP("The remaining test requires the Bluetooth permission granted");
    }
#endif
    QVERIFY(agent.isActive());
    QVERIFY(errorSpy.isEmpty());

#define RUN_DISCOVERY(maxTimeout, step, condition) \
    for (int scanTime = maxTimeout; (condition) && scanTime > 0; scanTime -= step) \
        QTest::qWait(step);

    // Wait for up to MaxScanTime for the scan to finish
    const int timeStep = 15000;
    RUN_DISCOVERY(MaxScanTime, timeStep, finishedSpy.isEmpty())

    QVERIFY(!agent.isActive());
    QVERIFY(errorSpy.size() <= 1);

    if (errorSpy.size()) {
        // For example, old iOS device could report it supports LE method,
        // but it actually does not.
        QVERIFY(supportedMethods == QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
        QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod);
    } else {
        QVERIFY(finishedSpy.size() == 1);
        QVERIFY(agent.error() == QBluetoothDeviceDiscoveryAgent::NoError);
        QVERIFY(agent.errorString().isEmpty());

        while (!discoveredSpy.isEmpty()) {
            const QBluetoothDeviceInfo info =
                qvariant_cast<QBluetoothDeviceInfo>(discoveredSpy.takeFirst().at(0));
            QVERIFY(info.isValid());
            // on Android we do find devices with unknown configuration
            if (info.coreConfigurations() != QBluetoothDeviceInfo::UnknownCoreConfiguration)
                QVERIFY(info.coreConfigurations() & expectedConfiguration);
        }
    }

    if (unsupportedMethods != QBluetoothDeviceDiscoveryAgent::NoMethod)
        return;

    // Both methods were reported as supported. We already tested them
    // above, now let's test first Classic then LE.
    finishedSpy.clear();
    errorSpy.clear();
    discoveredSpy.clear();

    agent.start(QBluetoothDeviceDiscoveryAgent::ClassicMethod);
    QVERIFY(agent.isActive());
    QVERIFY(errorSpy.isEmpty());
    QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::NoError);

    RUN_DISCOVERY(MaxScanTime, timeStep, finishedSpy.isEmpty())

    QVERIFY(!agent.isActive());
    QVERIFY(errorSpy.isEmpty());
    QCOMPARE(finishedSpy.size(), 1);

    finishedSpy.clear();
    discoveredSpy.clear();

    agent.start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
    QVERIFY(agent.isActive());
    QVERIFY(errorSpy.isEmpty());

    RUN_DISCOVERY(MaxScanTime, timeStep, finishedSpy.isEmpty() && errorSpy.isEmpty())

    QVERIFY(!agent.isActive());
    QVERIFY(errorSpy.size() <= 1);

    if (errorSpy.size()) {
        QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod);
        qDebug() << "QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods is inaccurate"
                    " on your platform/with your device, LowEnergyMethod is not supported";
    } else {
        QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::NoError);
        QCOMPARE(finishedSpy.size(), 1);
    }
}



void tst_QBluetoothDeviceDiscoveryAgent::tst_majorMinorMapping()
{
#if !defined(Q_OS_ANDROID)
    QSKIP("This test is only pertinent to Android!");
#else
    const QMap<QLatin1StringView, int> expectedMapping = {
        // AUDIO_VIDEO
        { "AUDIO_VIDEO_CAMCORDER"_L1, QBluetoothDeviceInfo::Camcorder },
        { "AUDIO_VIDEO_CAR_AUDIO"_L1, QBluetoothDeviceInfo::CarAudio },
        { "AUDIO_VIDEO_HANDSFREE"_L1, QBluetoothDeviceInfo::HandsFreeDevice },
        { "AUDIO_VIDEO_HEADPHONES"_L1, QBluetoothDeviceInfo::Headphones },
        { "AUDIO_VIDEO_HIFI_AUDIO"_L1, QBluetoothDeviceInfo::HiFiAudioDevice },
        { "AUDIO_VIDEO_LOUDSPEAKER"_L1, QBluetoothDeviceInfo::Loudspeaker },
        { "AUDIO_VIDEO_MICROPHONE"_L1, QBluetoothDeviceInfo::Microphone },
        { "AUDIO_VIDEO_PORTABLE_AUDIO"_L1, QBluetoothDeviceInfo::PortableAudioDevice },
        { "AUDIO_VIDEO_SET_TOP_BOX"_L1, QBluetoothDeviceInfo::SetTopBox },
        { "AUDIO_VIDEO_UNCATEGORIZED"_L1, QBluetoothDeviceInfo::UncategorizedAudioVideoDevice },
        { "AUDIO_VIDEO_VCR"_L1, QBluetoothDeviceInfo::Vcr },
        { "AUDIO_VIDEO_VIDEO_CAMERA"_L1, QBluetoothDeviceInfo::VideoCamera },
        { "AUDIO_VIDEO_VIDEO_CONFERENCING"_L1, QBluetoothDeviceInfo::VideoConferencing },
        { "AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER"_L1, QBluetoothDeviceInfo::VideoDisplayAndLoudspeaker },
        { "AUDIO_VIDEO_VIDEO_GAMING_TOY"_L1, QBluetoothDeviceInfo::GamingDevice },
        { "AUDIO_VIDEO_VIDEO_MONITOR"_L1, QBluetoothDeviceInfo::VideoMonitor },
        { "AUDIO_VIDEO_WEARABLE_HEADSET"_L1, QBluetoothDeviceInfo::WearableHeadsetDevice },

        // COMPUTER
        { "COMPUTER_UNCATEGORIZED"_L1, QBluetoothDeviceInfo::UncategorizedComputer },
        { "COMPUTER_DESKTOP"_L1, QBluetoothDeviceInfo::DesktopComputer },
        { "COMPUTER_HANDHELD_PC_PDA"_L1, QBluetoothDeviceInfo::HandheldComputer },
        { "COMPUTER_LAPTOP"_L1, QBluetoothDeviceInfo::LaptopComputer },
        { "COMPUTER_PALM_SIZE_PC_PDA"_L1, QBluetoothDeviceInfo::HandheldClamShellComputer },
        { "COMPUTER_SERVER"_L1, QBluetoothDeviceInfo::ServerComputer },
        { "COMPUTER_WEARABLE"_L1, QBluetoothDeviceInfo::WearableComputer },

        // HEALTH
        { "HEALTH_BLOOD_PRESSURE"_L1, QBluetoothDeviceInfo::HealthBloodPressureMonitor },
        { "HEALTH_DATA_DISPLAY"_L1, QBluetoothDeviceInfo::HealthDataDisplay },
        { "HEALTH_GLUCOSE"_L1, QBluetoothDeviceInfo::HealthGlucoseMeter },
        { "HEALTH_PULSE_OXIMETER"_L1, QBluetoothDeviceInfo::HealthPulseOximeter },
        { "HEALTH_PULSE_RATE"_L1, QBluetoothDeviceInfo::HealthStepCounter },
        { "HEALTH_THERMOMETER"_L1, QBluetoothDeviceInfo::HealthThermometer },
        { "HEALTH_UNCATEGORIZED"_L1, QBluetoothDeviceInfo::UncategorizedHealthDevice },
        { "HEALTH_WEIGHING"_L1, QBluetoothDeviceInfo::HealthWeightScale },

        // PHONE
        { "PHONE_CELLULAR"_L1, QBluetoothDeviceInfo::CellularPhone },
        { "PHONE_CORDLESS"_L1, QBluetoothDeviceInfo::CordlessPhone },
        { "PHONE_ISDN"_L1, QBluetoothDeviceInfo::CommonIsdnAccessPhone },
        { "PHONE_MODEM_OR_GATEWAY"_L1, QBluetoothDeviceInfo::WiredModemOrVoiceGatewayPhone },
        { "PHONE_SMART"_L1, QBluetoothDeviceInfo::SmartPhone },
        { "PHONE_UNCATEGORIZED"_L1, QBluetoothDeviceInfo::UncategorizedPhone },

        // TOY
        { "TOY_CONTROLLER"_L1, QBluetoothDeviceInfo::ToyController },
        { "TOY_DOLL_ACTION_FIGURE"_L1, QBluetoothDeviceInfo::ToyDoll },
        { "TOY_GAME"_L1, QBluetoothDeviceInfo::ToyGame },
        { "TOY_ROBOT"_L1, QBluetoothDeviceInfo::ToyRobot },
        { "TOY_UNCATEGORIZED"_L1, QBluetoothDeviceInfo::UncategorizedToy },
        { "TOY_VEHICLE"_L1, QBluetoothDeviceInfo::ToyVehicle },

        // WEARABLE
        { "WEARABLE_GLASSES"_L1, QBluetoothDeviceInfo::WearableGlasses },
        { "WEARABLE_HELMET"_L1, QBluetoothDeviceInfo::WearableHelmet },
        { "WEARABLE_JACKET"_L1, QBluetoothDeviceInfo::WearableJacket },
        { "WEARABLE_PAGER"_L1, QBluetoothDeviceInfo::WearablePager },
        { "WEARABLE_UNCATEGORIZED"_L1, QBluetoothDeviceInfo::UncategorizedWearableDevice },
        { "WEARABLE_WRIST_WATCH"_L1, QBluetoothDeviceInfo::WearableWristWatch },

        // PERIPHERAL
        { "PERIPHERAL_NON_KEYBOARD_NON_POINTING"_L1, QBluetoothDeviceInfo::UncategorizedPeripheral },
        { "PERIPHERAL_KEYBOARD"_L1, QBluetoothDeviceInfo::KeyboardPeripheral },
        { "PERIPHERAL_POINTING"_L1, QBluetoothDeviceInfo::PointingDevicePeripheral },
        { "PERIPHERAL_KEYBOARD_POINTING"_L1, QBluetoothDeviceInfo::KeyboardWithPointingDevicePeripheral }
    };

    qsizetype hits = 0;

    testEnumToString([&](const char *generatedName, int minor) {
        QLatin1StringView generatedNameL1(generatedName);

        auto printOnFailure = qScopeGuard([&] { qDebug("%s", generatedName); });

        QVERIFY(expectedMapping.contains(generatedNameL1));

        int expectedValue = expectedMapping.value(generatedNameL1);
        QCOMPARE(minor, expectedValue);

        printOnFailure.dismiss();

        ++hits;
    });

    QCOMPARE(hits, expectedMapping.size());
#endif
}

void tst_QBluetoothDeviceDiscoveryAgent::tst_androidScanRecordParsing_data()
{
#ifndef Q_OS_ANDROID
    QSKIP("This test only runs on Android");
#endif
    QTest::addColumn<QByteArray>("scanRecord");
    QTest::addColumn<int>("expectedServiceUuidCount");
    QTest::addColumn<bool>("expectName");
    QTest::addColumn<bool>("expectManufacturerData");

    // -----------------------------------------------------------------------
    // Baseline: well-formed scan record containing a 16-bit service UUID,
    // a complete local name, and manufacturer-specific data.
    // All three fields must be parsed correctly.
    //
    // Layout (each field: [nBytes] [ADType] [data...]):
    //   02 01 06        -- Flags (ADType 0x01); not parsed by Qt, skipped
    //   03 03 34 12     -- 16-bit UUID complete (0x1234 in little-endian)
    //   05 09 54 65 73 74 -- Complete local name: "Test"
    //   05 FF E0 00 AB CD -- Manufacturer data: company=0x00E0, payload=0xAB 0xCD
    // -----------------------------------------------------------------------
    QByteArray validRecord = QByteArray::fromHex("020106"      // Flags
                                                 "03033412"    // 16-bit UUID 0x1234
                                                 "050954657374"// "Test"
                                                 "05FFE000ABCD"); // Mfr data
    QTest::newRow("valid_record") << validRecord << 1 << true << true;

    QTest::newRow("zero_length") << QByteArray(1, 0x00) << 0 << false << false;

    QByteArray truncated;
    truncated += char(0x0A);          // nBytes = 10, claims 9 bytes of name data
    truncated += char(0x09);          // ADType: complete local name
    truncated += QByteArray("Hi");    // only 2 bytes available — record is truncated
    QTest::newRow("truncated_record") << truncated << 0 << false << false;

    // -----------------------------------------------------------------------
    // Issue 1 – length byte with the high bit set (e.g. 0xFF).
    //
    // When the length field is read as a signed char (the bug), 0xFF becomes
    // -1, making 'i += nBytes + 1' advance by zero — causing an infinite loop.
    //
    // This test will hang indefinitely with the unfixed implementation.
    // -----------------------------------------------------------------------
    QByteArray highBitLength;
    highBitLength += char(0xFF);          // length byte — high bit set
    highBitLength += char(0x09);          // ADType: complete local name
    highBitLength += QByteArray(30, 'A'); // 30 bytes of name data
    QTest::newRow("length_high_bit_set") << highBitLength << 0 << false << false;

    // -----------------------------------------------------------------------
    // Issue 2a – 128-bit UUID AD type with only 1 data byte (nBytes = 2).
    //
    // ADType128BitUuidComplete requires 16 bytes of data, so we'll read past
    // the end of the buffer without a proper length check.
    //
    // After the fix (per-case guard: if (nBytes >= 17)), no read occurs and
    // no UUID is produced.
    // -----------------------------------------------------------------------
    QByteArray short128BitUuid;
    short128BitUuid += char(0x02);  // nBytes = 2
    short128BitUuid += char(0x07);  // ADType128BitUuidComplete
    short128BitUuid += char(0xAB);  // only 1 data byte instead of 16
    QTest::newRow("128bit_uuid_too_short") << short128BitUuid << 0 << false << false;

    // -----------------------------------------------------------------------
    // Issue 2b – 32-bit UUID AD type with only 1 data byte (nBytes = 2).
    //
    // ADType32BitUuidComplete requires 4 bytes of data, so we'll read past
    // the end of the buffer without a proper length check.
    //
    // After the fix (per-case guard: if (nBytes >= 5)), no UUID is produced.
    // -----------------------------------------------------------------------
    QByteArray short32BitUuid;
    short32BitUuid += char(0x02);  // nBytes = 2
    short32BitUuid += char(0x05);  // ADType32BitUuidComplete
    short32BitUuid += char(0xAB);  // only 1 data byte instead of 4
    QTest::newRow("32bit_uuid_too_short") << short32BitUuid << 0 << false << false;

    // -----------------------------------------------------------------------
    // Issue 2c – 16-bit UUID AD type with no data bytes (nBytes = 1).
    //
    // ADType16BitUuidComplete requires 2 bytes of data, so we'll read past
    // the end of the buffer without a proper length check.
    //
    // After the fix (per-case guard: if (nBytes >= 3)), no UUID is produced.
    // -----------------------------------------------------------------------
    QByteArray short16BitUuid;
    short16BitUuid += char(0x01);  // nBytes = 1
    short16BitUuid += char(0x03);  // ADType16BitUuidComplete
    // no data bytes — buffer ends here
    QTest::newRow("16bit_uuid_too_short") << short16BitUuid << 0 << false << false;
}

#ifdef Q_OS_ANDROID

// Allocates a JVM byte array from a QByteArray. The caller is responsible for
// releasing the returned local reference via env->DeleteLocalRef().
static jbyteArray toJByteArray(QJniEnvironment &env, const QByteArray &data)
{
    jbyteArray arr = env->NewByteArray(jsize(data.size()));
    if (arr && !data.isEmpty())
        env->SetByteArrayRegion(arr, 0, jsize(data.size()),
                                reinterpret_cast<const jbyte *>(data.constData()));
    return arr;
}

#endif

void tst_QBluetoothDeviceDiscoveryAgent::tst_androidScanRecordParsing()
{
#ifdef Q_OS_ANDROID
    QFETCH(QByteArray, scanRecord);
    QFETCH(int, expectedServiceUuidCount);
    QFETCH(bool, expectName);
    QFETCH(bool, expectManufacturerData);

    QBluetoothDeviceInfo info{ QBluetoothAddress("00:11:22:33:44:55"_L1), QString(), 0 };

    QJniEnvironment env;
    jbyteArray jScanRecord = toJByteArray(env, scanRecord);

    parseScanRecord(info, jScanRecord);

    env->DeleteLocalRef(jScanRecord);

    QCOMPARE(info.serviceUuids().size(), expectedServiceUuidCount);

    if (expectedServiceUuidCount > 0) {
        // The valid-record row carries UUID 0x1234 — verify it was parsed correctly.
        QVERIFY(info.serviceUuids().contains(QBluetoothUuid(quint16(0x1234))));
    }

    QCOMPARE(!info.name().isEmpty(), expectName);

    if (expectManufacturerData) {
        // The valid-record row carries manufacturer data for company 0x00E0.
        const auto mfrData = info.manufacturerData();
        QVERIFY(mfrData.contains(0x00E0));
        QCOMPARE(mfrData.value(0x00E0), QByteArray::fromHex("ABCD"));
    }
#endif
}

namespace {

enum class CoInitType {
    Auto,
    ManualApartment,
    ManualMultithread,
};

} // namespace

void tst_QBluetoothDeviceDiscoveryAgent::tst_windowsDiscoveryInThread_data()
{
#ifndef Q_OS_WIN
    QSKIP("This is a Windows only test");
#endif
    QTest::addColumn<CoInitType>("coInitType");

    QTest::newRow("default_CoInit") << CoInitType::Auto;
    QTest::newRow("manual_multithreaded_CoInit") << CoInitType::ManualMultithread;
    QTest::newRow("manual_apartment_CoInit") << CoInitType::ManualApartment;
}

class HelperContextObject : public QObject
{
    Q_OBJECT
public slots:
    void onDiscoveryError(QBluetoothDeviceDiscoveryAgent::Error err)
    {
        m_error = err;
    }
    void onFinished()
    {
        m_finished = true;
    }
    void onCanceled()
    {
        m_canceled = true;
    }

public:
    QBluetoothDeviceDiscoveryAgent::Error m_error = QBluetoothDeviceDiscoveryAgent::NoError;
    bool m_finished = false;
    bool m_canceled = false;
};

void tst_QBluetoothDeviceDiscoveryAgent::tst_windowsDiscoveryInThread()
{
#ifdef Q_OS_WIN
    // This test checks that a Bluetooth device can be created in a secondary
    // thread. The manualCoInit flag is used to emulate the situation when
    // the secondary thread already called CoInitializeEx() with a different
    // threading model.

    QBluetoothLocalDevice localDevice;
    QBluetoothAddress address = localDevice.address();
    if (address.isNull())
        QSKIP("No local Bluetooth adapter found!");

    QFETCH(const CoInitType, coInitType);

    HelperContextObject context;

    using namespace std::chrono_literals;
    static constexpr auto DiscoveryTimeout = 10000ms;

    auto threadFunc = [coInitType, address, &context] {
        switch (coInitType) {
        case CoInitType::Auto:
            break; // nothing to do
        case CoInitType::ManualApartment:
            CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
            break;
        case CoInitType::ManualMultithread:
            CoInitializeEx(NULL, COINIT_MULTITHREADED);
            break;
        }

        auto guard = qScopeGuard([coInitType]{
            if (coInitType != CoInitType::Auto)
                CoUninitialize();
        });


        QBluetoothDeviceDiscoveryAgent discoveryAgent(address);

        QObject::connect(&discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished,
                         &context, &HelperContextObject::onFinished);
        QObject::connect(&discoveryAgent, &QBluetoothDeviceDiscoveryAgent::canceled,
                         &context, &HelperContextObject::onCanceled);
        QObject::connect(&discoveryAgent, &QBluetoothDeviceDiscoveryAgent::errorOccurred,
                         &context, &HelperContextObject::onDiscoveryError);

        discoveryAgent.start();

        QTest::qWait(DiscoveryTimeout);

        discoveryAgent.stop();
    };

    auto thread = std::unique_ptr<QThread>(QThread::create(std::move(threadFunc)));

    thread->start();

    QTRY_VERIFY_WITH_TIMEOUT(context.m_canceled || context.m_finished, DiscoveryTimeout * 2);
    QCOMPARE(context.m_error, QBluetoothDeviceDiscoveryAgent::NoError);
#endif // Q_OS_WIN
}

QTEST_MAIN(tst_QBluetoothDeviceDiscoveryAgent)

#include "tst_qbluetoothdevicediscoveryagent.moc"
