/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include <errno.h>
#include <getopt.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/uio.h>
#include <unistd.h>

#include <condition_variable>
#include <cstddef>
#include <mutex>
#include <queue>

#include <android-base/expected.h>
#include <android-base/logging.h>
#include <android/frameworks/stats/BnStats.h>
#include <android/frameworks/stats/IStats.h>
#include <android/trusty/stats/nw/setter/IStatsSetter.h>
#include <binder/RpcServer.h>
#include <binder/RpcSession.h>
#include <binder/RpcTransportRaw.h>
#include <binder/RpcTransportTipcAndroid.h>
#include <binder/RpcTrusty.h>
#include <trusty/tipc.h>

/** DOC:
 * ./build-root/build-qemu-generic-arm64-test-debug/run \
 *       --android $ANDROID_PROJECT_ROOT \
 *       --headless --shell-command \
 *       "/data/nativetest64/vendor/trusty_stats_test/trusty_stats_test"
 *
 * adb -s emulator-5554 shell \
 *       /data/nativetest64/vendor/trusty_stats_test/trusty_stats_test
 */
using ::android::base::unique_fd;
using ::android::binder::Status;
using ::android::frameworks::stats::BnStats;
using ::android::frameworks::stats::IStats;
using ::android::frameworks::stats::VendorAtom;
using ::android::frameworks::stats::VendorAtomValue;
using ::android::trusty::stats::nw::setter::IStatsSetter;

constexpr const char kTrustyDefaultDeviceName[] = "/dev/trusty-ipc-dev0";
constexpr const char kTrustyStatsSetterTest[] =
        "com.android.frameworks.stats.trusty.test.relayer.istats_setter";
constexpr const char kTrustyStatsSetterMetrics[] =
        "com.android.frameworks.stats.trusty.metrics.istats_setter";
constexpr const char kTrustyStatsPortTest[] = "com.android.trusty.stats.test";
constexpr const char kTrustyCrashPortTest[] = "com.android.trusty.crashtest";
constexpr const char kTrustyCrasherUuid[] = "7ee4dddc-177a-420a-96ea-5d413d88228e:crasher";

enum TrustyAtoms : int32_t {
    TrustyAppCrashed = 100072,
    TrustyError = 100145,
    TrustyStorageError = 100146
};

enum TestMsgHeader : int32_t {
    TEST_PASSED = 0,
    TEST_FAILED = 1,
    TEST_MESSAGE = 2,
};

namespace android {
namespace trusty {
namespace stats {

class Stats : public BnStats {
  public:
    Stats() : BnStats() {}

    Status reportVendorAtom(const VendorAtom& vendorAtom) override {
        const char* atomIdStr = vendorAtomStr(vendorAtom.atomId);
        ALOGD("Vendor atom reported of type: %s\n", atomIdStr);
        std::lock_guard lock(mLock);
        mQueueVendorAtom.push(vendorAtom);
        mCondVar.notify_one();
        return Status::ok();
    }

    status_t getVendorAtom(VendorAtom* pVendorAtom, int64_t waitForMs) {
        std::unique_lock lock(mLock);
        while (mQueueVendorAtom.empty()) {
            auto rc = mCondVar.wait_for(lock, std::chrono::milliseconds(waitForMs));
            if (rc == std::cv_status::timeout) {
                return TIMED_OUT;
            }
        }
        *pVendorAtom = mQueueVendorAtom.front();
        mQueueVendorAtom.pop();
        return NO_ERROR;
    }

  private:
    const char* vendorAtomStr(int32_t atomId) {
        switch (atomId) {
            case TrustyAtoms::TrustyAppCrashed:
                return "TrustyAtoms::TrustyAppCrashed";
            case TrustyAtoms::TrustyError:
                return "TrustyAtoms::TrustyError";
            case TrustyAtoms::TrustyStorageError:
                return "TrustyAtoms::TrustyStorageError";
            default:
                return "unknown TrustyAtoms type";
        }
    }
    std::mutex mLock;
    std::condition_variable mCondVar;
    std::queue<VendorAtom> mQueueVendorAtom;
};

class TrustyStatsTestBase : public ::testing::Test {
  protected:
    TrustyStatsTestBase(std::string&& portNameStatsSetter, std::string&& portNamePortTest)
        : mPortTestFd(-1),
          mPortNameStatsSetter(std::move(portNameStatsSetter)),
          mPortNamePortTest(std::move(portNamePortTest)) {}

    void SetUp() override {
        // Commenting out the server portion because we do not have any direct
        // incoming call Calls from TA are currently being handled on the mSession's
        // extra thread. android::sp<::android::RpcServer> server =
        // ::android::RpcServer::make(::android::RpcTransportCtxFactoryRaw::make());

        mStats = android::sp<Stats>::make();
        // Increasing number of incoming threads on mSession to be able to receive
        // callbacks
        auto session_initializer = [](sp<RpcSession>& session) {
            session->setMaxIncomingThreads(1);
        };

        ASSERT_FALSE(mSession);
        mSession = RpcTrustyConnectWithSessionInitializer(
                kTrustyDefaultDeviceName, mPortNameStatsSetter.c_str(), session_initializer);
        ASSERT_TRUE(mSession);

        auto root = mSession->getRootObject();
        ASSERT_TRUE(root);
        auto statsSetter = IStatsSetter::asInterface(root);
        ASSERT_TRUE(statsSetter);
        statsSetter->setInterface(mStats);
    }
    void TearDown() override {
        // close connection to unitest app
        if (mPortTestFd != -1) {
            tipc_close(mPortTestFd);
        }
        mPortTestFd = -1;

        if (mSession) {
            // shutdownAndWait here races with sending out the DecStrong
            // messages after reportVendorAtom returns, so we delay it a little
            // bit to give the messages time to go out over the transport
            usleep(50000);
            ASSERT_TRUE(mSession->shutdownAndWait(true));
        }
        mSession.clear();
        mStats.clear();
    }
    void StartPortTest() {
        // connect to unitest app
        mPortTestFd = tipc_connect(kTrustyDefaultDeviceName, mPortNamePortTest.c_str());
        if (mPortTestFd < 0) {
            ALOGE("Failed to connect to '%s' app: %s\n", kTrustyStatsPortTest,
                  strerror(-mPortTestFd));
        }
        ASSERT_GT(mPortTestFd, 0);
    }
    void WaitPortTestDone() {
        // wait for test to complete
        char rxBuf[1024];
        const char prolog[] = "Trusty PORT_TEST:";
        strncpy(rxBuf, prolog, sizeof(prolog) - 1);
        char* pRxBuf = rxBuf + sizeof(prolog) - 1;
        size_t remainingBufSize = sizeof(rxBuf) - sizeof(prolog) - 1;

        ASSERT_NE(mPortTestFd, -1);
        for (;;) {
            int rc = read(mPortTestFd, pRxBuf, remainingBufSize);
            ASSERT_GT(rc, 0);
            ASSERT_LT(rc, (int)remainingBufSize);
            if (pRxBuf[0] == TEST_PASSED) {
                break;
            } else if (pRxBuf[0] == TEST_FAILED) {
                break;
            } else if (pRxBuf[0] == TEST_MESSAGE) {
                pRxBuf[0] = ' ';
                write(STDOUT_FILENO, rxBuf, rc + sizeof(prolog) - 1);
            } else {
                ALOGE("Bad message header: %d\n", rxBuf[0]);
                break;
            }
        }
        ASSERT_EQ(pRxBuf[0], TEST_PASSED);
    }

    android::sp<Stats> mStats;

  private:
    android::sp<RpcSession> mSession;
    int mPortTestFd;
    std::string mPortNameStatsSetter;
    std::string mPortNamePortTest;
};

class TrustyStatsTest : public TrustyStatsTestBase {
  protected:
    TrustyStatsTest() : TrustyStatsTestBase(kTrustyStatsSetterTest, kTrustyStatsPortTest) {}
};

class TrustyMetricsCrashTest : public TrustyStatsTestBase {
  protected:
    TrustyMetricsCrashTest()
        : TrustyStatsTestBase(kTrustyStatsSetterMetrics, kTrustyCrashPortTest) {}
};

TEST_F(TrustyStatsTest, CheckAtoms) {
    int atomAppCrashedCnt = 0;
    int atomStorageErrorCnt = 0;
    int atomTrustyErrorCnt = 0;
    uint64_t blockForMs = 500;
    StartPortTest();
    WaitPortTestDone();
    for (;;) {
        VendorAtom vendorAtom;
        auto status = mStats->getVendorAtom(&vendorAtom, blockForMs);
        ASSERT_THAT(status, ::testing::AnyOf(NO_ERROR, TIMED_OUT));
        if (status == TIMED_OUT) {
            // No more atoms
            break;
        }

        ASSERT_THAT(vendorAtom.atomId,
                    ::testing::AnyOf(::testing::Eq(TrustyAtoms::TrustyAppCrashed),
                                     ::testing::Eq(TrustyAtoms::TrustyError),
                                     ::testing::Eq(TrustyAtoms::TrustyStorageError)));
        ASSERT_STREQ(String8(vendorAtom.reverseDomainName), "google.android.trusty");
        switch (vendorAtom.atomId) {
            case TrustyAtoms::TrustyAppCrashed:
                ++atomAppCrashedCnt;
                ASSERT_STREQ(String8(vendorAtom.values[0].get<VendorAtomValue::stringValue>()),
                             "5247d19b-cf09-4272-a450-3ef20dbefc14");
                break;
            case TrustyAtoms::TrustyStorageError:
                ++atomStorageErrorCnt;
                ASSERT_EQ(vendorAtom.values[0].get<VendorAtomValue::intValue>(), 5);
                ASSERT_STREQ(String8(vendorAtom.values[1].get<VendorAtomValue::stringValue>()),
                             "5247d19b-cf09-4272-a450-3ef20dbefc14");
                ASSERT_STREQ(String8(vendorAtom.values[2].get<VendorAtomValue::stringValue>()),
                             "5247d19b-cf09-4272-a450-3ef20dbefc14");
                ASSERT_EQ(vendorAtom.values[3].get<VendorAtomValue::intValue>(), 1);
                ASSERT_EQ(vendorAtom.values[4].get<VendorAtomValue::intValue>(), 3);
                ASSERT_EQ(vendorAtom.values[5].get<VendorAtomValue::longValue>(),
                          0x4BCDEFABBAFEDCBALL);
                ASSERT_EQ(vendorAtom.values[6].get<VendorAtomValue::intValue>(), 4);
                ASSERT_EQ(vendorAtom.values[7].get<VendorAtomValue::longValue>(), 1023);
                break;
            case TrustyAtoms::TrustyError:
                ++atomTrustyErrorCnt;
                break;
            default:
                FAIL() << "Unknown vendor atom ID: " << vendorAtom.atomId;
                break;
        }
    };
    ASSERT_EQ(atomAppCrashedCnt, 1);
    ASSERT_EQ(atomStorageErrorCnt, 1);
    ASSERT_EQ(atomTrustyErrorCnt, 0);
}

TEST_F(TrustyMetricsCrashTest, CheckTrustyCrashAtoms) {
    const std::vector<uint32_t> kExpectedCrashReasonsArm64{
            0x00000001U,  // exit_failure (twice)
            0x00000001U,
            0x92000004U,  // read_null_ptr
            0xf200002aU,  // brk_instruction
            0x92000004U,  // read_bad_ptr
            0x92000044U,  // crash_write_bad_ptr
            0x9200004fU,  // crash_write_ro_ptr
            0x8200000fU,  // crash_exec_rodata
            0x8200000fU,  // crash_exec_data
    };
    const std::vector<uint32_t> kExpectedCrashReasonsArm32{
            0x00000001U,  // exit_failure (twice)
            0x00000001U,
            0x20000007U,  // read_null_ptr
            0x20000007U,  // read_bad_ptr
            0x20000807U,  // crash_write_bad_ptr
            0x2000080fU,  // crash_write_ro_ptr
            0x3000000fU,  // crash_exec_rodata
            0x3000000fU,  // crash_exec_data
    };

    int expectedAtomCnt = 7;
    int atomAppCrashedCnt = 0;
    int atomStorageErrorCnt = 0;
    int atomTrustyErrorCnt = 0;
    std::vector<uint32_t> atomCrashReasons;
    uint64_t blockForMs = 500;
    StartPortTest();
    WaitPortTestDone();
    for (;;) {
        VendorAtom vendorAtom;
        auto status = mStats->getVendorAtom(&vendorAtom, blockForMs);
        ASSERT_THAT(status, ::testing::AnyOf(NO_ERROR, TIMED_OUT));
        if (status == TIMED_OUT) {
            // No more atoms
            break;
        }

        ASSERT_THAT(vendorAtom.atomId,
                    ::testing::AnyOf(::testing::Eq(TrustyAtoms::TrustyAppCrashed),
                                     ::testing::Eq(TrustyAtoms::TrustyError),
                                     ::testing::Eq(TrustyAtoms::TrustyStorageError)));
        ASSERT_STREQ(String8(vendorAtom.reverseDomainName), "google.android.trusty");

        switch (vendorAtom.atomId) {
            case TrustyAtoms::TrustyAppCrashed:
                ++atomAppCrashedCnt;
                ASSERT_STREQ(String8(vendorAtom.values[0].get<VendorAtomValue::stringValue>()),
                             kTrustyCrasherUuid);
                atomCrashReasons.push_back(vendorAtom.values[1].get<VendorAtomValue::intValue>());
                break;
            case TrustyAtoms::TrustyStorageError:
                ++atomStorageErrorCnt;
                break;
            case TrustyAtoms::TrustyError:
                ++atomTrustyErrorCnt;
                ASSERT_STREQ(String8(vendorAtom.values[1].get<VendorAtomValue::stringValue>()), "");
                break;
            default:
                FAIL() << "Unknown vendor atom ID: " << vendorAtom.atomId;
        }
    }
    ASSERT_GE(atomAppCrashedCnt, expectedAtomCnt - 1);
    ASSERT_EQ(atomStorageErrorCnt, 0);
    // There is one dropped event left over from Trusty boot,
    // it may show up here
    ASSERT_LE(atomTrustyErrorCnt, 1);
    ASSERT_THAT(atomCrashReasons,
                ::testing::AnyOf(kExpectedCrashReasonsArm64, kExpectedCrashReasonsArm32));
};

}  // namespace stats
}  // namespace trusty
}  // namespace android