/*
 * 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.
 */

#pragma once

#include <chrono>
#include <cstdlib>
#include <ctime>
#include <random>
#include <string>
#include <unordered_map>
#include <vector>
#include <algorithm>
#include <memory>
#include <functional>
#include <numeric>

#include "gtest/gtest.h"
#include "opendal.hpp"

#ifdef OPENDAL_ENABLE_ASYNC
#include "opendal_async.hpp"
#include "cppcoro/sync_wait.hpp"
#include "cppcoro/task.hpp"
#endif

extern char** environ;

namespace opendal::test {

/**
 * @brief Test configuration and environment setup
 */
class TestConfig {
public:
    static TestConfig& instance() {
        static TestConfig instance;
        return instance;
    }
    
    const std::string& service_name() const { return service_name_; }
    const std::unordered_map<std::string, std::string>& config() const { return config_; }
    bool disable_random_root() const { return disable_random_root_; }
    
    void initialize() {
        // Read service name from environment
        const char* env_service = std::getenv("OPENDAL_TEST");
        if (env_service) {
            service_name_ = env_service;
        } else {
            service_name_ = "memory"; // Default for tests
        }
        
        // Read configuration from environment variables
        std::string prefix = "opendal_" + service_name_ + "_";
        std::transform(prefix.begin(), prefix.end(), prefix.begin(), ::tolower);
        
        for (char** env = environ; *env != nullptr; ++env) {
            std::string env_var(*env);
            auto eq_pos = env_var.find('=');
            if (eq_pos != std::string::npos) {
                std::string key = env_var.substr(0, eq_pos);
                std::string value = env_var.substr(eq_pos + 1);
                
                std::transform(key.begin(), key.end(), key.begin(), ::tolower);
                
                if (key.find(prefix) == 0) {
                    std::string config_key = key.substr(prefix.length());
                    config_[config_key] = value;
                }
            }
        }
        
        // Check if random root is disabled
        const char* disable_random = std::getenv("OPENDAL_DISABLE_RANDOM_ROOT");
        disable_random_root_ = (disable_random && std::string(disable_random) == "true");
        
        if (!disable_random_root_) {
            // Add random root path to avoid conflicts
            std::string root = config_.count("root") ? config_["root"] : "/";
            if (root.back() != '/') root += "/";
            root += generate_uuid() + "/";
            config_["root"] = root;
        }
    }
    
private:
    std::string service_name_;
    std::unordered_map<std::string, std::string> config_;
    bool disable_random_root_ = false;
    
    std::string generate_uuid() {
        std::random_device rd;
        std::mt19937 gen(rd());
        std::uniform_int_distribution<> dis(0, 15);
        
        const char* hex = "0123456789abcdef";
        std::string uuid(36, '-');
        
        for (int i = 0; i < 36; ++i) {
            if (i == 8 || i == 13 || i == 18 || i == 23) continue;
            uuid[i] = hex[dis(gen)];
        }
        
        return uuid;
    }
};

/**
 * @brief Test data generator utilities
 */
class TestData {
public:
    static std::string random_string(size_t length = 100) {
        static std::random_device rd;
        static std::mt19937 gen(rd());
        // Use only alphanumeric characters and safe symbols for maximum compatibility
        static std::vector<char> safe_chars;
        if (safe_chars.empty()) {
            // Include only alphanumeric characters and a few safe symbols
            for (char c = '0'; c <= '9'; ++c) safe_chars.push_back(c);
            for (char c = 'A'; c <= 'Z'; ++c) safe_chars.push_back(c); 
            for (char c = 'a'; c <= 'z'; ++c) safe_chars.push_back(c);
            // Add only very safe special characters
            safe_chars.push_back('_');
            safe_chars.push_back('-');
        }
        static std::uniform_int_distribution<> dis(0, safe_chars.size() - 1);
        
        std::string result;
        result.reserve(length);
        for (size_t i = 0; i < length; ++i) {
            result += safe_chars[dis(gen)];
        }
        return result;
    }
    
    static std::vector<uint8_t> random_bytes(size_t length = 100) {
        static std::random_device rd;
        static std::mt19937 gen(rd());
        static std::uniform_int_distribution<uint8_t> dis(0, 255);
        
        std::vector<uint8_t> result;
        result.reserve(length);
        for (size_t i = 0; i < length; ++i) {
            result.push_back(dis(gen));
        }
        return result;
    }
    
    static std::string random_path(const std::string& prefix = "test_") {
        return prefix + random_string(10);
    }
    
    static std::string random_dir_path(const std::string& prefix = "test_dir_") {
        auto path = prefix + random_string(10);
        if (path.back() != '/') path += "/";
        return path;
    }
};

/**
 * @brief Performance measurement utilities
 */
class PerformanceTimer {
public:
    void start() {
        start_time_ = std::chrono::high_resolution_clock::now();
    }
    
    void stop() {
        end_time_ = std::chrono::high_resolution_clock::now();
    }
    
    std::chrono::duration<double> elapsed() const {
        return end_time_ - start_time_;
    }
    
    double elapsed_ms() const {
        return std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(elapsed()).count();
    }
    
    double elapsed_us() const {
        return std::chrono::duration_cast<std::chrono::duration<double, std::micro>>(elapsed()).count();
    }
    
private:
    std::chrono::high_resolution_clock::time_point start_time_;
    std::chrono::high_resolution_clock::time_point end_time_;
};

/**
 * @brief Base test fixture for OpenDAL tests
 */
class OpenDALTest : public ::testing::Test {
protected:
    opendal::Operator op_;
    opendal::Capability capability_;
    std::mt19937 rng_;
    
    void SetUp() override {
        auto& config = TestConfig::instance();
        rng_.seed(std::time(nullptr));
        
        try {
            op_ = opendal::Operator(config.service_name(), config.config());
            ASSERT_TRUE(op_.Available()) << "Operator not available for service: " << config.service_name();
            capability_ = op_.Info();
        } catch (const std::exception& e) {
            GTEST_SKIP() << "Failed to create operator: " << e.what();
        }
    }
    
    void TearDown() override {
        // Cleanup if needed
    }
    
    // Helper methods for capability checking based on service name
    bool supports_write() const {
        return capability_.write;
    }
    
    bool supports_delete() const {
        return capability_.delete_feature;
    }
    
    bool supports_create_dir() const {
        return capability_.create_dir;
    }
    
    bool supports_list() const {
        return capability_.list;
    }
    
    bool supports_copy() const {
        return capability_.copy;
    }
    
    bool supports_rename() const {
        return capability_.rename;
    }
    
    // Helper methods
    std::string random_path() { return TestData::random_path(); }
    std::string random_dir_path() { return TestData::random_dir_path(); }
    std::string random_string(size_t len = 100) { return TestData::random_string(len); }
    std::vector<uint8_t> random_bytes(size_t len = 100) { return TestData::random_bytes(len); }
};

#ifdef OPENDAL_ENABLE_ASYNC
/**
 * @brief Base test fixture for async OpenDAL tests
 */
class AsyncOpenDALTest : public ::testing::Test {
protected:
    std::optional<opendal::async::Operator> op_;
    std::mt19937 rng_;
    
    void SetUp() override {
        auto& config = TestConfig::instance();
        rng_.seed(std::time(nullptr));
        
        try {
            op_ = opendal::async::Operator(config.service_name(), config.config());
        } catch (const std::exception& e) {
            GTEST_SKIP() << "Failed to create async operator: " << e.what();
        }
    }
    
    void TearDown() override {
        // Cleanup if needed
    }
    
    // Helper methods
    std::string random_path() { return TestData::random_path(); }
    std::string random_dir_path() { return TestData::random_dir_path(); }
    std::string random_string(size_t len = 100) { return TestData::random_string(len); }
    std::vector<uint8_t> random_bytes(size_t len = 100) { return TestData::random_bytes(len); }
};
#endif

/**
 * @brief Benchmark test fixture
 */
class BenchmarkTest : public OpenDALTest {
protected:
    PerformanceTimer timer_;
    
    void benchmark_operation(const std::string& operation_name, std::function<void()> operation, int iterations = 1000) {
        std::vector<double> times;
        times.reserve(iterations);
        
        for (int i = 0; i < iterations; ++i) {
            timer_.start();
            operation();
            timer_.stop();
            times.push_back(timer_.elapsed_ms());
        }
        
        // Calculate statistics
        std::sort(times.begin(), times.end());
        double min_time = times.front();
        double max_time = times.back();
        double median_time = times[times.size() / 2];
        double avg_time = std::accumulate(times.begin(), times.end(), 0.0) / times.size();
        
        std::cout << "Benchmark: " << operation_name << std::endl;
        std::cout << "  Iterations: " << iterations << std::endl;
        std::cout << "  Min: " << min_time << " ms" << std::endl;
        std::cout << "  Max: " << max_time << " ms" << std::endl;
        std::cout << "  Avg: " << avg_time << " ms" << std::endl;
        std::cout << "  Median: " << median_time << " ms" << std::endl;
    }
};

// Initialization function to be called in main()
inline void initialize_test_framework() {
    TestConfig::instance().initialize();
}

} // namespace opendal::test

// Convenience macros
#define OPENDAL_TEST_F(test_fixture, test_name) \
    TEST_F(test_fixture, test_name)

#define OPENDAL_SKIP_IF_NO_SERVICE() \
    do { \
        if (opendal::test::TestConfig::instance().service_name().empty()) { \
            GTEST_SKIP() << "No service configured for testing"; \
        } \
    } while(0) 

#define OPENDAL_SKIP_IF_UNSUPPORTED_WRITE() \
    do { \
        if (!this->supports_write()) { \
            GTEST_SKIP() << "Write operations not supported by service: " << opendal::test::TestConfig::instance().service_name(); \
        } \
    } while(0)

#define OPENDAL_SKIP_IF_UNSUPPORTED_DELETE() \
    do { \
        if (!this->supports_delete()) { \
            GTEST_SKIP() << "Delete operations not supported by service: " << opendal::test::TestConfig::instance().service_name(); \
        } \
    } while(0)

#define OPENDAL_SKIP_IF_UNSUPPORTED_CREATE_DIR() \
    do { \
        if (!this->supports_create_dir()) { \
            GTEST_SKIP() << "Create directory operations not supported by service: " << opendal::test::TestConfig::instance().service_name(); \
        } \
    } while(0)

#define OPENDAL_SKIP_IF_UNSUPPORTED_LIST() \
    do { \
        if (!this->supports_list()) { \
            GTEST_SKIP() << "List operations not supported by service: " << opendal::test::TestConfig::instance().service_name(); \
        } \
    } while(0)

#define OPENDAL_SKIP_IF_UNSUPPORTED_COPY() \
    do { \
        if (!this->supports_copy()) { \
            GTEST_SKIP() << "Copy operations not supported by service: " << opendal::test::TestConfig::instance().service_name(); \
        } \
    } while(0)

#define OPENDAL_SKIP_IF_UNSUPPORTED_RENAME() \
    do { \
        if (!this->supports_rename()) { \
            GTEST_SKIP() << "Rename operations not supported by service: " << opendal::test::TestConfig::instance().service_name(); \
        } \
    } while(0)
