/*
 * Copyright (C) 2026 UBports Foundation.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "../../src/RcloneClient.h"
#include <utils/ExpectThrow.h>
#include <utils/RcloneTestUtils.h>
#include <testsetup.h>

#include <gtest/gtest.h>
#include <QCoreApplication>
#include <QFile>
#include <QJsonObject>
#include <QNetworkReply>
#include <QTemporaryDir>
#include <QTest>
#include <lomiri/storage/provider/Exceptions.h>

#include <memory>

using namespace std;
using namespace lomiri::storage::provider;

namespace
{

constexpr int CALLBACK_WAIT_TIME = 30000;

// Wrap an RcCallback to store its results
struct RcResult
{
    QJsonObject result;
    boost::exception_ptr error;
    bool done = false;

    RcloneClient::RcCallback callback()
    {
        return [this](QJsonObject const& r, boost::exception_ptr const& e) {
            result = r;
            error = e;
            done = true;
        };
    }
};

bool spin_until(bool const& done, int timeout = CALLBACK_WAIT_TIME)
{
    return QTest::qWaitFor([&done] { return done; }, timeout);
}

}  // namespace

class RcloneClientTests : public ::testing::Test
{
protected:
    void SetUp() override
    {
        tmp_dir_.reset(new QTemporaryDir(TEST_BIN_DIR "/rclone-client.XXXXXX"));
        ASSERT_TRUE(tmp_dir_->isValid());
        fs_ = local_fs_for(tmp_dir_->path());
    }

    unique_ptr<QTemporaryDir> tmp_dir_;
    string fs_;
};

TEST_F(RcloneClientTests, queues_calls_until_ready)
{
    RcloneClient client;
    RcResult noop;
    client.call(QStringLiteral("rc/noop"), QJsonObject{{"value", "queued"}},
                noop.callback());
    ASSERT_FALSE(noop.done); // Queued / Running

    ASSERT_TRUE(spin_until(noop.done));
    ASSERT_FALSE(noop.error);
    // rc/noop echoes its parameters back.
    EXPECT_EQ("queued", noop.result.value("value").toString());
}

TEST_F(RcloneClientTests, ensure_configless_mode)
{
    // Should ignore env config
    QString const config_path = tmp_dir_->path() + "/rclone.conf";
    qputenv("RCLONE_CONFIG", config_path.toUtf8());

    RcloneClient client;
    RcResult created;
    client.call(QStringLiteral("config/create"), QJsonObject{
        {"name", "probe"},
        {"type", "webdav"},
        {"parameters", QJsonObject{
            {"url", QStringLiteral("http://127.0.0.1:1/")},
            {"vendor", QStringLiteral("nextcloud")},
            {"user", QStringLiteral("joe")},
            {"pass", QStringLiteral("3VKdZ4GKlOYXbiiDzrT9QGGH3z0n1LZ7")},
        }},
    }, created.callback());
    ASSERT_TRUE(spin_until(created.done));
    ASSERT_FALSE(created.error);

    RcResult dump;
    client.call(QStringLiteral("config/dump"), QJsonObject(), dump.callback());
    ASSERT_TRUE(spin_until(dump.done));
    ASSERT_FALSE(dump.error);
    EXPECT_TRUE(dump.result.contains(QStringLiteral("probe")));

    EXPECT_FALSE(QFile::exists(config_path));

    qunsetenv("RCLONE_CONFIG");
}

TEST_F(RcloneClientTests, when_ready_then_direct_call)
{
    RcloneClient client;
    RcResult ready;
    client.when_ready(ready.callback());
    ASSERT_TRUE(spin_until(ready.done));
    ASSERT_FALSE(ready.error);
    EXPECT_TRUE(ready.result.isEmpty());

    // Dispatched without queueing
    RcResult noop;
    client.call(QStringLiteral("rc/noop"), QJsonObject{{"value", "direct"}},
                noop.callback());
    ASSERT_TRUE(spin_until(noop.done));
    ASSERT_FALSE(noop.error);
    EXPECT_EQ("direct", noop.result.value("value").toString());
}

TEST_F(RcloneClientTests, unknown_endpoint_is_an_error)
{
    RcloneClient client;
    RcResult call;
    client.call(QStringLiteral("operations/not-existing-op"), QJsonObject(),
                call.callback());
    ASSERT_TRUE(spin_until(call.done));
    ASSERT_TRUE(call.error);
    expect_throw<UnknownException>(call.error);
}

TEST_F(RcloneClientTests, rc_error_is_translated_with_the_item_id)
{
    RcloneClient client;
    RcResult call;
    client.call(QStringLiteral("operations/list"),
                RcloneClient::make_params(fs_, "nodir"),
                "nodir/", call.callback());
    ASSERT_TRUE(spin_until(call.done));
    ASSERT_TRUE(call.error);
    auto e = expect_throw<NotExistsException>(call.error);
    EXPECT_EQ("nodir/", e.key());
}

TEST_F(RcloneClientTests, missing_parameter_is_a_comms_error)
{
    RcloneClient client;
    RcResult call;
    client.call(QStringLiteral("operations/list"), QJsonObject(),
                call.callback());
    ASSERT_TRUE(spin_until(call.done));
    ASSERT_TRUE(call.error);
    expect_throw<RemoteCommsException>(call.error);
}

TEST_F(RcloneClientTests, serve_url_encodes_the_fs_and_the_path)
{
    RcloneClient client;
    RcResult ready;
    client.when_ready(ready.callback());
    ASSERT_TRUE(spin_until(ready.done));

    QUrl const url = client.serve_url(":local:/tmp/x/", "a b/ü.txt");
    EXPECT_EQ(client.base_url().toEncoded() + "[:local:/tmp/x/]/a%20b/%C3%BC.txt",
              url.toEncoded());
}

TEST_F(RcloneClientTests, serve_url_fetches_file_contents)
{
    RcloneClient client;
    QFile file(tmp_dir_->path() + "/a b.txt");
    ASSERT_TRUE(file.open(QIODevice::WriteOnly));
    ASSERT_EQ(6, file.write("hello\n"));
    file.close();

    RcResult ready;
    client.when_ready(ready.callback());
    ASSERT_TRUE(spin_until(ready.done));
    ASSERT_FALSE(ready.error);

    unique_ptr<QNetworkReply> reply(client.network().get(
        client.authenticated(client.serve_url(fs_, "a b.txt"))));
    ASSERT_TRUE(QTest::qWaitFor([&reply] { return reply->isFinished(); },
                                CALLBACK_WAIT_TIME));

    EXPECT_EQ(200, reply->attribute(
        QNetworkRequest::HttpStatusCodeAttribute).toInt());
    EXPECT_EQ("hello\n", reply->readAll());
}

TEST_F(RcloneClientTests, calls_fail_when_the_daemon_cannot_start)
{
    QByteArray const path = qgetenv("PATH");
    qputenv("PATH", "/nonexistent");
    RcloneClient client;
    qputenv("PATH", path);

    // Wait for fail (still initializing)
    RcResult queued;
    client.call(QStringLiteral("rc/noop"), QJsonObject(), queued.callback());
    ASSERT_TRUE(spin_until(queued.done));
    ASSERT_TRUE(queued.error);
    expect_throw<ResourceException>(queued.error);

    // Fail immediately
    RcResult after;
    client.call(QStringLiteral("rc/noop"), QJsonObject(), after.callback());
    EXPECT_TRUE(after.done);
    ASSERT_TRUE(after.error);
    expect_throw<ResourceException>(after.error);
}

int main(int argc, char** argv)
{
    QCoreApplication app(argc, argv);
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}
