// Copyright 2019 Bloomberg Finance L.P
//
// 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 <buildboxcommonmetrics_durationmetrictimer.h>
#include <buildboxcommonmetrics_metriccollectorfactoryutil.h>
#include <buildboxcommonmetrics_statsdpublisher.h>
#include <buildboxcommonmetrics_statsdpublisheroptions.h>

#include <gtest/gtest.h>
#include <sstream>
#include <stdexcept>
#include <string>
#include <sys/stat.h>
#include <unistd.h>

using namespace buildboxcommon::buildboxcommonmetrics;

class MockValueType {
  private:
  public:
    static const bool isAggregatable = false;
    MockValueType() {}
    int value() { return -1; }
    const std::string toStatsD(const std::string &name) const { return name; }
};

class TempFile {
  private:
    std::string d_name;
    int d_fd;

  public:
    TempFile()
    {
        std::string tmpName = "/tmp/buildbox-statsd-test-XXXXXX";
        d_fd = mkstemp(&tmpName[0]);
        if (d_fd == -1) {
            throw std::runtime_error("Failed to create temporary file");
        }
        if (fchmod(d_fd, S_IRUSR | S_IWUSR) == -1) {
            close(d_fd);
            unlink(tmpName.c_str());
            throw std::runtime_error("Failed to set file permissions");
        }
        d_name = std::move(tmpName);
    }
    ~TempFile()
    {
        if (d_fd != -1) {
            close(this->d_fd);
        }
        unlink(d_name.c_str());
    }

    // Disable copy and move.
    TempFile(const TempFile &) = delete;
    TempFile &operator=(const TempFile &) = delete;
    TempFile(TempFile &&) = delete;
    TempFile &operator=(TempFile &&) = delete;

    std::string name() { return d_name; }

    std::string read()
    {
        if (lseek(d_fd, 0, SEEK_SET) == -1) {
            throw std::runtime_error(
                "Failed to seek to the beginning of the file");
        }
        struct stat statResult{};
        if (fstat(d_fd, &statResult) == -1) {
            throw std::runtime_error("Failed to get file status");
        }

        std::string buffer(statResult.st_size, '\0');
        off_t pos = 0;
        while (pos < statResult.st_size) {
            ssize_t bytesRead =
                pread(d_fd, &buffer[pos], statResult.st_size - pos, pos);
            if (bytesRead == -1) {
                throw std::runtime_error("Failed to read from file");
            }
            pos += bytesRead;
        }
        return buffer;
    }
};

class AnotherMockValueType1 : public MockValueType {};

class AnotherMockValueType2 : public MockValueType {};

TEST(MetricsTest, StatsDPublisherTestWriteToFile)
{
    TempFile outputFile;
    const auto metricsFile = outputFile.name();

    // Initialize a StatsDPublisher for MockValueType
    StatsDPublisher<MockValueType> myPublisher(
        StatsDPublisherOptions::PublishMethod::File, metricsFile);

    // Publish
    myPublisher.publish();
    // Expect the file to be empty since there were no actual metrics
    EXPECT_EQ("", outputFile.read());

    // Store "my-metric"
    MetricCollectorFactoryUtil::store("my-metric", MockValueType());
    // Publish
    myPublisher.publish();
    // Expect to have `my-metric` published
    EXPECT_EQ("my-metric\n", outputFile.read());

    // Store "another-metric"
    MetricCollectorFactoryUtil::store("another-metric", MockValueType());
    // Publish
    myPublisher.publish();
    // Expect to have those two metrics
    const auto fileContents = outputFile.read();
    EXPECT_NE(std::string::npos, fileContents.find("my-metric"));
    EXPECT_NE(std::string::npos, fileContents.find("another-metric"));
}

TEST(MetricsTest, StatsDPublisherTestWriteToFile2ValueTypes)
{
    TempFile metricsFile;
    const auto metricsFilename = metricsFile.name();

    // Initialize a StatsDPublisher for MockValueType
    StatsDPublisher<AnotherMockValueType1, AnotherMockValueType2> myPublisher(
        StatsDPublisherOptions::PublishMethod::File, metricsFilename);

    // Publish
    myPublisher.publish();
    // Expect the file to be empty since there were no actual metrics
    {
        const auto fileContents = metricsFile.read();
        EXPECT_EQ("", fileContents);
    }

    // Store "my-metric"
    MetricCollectorFactoryUtil::store("my-metric", AnotherMockValueType1());
    // Publish
    myPublisher.publish();
    // Expect to have `my-metric` published
    {
        const auto fileContents = metricsFile.read();
        EXPECT_EQ("my-metric\n", fileContents);
    }

    // Store "additional-metric"
    MetricCollectorFactoryUtil::store("additional-metric",
                                      AnotherMockValueType2());
    // Publish
    myPublisher.publish();
    {
        const auto fileContents = metricsFile.read();
        // Expect to have those two metrics
        EXPECT_NE(std::string::npos, fileContents.find("my-metric"));
        EXPECT_NE(std::string::npos, fileContents.find("additional-metric"));
    }

    // Store "another-metric"
    MetricCollectorFactoryUtil::store("another-metric",
                                      AnotherMockValueType1());
    // Publish
    myPublisher.publish();
    {
        const auto fileContents = metricsFile.read();
        // Expect to have those two metrics
        EXPECT_NE(std::string::npos, fileContents.find("my-metric"));
        EXPECT_NE(std::string::npos, fileContents.find("another-metric"));
        EXPECT_NE(std::string::npos, fileContents.find("additional-metric"));
    }
}

TEST(MetricsTest, StatsDPublisherTestInfluxGraphiteMetricTagging)
{
    TempFile metricsFile;
    const auto metricsFilename = metricsFile.name();

    // Initialize a StatsDPublisher for MockValueType
    StatsDPublisher<AnotherMockValueType1, AnotherMockValueType2> myPublisher(
        StatsDPublisherOptions::PublishMethod::File, metricsFilename, 0,
        ",foo=bar");

    // Publish
    myPublisher.publish();
    // Expect the file to be empty since there were no actual metrics
    {
        const auto fileContents = metricsFile.read();
        EXPECT_EQ("", fileContents);
    }

    // Store "my-metric:1|c"
    MetricCollectorFactoryUtil::store("my-metric:1|c",
                                      AnotherMockValueType1());
    // Publish
    myPublisher.publish();
    // Expect to have `my-metric,foo=bar:1|c` published
    {
        const auto fileContents = metricsFile.read();
        EXPECT_EQ("my-metric,foo=bar:1|c\n", fileContents);
    }
}

TEST(MetricsTest, StatsDPublisherTestDogMetricTagging)
{
    TempFile metricsFile;
    const auto metricsFilename = metricsFile.name();

    // Initialize a StatsDPublisher for MockValueType
    StatsDPublisher<AnotherMockValueType1, AnotherMockValueType2> myPublisher(
        StatsDPublisherOptions::PublishMethod::File, metricsFilename, 0,
        "|#foo=bar");

    // Publish
    myPublisher.publish();
    // Expect the file to be empty since there were no actual metrics
    {
        const auto fileContents = metricsFile.read();
        EXPECT_EQ("", fileContents);
    }

    // Store "my-metric:1|c"
    MetricCollectorFactoryUtil::store("my-metric:1|c",
                                      AnotherMockValueType1());
    // Publish
    myPublisher.publish();
    // Expect to have `my-metric:1|c|#foo=bar` published
    {
        const auto fileContents = metricsFile.read();
        EXPECT_EQ("my-metric:1|c|#foo=bar\n", fileContents);
    }
}
