// 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 <gtest/gtest.h>

#include <binder/IPCThreadState.h>
#include "src/StatsLogProcessor.h"
#include "src/StatsService.h"
#include "src/stats_log_util.h"
#include "tests/statsd_test_util.h"

#include <vector>

namespace android {
namespace os {
namespace statsd {

#ifdef __ANDROID__

const string kAndroid = "android";
const string kApp1 = "app1.sharing.1";
const int kConfigKey = 789130123;  // Randomly chosen to avoid collisions with existing configs.

void SendConfig(StatsService& service, const StatsdConfig& config) {
    string str;
    config.SerializeToString(&str);
    std::vector<uint8_t> configAsVec(str.begin(), str.end());
    bool success;
    service.addConfiguration(kConfigKey, configAsVec, String16(kAndroid.c_str()));
}

ConfigMetricsReport GetReports(sp<StatsLogProcessor> processor, int64_t timestamp,
                               bool include_current = false) {
    vector<uint8_t> output;
    IPCThreadState* ipc = IPCThreadState::self();
    ConfigKey configKey(ipc->getCallingUid(), kConfigKey);
    processor->onDumpReport(configKey, timestamp, include_current /* include_current_bucket*/,
                            ADB_DUMP, &output);
    ConfigMetricsReportList reports;
    reports.ParseFromArray(output.data(), output.size());
    EXPECT_EQ(1, reports.reports_size());
    return reports.reports(0);
}

StatsdConfig MakeConfig() {
    StatsdConfig config;
    config.add_allowed_log_source("AID_ROOT");  // LogEvent defaults to UID of root.

    auto appCrashMatcher = CreateProcessCrashAtomMatcher();
    *config.add_atom_matcher() = appCrashMatcher;
    auto countMetric = config.add_count_metric();
    countMetric->set_id(StringToId("AppCrashes"));
    countMetric->set_what(appCrashMatcher.id());
    countMetric->set_bucket(FIVE_MINUTES);
    return config;
}

StatsdConfig MakeValueMetricConfig(int64_t minTime) {
    StatsdConfig config;
    config.add_allowed_log_source("AID_ROOT");  // LogEvent defaults to UID of root.

    auto temperatureAtomMatcher = CreateTemperatureAtomMatcher();
    *config.add_atom_matcher() = temperatureAtomMatcher;
    *config.add_atom_matcher() = CreateScreenTurnedOnAtomMatcher();
    *config.add_atom_matcher() = CreateScreenTurnedOffAtomMatcher();

    auto valueMetric = config.add_value_metric();
    valueMetric->set_id(123456);
    valueMetric->set_what(temperatureAtomMatcher.id());
    *valueMetric->mutable_value_field() =
            CreateDimensions(android::util::TEMPERATURE, {3 /* temperature degree field */});
    *valueMetric->mutable_dimensions_in_what() =
            CreateDimensions(android::util::TEMPERATURE, {2 /* sensor name field */});
    valueMetric->set_bucket(FIVE_MINUTES);
    valueMetric->set_min_bucket_size_nanos(minTime);
    valueMetric->set_use_absolute_value_on_reset(true);
    return config;
}

StatsdConfig MakeGaugeMetricConfig(int64_t minTime) {
    StatsdConfig config;
    config.add_allowed_log_source("AID_ROOT");  // LogEvent defaults to UID of root.

    auto temperatureAtomMatcher = CreateTemperatureAtomMatcher();
    *config.add_atom_matcher() = temperatureAtomMatcher;
    *config.add_atom_matcher() = CreateScreenTurnedOnAtomMatcher();
    *config.add_atom_matcher() = CreateScreenTurnedOffAtomMatcher();

    auto gaugeMetric = config.add_gauge_metric();
    gaugeMetric->set_id(123456);
    gaugeMetric->set_what(temperatureAtomMatcher.id());
    gaugeMetric->mutable_gauge_fields_filter()->set_include_all(true);
    *gaugeMetric->mutable_dimensions_in_what() =
            CreateDimensions(android::util::TEMPERATURE, {2 /* sensor name field */});
    gaugeMetric->set_bucket(FIVE_MINUTES);
    gaugeMetric->set_min_bucket_size_nanos(minTime);
    return config;
}

TEST(PartialBucketE2eTest, TestCountMetricWithoutSplit) {
    StatsService service(nullptr);
    SendConfig(service, MakeConfig());
    int64_t start = getElapsedRealtimeNs();  // This is the start-time the metrics producers are
                                             // initialized with.

    service.mProcessor->OnLogEvent(CreateAppCrashEvent(100, start + 1).get());
    service.mProcessor->OnLogEvent(CreateAppCrashEvent(100, start + 2).get());

    ConfigMetricsReport report = GetReports(service.mProcessor, start + 3);
    // Expect no metrics since the bucket has not finished yet.
    EXPECT_EQ(0, report.metrics_size());
}

TEST(PartialBucketE2eTest, TestCountMetricNoSplitOnNewApp) {
    StatsService service(nullptr);
    SendConfig(service, MakeConfig());
    int64_t start = getElapsedRealtimeNs();  // This is the start-time the metrics producers are
                                             // initialized with.

    // Force the uidmap to update at timestamp 2.
    service.mProcessor->OnLogEvent(CreateAppCrashEvent(100, start + 1).get());
    // This is a new installation, so there shouldn't be a split (should be same as the without
    // split case).
    service.mUidMap->updateApp(start + 2, String16(kApp1.c_str()), 1, 2);
    // Goes into the second bucket.
    service.mProcessor->OnLogEvent(CreateAppCrashEvent(100, start + 3).get());

    ConfigMetricsReport report = GetReports(service.mProcessor, start + 4);
    EXPECT_EQ(0, report.metrics_size());
}

TEST(PartialBucketE2eTest, TestCountMetricSplitOnUpgrade) {
    StatsService service(nullptr);
    SendConfig(service, MakeConfig());
    int64_t start = getElapsedRealtimeNs();  // This is the start-time the metrics producers are
                                             // initialized with.
    service.mUidMap->updateMap(start, {1}, {1}, {String16(kApp1.c_str())});

    // Force the uidmap to update at timestamp 2.
    service.mProcessor->OnLogEvent(CreateAppCrashEvent(100, start + 1).get());
    service.mUidMap->updateApp(start + 2, String16(kApp1.c_str()), 1, 2);
    // Goes into the second bucket.
    service.mProcessor->OnLogEvent(CreateAppCrashEvent(100, start + 3).get());

    ConfigMetricsReport report = GetReports(service.mProcessor, start + 4);
    backfillStartEndTimestamp(&report);
    EXPECT_EQ(1, report.metrics_size());
    EXPECT_TRUE(report.metrics(0).count_metrics().data(0).bucket_info(0).
                    has_start_bucket_elapsed_nanos());
    EXPECT_TRUE(report.metrics(0).count_metrics().data(0).bucket_info(0).
                    has_end_bucket_elapsed_nanos());
    EXPECT_EQ(1, report.metrics(0).count_metrics().data(0).bucket_info(0).count());
}

TEST(PartialBucketE2eTest, TestCountMetricSplitOnRemoval) {
    StatsService service(nullptr);
    SendConfig(service, MakeConfig());
    int64_t start = getElapsedRealtimeNs();  // This is the start-time the metrics producers are
                                             // initialized with.
    service.mUidMap->updateMap(start, {1}, {1}, {String16(kApp1.c_str())});

    // Force the uidmap to update at timestamp 2.
    service.mProcessor->OnLogEvent(CreateAppCrashEvent(100, start + 1).get());
    service.mUidMap->removeApp(start + 2, String16(kApp1.c_str()), 1);
    // Goes into the second bucket.
    service.mProcessor->OnLogEvent(CreateAppCrashEvent(100, start + 3).get());

    ConfigMetricsReport report = GetReports(service.mProcessor, start + 4);
    backfillStartEndTimestamp(&report);
    EXPECT_EQ(1, report.metrics_size());
    EXPECT_TRUE(report.metrics(0).count_metrics().data(0).bucket_info(0).
                    has_start_bucket_elapsed_nanos());
    EXPECT_TRUE(report.metrics(0).count_metrics().data(0).bucket_info(0).
                    has_end_bucket_elapsed_nanos());
    EXPECT_EQ(1, report.metrics(0).count_metrics().data(0).bucket_info(0).count());
}

TEST(PartialBucketE2eTest, TestValueMetricWithoutMinPartialBucket) {
    StatsService service(nullptr);
    // Partial buckets don't occur when app is first installed.
    service.mUidMap->updateApp(1, String16(kApp1.c_str()), 1, 1);
    SendConfig(service, MakeValueMetricConfig(0));
    int64_t start = getElapsedRealtimeNs();  // This is the start-time the metrics producers are
                                             // initialized with.

    service.mProcessor->informPullAlarmFired(5 * 60 * NS_PER_SEC + start);
    service.mUidMap->updateApp(5 * 60 * NS_PER_SEC + start + 2, String16(kApp1.c_str()), 1, 2);

    ConfigMetricsReport report =
            GetReports(service.mProcessor, 5 * 60 * NS_PER_SEC + start + 100, true);
    EXPECT_EQ(1, report.metrics_size());
    EXPECT_EQ(0, report.metrics(0).value_metrics().skipped_size());
}

TEST(PartialBucketE2eTest, TestValueMetricWithMinPartialBucket) {
    StatsService service(nullptr);
    // Partial buckets don't occur when app is first installed.
    service.mUidMap->updateApp(1, String16(kApp1.c_str()), 1, 1);
    SendConfig(service, MakeValueMetricConfig(60 * NS_PER_SEC /* One minute */));
    int64_t start = getElapsedRealtimeNs();  // This is the start-time the metrics producers are
                                             // initialized with.

    const int64_t endSkipped = 5 * 60 * NS_PER_SEC + start + 2;
    service.mProcessor->informPullAlarmFired(5 * 60 * NS_PER_SEC + start);
    service.mUidMap->updateApp(endSkipped, String16(kApp1.c_str()), 1, 2);

    ConfigMetricsReport report =
            GetReports(service.mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC, true);
    backfillStartEndTimestamp(&report);
    EXPECT_EQ(1, report.metrics_size());
    EXPECT_EQ(1, report.metrics(0).value_metrics().skipped_size());
    EXPECT_TRUE(report.metrics(0).value_metrics().skipped(0).has_start_bucket_elapsed_nanos());
    // Can't test the start time since it will be based on the actual time when the pulling occurs.
    EXPECT_EQ(MillisToNano(NanoToMillis(endSkipped)),
              report.metrics(0).value_metrics().skipped(0).end_bucket_elapsed_nanos());
}

TEST(PartialBucketE2eTest, TestGaugeMetricWithoutMinPartialBucket) {
    StatsService service(nullptr);
    // Partial buckets don't occur when app is first installed.
    service.mUidMap->updateApp(1, String16(kApp1.c_str()), 1, 1);
    SendConfig(service, MakeGaugeMetricConfig(0));
    int64_t start = getElapsedRealtimeNs();  // This is the start-time the metrics producers are
                                             // initialized with.

    service.mProcessor->informPullAlarmFired(5 * 60 * NS_PER_SEC + start);
    service.mUidMap->updateApp(5 * 60 * NS_PER_SEC + start + 2, String16(kApp1.c_str()), 1, 2);

    ConfigMetricsReport report =
            GetReports(service.mProcessor, 5 * 60 * NS_PER_SEC + start + 100, true);
    EXPECT_EQ(1, report.metrics_size());
    EXPECT_EQ(0, report.metrics(0).gauge_metrics().skipped_size());
}

TEST(PartialBucketE2eTest, TestGaugeMetricWithMinPartialBucket) {
    StatsService service(nullptr);
    // Partial buckets don't occur when app is first installed.
    service.mUidMap->updateApp(1, String16(kApp1.c_str()), 1, 1);
    SendConfig(service, MakeGaugeMetricConfig(60 * NS_PER_SEC /* One minute */));
    int64_t start = getElapsedRealtimeNs();  // This is the start-time the metrics producers are
                                             // initialized with.

    const int64_t endSkipped = 5 * 60 * NS_PER_SEC + start + 2;
    service.mProcessor->informPullAlarmFired(5 * 60 * NS_PER_SEC + start);
    service.mUidMap->updateApp(endSkipped, String16(kApp1.c_str()), 1, 2);

    ConfigMetricsReport report =
            GetReports(service.mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC, true);
    backfillStartEndTimestamp(&report);
    EXPECT_EQ(1, report.metrics_size());
    EXPECT_EQ(1, report.metrics(0).gauge_metrics().skipped_size());
    // Can't test the start time since it will be based on the actual time when the pulling occurs.
    EXPECT_TRUE(report.metrics(0).gauge_metrics().skipped(0).has_start_bucket_elapsed_nanos());
    EXPECT_EQ(MillisToNano(NanoToMillis(endSkipped)),
              report.metrics(0).gauge_metrics().skipped(0).end_bucket_elapsed_nanos());
}

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

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