/*
 * Copyright 2023 Bloomberg LP
 *
 * 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 <buildboxcommon_streamingstandardoutputifstreamfilemonitor.h>
#include <gtest/gtest.h>

#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_temporaryfile.h>

#include <fstream>
#include <functional>
#include <iostream>
#include <memory>

using namespace buildboxcommon;
using namespace testing;

struct StreamingStandardOutputIfstreamFileMonitorTestFixture
    : public testing::Test {

    StreamingStandardOutputIfstreamFileMonitorTestFixture()
        : file_monitor(
              std::make_unique<StreamingStandardOutputIfstreamFileMonitor>(
                  monitored_file.strname(),
                  std::bind(
                      &StreamingStandardOutputIfstreamFileMonitorTestFixture::
                          dataReady,
                      this, std::placeholders::_1),
                  10L, 10L))
    {
    }

    bool dataReady(
        const StreamingStandardOutputIfstreamFileMonitor::FileChunk &chunk)
    {
        if (data_ready_callback) {
            return data_ready_callback(chunk);
        }
        return false;
    }

    buildboxcommon::TemporaryFile monitored_file;
    StreamingStandardOutputIfstreamFileMonitor::DataReadyCallback
        data_ready_callback;
    std::unique_ptr<StreamingStandardOutputIfstreamFileMonitor> file_monitor;
};

TEST_F(StreamingStandardOutputIfstreamFileMonitorTestFixture, TestStop)
{
    ASSERT_NO_THROW(file_monitor->stop());
}

TEST_F(StreamingStandardOutputIfstreamFileMonitorTestFixture, MonitorEmptyFile)
{
    bool callback_invoked = false;
    StreamingStandardOutputIfstreamFileMonitor::DataReadyCallback
        dummy_callback =
            [&callback_invoked](
                const StreamingStandardOutputIfstreamFileMonitor::FileChunk
                    &) {
                callback_invoked = true;
                return true;
            };
    data_ready_callback = dummy_callback;

    ASSERT_NO_THROW(file_monitor->stop());
    ASSERT_FALSE(callback_invoked);
}

TEST_F(StreamingStandardOutputIfstreamFileMonitorTestFixture, StopMoreThanOnce)
{
    ASSERT_NO_THROW(file_monitor->stop());
    ASSERT_NO_THROW(file_monitor->stop());
}

TEST_F(StreamingStandardOutputIfstreamFileMonitorTestFixture, ReadDataAndStop)
{
    std::string data_read;
    data_ready_callback =
        [&data_read](
            const StreamingStandardOutputIfstreamFileMonitor::FileChunk
                &chunk) {
            data_read.append(chunk.ptr(), chunk.size());
            return true;
        };

    // Writing data to a file in two chunks:
    std::ofstream ofs(monitored_file.name(), std::ofstream::out);
    ofs << "Hello, ";
    ofs << "world!\n";
    ofs.close();
    file_monitor.reset();

    ASSERT_EQ(data_read, "Hello, world!\n");
}

TEST_F(StreamingStandardOutputIfstreamFileMonitorTestFixture,
       ReadDataAndDestroy)
{
    buildboxcommon::TemporaryFile file;

    std::string data_read;
    {
        StreamingStandardOutputIfstreamFileMonitor monitor(
            file.strname(),
            [&data_read](
                const StreamingStandardOutputIfstreamFileMonitor::FileChunk
                    &chunk) {
                data_read.append(chunk.ptr(), chunk.size());
                return true;
            },
            10, 10);

        std::ofstream ofs(file.strname(), std::ofstream::out);
        ofs << "Hello!";
        ofs.close();
    }

    ASSERT_EQ(data_read, "Hello!");
}

TEST_F(StreamingStandardOutputIfstreamFileMonitorTestFixture,
       MonitorWithSmallBuffer)
{

    std::string data_read;
    file_monitor =
        std::make_unique<StreamingStandardOutputIfstreamFileMonitor>(
            monitored_file.strname(),
            [&data_read](
                const StreamingStandardOutputIfstreamFileMonitor::FileChunk
                    &chunk) {
                data_read.append(chunk.ptr(), chunk.size());
                return true;
            },
            10, 10, 5);

    std::ofstream ofs(monitored_file.strname(), std::ofstream::out);
    for (int i = 1; i <= 20; i++) {
        ofs << std::string(i, '.');
    }
    ofs.close();
    file_monitor.reset();

    ASSERT_EQ(data_read, std::string(210, '.'));
}

TEST_F(StreamingStandardOutputIfstreamFileMonitorTestFixture, MonitorThenError)
{

    std::string data_read;
    data_ready_callback =
        [&](const StreamingStandardOutputIfstreamFileMonitor::FileChunk
                &chunk) { return false; };

    // Writing data to a file in two chunks:
    std::ofstream ofs(monitored_file.name(), std::ofstream::out);
    ofs << "Hello, ";
    ofs << "world!\n";
    file_monitor.reset();
    ofs.close();

    ASSERT_EQ(data_read, "");
}

// This test case depends on time/sleep and can be flaky on slow machines
TEST_F(StreamingStandardOutputIfstreamFileMonitorTestFixture,
       TestTimeoutBehavior)
{
    std::string data_read;
    file_monitor =
        std::make_unique<StreamingStandardOutputIfstreamFileMonitor>(
            monitored_file.strname(),
            [&data_read](
                const StreamingStandardOutputIfstreamFileMonitor::FileChunk
                    &chunk) {
                data_read.append(chunk.ptr(), chunk.size());
                return true;
            },
            500, 500, 100);

    std::ofstream ofs(monitored_file.strname(), std::ofstream::out);
    ofs << "hello\n" << std::flush;
    ASSERT_EQ(data_read, "");
    std::this_thread::sleep_for(std::chrono::milliseconds(600));
    ASSERT_EQ(data_read, "hello\n");
    ofs.close();
}
