//! End-to-End Enrollment Test Suite
//!
//! Comprehensive tests verifying the complete enrollment flow from CLI invocation
//! through certificate provisioning and whitelist updates.
//!
//! # Test Strategy
//! - wiremock provides in-process HTTP mock server simulating manager API
//! - tempfile ensures isolated filesystem state per test with automatic cleanup
//! - serial_test prevents port conflicts between concurrent test runs
//! - Mock manager simulates realistic approval delays (1-2 polls before approved)
//!
//! # Coverage
//! 1. Full happy-path enrollment (register → poll → provision)
//! 2. Enrollment denied flow with clean failure state
//! 3. Enrollment timeout after max attempts
//! 4. Certificate file permission verification (0o600 keys, 0o644 certs)
//! 5. Whitelist append with duplicate prevention
//! 6. Signal handling during polling (graceful shutdown via timeout simulation)

use linux_patch_api::config::loader::TlsConfig;
use linux_patch_api::enroll::client::EnrollmentClient;
use linux_patch_api::enroll::provision;
use serial_test::serial;
use std::os::unix::fs::PermissionsExt;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use tempfile::TempDir;
use wiremock::matchers::{method, path, path_regex};
use wiremock::{Mock, MockServer, ResponseTemplate};

/// Test constants
const TEST_TOKEN: &str = "test_enrollment_token";
const POLL_INTERVAL_SECONDS: u64 = 1; // Fast polling for tests

// =============================================================================
// Dummy PEM data for testing - valid PEM structure with BEGIN/END markers
// =============================================================================

const DUMMY_CA_PEM: &str = "-----BEGIN CERTIFICATE-----\nMIIBkTCB+wIJAKHBfpegPjMCMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMMBnRh\nc3RjYTAeFw0yNDAxMDEwMDAwMDBaFw0yNTAxMDEwMDAwMDBaMBExDzANBgNVBAMM\nBnRlc3RjYTBcMA0GCSqGSIb3DQEBAQUAAgEAA0sAMEgCQQC7o5ECujKlLIZmHoRN\nd8EEp+mRhJ2i0M4HtTmMy1VSdvCVrXvMJkbz3KoQxRqVMd6yBZKwWgyIePCNMSVh\nAgMBAAEwDQYJKoZIhvcNAQELBQADQQC7a29sYWJlbGVfZGF0YV9mb3JfdGVzdGlu\nZ19vbmx5X25vdF9hX3JlYWxfY2VydGlmaWNhdGU=\n-----END CERTIFICATE-----";

const DUMMY_SERVER_PEM: &str = "-----BEGIN CERTIFICATE-----\nMIIBkTCB+wIJAKHBfpegPjMDMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMMBnRh\nc3RjYTAeFw0yNDAxMDEwMDAwMDBaFw0yNTAxMDEwMDAwMDBaMBExDzANBgNVBAMM\nBnNlcnZlcjBcMA0GCSqGSIb3DQEBAQUAAgEAA0sAMEgCQQC7o5ECujKlLIZmHoRN\nd8EEp+mRhJ2i0M4HtTmMy1VSdvCVrXvMJkbz3KoQxRqVMd6yBZKwWgyIePCNMSVh\nAgMBAAEwDQYJKoZIhvcNAQELBQADQQC7a29sYWJlbGVfZGF0YV9mb3JfdGVzdGlu\nZ19vbmx5X25vdF9hX3JlYWxfY2VydGlmaWNhdGU=\n-----END CERTIFICATE-----";

const DUMMY_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAu6ORAroypSyGZh6E\nTXfBBKfpkYSdotDOB7U5jMtVUnbwna17zCZG89yqEMUalTHesgWSsFoMiHjwjTEl\nYQIDAQABAkADdd2F0YV9mb3JfdGVzdGluZ19vbmx5X25vdF9hX3JlYWxfa2V5\nX2RhdGFfZXhhbXBsZV9mb3JfcGlwZWxpbmVfdGVzdGluZwIhAOdvbnBseWZvcmVu\ncm9sbG1lbnR0ZXN0aW5ncHVycG9zZXNvbmx5d2l0aGluZW5yb2xs\n-----END PRIVATE KEY-----";

// =============================================================================
// Helper Functions
// =============================================================================

/// Create a mock manager server and return base URL.
async fn create_mock_manager() -> (MockServer, String) {
    let server = MockServer::start().await;
    let base_url = server.uri(); // e.g., "http://127.0.0.1:XXXXX"
    (server, base_url)
}

/// Create temporary directories for certificate and whitelist file operations.
fn create_temp_dirs() -> (TempDir, TempDir) {
    let cert_dir = tempfile::tempdir().expect("Failed to create temp cert directory");
    let whitelist_dir = tempfile::tempdir().expect("Failed to create temp whitelist directory");
    (cert_dir, whitelist_dir)
}

/// Initialize an empty whitelist YAML file at the given path.
/// Required because WhitelistManager::new() loads existing config on construction.
fn init_empty_whitelist(path: &str) {
    std::fs::write(path, "entries: []\n").expect("Failed to create initial whitelist file");
}

/// Build a TLS config pointing to the temp certificate directory.
fn build_tls_config(cert_dir: &std::path::Path) -> TlsConfig {
    TlsConfig {
        enabled: true,
        port: 12443,
        ca_cert: cert_dir.join("ca.pem").to_string_lossy().to_string(),
        server_cert: cert_dir.join("server.pem").to_string_lossy().to_string(),
        server_key: cert_dir
            .join("server.key.pem")
            .to_string_lossy()
            .to_string(),
        min_tls_version: "1.3".to_string(),
    }
}

/// Build an EnrollmentClient pointing at the mock server.
/// Uses a test report_ip so enrollment works inside Docker containers
/// where the only IPs are in the 172.16.0.0/12 bridge range (filtered).
fn build_client(base_url: &str) -> EnrollmentClient {
    EnrollmentClient::with_ip_overrides(base_url, None, Some("192.168.1.10".to_string()))
}

// =============================================================================
// Test 1: Full Enrollment Flow (Happy Path)
//
// Start mock manager with approval workflow, call run_enrollment() phases,
// verify registration request sent, polling executes, PKI bundle received,
// certificate files written with correct permissions, manager IP appended to
// whitelist YAML, all three phases complete without error.
// =============================================================================

#[actix_rt::test]
#[serial]
async fn test_full_enrollment_flow_happy_path() {
    let (server, base_url) = create_mock_manager().await;
    let (cert_dir, whitelist_dir) = create_temp_dirs();

    let ca_cert_path = cert_dir.path().join("ca.pem");
    let server_cert_path = cert_dir.path().join("server.pem");
    let server_key_path = cert_dir.path().join("server.key.pem");
    let whitelist_path = whitelist_dir
        .path()
        .join("whitelist.yaml")
        .to_string_lossy()
        .to_string();

    init_empty_whitelist(&whitelist_path);

    // Mock manager: simulate realistic 1-poll delay before approval
    let poll_count = Arc::new(AtomicU32::new(0));
    let poll_count_clone = poll_count.clone();

    Mock::given(method("POST"))
        .and(path("/api/v1/enroll"))
        .respond_with(
            ResponseTemplate::new(202)
                .set_body_string(format!(r#"{{"polling_token": "{}"}}"#, TEST_TOKEN)),
        )
        .named("enroll_registration")
        .mount(&server)
        .await;

    Mock::given(method("GET"))
        .and(path_regex(r"/api/v1/enroll/status/.+"))
        .respond_with(move |_req: &wiremock::Request| {
            let count = poll_count_clone.fetch_add(1, Ordering::SeqCst);
            if count < 1 {
                // First poll returns pending (simulates admin review delay)
                ResponseTemplate::new(200).set_body_string(r#"{"status": "pending"}"#)
            } else {
                // Second poll returns approved with full PKI bundle
                ResponseTemplate::new(200).set_body_string(format!(
                    r#"{{
                        "status": "approved",
                        "ca_crt": {},
                        "server_crt": {},
                        "server_key": {}
                    }}"#,
                    serde_json::to_string(DUMMY_CA_PEM).unwrap(),
                    serde_json::to_string(DUMMY_SERVER_PEM).unwrap(),
                    serde_json::to_string(DUMMY_KEY_PEM).unwrap(),
                ))
            }
        })
        .named("status_polling")
        .mount(&server)
        .await;

    let client = build_client(&base_url);

    // Phase 1: Registration
    let response = client
        .register()
        .await
        .expect("Registration should succeed");
    assert_eq!(response.polling_token, TEST_TOKEN);

    // Phase 2: Polling (should get pending first, then approved)
    let bundle = client
        .poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 5)
        .await
        .expect("Polling should succeed with approval after pending");

    assert!(!bundle.ca_crt.is_empty());
    assert!(!bundle.server_crt.is_empty());
    assert!(!bundle.server_key.is_empty());

    // Phase 3: PKI Provisioning
    let tls_config = build_tls_config(cert_dir.path());
    provision::provision_pki_bundle(
        &bundle.ca_crt,
        &bundle.server_crt,
        &bundle.server_key,
        Some(&tls_config),
    )
    .await
    .expect("PKI provisioning should succeed");

    // Phase 3b: Whitelist update (manager_ip for localhost URL returns 127.0.0.1)
    let manager_ip = client
        .manager_ip()
        .await
        .expect("Should resolve manager IP");
    provision::append_manager_to_whitelist(&manager_ip, &whitelist_path)
        .await
        .expect("Whitelist append should succeed");

    // Verify: all certificate files written to temp directory
    assert!(ca_cert_path.exists(), "CA cert file should exist");
    assert!(server_cert_path.exists(), "Server cert file should exist");
    assert!(server_key_path.exists(), "Server key file should exist");

    // Verify: correct permissions (key=0o600, certs=0o644)
    let key_perms = std::fs::metadata(&server_key_path)
        .unwrap()
        .permissions()
        .mode()
        & 0o777;
    assert_eq!(key_perms, 0o600, "Key file should have 0o600 permissions");

    let ca_perms = std::fs::metadata(&ca_cert_path)
        .unwrap()
        .permissions()
        .mode()
        & 0o777;
    assert_eq!(ca_perms, 0o644, "CA cert should have 0o644 permissions");

    let server_perms = std::fs::metadata(&server_cert_path)
        .unwrap()
        .permissions()
        .mode()
        & 0o777;
    assert_eq!(
        server_perms, 0o644,
        "Server cert should have 0o644 permissions"
    );

    // Verify: whitelist contains manager IP
    let wl_content = std::fs::read_to_string(&whitelist_path).unwrap();
    let wl_config: serde_yaml::Value = serde_yaml::from_str(&wl_content).unwrap();
    let entries = wl_config.get("entries").unwrap().as_sequence().unwrap();
    assert!(
        entries.iter().any(|e| e.as_str().unwrap() == manager_ip),
        "Whitelist should contain manager IP {}",
        manager_ip
    );
}

// =============================================================================
// Test 2: Enrollment Denied Flow
//
// Mock server returns denied status on first poll.
// Verify enrollment fails with clear denial message, no certificate files
// written (clean failure state), no whitelist modifications.
// =============================================================================

#[actix_rt::test]
#[serial]
async fn test_enrollment_denied_flow() {
    let (server, base_url) = create_mock_manager().await;
    let (cert_dir, _whitelist_dir) = create_temp_dirs();

    let whitelist_path = _whitelist_dir
        .path()
        .join("whitelist.yaml")
        .to_string_lossy()
        .to_string();
    init_empty_whitelist(&whitelist_path);

    Mock::given(method("POST"))
        .and(path("/api/v1/enroll"))
        .respond_with(
            ResponseTemplate::new(202).set_body_string(r#"{"polling_token": "denied_token"}"#),
        )
        .named("registration")
        .mount(&server)
        .await;

    Mock::given(method("GET"))
        .and(path_regex(r"/api/v1/enroll/status/.+"))
        .respond_with(ResponseTemplate::new(200).set_body_string(r#"{"status": "denied"}"#))
        .named("status_denied")
        .expect(1) // Exactly one poll attempt before denial
        .mount(&server)
        .await;

    let client = build_client(&base_url);

    // Phase 1: Registration succeeds even for denied enrollment
    let response = client
        .register()
        .await
        .expect("Registration should succeed");
    assert_eq!(response.polling_token, "denied_token");

    // Phase 2: Polling returns denial error
    let result = client
        .poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 10)
        .await;

    assert!(
        result.is_err(),
        "Should receive error for denied enrollment"
    );
    let err_msg = result.unwrap_err().to_string();
    assert!(
        err_msg.contains("denied"),
        "Error message should mention denial, got: {}",
        err_msg
    );

    // Verify: no certificate files written (clean failure state)
    let ca_path = cert_dir.path().join("ca.pem");
    let server_cert_path = cert_dir.path().join("server.pem");
    let server_key_path = cert_dir.path().join("server.key.pem");

    assert!(
        !ca_path.exists(),
        "CA cert should NOT exist after denied enrollment"
    );
    assert!(
        !server_cert_path.exists(),
        "Server cert should NOT exist after denied enrollment"
    );
    assert!(
        !server_key_path.exists(),
        "Server key should NOT exist after denied enrollment"
    );

    // Verify: no whitelist modifications on failed enrollment
    let wl_content = std::fs::read_to_string(&whitelist_path).unwrap();
    let wl_config: serde_yaml::Value = serde_yaml::from_str(&wl_content).unwrap();
    let entries = wl_config.get("entries").and_then(|e| e.as_sequence());
    assert!(
        entries.map_or(true, |e| e.is_empty()),
        "Whitelist should remain empty after denied enrollment"
    );
}

// =============================================================================
// Test 3: Enrollment Timeout Flow
//
// Mock server always returns pending. Call with max_attempts=3.
// Verify enrollment fails after 3 attempts with timeout error, clean failure
// state (no partial files on disk).
// =============================================================================

#[actix_rt::test]
#[serial]
async fn test_enrollment_timeout_flow() {
    let (server, base_url) = create_mock_manager().await;
    let (cert_dir, _whitelist_dir) = create_temp_dirs();

    Mock::given(method("POST"))
        .and(path("/api/v1/enroll"))
        .respond_with(
            ResponseTemplate::new(202).set_body_string(r#"{"polling_token": "timeout_token"}"#),
        )
        .named("registration")
        .mount(&server)
        .await;

    Mock::given(method("GET"))
        .and(path_regex(r"/api/v1/enroll/status/.+"))
        .respond_with(ResponseTemplate::new(200).set_body_string(r#"{"status": "pending"}"#))
        .named("status_always_pending")
        .expect(3) // Exactly 3 poll attempts before timeout
        .mount(&server)
        .await;

    let client = build_client(&base_url);
    let response = client
        .register()
        .await
        .expect("Registration should succeed");

    // Poll with max_attempts=3 - should timeout after exactly 3 attempts
    let result = client
        .poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 3)
        .await;

    assert!(result.is_err(), "Should timeout after max attempts");
    let err_msg = result.unwrap_err().to_string();
    assert!(
        err_msg.contains("timed out") || err_msg.contains("timeout"),
        "Error should mention timeout, got: {}",
        err_msg
    );

    // Verify: no partial certificate files on disk
    let ca_path = cert_dir.path().join("ca.pem");
    let server_cert_path = cert_dir.path().join("server.pem");
    let server_key_path = cert_dir.path().join("server.key.pem");

    assert!(!ca_path.exists(), "CA cert should NOT exist after timeout");
    assert!(
        !server_cert_path.exists(),
        "Server cert should NOT exist after timeout"
    );
    assert!(
        !server_key_path.exists(),
        "Server key should NOT exist after timeout"
    );
}

// =============================================================================
// Test 4: Certificate Permission Verification
//
// After successful enrollment, verify file permissions:
// - Key file: 0o600 (owner rw only)
// - Certificate files: 0o644 (owner rw, group/others read)
// Verify atomic write pattern (no partial .tmp or .bak files left on disk).
// =============================================================================

#[actix_rt::test]
#[serial]
async fn test_certificate_permission_verification() {
    let (server, base_url) = create_mock_manager().await;
    let (cert_dir, _whitelist_dir) = create_temp_dirs();

    Mock::given(method("POST"))
        .and(path("/api/v1/enroll"))
        .respond_with(
            ResponseTemplate::new(202).set_body_string(r#"{"polling_token": "perm_token"}"#),
        )
        .mount(&server)
        .await;

    Mock::given(method("GET"))
        .and(path_regex(r"/api/v1/enroll/status/.+"))
        .respond_with(ResponseTemplate::new(200).set_body_string(format!(
            r#"{{
                    "status": "approved",
                    "ca_crt": {},
                    "server_crt": {},
                    "server_key": {}
                }}"#,
            serde_json::to_string(DUMMY_CA_PEM).unwrap(),
            serde_json::to_string(DUMMY_SERVER_PEM).unwrap(),
            serde_json::to_string(DUMMY_KEY_PEM).unwrap(),
        )))
        .mount(&server)
        .await;

    let client = build_client(&base_url);
    let response = client
        .register()
        .await
        .expect("Registration should succeed");
    let bundle = client
        .poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 5)
        .await
        .expect("Should receive approved PkiBundle");

    // Provision to temp directory
    let tls_config = build_tls_config(cert_dir.path());
    provision::provision_pki_bundle(
        &bundle.ca_crt,
        &bundle.server_crt,
        &bundle.server_key,
        Some(&tls_config),
    )
    .await
    .expect("PKI provisioning should succeed");

    // Verify key file: 0o600 (owner read/write only)
    let key_path = cert_dir.path().join("server.key.pem");
    let key_perms = std::fs::metadata(&key_path).unwrap().permissions().mode() & 0o777;
    assert_eq!(
        key_perms, 0o600,
        "Key file must have exactly 0o600 permissions (owner rw only)"
    );

    // Verify CA cert: 0o644 (owner rw, group/others read)
    let ca_path = cert_dir.path().join("ca.pem");
    let ca_perms = std::fs::metadata(&ca_path).unwrap().permissions().mode() & 0o777;
    assert_eq!(
        ca_perms, 0o644,
        "CA certificate must have exactly 0o644 permissions"
    );

    // Verify server cert: 0o644 (owner rw, group/others read)
    let server_cert_path = cert_dir.path().join("server.pem");
    let server_perms = std::fs::metadata(&server_cert_path)
        .unwrap()
        .permissions()
        .mode()
        & 0o777;
    assert_eq!(
        server_perms, 0o644,
        "Server certificate must have exactly 0o644 permissions"
    );

    // Verify atomic write: no partial .tmp files left on disk
    for entry in std::fs::read_dir(cert_dir.path()).unwrap() {
        let entry = entry.unwrap();
        let name = entry.file_name().to_string_lossy().to_string();
        assert!(
            !name.ends_with(".tmp"),
            "No .tmp partial files should remain after atomic write"
        );
    }

    // Verify content integrity - PEM data written correctly
    let ca_content = std::fs::read_to_string(&ca_path).unwrap();
    assert!(ca_content.contains("BEGIN CERTIFICATE"));
    assert!(ca_content.contains("END CERTIFICATE"));

    let key_content = std::fs::read_to_string(&key_path).unwrap();
    assert!(
        key_content.contains("BEGIN PRIVATE KEY") || key_content.contains("BEGIN RSA PRIVATE KEY")
    );
}

// =============================================================================
// Test 5: Whitelist Append Verification
//
// After successful enrollment, verify whitelist YAML contains manager IP.
// Verify no duplicate entries if enrollment runs twice with same manager IP.
// Verify YAML format preserved correctly.
// =============================================================================

#[actix_rt::test]
#[serial]
async fn test_whitelist_append_verification() {
    let (server, base_url) = create_mock_manager().await;
    let (_cert_dir, whitelist_dir) = create_temp_dirs();

    let whitelist_path = whitelist_dir
        .path()
        .join("whitelist.yaml")
        .to_string_lossy()
        .to_string();
    init_empty_whitelist(&whitelist_path);

    Mock::given(method("POST"))
        .and(path("/api/v1/enroll"))
        .respond_with(
            ResponseTemplate::new(202).set_body_string(r#"{"polling_token": "wl_token"}"#),
        )
        .mount(&server)
        .await;

    Mock::given(method("GET"))
        .and(path_regex(r"/api/v1/enroll/status/.+"))
        .respond_with(ResponseTemplate::new(200).set_body_string(format!(
            r#"{{
                    "status": "approved",
                    "ca_crt": {},
                    "server_crt": {},
                    "server_key": {}
                }}"#,
            serde_json::to_string(DUMMY_CA_PEM).unwrap(),
            serde_json::to_string(DUMMY_SERVER_PEM).unwrap(),
            serde_json::to_string(DUMMY_KEY_PEM).unwrap(),
        )))
        .mount(&server)
        .await;

    let client = build_client(&base_url);
    let response = client
        .register()
        .await
        .expect("Registration should succeed");
    let _bundle = client
        .poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 5)
        .await
        .expect("Should receive approved PkiBundle");

    // First enrollment: append to whitelist
    let manager_ip = client
        .manager_ip()
        .await
        .expect("Should resolve manager IP");
    provision::append_manager_to_whitelist(&manager_ip, &whitelist_path)
        .await
        .expect("First whitelist append should succeed");

    // Verify: whitelist contains manager IP after first enrollment
    let wl_content = std::fs::read_to_string(&whitelist_path).unwrap();
    let wl_config: serde_yaml::Value = serde_yaml::from_str(&wl_content).unwrap();
    let entries: Vec<&str> = wl_config
        .get("entries")
        .unwrap()
        .as_sequence()
        .unwrap()
        .iter()
        .map(|e| e.as_str().unwrap())
        .collect();

    assert_eq!(
        entries.iter().filter(|&&e| e == manager_ip).count(),
        1,
        "Whitelist should contain exactly one entry for manager IP after first enrollment"
    );

    // Second enrollment with same manager IP: verify no duplicate
    provision::append_manager_to_whitelist(&manager_ip, &whitelist_path)
        .await
        .expect("Second whitelist append should succeed (no-op for duplicate)");

    // Verify: still only one entry after second enrollment (duplicate prevention)
    let wl_content = std::fs::read_to_string(&whitelist_path).unwrap();
    let wl_config: serde_yaml::Value = serde_yaml::from_str(&wl_content).unwrap();
    let entries: Vec<&str> = wl_config
        .get("entries")
        .unwrap()
        .as_sequence()
        .unwrap()
        .iter()
        .map(|e| e.as_str().unwrap())
        .collect();

    assert_eq!(
        entries.iter().filter(|&&e| e == manager_ip).count(),
        1,
        "Whitelist should still have exactly one entry (no duplicate) after second enrollment"
    );

    // Verify: YAML format is valid and parseable
    assert!(
        wl_content.contains("entries:"),
        "YAML should contain 'entries:' key"
    );
}

// =============================================================================
// Test 6: Signal Handling During Polling (Graceful Shutdown)
//
// Mock server returns pending indefinitely.
// Verify graceful shutdown with appropriate error message when max attempts
// exhausted (simulates SIGTERM interrupt during polling loop).
// Verify cleanup of any partial state (no leftover files).
// =============================================================================

#[actix_rt::test]
#[serial]
async fn test_signal_handling_during_polling() {
    let (server, base_url) = create_mock_manager().await;
    let (cert_dir, _whitelist_dir) = create_temp_dirs();

    Mock::given(method("POST"))
        .and(path("/api/v1/enroll"))
        .respond_with(
            ResponseTemplate::new(202).set_body_string(r#"{"polling_token": "signal_token"}"#),
        )
        .mount(&server)
        .await;

    Mock::given(method("GET"))
        .and(path_regex(r"/api/v1/enroll/status/.+"))
        .respond_with(ResponseTemplate::new(200).set_body_string(r#"{"status": "pending"}"#))
        .named("always_pending")
        .expect(3) // Exactly 3 polls before graceful shutdown
        .mount(&server)
        .await;

    let client = build_client(&base_url);
    let response = client
        .register()
        .await
        .expect("Registration should succeed");

    // Poll with max_attempts=3, interval=1s
    // This simulates SIGTERM interrupt by exhausting attempts (graceful shutdown)
    let result = client
        .poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 3)
        .await;

    // Verify: graceful shutdown with appropriate error message
    assert!(result.is_err(), "Should fail gracefully after max attempts");
    let err_msg = result.unwrap_err().to_string();
    assert!(
        err_msg.contains("timed out") || err_msg.contains("timeout"),
        "Error should indicate graceful shutdown/timeout, got: {}",
        err_msg
    );

    // Verify: cleanup of any partial state (no leftover files)
    let remaining: Vec<_> = std::fs::read_dir(cert_dir.path())
        .unwrap()
        .map(|e| e.unwrap().file_name().to_string_lossy().to_string())
        .collect();
    assert!(
        remaining.is_empty(),
        "No partial files should remain after graceful shutdown: {:?}",
        remaining
    );
}

// =============================================================================
// Test 7: Whitelist YAML Format Preservation
//
// Verify the whitelist YAML maintains proper structure after enrollment.
// Ensures entries array format is correct and machine-readable.
// =============================================================================

#[actix_rt::test]
#[serial]
async fn test_whitelist_yaml_format_preservation() {
    let (server, base_url) = create_mock_manager().await;
    let (_cert_dir, whitelist_dir) = create_temp_dirs();

    let whitelist_path = whitelist_dir
        .path()
        .join("whitelist.yaml")
        .to_string_lossy()
        .to_string();
    init_empty_whitelist(&whitelist_path);

    Mock::given(method("POST"))
        .and(path("/api/v1/enroll"))
        .respond_with(
            ResponseTemplate::new(202).set_body_string(r#"{"polling_token": "yaml_token"}"#),
        )
        .mount(&server)
        .await;

    Mock::given(method("GET"))
        .and(path_regex(r"/api/v1/enroll/status/.+"))
        .respond_with(ResponseTemplate::new(200).set_body_string(format!(
            r#"{{
                    "status": "approved",
                    "ca_crt": {},
                    "server_crt": {},
                    "server_key": {}
                }}"#,
            serde_json::to_string(DUMMY_CA_PEM).unwrap(),
            serde_json::to_string(DUMMY_SERVER_PEM).unwrap(),
            serde_json::to_string(DUMMY_KEY_PEM).unwrap(),
        )))
        .mount(&server)
        .await;

    let client = build_client(&base_url);
    let response = client
        .register()
        .await
        .expect("Registration should succeed");
    let _bundle = client
        .poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 5)
        .await
        .expect("Should receive approved PkiBundle");

    // Provision and append to whitelist
    let manager_ip = client
        .manager_ip()
        .await
        .expect("Should resolve manager IP");
    provision::append_manager_to_whitelist(&manager_ip, &whitelist_path)
        .await
        .expect("Whitelist append should succeed");

    // Verify: whitelist file exists and is valid YAML
    let wl_content = std::fs::read_to_string(&whitelist_path).unwrap();

    // Parse as serde_yaml to verify format
    let wl_config: serde_yaml::Value =
        serde_yaml::from_str(&wl_content).expect("Whitelist should be valid YAML after enrollment");

    // Verify structure: entries key exists and is a sequence
    assert!(
        wl_config.get("entries").is_some(),
        "YAML must contain 'entries' key"
    );
    let entries = wl_config.get("entries").unwrap();
    assert!(entries.is_sequence(), "'entries' must be a YAML sequence");

    // Verify: exactly one entry matching manager IP
    let entry_list = entries.as_sequence().unwrap();
    assert_eq!(entry_list.len(), 1, "Should have exactly 1 whitelist entry");
    assert_eq!(
        entry_list[0].as_str().unwrap(),
        manager_ip,
        "Entry should match manager IP"
    );
}
