// Copyright (C) 2017 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 "StatsService.h"

#include <android/binder_interface_utils.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <stdio.h>

#include "config/ConfigKey.h"
#include "packages/UidMap.h"
#include "src/statsd_config.pb.h"
#include "tests/statsd_test_util.h"

using namespace android;
using namespace testing;

namespace android {
namespace os {
namespace statsd {

using android::util::ProtoOutputStream;
using ::ndk::SharedRefBase;

#ifdef __ANDROID__

namespace {

const int64_t metricId = 123456;
const int32_t ATOM_TAG = util::SUBSYSTEM_SLEEP_STATE;

StatsdConfig CreateStatsdConfig(const GaugeMetric::SamplingType samplingType) {
    StatsdConfig config;
    config.add_default_pull_packages("AID_ROOT");  // Fake puller is registered with root.
    auto atomMatcher = CreateSimpleAtomMatcher("TestMatcher", ATOM_TAG);
    *config.add_atom_matcher() = atomMatcher;
    *config.add_gauge_metric() =
            createGaugeMetric("GAUGE1", atomMatcher.id(), samplingType, nullopt, nullopt);
    config.set_hash_strings_in_metric_report(false);
    return config;
}

class FakeSubsystemSleepCallbackWithTiming : public FakeSubsystemSleepCallback {
public:
    Status onPullAtom(int atomTag,
                      const shared_ptr<IPullAtomResultReceiver>& resultReceiver) override {
        mPullTimeNs = getElapsedRealtimeNs();
        return FakeSubsystemSleepCallback::onPullAtom(atomTag, resultReceiver);
    }
    int64_t mPullTimeNs = 0;
};

}  // namespace

TEST(StatsServiceTest, TestAddConfig_simple) {
    const sp<UidMap> uidMap = new UidMap();
    shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(
            uidMap, /* queue */ nullptr, std::make_shared<LogEventFilter>());
    const int kConfigKey = 12345;
    const int kCallingUid = 123;
    StatsdConfig config;
    config.set_id(kConfigKey);
    string serialized = config.SerializeAsString();

    EXPECT_TRUE(service->addConfigurationChecked(kCallingUid, kConfigKey,
                                                 {serialized.begin(), serialized.end()}));
    service->removeConfiguration(kConfigKey, kCallingUid);
    ConfigKey configKey(kCallingUid, kConfigKey);
    service->mProcessor->onDumpReport(configKey, getElapsedRealtimeNs(),
                                      false /* include_current_bucket*/, true /* erase_data */,
                                      ADB_DUMP, NO_TIME_CONSTRAINTS, nullptr);
}

TEST(StatsServiceTest, TestAddConfig_empty) {
    const sp<UidMap> uidMap = new UidMap();
    shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(
            uidMap, /* queue */ nullptr, std::make_shared<LogEventFilter>());
    string serialized = "";
    const int kConfigKey = 12345;
    const int kCallingUid = 123;
    EXPECT_TRUE(service->addConfigurationChecked(kCallingUid, kConfigKey,
                                                 {serialized.begin(), serialized.end()}));
    service->removeConfiguration(kConfigKey, kCallingUid);
    ConfigKey configKey(kCallingUid, kConfigKey);
    service->mProcessor->onDumpReport(configKey, getElapsedRealtimeNs(),
                                      false /* include_current_bucket*/, true /* erase_data */,
                                      ADB_DUMP, NO_TIME_CONSTRAINTS, nullptr);
}

TEST(StatsServiceTest, TestAddConfig_invalid) {
    const sp<UidMap> uidMap = new UidMap();
    shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(
            uidMap, /* queue */ nullptr, std::make_shared<LogEventFilter>());
    string serialized = "Invalid config!";

    EXPECT_FALSE(
            service->addConfigurationChecked(123, 12345, {serialized.begin(), serialized.end()}));
}

TEST(StatsServiceTest, TestGetUidFromArgs) {
    Vector<String8> args;
    args.push(String8("-1"));
    args.push(String8("0"));
    args.push(String8("1"));
    args.push(String8("a1"));
    args.push(String8(""));

    int32_t uid;

    const sp<UidMap> uidMap = new UidMap();
    shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(
            uidMap, /* queue */ nullptr, std::make_shared<LogEventFilter>());
    service->mEngBuild = true;

    // "-1"
    EXPECT_FALSE(service->getUidFromArgs(args, 0, uid));

    // "0"
    EXPECT_TRUE(service->getUidFromArgs(args, 1, uid));
    EXPECT_EQ(0, uid);

    // "1"
    EXPECT_TRUE(service->getUidFromArgs(args, 2, uid));
    EXPECT_EQ(1, uid);

    // "a1"
    EXPECT_FALSE(service->getUidFromArgs(args, 3, uid));

    // ""
    EXPECT_FALSE(service->getUidFromArgs(args, 4, uid));

    // For a non-userdebug, uid "1" cannot be impersonated.
    service->mEngBuild = false;
    EXPECT_FALSE(service->getUidFromArgs(args, 2, uid));
}

TEST_F(StatsServiceConfigTest, StatsServiceStatsdInitTest) {
    // used for error threshold tolerance due to sleep() is involved
    const int64_t ERROR_THRESHOLD_NS = 25 * 1000000;  // 25 ms
    const int INIT_DELAY_SEC = 3;

    auto pullAtomCallback = SharedRefBase::make<FakeSubsystemSleepCallbackWithTiming>();

    // TODO: evaluate to use service->registerNativePullAtomCallback() API
    service->mPullerManager->RegisterPullAtomCallback(/*uid=*/0, ATOM_TAG, NS_PER_SEC,
                                                      NS_PER_SEC * 10, {}, pullAtomCallback);

    StatsdConfig config = CreateStatsdConfig(GaugeMetric::RANDOM_ONE_SAMPLE);
    config.set_id(kConfigKey);
    const int64_t createConfigTimeNs = getElapsedRealtimeNs();
    ASSERT_TRUE(sendConfig(config));
    ASSERT_EQ(2, pullAtomCallback->pullNum);

    service->mProcessor->mPullerManager->ForceClearPullerCache();

    const int64_t initCompletedTimeNs = getElapsedRealtimeNs();
    service->onStatsdInitCompleted(INIT_DELAY_SEC);
    ASSERT_EQ(3, pullAtomCallback->pullNum);

    // Checking pull with or without delay according to the flag value
    const int64_t lastPullNs = pullAtomCallback->mPullTimeNs;

    EXPECT_GE(lastPullNs, initCompletedTimeNs + INIT_DELAY_SEC * NS_PER_SEC);
    EXPECT_LE(lastPullNs, initCompletedTimeNs + INIT_DELAY_SEC * NS_PER_SEC + ERROR_THRESHOLD_NS);

    const int64_t bucketSizeNs =
            TimeUnitToBucketSizeInMillis(config.gauge_metric(0).bucket()) * 1000000;
    const int64_t dumpReportTsNanos = createConfigTimeNs + bucketSizeNs + NS_PER_SEC;

    vector<uint8_t> output;
    ConfigKey configKey(kCallingUid, kConfigKey);
    service->mProcessor->onDumpReport(configKey, dumpReportTsNanos,
                                      /*include_current_bucket=*/false, /*erase_data=*/true,
                                      ADB_DUMP, FAST, &output);
    ConfigMetricsReportList reports;
    reports.ParseFromArray(output.data(), output.size());
    ASSERT_EQ(1, reports.reports_size());

    backfillDimensionPath(&reports);
    backfillStartEndTimestamp(&reports);
    backfillAggregatedAtoms(&reports);
    StatsLogReport::GaugeMetricDataWrapper gaugeMetrics =
            reports.reports(0).metrics(0).gauge_metrics();
    ASSERT_EQ(gaugeMetrics.skipped_size(), 0);
    ASSERT_GT((int)gaugeMetrics.data_size(), 0);
    const auto data = gaugeMetrics.data(0);
    ASSERT_EQ(2, data.bucket_info_size());

    const auto bucketInfo0 = data.bucket_info(0);
    const auto bucketInfo1 = data.bucket_info(1);

    EXPECT_GE(NanoToMillis(bucketInfo0.start_bucket_elapsed_nanos()),
              NanoToMillis(createConfigTimeNs));
    EXPECT_LE(NanoToMillis(bucketInfo0.start_bucket_elapsed_nanos()),
              NanoToMillis(createConfigTimeNs + ERROR_THRESHOLD_NS));

    EXPECT_EQ(NanoToMillis(bucketInfo0.end_bucket_elapsed_nanos()),
              NanoToMillis(bucketInfo1.start_bucket_elapsed_nanos()));

    ASSERT_EQ(1, bucketInfo1.atom_size());
    ASSERT_GT(bucketInfo1.atom(0).subsystem_sleep_state().time_millis(), 0);

    EXPECT_GE(NanoToMillis(bucketInfo1.start_bucket_elapsed_nanos()),
              NanoToMillis(initCompletedTimeNs + INIT_DELAY_SEC * NS_PER_SEC));
    EXPECT_LE(NanoToMillis(bucketInfo1.start_bucket_elapsed_nanos()),
              NanoToMillis(initCompletedTimeNs + INIT_DELAY_SEC * NS_PER_SEC + ERROR_THRESHOLD_NS));

    // this check confirms that bucket end is not affected by the StatsService init delay
    EXPECT_EQ(NanoToMillis(bucketInfo1.end_bucket_elapsed_nanos()),
              NanoToMillis(service->mProcessor->mTimeBaseNs + bucketSizeNs));
}

#else
GTEST_LOG_(INFO) << "This test does nothing.\n";
#endif

}  // namespace statsd
}  // namespace os
}  // namespace android
