/*
 * gRPC smoke test for the hyperswitch-payments Rust gRPC SDK.
 *
 * For each supported flow (filtered by data/field_probe/{connector}.json),
 * calls the connector's build_*_request() builder to construct the proto
 * request, then dispatches it directly through the GrpcClient.
 *
 * No grpc_* wrapper functions are needed in the connector Rust file.
 *
 * Usage:
 *   cargo run --bin grpc-smoke-test -- --creds-file creds.json --connector stripe
 *   cargo run --bin grpc-smoke-test -- --endpoint http://localhost:8000 \
 *       --connector stripe --auth-type header-key --api-key sk_test_...
 */

// Suppress clippy warnings for generated example code
#![allow(clippy::needless_update)]

// Connector modules auto-generated by build.rs from examples/{connector}/*.rs.
mod connectors {
    #![allow(clippy::needless_update)]
    include!(concat!(env!("OUT_DIR"), "/connectors.rs"));
}

// Self-auth helper async fns (capture, void) — must be at module level.
include!(concat!(env!("OUT_DIR"), "/grpc_helpers.rs"));

use grpc_api_types::health_check::{health_client::HealthClient, HealthCheckRequest};
use hyperswitch_payments_client::{
    build_connector_config, ConnectorSpecificConfig, GrpcClient, GrpcConfig,
};
use std::error::Error;
use std::time::Duration;
use tokio::time::sleep;
use tonic::{transport::Channel, Code as TonicCode};

// ── ANSI helpers ──────────────────────────────────────────────────────────────
fn no_color() -> bool {
    std::env::var("NO_COLOR").is_ok()
        || (std::env::var("FORCE_COLOR").is_err()
            && std::env::var("TERM").map_or(true, |t| t.is_empty() || t == "dumb"))
}
fn c(code: &str, text: &str) -> String {
    if no_color() {
        text.to_string()
    } else {
        format!("\x1b[{code}m{text}\x1b[0m")
    }
}
fn green(t: &str) -> String {
    c("32", t)
}
fn yellow(t: &str) -> String {
    c("33", t)
}
fn red(t: &str) -> String {
    c("31", t)
}
fn grey(t: &str) -> String {
    c("90", t)
}
fn bold(t: &str) -> String {
    c("1", t)
}

// ── Scenario result tracking ─────────────────────────────────────────────────

#[derive(Debug)]
struct ScenarioResult {
    status: &'static str, // "passed" | "skipped" | "failed"
    #[allow(dead_code)]
    message: Option<String>, // stored but not currently displayed
    reason: Option<String>, // for skipped
    error: Option<String>,
}

#[derive(Debug)]
struct ConnectorResult {
    connector: String,
    status: &'static str, // "passed" | "failed" | "skipped"
    scenarios: Vec<(String, ScenarioResult)>,
    error: Option<String>,
}

// ── Credentials loading ───────────────────────────────────────────────────────

/// Extract a string value from a creds.json field, which may be either
/// a plain string or a `{"value": "..."}` wrapper object.
fn extract_creds_value(obj: &serde_json::Value, keys: &[&str]) -> Option<String> {
    for key in keys {
        match obj.get(*key) {
            Some(serde_json::Value::String(s)) if !s.is_empty() => return Some(s.clone()),
            Some(serde_json::Value::Object(m)) => {
                if let Some(serde_json::Value::String(inner)) = m.get("value") {
                    if !inner.is_empty() {
                        return Some(inner.clone());
                    }
                }
            }
            _ => {}
        }
    }
    None
}

// Kept for backward-compat: the old flat grpc-creds.json format where
// endpoint, connector, auth_type, api_key are top-level fields.
#[derive(serde::Deserialize)]
struct FlatCreds {
    endpoint: String,
    connector: String,
    /// Connector-specific configuration for x-connector-config header
    connector_config: serde_json::Value,
}

impl From<FlatCreds> for GrpcConfig {
    fn from(c: FlatCreds) -> Self {
        GrpcConfig {
            endpoint: c.endpoint,
            connector: c.connector,
            connector_config: c.connector_config,
        }
    }
}

// ── Health-check polling ──────────────────────────────────────────────────────

async fn wait_for_server(endpoint: &str, max_secs: u32) -> Result<(), String> {
    for attempt in 1..=max_secs {
        let channel = Channel::from_shared(endpoint.to_string())
            .map_err(|e| format!("Invalid endpoint URI: {e}"))?
            .connect_lazy();

        let mut health = HealthClient::new(channel);
        match health
            .check(HealthCheckRequest {
                service: String::new(),
            })
            .await
        {
            Ok(_) => return Ok(()),
            Err(s) if s.code() == TonicCode::Unavailable => {
                eprintln!(
                    "  {}",
                    grey(&format!(
                        "attempt {attempt}/{max_secs}: server not ready yet, retrying…"
                    ))
                );
                sleep(Duration::from_secs(1)).await;
            }
            Err(_) => return Ok(()), // any other status → server is up
        }
    }
    Err(format!(
        "server not ready after {max_secs}s — is grpc-server running at {endpoint}?"
    ))
}

// ── gRPC scenario dispatch ────────────────────────────────────────────────────

/// Calls all gRPC flows for `connector_name` discovered by build.rs.
/// build.rs generates grpc_scenarios.rs as a match expression that calls
/// connector builder functions directly — no grpc_* wrappers needed.
#[allow(unused_variables)]
async fn run_grpc_scenarios(
    connector_name: &str,
    client: &GrpcClient,
) -> Vec<(String, Result<String, Box<dyn Error>>)> {
    include!(concat!(env!("OUT_DIR"), "/grpc_scenarios.rs"))
}

fn is_transport_error(detail: &str) -> bool {
    detail.contains("Unavailable")
        || detail.contains("DeadlineExceeded")
        || detail.contains("connection refused")
        || detail.contains("transport error")
}

// ── Argument parsing ──────────────────────────────────────────────────────────

struct Args {
    creds_file: Option<String>,
    endpoint: Option<String>,
    connector: Option<String>,
}

fn parse_args() -> Args {
    let mut args = Args {
        creds_file: None,
        endpoint: None,
        connector: None,
    };

    let raw: Vec<String> = std::env::args().skip(1).collect();
    let mut i = 0;
    while i < raw.len() {
        match raw[i].as_str() {
            "--creds-file" if i + 1 < raw.len() => {
                args.creds_file = Some(raw[i + 1].clone());
                i += 1;
            }
            "--endpoint" if i + 1 < raw.len() => {
                args.endpoint = Some(raw[i + 1].clone());
                i += 1;
            }
            "--connector" if i + 1 < raw.len() => {
                args.connector = Some(raw[i + 1].clone());
                i += 1;
            }
            "--help" | "-h" => {
                println!("Usage: grpc-smoke-test [options]");
                println!("  --creds-file <path>   JSON credentials file");
                println!(
                    "  --endpoint <url>      gRPC server endpoint (default: http://localhost:8000)"
                );
                println!("  --connector <name>    Connector name (default: stripe)");
                std::process::exit(0);
            }
            _ => {}
        }
        i += 1;
    }
    args
}

fn build_config(args: Args) -> Result<GrpcConfig, String> {
    let creds_file = args.creds_file.as_deref().unwrap_or("creds.json");
    let mut config = if std::path::Path::new(creds_file).exists() {
        let text = std::fs::read_to_string(creds_file)
            .map_err(|e| format!("Failed to read {creds_file}: {e}"))?;
        let raw: serde_json::Value = serde_json::from_str(&text)
            .map_err(|e| format!("Invalid JSON in {creds_file}: {e}"))?;

        // Flat format: top-level "endpoint" and "connector" keys with "connector_config".
        if raw.get("endpoint").and_then(|v| v.as_str()).is_some() {
            let creds: FlatCreds = serde_json::from_value(raw)
                .map_err(|e| format!("Invalid format in {creds_file}: {e}"))?;
            creds.into()
        } else {
            // creds.json format: { "<connector>": { "api_key": {"value": "..."}, ... } }
            let connector = args.connector.as_deref().unwrap_or("stripe");
            let entry = raw
                .get(connector)
                .ok_or_else(|| format!("Connector '{connector}' not found in {creds_file}"))?;

            // Build connector-specific config from legacy format
            let api_key = extract_creds_value(entry, &["api_key"]).unwrap_or_default();
            let api_secret = extract_creds_value(entry, &["api_secret"]);
            let key1 = extract_creds_value(entry, &["key1"]);
            let merchant_id = extract_creds_value(entry, &["merchant_account", "merchant_id"]);
            let tenant_id = extract_creds_value(entry, &["tenant_id"]);

            // Capitalize connector name for config key
            let connector_variant = connector
                .chars()
                .enumerate()
                .map(|(i, c)| if i == 0 { c.to_ascii_uppercase() } else { c })
                .collect::<String>();

            GrpcConfig {
                endpoint: args
                    .endpoint
                    .clone()
                    .unwrap_or_else(|| "http://localhost:8000".to_string()),
                connector: connector.to_string(),
                connector_config: build_connector_config(
                    connector_variant,
                    ConnectorSpecificConfig {
                        api_key: Some(api_key),
                        api_secret,
                        key1,
                        merchant_id,
                        tenant_id,
                    },
                ),
            }
        }
    } else {
        Err(format!(
            "Credentials file '{creds_file}' not found. Provide it with --creds-file"
        ))?
    };
    if let Some(v) = args.endpoint {
        config.endpoint = v;
    }
    if let Some(v) = args.connector {
        config.connector = v;
    }
    Ok(config)
}

fn print_result(result: &ConnectorResult) {
    match result.status {
        "passed" => {
            let passed_count = result
                .scenarios
                .iter()
                .filter(|(_, s)| s.status == "passed")
                .count();
            let skipped_count = result
                .scenarios
                .iter()
                .filter(|(_, s)| s.status == "skipped")
                .count();
            println!(
                "{} ({} passed, {} skipped)",
                green("  PASSED"),
                passed_count,
                skipped_count
            );
            for (key, detail) in &result.scenarios {
                match detail.status {
                    "passed" => println!("{}    {}: ✓", green(""), key),
                    "skipped" => println!(
                        "{}    {}: ~ skipped ({})",
                        yellow(""),
                        key,
                        detail.reason.as_deref().unwrap_or("unknown")
                    ),
                    _ => {}
                }
            }
        }
        "skipped" => {
            let reason = result.error.as_deref().unwrap_or("unknown");
            println!("{}", grey(&format!("  SKIPPED ({reason})")));
        }
        _ => {
            println!("{}", red("  FAILED"));
            for (key, detail) in &result.scenarios {
                if detail.status == "failed" {
                    println!(
                        "{} — {}",
                        red(&format!("    {}: ✗ FAILED", key)),
                        detail.error.as_deref().unwrap_or("unknown error")
                    );
                }
            }
            if let Some(e) = &result.error {
                println!("{}", red(&format!("  Error: {e}")));
            }
        }
    }
}

fn print_summary(results: &[ConnectorResult]) -> i32 {
    println!("\n{}", "=".repeat(60));
    println!("{}", bold("TEST SUMMARY"));
    println!("{}\n", "=".repeat(60));

    let passed = results.iter().filter(|r| r.status == "passed").count();
    let skipped = results.iter().filter(|r| r.status == "skipped").count();
    let failed = results.iter().filter(|r| r.status == "failed").count();

    // Count per-scenario statuses
    let mut total_flows_passed = 0;
    let mut total_flows_skipped = 0;
    let mut total_flows_failed = 0;
    for r in results {
        for (_, scenario) in &r.scenarios {
            match scenario.status {
                "passed" => total_flows_passed += 1,
                "skipped" => total_flows_skipped += 1,
                "failed" => total_flows_failed += 1,
                _ => {}
            }
        }
    }

    println!("Total connectors:   {}", results.len());
    println!("{}", green(&format!("Passed:  {passed}")));
    println!(
        "{}",
        grey(&format!(
            "Skipped: {skipped} (no examples or placeholder credentials)"
        ))
    );
    let failed_str = format!("Failed:  {failed}");
    println!(
        "{}",
        if failed > 0 {
            red(&failed_str)
        } else {
            green(&failed_str)
        }
    );
    println!();
    println!("Flow results:");
    println!(
        "{}",
        green(&format!("  {} flows PASSED", total_flows_passed))
    );
    if total_flows_skipped > 0 {
        println!(
            "{}",
            yellow(&format!(
                "  {} flows SKIPPED (connector errors)",
                total_flows_skipped
            ))
        );
    }
    if total_flows_failed > 0 {
        println!("{}", red(&format!("  {} flows FAILED", total_flows_failed)));
    }
    println!();

    if failed > 0 {
        println!("{}", red("Failed connectors:"));
        for r in results {
            if r.status == "failed" {
                let detail = r.error.as_deref().unwrap_or("see scenarios above");
                println!("{}: {detail}", red(&format!("  - {}", r.connector)));
            }
        }
        println!();
        return 1;
    }

    if passed == 0 && skipped > 0 {
        println!("{}", yellow("All tests skipped (no valid flows found)"));
        return 1;
    }

    println!("{}", green("All tests completed successfully!"));
    0
}

// ── Main ──────────────────────────────────────────────────────────────────────

#[tokio::main]
async fn main() {
    let args = parse_args();
    let config = build_config(args).unwrap_or_else(|e| {
        eprintln!("{}", red(&format!("Configuration error: {e}")));
        std::process::exit(1);
    });

    // Print header
    println!("\n{}", "=".repeat(60));
    println!("{}", bold("Rust gRPC Smoke Test"));
    println!("{}\n", "=".repeat(60));

    // ── Wait for server ───────────────────────────────────────────────────────
    println!("Waiting for gRPC server (up to 30s)…");
    if let Err(e) = wait_for_server(&config.endpoint, 30).await {
        eprintln!("{}", red(&format!("Server not available: {e}")));
        std::process::exit(1);
    }
    println!("{}\n", green("  Server ready."));

    // ── Connect ───────────────────────────────────────────────────────────────
    let connector_name = config.connector.clone();
    let endpoint = config.endpoint.clone();
    let client = GrpcClient::new(config).await.unwrap_or_else(|e| {
        eprintln!("{}", red(&format!("Failed to connect to {endpoint}: {e}")));
        std::process::exit(1);
    });

    // ── Scenarios ─────────────────────────────────────────────────────────────
    println!("Running gRPC scenarios for {}…", bold(&connector_name));
    let scenario_results = run_grpc_scenarios(&connector_name, &client).await;

    let mut result = ConnectorResult {
        connector: connector_name.clone(),
        status: "passed",
        scenarios: vec![],
        error: None,
    };

    if scenario_results.is_empty() {
        result.status = "skipped";
        result.error = Some(format!(
            "no supported gRPC flows found for {connector_name}. Add `pub fn build_*_request(...)` builder functions in examples/{connector_name}/{connector_name}.rs and ensure flows are marked as supported in data/field_probe/{connector_name}.json."
        ));
        println!(
            "{}",
            yellow(&format!(
                "  SKIPPED — no supported gRPC flows found for {connector_name}"
            ))
        );
    } else {
        for (key, scenario_result) in scenario_results {
            print!("  [{key}] running … ");
            use std::io::Write;
            std::io::stdout().flush().ok();

            match scenario_result {
                Ok(msg) => {
                    println!("{} {}", green("PASSED"), grey(&format!("— {msg}")));
                    result.scenarios.push((
                        key,
                        ScenarioResult {
                            status: "passed",
                            message: Some(msg),
                            reason: None,
                            error: None,
                        },
                    ));
                }
                Err(e) => {
                    let detail = e.to_string();
                    if is_transport_error(&detail) {
                        println!("{} {}", red("FAILED"), grey(&format!("— {detail}")));
                        result.scenarios.push((
                            key,
                            ScenarioResult {
                                status: "failed",
                                message: None,
                                reason: None,
                                error: Some(detail),
                            },
                        ));
                        result.status = "failed";
                    } else {
                        println!(
                            "{} {}",
                            yellow("SKIPPED (connector error)"),
                            grey(&format!("— {detail}"))
                        );
                        result.scenarios.push((
                            key,
                            ScenarioResult {
                                status: "skipped",
                                message: Some(detail),
                                reason: Some("connector_error".to_string()),
                                error: None,
                            },
                        ));
                    }
                }
            }
        }

        // Update connector status based on scenarios
        if result.status != "failed" {
            let all_passed_or_skipped = result
                .scenarios
                .iter()
                .all(|(_, s)| s.status == "passed" || s.status == "skipped");
            result.status = if all_passed_or_skipped {
                "passed"
            } else {
                "failed"
            };
        }
    }

    // ── Result ────────────────────────────────────────────────────────────────
    println!();
    print_result(&result);
    println!();

    let exit_code = print_summary(&[result]);
    std::process::exit(exit_code);
}
