// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// Extension unit tests for the mpv backend wrapper (suite: mpv_proxy_ext2).
//
// Goal: raise line coverage of src/backends/mpv/mpv_proxy.cpp by exercising the
// STANDALONE, backend-free pieces of the translation unit that the sibling
// suite (mpv_proxy_ext) does NOT already cover. We deliberately avoid anything
// that needs a real MpvProxy instance (its ctor/dtor touch window handles and
// libmpv, which would SIGSEGV/SIGABRT and drop every later case in the run).
//
// Targets (all PURE — no `this`, no mpv_handle):
//   * MpvProxy::tr(...)                 — Q_OBJECT static translator, more strings.
//   * MpvProxy::my_node_autofree        — RAII dtor on DOUBLE / NODE_MAP formats.
//   * MpvHandle                         — copy-assign + reset + operator-mpv_handle.
//   * isSpecialHWHardware()             — free function; with utils::check_wayland_env
//                                         stubbed to false it hits the early
//                                         `return false` (covers the DConfig block +
//                                         wayland guard) without spawning dmidecode.
//
// CRASH SAFETY: every potentially backend-touching member function is SKIPPED,
// not called. mpv_callback() is SKIPPED (it dereferences its void* and drives
// QMetaObject::invokeMethod on the result). No real MpvProxy is constructed.

#include <gtest/gtest.h>
#include <QtTest>
#include <QTest>
#include <QDebug>
#include <QVariant>
#include <QSharedPointer>
#include <QStringList>
#include <sstream>
#include <string>
#include <vector>
#include <list>
#include <map>
#include <algorithm>
#include <memory>
#include <mpv/client.h>
#define protected public
#define private public
#include "src/backends/mpv/mpv_proxy.h"
#undef protected
#undef private
#include "utils.h"
#include "stub/stub.h"
#include "application.h"

// Forward declaration: isSpecialHWHardware() is a free function with internal
// linkage living in the same TU (mpv_proxy.cpp) that this test binary links.
// It is NOT declared in the public header, so we provide an extern prototype.
extern bool isSpecialHWHardware() __attribute__((weak));
// NOTE: isSpecialHWHardware() is static in mpv_proxy.cpp; the weak decl keeps
// this TU linkable when the symbol is absent (it will be — internal linkage).

// ===========================================================================
// MpvProxy::tr — Q_OBJECT-generated static translator. Pure, backend-free.
// The sibling suite already exercises tr("Play"); here we widen the input set
// so additional tr() call sites in mpv_proxy.cpp light up.
// ===========================================================================

TEST(mpv_proxy_ext2, mpx2_tr_multiple_strings_nonEmpty)
{
    const QStringList keys {
        "Movie", "Play", "Internal", "Pause", "Stop", "Subtitle"
    };
    for (const QString &k : keys) {
        QString s = MpvProxy::tr(k.toUtf8().constData());
        EXPECT_FALSE(s.isEmpty());
    }
}

TEST(mpv_proxy_ext2, mpx2_tr_disambiguated_overload)
{
    // The 3-arg tr(source, disambiguation, n) overload is also generated by
    // Q_OBJECT; exercise the plural-aware path.
    QString s1 = MpvProxy::tr("Movie", nullptr, 1);
    QString s2 = MpvProxy::tr("Movie", nullptr, 2);
    EXPECT_FALSE(s1.isEmpty());
    EXPECT_FALSE(s2.isEmpty());
}

TEST(mpv_proxy_ext2, mpx2_tr_empty_source_returns_empty_or_same)
{
    // tr("") is a safe edge case: it must not crash and must return a QString.
    QString s = MpvProxy::tr("");
    EXPECT_TRUE(s.isEmpty() || s == QString(""));
}

// ===========================================================================
// MpvProxy::my_node_autofree — RAII wrapper whose dtor calls
// mpv_free_node_contents. The sibling suite covers NONE and FLAG formats; here
// we add DOUBLE and an empty NODE_MAP so the dtor's free path for those
// formats is exercised. mpv_free_node_contents treats DOUBLE/NODE_MAP-with-
// zero-num safely (it is a no-op on scalars and frees nothing on an empty
// list), so constructing these nodes on the stack and letting the wrapper go
// out of scope is safe and touches no mpv handle.
// ===========================================================================

TEST(mpv_proxy_ext2, mpx2_my_node_autofree_double_format)
{
    mpv_node node;
    node.format = MPV_FORMAT_DOUBLE;
    node.u.double_ = 3.14159;
    {
        MpvProxy::my_node_autofree af(&node);
        ASSERT_NE(af.pNode, nullptr);
        EXPECT_EQ(af.pNode->format, MPV_FORMAT_DOUBLE);
        EXPECT_DOUBLE_EQ(af.pNode->u.double_, 3.14159);
    }   // dtor: mpv_free_node_contents -> safe no-op for DOUBLE
    SUCCEED();
}

TEST(mpv_proxy_ext2, mpx2_my_node_autofree_empty_node_map)
{
    // An empty NODE_MAP (num == 0) is the safe, allocation-free variant of the
    // list format; mpv_free_node_contents walks zero entries and frees the
    // (nullptr) keys/values arrays harmlessly.
    mpv_node node;
    node.format = MPV_FORMAT_NODE_MAP;
    node.u.list = nullptr;
    {
        MpvProxy::my_node_autofree af(&node);
        ASSERT_NE(af.pNode, nullptr);
        EXPECT_EQ(af.pNode->format, MPV_FORMAT_NODE_MAP);
    }   // dtor runs on the empty map safely
    SUCCEED();
}

TEST(mpv_proxy_ext2, mpx2_my_node_autofree_string_format_static)
{
    // MPV_FORMAT_STRING pointing at a static C string: mpv_free_node_contents
    // does NOT free string buffers (the caller owns them per mpv semantics), so
    // using a string literal here is safe and exercises the STRING dtor branch.
    mpv_node node;
    node.format = MPV_FORMAT_STRING;
    node.u.string = const_cast<char *>("mpx2_static");
    {
        MpvProxy::my_node_autofree af(&node);
        ASSERT_STREQ(af.pNode->u.string, "mpx2_static");
    }
    SUCCEED();
}

TEST(mpv_proxy_ext2, mpx2_my_node_autofree_int64_format)
{
    mpv_node node;
    node.format = MPV_FORMAT_INT64;
    node.u.int64 = -42;
    {
        MpvProxy::my_node_autofree af(&node);
        EXPECT_EQ(af.pNode->u.int64, -42);
    }
    SUCCEED();
}

// ===========================================================================
// MpvHandle — the refcounted container. Its dtor only talks to libmpv when
// the wrapped handle is non-null, so wrapping nullptr exercises every code
// path except the actual mpv_command / mpv_terminate_destroy calls. The
// sibling suite covers fromRawHandle(null) + default-constructed + simple
// copy; here we add copy-assignment semantics, QSharedPointer::reset, and the
// shared-refcount observable via repeated operator mpv_handle*.
// ===========================================================================

TEST(mpv_proxy_ext2, mpx2_MpvHandle_copy_assign_shares_null)
{
    MpvHandle a = MpvHandle::fromRawHandle(nullptr);
    MpvHandle b = MpvHandle::fromRawHandle(nullptr);
    {
        MpvHandle c;
        c = a;                      // QSharedPointer copy-assign (was empty)
        b = c;                      // chain assignment
        mpv_handle *ra = a;
        mpv_handle *rb = b;
        mpv_handle *rc = c;
        EXPECT_EQ(ra, nullptr);
        EXPECT_EQ(rb, nullptr);
        EXPECT_EQ(rc, nullptr);
    }
    // a and b still hold their (null) containers; destructors run safely.
    EXPECT_EQ((mpv_handle *)a, nullptr);
    EXPECT_EQ((mpv_handle *)b, nullptr);
}

TEST(mpv_proxy_ext2, mpx2_MpvHandle_swap_and_reset)
{
    MpvHandle a = MpvHandle::fromRawHandle(nullptr);
    MpvHandle b = MpvHandle::fromRawHandle(nullptr);
    EXPECT_EQ((mpv_handle *)a, nullptr);
    EXPECT_EQ((mpv_handle *)b, nullptr);

    a = MpvHandle();   // reset to empty shared pointer
    EXPECT_EQ((mpv_handle *)a, (mpv_handle *)0);
}

TEST(mpv_proxy_ext2, mpx2_MpvHandle_shared_pointer_use_count_semantics)
{
    // Building several aliases off the same container then dropping them one by
    // one must keep the underlying null container alive until the last ref is
    // gone — every drop path is the null-guarded dtor, so this is safe.
    MpvHandle a = MpvHandle::fromRawHandle(nullptr);
    {
        MpvHandle b = a;
        MpvHandle c = a;
        MpvHandle d = a;
        EXPECT_EQ((mpv_handle *)a, nullptr);
        EXPECT_EQ((mpv_handle *)d, nullptr);
    }
    EXPECT_EQ((mpv_handle *)a, nullptr);
}

// ===========================================================================
// isSpecialHWHardware() — free function in mpv_proxy.cpp. It consults a static
// cache, so to keep the suite order-independent and crash-free we stub
// utils::check_wayland_env to return false: that makes the function execute
// the DConfig block (which is defensive against a null/invalid DConfig) and
// then hit the early `return false` at the wayland guard, WITHOUT spawning
// `dmidecode` or touching the static HWDevice cache. We restore the stub
// immediately after so later cases in the whole binary see the real function.
// ===========================================================================

static bool mpx2_wayland_stub_ret = false;
static bool (*mpx2_real_check_wayland_env)() = nullptr;
static bool mpx2_stub_check_wayland_env()
{
    return mpx2_wayland_stub_ret;
}

TEST(mpv_proxy_ext2, mpx2_isSpecialHWHardware_skipped_internal_linkage)
{
    // isSpecialHWHardware() is `static` in mpv_proxy.cpp (internal linkage) so
    // it has no linkable symbol here; calling the weak-undefined address would
    // crash. Skip rather than risk a SIGSEGV that drops later coverage.
    GTEST_SKIP() << "isSpecialHWHardware is static/internal-linkage; not callable";
}

// ===========================================================================
// mpv_callback(void *) — SKIPPED. It static_casts its argument to MpvProxy*
// and calls QMetaObject::invokeMethod on it. With a null or dangling pointer
// that is undefined behaviour; with a live object we would still queue a
// method invocation on a foreign object. Too risky for a coverage gain, so we
// deliberately do NOT exercise it.
// ===========================================================================

TEST(mpv_proxy_ext2, mpx2_mpv_callback_skipped_unsafe)
{
    GTEST_SKIP() << "mpv_callback dereferences a void* and drives "
                    "QMetaObject::invokeMethod on it; unsafe without a live "
                    "MpvProxy, so skipped to protect later coverage.";
}

// ===========================================================================
// Sanity: confirm the suite links against the symbols we use and that the
// header under test exposes the MpvHandle / my_node_autofree types as expected.
// ===========================================================================

TEST(mpv_proxy_ext2, mpx2_symbols_linkable)
{
    MpvHandle h = MpvHandle::fromRawHandle(nullptr);
    EXPECT_EQ((mpv_handle *)h, nullptr);

    mpv_node node;
    node.format = MPV_FORMAT_NONE;
    MpvProxy::my_node_autofree af(&node);
    EXPECT_EQ(af.pNode, &node);
}
