/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 "paimon/common/metrics/histogram.h"

#include <algorithm>
#include <cmath>
#include <limits>
#include <random>
#include <vector>

#include "gtest/gtest.h"
#include "paimon/common/metrics/metrics_impl.h"
#include "paimon/testing/utils/testharness.h"
#include "rapidjson/document.h"

namespace paimon::test {

TEST(HistogramImplTest, TestBasicStats) {
    HistogramImpl h;
    h.Add(1);
    h.Add(2);
    h.Add(3);

    HistogramStats s = h.GetStats();
    EXPECT_EQ(s.count, 3);
    EXPECT_DOUBLE_EQ(s.min, 1);
    EXPECT_DOUBLE_EQ(s.max, 3);
    EXPECT_NEAR(s.average, 2.0, 1e-12);
    EXPECT_NEAR(s.stddev, std::sqrt(2.0 / 3.0), 1e-12);
    EXPECT_DOUBLE_EQ(s.p50, 2);
    EXPECT_DOUBLE_EQ(s.p90, 3);
    EXPECT_DOUBLE_EQ(s.p95, 3);
    EXPECT_DOUBLE_EQ(s.p99, 3);
    EXPECT_DOUBLE_EQ(s.p999, 3);
}

TEST(HistogramImplTest, TestLargeDatasetExactMoments) {
    HistogramImpl h;

    std::mt19937 rng(12345);
    std::uniform_int_distribution<int> dist(-100, 5000);

    const int32_t n = 20000;
    double sum = 0;
    double sum_squares = 0;
    double minv = std::numeric_limits<double>::infinity();
    double maxv = -std::numeric_limits<double>::infinity();

    for (int32_t i = 0; i < n; ++i) {
        const auto v = static_cast<double>(dist(rng));
        h.Add(v);
        sum += v;
        sum_squares += v * v;
        minv = std::min(minv, v);
        maxv = std::max(maxv, v);
    }

    HistogramStats s = h.GetStats();
    EXPECT_EQ(s.count, static_cast<uint64_t>(n));
    EXPECT_DOUBLE_EQ(s.sum, sum);
    EXPECT_DOUBLE_EQ(s.min, minv);
    EXPECT_DOUBLE_EQ(s.max, maxv);

    const double mean = sum / static_cast<double>(n);
    const double ex2 = sum_squares / static_cast<double>(n);
    const double var = std::max(0.0, ex2 - mean * mean);
    const double stddev = std::sqrt(var);
    EXPECT_DOUBLE_EQ(s.average, mean);
    EXPECT_DOUBLE_EQ(s.stddev, stddev);

    // Percentiles should be inside [min, max] and monotonically increasing.
    EXPECT_GE(s.p50, s.min);
    EXPECT_LE(s.p50, s.max);
    EXPECT_GE(s.p90, s.p50);
    EXPECT_GE(s.p95, s.p90);
    EXPECT_GE(s.p99, s.p95);
    EXPECT_GE(s.p999, s.p99);
    EXPECT_LE(s.p999, s.max);
}

TEST(HistogramImplTest, TestMergeMatchesSingleHistogram) {
    std::mt19937 rng(7);
    std::uniform_int_distribution<int> dist(0, 100000);
    const int32_t n = 50000;

    std::vector<double> values;
    values.reserve(n);
    for (int32_t i = 0; i < n; ++i) {
        values.push_back(static_cast<double>(dist(rng)));
    }

    HistogramImpl left;
    HistogramImpl right;
    HistogramImpl all;

    for (int32_t i = 0; i < n; ++i) {
        all.Add(values[i]);
        if (i % 2 == 0) {
            left.Add(values[i]);
        } else {
            right.Add(values[i]);
        }
    }
    left.Merge(right);

    HistogramStats merged = left.GetStats();
    HistogramStats single = all.GetStats();

    EXPECT_EQ(merged.count, single.count);
    EXPECT_DOUBLE_EQ(merged.sum, single.sum);
    EXPECT_DOUBLE_EQ(merged.min, single.min);
    EXPECT_DOUBLE_EQ(merged.max, single.max);
    EXPECT_DOUBLE_EQ(merged.average, single.average);
    EXPECT_DOUBLE_EQ(merged.stddev, single.stddev);
    EXPECT_DOUBLE_EQ(merged.p50, single.p50);
    EXPECT_DOUBLE_EQ(merged.p90, single.p90);
    EXPECT_DOUBLE_EQ(merged.p95, single.p95);
    EXPECT_DOUBLE_EQ(merged.p99, single.p99);
    EXPECT_DOUBLE_EQ(merged.p999, single.p999);
}

TEST(HistogramImplTest, TestCloneConsistencyAndIndependence) {
    HistogramImpl h;
    // Include a mix of values across buckets; NaN should be ignored.
    const std::vector<double> values = {1,   2,   3,  10,
                                        100, 0.5, -5, std::numeric_limits<double>::quiet_NaN()};
    for (double v : values) {
        h.Add(v);
    }

    auto cloned_base = h.Clone();
    auto cloned = std::dynamic_pointer_cast<HistogramImpl>(cloned_base);
    ASSERT_TRUE(cloned != nullptr);

    // Capture original state after Clone() for easier post-mortem inspection.
    // Clone() should not mutate the source.
    const auto after_snapshot = h.GetSnapshot();
    const auto after_stats = h.GetStats();

    const auto cloned_snapshot = cloned->GetSnapshot();
    EXPECT_EQ(cloned_snapshot.count, after_snapshot.count);
    EXPECT_DOUBLE_EQ(cloned_snapshot.sum, after_snapshot.sum);
    EXPECT_DOUBLE_EQ(cloned_snapshot.sum_squares, after_snapshot.sum_squares);
    EXPECT_DOUBLE_EQ(cloned_snapshot.min, after_snapshot.min);
    EXPECT_DOUBLE_EQ(cloned_snapshot.max, after_snapshot.max);
    EXPECT_EQ(cloned_snapshot.bucket_counts, after_snapshot.bucket_counts);

    const auto cloned_stats = cloned->GetStats();
    EXPECT_EQ(cloned_stats.count, after_stats.count);
    EXPECT_DOUBLE_EQ(cloned_stats.sum, after_stats.sum);
    EXPECT_DOUBLE_EQ(cloned_stats.min, after_stats.min);
    EXPECT_DOUBLE_EQ(cloned_stats.max, after_stats.max);
    EXPECT_DOUBLE_EQ(cloned_stats.average, after_stats.average);
    EXPECT_DOUBLE_EQ(cloned_stats.stddev, after_stats.stddev);
    EXPECT_DOUBLE_EQ(cloned_stats.p50, after_stats.p50);
    EXPECT_DOUBLE_EQ(cloned_stats.p90, after_stats.p90);
    EXPECT_DOUBLE_EQ(cloned_stats.p95, after_stats.p95);
    EXPECT_DOUBLE_EQ(cloned_stats.p99, after_stats.p99);
    EXPECT_DOUBLE_EQ(cloned_stats.p999, after_stats.p999);

    // Mutating original should not affect cloned.
    h.Add(42);
    EXPECT_EQ(cloned->GetSnapshot().count, after_snapshot.count);
    EXPECT_EQ(h.GetSnapshot().count, after_snapshot.count + 1);

    // Mutating cloned should not affect original.
    cloned->Add(84);
    EXPECT_EQ(cloned->GetSnapshot().count, after_snapshot.count + 1);
    EXPECT_EQ(h.GetSnapshot().count, after_snapshot.count + 1);
}

TEST(MetricsImplHistogramTest, TestMergeAndOverwrite) {
    auto metrics1 = std::make_shared<MetricsImpl>();
    metrics1->ObserveHistogram("h", 1);
    metrics1->ObserveHistogram("h", 2);

    auto metrics2 = std::make_shared<MetricsImpl>();
    metrics2->ObserveHistogram("h", 3);
    metrics2->ObserveHistogram("h", 4);

    metrics1->Merge(metrics2);

    ASSERT_OK_AND_ASSIGN(HistogramStats s, metrics1->GetHistogramStats("h"));
    EXPECT_EQ(s.count, 4);
    EXPECT_DOUBLE_EQ(s.min, 1);
    EXPECT_DOUBLE_EQ(s.max, 4);
    EXPECT_NEAR(s.average, 2.5, 1e-12);
    EXPECT_DOUBLE_EQ(s.p50, 2);
    EXPECT_DOUBLE_EQ(s.p99, 4);

    auto metrics3 = std::make_shared<MetricsImpl>();
    metrics3->ObserveHistogram("h2", 100);
    metrics1->Overwrite(metrics3);
    ASSERT_NOK_WITH_MSG(metrics1->GetHistogramStats("h"), "Key error: histogram 'h' not found");
    ASSERT_OK_AND_ASSIGN(HistogramStats s2, metrics1->GetHistogramStats("h2"));
    EXPECT_EQ(s2.count, 1);
    EXPECT_DOUBLE_EQ(s2.min, 100);
    EXPECT_DOUBLE_EQ(s2.max, 100);
    EXPECT_DOUBLE_EQ(s2.stddev, 0);
}

TEST(MetricsImplHistogramTest, TestToStringWithHistogram) {
    auto metrics = std::make_shared<MetricsImpl>();
    metrics->SetCounter("k1", 1);
    metrics->ObserveHistogram("h", 1);
    metrics->ObserveHistogram("h", 2);

    rapidjson::Document doc;
    doc.Parse(metrics->ToString().c_str());
    ASSERT_TRUE(doc.IsObject());
    ASSERT_TRUE(doc.HasMember("k1"));
    EXPECT_EQ(doc["k1"].GetUint64(), 1);

    ASSERT_TRUE(doc.HasMember("h.count"));
    EXPECT_EQ(doc["h.count"].GetUint64(), 2);
    ASSERT_TRUE(doc.HasMember("h.min"));
    EXPECT_TRUE(doc["h.min"].IsNumber());
    ASSERT_TRUE(doc.HasMember("h.p99"));
    EXPECT_TRUE(doc["h.p99"].IsNumber());

    ASSERT_TRUE(doc.HasMember("h.p99.9"));
    EXPECT_TRUE(doc["h.p99.9"].IsNumber());
    ASSERT_TRUE(doc.HasMember("h.stddev"));
    EXPECT_TRUE(doc["h.stddev"].IsNumber());
}

}  // namespace paimon::test
