//! Centralized helper functions for WireMock-based wire tests.
//!
//! This module provides utilities for managing WireMock interactions during integration tests.
//!
//! # Running Wire Tests
//!
//! **Recommended**: Use the generated script for automatic lifecycle management:
//! ```bash
//! chmod +x run_wire_tests.sh       # First time only
//! ./run_wire_tests.sh              # Run all wire tests
//! ./run_wire_tests.sh --test foo   # Run specific test
//! ```
//!
//! **Manual**: If running tests directly with cargo:
//! ```bash
//! docker compose -f wiremock/docker-compose.test.yml up -d --wait
//! RUN_WIRE_TESTS=true cargo test -- --test-threads=1
//! docker compose -f wiremock/docker-compose.test.yml down
//! ```
//!
//! This is automatically generated and should not be modified manually.

use reqwest::Client;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::process::Command;
use std::sync::Once;

/// The base URL for WireMock
pub fn get_wiremock_base_url() -> String {
    std::env::var("WIREMOCK_URL").unwrap_or_else(|_| "http://localhost:8080".to_string())
}

fn get_wiremock_admin_url() -> String {
    format!("{}/__admin", get_wiremock_base_url())
}
const WIREMOCK_COMPOSE_FILE: &str = "wiremock/docker-compose.test.yml";

static START: Once = Once::new();

/// Check if WireMock is already running by hitting the health endpoint
fn is_wiremock_running() -> bool {
    Command::new("curl")
        .args(["-s", "-f", &format!("{}/health", get_wiremock_admin_url())])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

/// Called automatically when test binary loads.
/// Starts the WireMock container if RUN_WIRE_TESTS=true and WIREMOCK_URL is not already set.
#[ctor::ctor]
fn start_wiremock() {
    if std::env::var("RUN_WIRE_TESTS").as_deref() == Ok("true") {
        // If WIREMOCK_URL is already set (external orchestration), skip container management
        if std::env::var("WIREMOCK_URL").is_ok() {
            return;
        }

        START.call_once(|| {
            // Check if docker-compose file exists
            if !std::path::Path::new(WIREMOCK_COMPOSE_FILE).exists() {
                return;
            }

            // Check if WireMock is already running (idempotent setup)
            if is_wiremock_running() {
                return;
            }

            println!("Starting WireMock container...");

            // Start the container and wait for it to be healthy
            let status = Command::new("docker")
                .args(["compose", "-f", WIREMOCK_COMPOSE_FILE, "up", "-d", "--wait"])
                .status();

            if let Err(e) = status {
                eprintln!("Failed to start WireMock container: {}", e);
                return;
            }

            // Discover the dynamically assigned port and set WIREMOCK_URL
            let port_output = Command::new("docker")
                .args([
                    "compose",
                    "-f",
                    WIREMOCK_COMPOSE_FILE,
                    "port",
                    "wiremock",
                    "8080",
                ])
                .output();

            if let Ok(output) = port_output {
                let port_str = String::from_utf8_lossy(&output.stdout);
                let port_str = port_str.trim();
                if let Some(port) = port_str.split(':').last() {
                    let url = format!("http://localhost:{}", port.trim());
                    // Set the env var so get_wiremock_base_url() picks it up
                    std::env::set_var("WIREMOCK_URL", &url);
                    println!("WireMock container is ready at {}", url);
                }
            }
        });
    }
}

// Note: We intentionally do NOT stop WireMock automatically.
// Since cargo test runs each test file as a separate binary, stopping on each
// binary exit would cause constant start/stop cycles.
// Users can manually stop with: docker compose -f wiremock/docker-compose.test.yml down

/// Resets all WireMock request journal entries.
///
/// Call this at the beginning of each test to ensure a clean state.
///
/// # Returns
/// Returns Ok(()) on success, or an error if the reset fails.
pub async fn reset_wiremock_requests() -> Result<(), Box<dyn std::error::Error>> {
    Client::new()
        .delete(format!("{}/requests", get_wiremock_admin_url()))
        .send()
        .await?;
    Ok(())
}

/// Verifies the number of requests made to WireMock for a specific endpoint.
///
/// # Arguments
/// * `method` - The HTTP method (GET, POST, etc.)
/// * `url_path` - The URL path to match
/// * `query_params` - Optional query parameters to match
/// * `expected` - Expected number of requests
///
/// # Returns
/// Returns Ok(()) if the expected count matches, or panics with an assertion error.
pub async fn verify_request_count(
    method: &str,
    url_path: &str,
    query_params: Option<HashMap<String, serde_json::Value>>,
    expected: usize,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut request_body = json!({
        "method": method,
        "urlPath": url_path,
    });

    if let Some(params) = query_params {
        let query_parameters: Value = params
            .into_iter()
            .map(|(k, v)| {
                let matcher = if v.is_array() {
                    let items: Vec<Value> = v
                        .as_array()
                        .unwrap()
                        .iter()
                        .filter_map(|item| item.as_str())
                        .map(|s| json!({"equalTo": s}))
                        .collect();
                    json!({"hasExactly": items})
                } else if let Some(s) = v.as_str() {
                    json!({"equalTo": s})
                } else {
                    json!({"equalTo": v.to_string()})
                };
                (k, matcher)
            })
            .collect();
        request_body["queryParameters"] = query_parameters;
    }

    let response = Client::new()
        .post(format!("{}/requests/find", get_wiremock_admin_url()))
        .json(&request_body)
        .send()
        .await?;

    let result: Value = response.json().await?;
    let requests = result["requests"]
        .as_array()
        .ok_or("Invalid response from WireMock")?;

    assert_eq!(
        requests.len(),
        expected,
        "Expected {} requests, found {}",
        expected,
        requests.len()
    );

    Ok(())
}
