/**
 * @file pgsql-options_startup_params-t.cpp
 * @brief Comprehensive test for startup parameter handling in options
 *
 * Tests:
 * - All ProxySQL tracked variables with spaces (success/failure)
 * - Untracked parameters
 * - Edge cases (escaped spaces, multiple spaces, trailing spaces)
 * - Strict verification of error handling
 */

#include <unistd.h>
#include <string>
#include <sstream>
#include <vector>
#include <utility>
#include "libpq-fe.h"
#include "command_line.h"
#include "tap.h"
#include "utils.h"

CommandLine cl;
using PGConnPtr = std::unique_ptr<PGconn, decltype(&PQfinish)>;

struct TestCase {
    std::string name;
    std::string options;
    bool should_succeed;        // true = expect connection success
    std::string expected_error; // substring expected in error message (if should_fail)
    std::string description;
};

// Helper to create connection
PGConnPtr createConnection(const std::string& options, std::string& error_msg) {
    std::stringstream ss;
    ss << "host=" << cl.pgsql_host << " port=" << cl.pgsql_port;
    ss << " user=" << cl.pgsql_username << " password=" << cl.pgsql_password;
    ss << " sslmode=disable";
    if (!options.empty()) {
        ss << " options='" << options << "'";
    }

    PGconn* conn = PQconnectdb(ss.str().c_str());
    if (PQstatus(conn) != CONNECTION_OK) {
        error_msg = PQerrorMessage(conn);
        PQfinish(conn);
        return PGConnPtr(nullptr, &PQfinish);
    }
    error_msg.clear();
    return PGConnPtr(conn, &PQfinish);
}

// Helper to get variable value
std::string getVar(PGconn* conn, const char* name) {
    PGresult* res = PQexec(conn, (std::string("SHOW ") + name).c_str());
    if (!res || PQresultStatus(res) != PGRES_TUPLES_OK) {
        PQclear(res);
        return "";
    }
    char* val = PQgetvalue(res, 0, 0);
    std::string result = val ? val : "";
    PQclear(res);
    return result;
}

int main(int argc, char** argv) {
    if (cl.getEnv()) return exit_status();

    // Build comprehensive test cases
    std::vector<TestCase> tests;

    // 0. client_encoding (has alias "names")
    tests.push_back({"client_encoding",
        "-c client_encoding=LATIN1",
        true, "", "client_encoding"});
    tests.push_back({"names_alias",
        "-c names=UTF8",
        true, "", "names alias for client_encoding"});

    // 1. DateStyle with escaped space
    // For escaped space: C++ "\\\\" becomes actual "\\" which libpq sees as "\ " (escaped space)
    tests.push_back({"datestyle_escaped",
        "-c DateStyle=SQL,\\\\ DMY",
        true, "", "DateStyle with escaped space"});
    tests.push_back({"datestyle_postgres",
        "-c DateStyle=Postgres,\\\\ DMY",
        true, "", "DateStyle Postgres format"});
    tests.push_back({"datestyle_iso_mdy",
        "-c DateStyle=ISO,\\\\ MDY",
        true, "", "DateStyle ISO MDY"});

    // 2. IntervalStyle
    tests.push_back({"intervalstyle_iso8601",
        "-c IntervalStyle=iso_8601",
        true, "", "IntervalStyle iso_8601"});
    tests.push_back({"intervalstyle_postgres",
        "-c IntervalStyle=postgres",
        true, "", "IntervalStyle postgres"});

    // 3. standard_conforming_strings
    tests.push_back({"standard_conforming_strings_off",
        "-c standard_conforming_strings=off",
        true, "", "standard_conforming_strings=off"});
    tests.push_back({"standard_conforming_strings_on",
        "-c standard_conforming_strings=on",
        true, "", "standard_conforming_strings=on"});

    // 4. TimeZone (has alias "TIME ZONE")
    tests.push_back({"timezone_pst",
        "-c TimeZone=PST8PDT",
        true, "", "TimeZone PST8PDT"});
    tests.push_back({"timezone_est",
        "-c TimeZone=EST5EDT",
        true, "", "TimeZone EST5EDT"});
    tests.push_back({"timezone_utc",
        "-c TimeZone=UTC",
        true, "", "TimeZone UTC"});
    tests.push_back({"time_zone_alias",
        "-c TimeZone=GMT",
        true, "", "TimeZone alias"});

    // Non-critical params (PGSQL_NAME_LAST_LOW_WM+1 to PGSQL_NAME_LAST_HIGH_WM-1)

    // 6. allow_in_place_tablespaces
    tests.push_back({"allow_in_place_tablespaces",
        "-c allow_in_place_tablespaces=on",
        true, "", "allow_in_place_tablespaces"});

    // 7. bytea_output
    tests.push_back({"bytea_output_escape",
        "-c bytea_output=escape",
        true, "", "bytea_output=escape"});
    tests.push_back({"bytea_output_hex",
        "-c bytea_output=hex",
        true, "", "bytea_output=hex"});

    // 8. client_min_messages
    tests.push_back({"client_min_messages",
        "-c client_min_messages=warning",
        true, "", "client_min_messages"});

    // 9-15. enable_* parameters
    tests.push_back({"enable_bitmapscan",
        "-c enable_bitmapscan=off",
        true, "", "enable_bitmapscan"});
    tests.push_back({"enable_hashjoin",
        "-c enable_hashjoin=off",
        true, "", "enable_hashjoin"});
    tests.push_back({"enable_indexscan",
        "-c enable_indexscan=off",
        true, "", "enable_indexscan"});
    tests.push_back({"enable_nestloop",
        "-c enable_nestloop=off",
        true, "", "enable_nestloop"});
    tests.push_back({"enable_seqscan",
        "-c enable_seqscan=off",
        true, "", "enable_seqscan"});
    tests.push_back({"enable_sort",
        "-c enable_sort=off",
        true, "", "enable_sort"});

    // 16. escape_string_warning
    tests.push_back({"escape_string_warning",
        "-c escape_string_warning=off",
        true, "", "escape_string_warning"});

    // 17. extra_float_digits
    tests.push_back({"extra_float_digits_2",
        "-c extra_float_digits=2",
        true, "", "extra_float_digits"});

    // 18. maintenance_work_mem (with space in value, must escape)
    tests.push_back({"maintenance_work_mem",
        "-c maintenance_work_mem=64MB",
        true, "", "maintenance_work_mem"});

    // 19. search_path (has NO_STRIP_VALUE flag, handles commas specially)
    tests.push_back({"searchpath_simple",
        "-c search_path=public",
        true, "", "search_path simple"});
    tests.push_back({"searchpath_escaped_space",
        "-c search_path=public,\\\\ private",
        true, "", "search_path with escaped space"});
    tests.push_back({"searchpath_quoted",
        "-c search_path=\"\\$user\",public",
        true, "", "search_path with quoted user"});

    // 20. synchronous_commit
    tests.push_back({"synchronous_commit",
        "-c synchronous_commit=off",
        true, "", "synchronous_commit"});

    // Multiple parameters with proper escaping
    tests.push_back({"multiple_critical",
        "-c DateStyle=SQL,\\\\ DMY -c TimeZone=PST8PDT -c client_encoding=UTF8",
        true, "", "Multiple critical params with escaped space"});
    tests.push_back({"multiple_all",
        "-c DateStyle=ISO,\\\\ MDY -c TimeZone=UTC -c client_encoding=UTF8 -c enable_seqscan=off -c bytea_output=escape",
        true, "", "Multiple params: critical + non-critical"});

    // ============================================================
    // FAILURE CASES: Unescaped spaces (should fail)
    // ============================================================

    // DateStyle with unescaped space (the main bug case)
    tests.push_back({"datestyle_unescaped",
        "-c DateStyle=SQL, DMY",
        false, "unescaped space", "DateStyle with unescaped space (should fail)"});

    // TimeZone with unescaped space
    tests.push_back({"timezone_unescaped",
        "-c TimeZone=New York",
        false, "unescaped space", "TimeZone with unescaped space"});

    // search_path with unescaped space
    tests.push_back({"searchpath_unescaped",
        "-c search_path=public, private",
        false, "unescaped space", "search_path with unescaped space"});

    // maintenance_work_mem with unescaped space
    tests.push_back({"maintenance_work_mem_unescaped",
        "-c maintenance_work_mem=128 MB",
        false, "unescaped space", "maintenance_work_mem with unescaped space"});

    // Trailing space after value
    tests.push_back({"trailing_space",
        "-c DateStyle=ISO, MDY ",
        false, "unescaped space", "Trailing space after value"});

    // Multiple spaces between options (one unescaped in value)
    tests.push_back({"multi_space_fail",
        "-c DateStyle=Postgres, DMY  -c TimeZone=PST",
        false, "unescaped space", "Multiple spaces with unescaped space in first value"});

    // bytea_output with unescaped space
    tests.push_back({"bytea_unescaped",
        "-c bytea_output=escape mode",
        false, "unescaped space", "bytea_output with unescaped space"});

    // client_min_messages with unescaped space
    tests.push_back({"client_min_messages_unescaped",
        "-c client_min_messages=log debug",
        false, "unescaped space", "client_min_messages with unescaped space"});

    // ============================================================
    // UNTRACKED PARAMETERS: Should work but lock hostgroup
    // ============================================================

    // Valid untracked parameter
    tests.push_back({"untracked_valid",
        "-c geqo=off",
        true, "", "Untracked parameter (geqo)"});
    tests.push_back({"untracked_join_collapse_limit",
        "-c join_collapse_limit=8",
        true, "", "Untracked parameter (join_collapse_limit)"});

    // ============================================================
    // EDGE CASES: Various escape scenarios
    // ============================================================

    // Multiple spaces needing escape
    tests.push_back({"multi_space_escape",
        "-c search_path=a,\\\\ b,\\\\ c",
        true, "", "Multiple escaped spaces"});

    // Space at start of value (escaped)
    tests.push_back({"space_at_start",
        "-c search_path=\\\\ public",
        true, "", "Escaped space at start of value"});

    // ============================================================
    // SPECIAL CASES: Empty and malformed
    // ============================================================

    // Empty options
    tests.push_back({"empty_options",
        "",
        true, "", "Empty options (no options)"});

    // Just spaces in options
    tests.push_back({"only_spaces",
        "   ",
        true, "", "Options with only spaces"});

    // No equals sign (malformed - should FAIL now)
    tests.push_back({"no_equals",
        "-c DateStyle",
        false, "missing '='", "Malformed option without ="});

    // Empty value after equals (valid - empty value)
    tests.push_back({"empty_value",
        "-c DateStyle=",
        true, "", "Empty value after equals"});

    // ============================================================
    // MALFORMED OPTIONS: Should be REJECTED (not skipped)
    // ============================================================

    // Token doesn't start with -c or --
    tests.push_back({"malformed_no_prefix",
        "foo=value",
        false, "token must start with", "Malformed: no -c or -- prefix"});

    // Space between key and =
    tests.push_back({"malformed_space_before_equals",
        "-c DateStyle =value",
        false, "missing '='", "Malformed: space before ="});

    // Empty key
    tests.push_back({"malformed_empty_key",
        "-c =value",
        false, "empty key", "Malformed: empty key before ="});

    // Multiple tokens without proper -c
    tests.push_back({"malformed_multiple_tokens",
        "-c foo -c bar=value",
        false, "missing '='", "Malformed: space in key"});

    // ============================================================
    // PROXY VALIDATION: Verify values are actually applied
    // ============================================================

    // Verify DateStyle is actually set
    // This will be verified separately after connection

    const int num_tests = tests.size() + 4; // +4 for verification tests
    plan(num_tests);

    int pass_count = 0;
    int fail_count = 0;

    // Run all test cases
    for (const auto& tc : tests) {
        diag("\n=== Test: %s ===", tc.name.c_str());
        diag("Options: '%s'", tc.options.c_str());
        diag("Description: %s", tc.description.c_str());

        std::string error_msg;
        PGConnPtr conn = createConnection(tc.options, error_msg);
        bool connected = (conn != nullptr);

        bool test_passed;
        if (tc.should_succeed) {
            test_passed = connected;
            if (!test_passed) {
                diag("FAILED: Expected success but got error: %s", error_msg.c_str());
            }
        } else {
            test_passed = !connected;
            if (!test_passed) {
                diag("FAILED: Expected failure but connection succeeded");
            } else if (!tc.expected_error.empty()) {
                // Verify error message contains expected text
                if (error_msg.find(tc.expected_error) == std::string::npos) {
                    diag("WARNING: Error message doesn't contain '%s'", tc.expected_error.c_str());
                    diag("Actual error: %s", error_msg.c_str());
                }
            }
        }

        ok(test_passed, "%s", tc.description.c_str());

        if (test_passed) pass_count++;
        else fail_count++;
    }

    // Additional verification: Check that values are actually applied
    diag("\n=== Verification: Check values are actually applied ===");

    // Test 1: Verify escaped DateStyle is applied correctly
    {
        diag("\n--- Verifying DateStyle with escaped space is correct ---");
        std::string error;
        PGConnPtr conn = createConnection("-c DateStyle=SQL,\\\\ DMY", error);
        if (conn) {
            std::string val = getVar(conn.get(), "DateStyle");
            bool correct = (val.find("SQL") != std::string::npos && val.find("DMY") != std::string::npos);
            diag("Expected: SQL, DMY");
            diag("Got: %s", val.c_str());
            ok(correct, "DateStyle escaped space: value correctly applied");
        } else {
            diag("Connection failed: %s", error.c_str());
            ok(false, "DateStyle escaped space: value correctly applied");
        }
    }

    // Test 2: Verify partial value auto-completion works
    {
        diag("\n--- Verifying partial DateStyle (no escape needed) ---");
        std::string error;
        PGConnPtr conn = createConnection("-c DateStyle=MDY", error);
        if (conn) {
            std::string val = getVar(conn.get(), "DateStyle");
            // Should be completed to "ISO, MDY" (format from default, order from param)
            bool has_order = (val.find("MDY") != std::string::npos);
            diag("Got: %s (should contain MDY)", val.c_str());
            ok(has_order, "DateStyle partial value auto-completed");
        } else {
            diag("Connection failed: %s", error.c_str());
            ok(false, "DateStyle partial value auto-completed");
        }
    }

    // Test 3: Verify RESET reverts to startup value
    {
        diag("\n--- Verifying RESET reverts to startup value ---");
        std::string error;
        PGConnPtr conn = createConnection("-c DateStyle=SQL,\\\\ DMY", error);
        if (conn) {
            std::string startup_val = getVar(conn.get(), "DateStyle");
            diag("Startup value: %s", startup_val.c_str());

            // SET to different value
            PGresult* set_res = PQexec(conn.get(), "SET DateStyle = 'Postgres, MDY'");
            const bool set_ok = set_res && PQresultStatus(set_res) == PGRES_COMMAND_OK;
            if (!set_ok) {
                diag("SET failed: %s", set_res ? PQresultErrorMessage(set_res) : "null result");
            }
            PQclear(set_res);
            std::string set_val = getVar(conn.get(), "DateStyle");
            diag("After SET: %s", set_val.c_str());

            // Verify SET actually changed the value
            const bool set_changed = (set_val != startup_val);
            if (!set_changed) {
                diag("SET did not change value: startup='%s', after SET='%s'", startup_val.c_str(), set_val.c_str());
            }

            // RESET
            PGresult* reset_res = PQexec(conn.get(), "RESET DateStyle");
            const bool reset_ok = reset_res && PQresultStatus(reset_res) == PGRES_COMMAND_OK;
            if (!reset_ok) {
                diag("RESET failed: %s", reset_res ? PQresultErrorMessage(reset_res) : "null result");
            }
            PQclear(reset_res);
            std::string reset_val = getVar(conn.get(), "DateStyle");
            diag("After RESET: %s", reset_val.c_str());

            // Verify all conditions: SET worked, RESET worked, and value reverted
            bool reverted = set_ok && set_changed && reset_ok && (reset_val == startup_val);
            ok(reverted, "RESET reverts to startup value");
        } else {
            diag("Connection failed: %s", error.c_str());
            ok(false, "RESET reverts to startup value");
        }
    }

    // Test 4: Verify search_path with escaped space
    {
        diag("\n--- Verifying search_path with escaped space ---");
        std::string error;
        PGConnPtr conn = createConnection("-c search_path=public,\\\\ private", error);
        if (conn) {
            std::string val = getVar(conn.get(), "search_path");
            // search_path should contain both schemas
            bool has_public = (val.find("public") != std::string::npos);
            bool has_private = (val.find("private") != std::string::npos);
            diag("Expected: public, private");
            diag("Got: %s", val.c_str());
            ok(has_public && has_private, "search_path with escaped space applied");
        } else {
            diag("Connection failed: %s", error.c_str());
            ok(false, "search_path with escaped space applied");
        }
    }

    // Summary
    diag("\n=== SUMMARY ===");
    diag("Passed: %d, Failed: %d", pass_count, fail_count);

    return exit_status();
}
