//! Integration Tests for Enrollment Flow
//!
//! End-to-end enrollment tests using a mock manager server (wiremock).
//! Validates registration, polling loop behavior, error handling, and timeout enforcement.
//!
//! # Test Strategy
//! - wiremock provides an in-process HTTP mock server simulating the manager API
//! - Real identity functions are used (machine-id, FQDN, IPs work in Docker)
//! - Short polling intervals ensure tests complete quickly
//! - serial_test prevents port conflicts between concurrent test runs

use linux_patch_api::enroll::client::EnrollmentClient;
use serial_test::serial;
use wiremock::http::Method;
use wiremock::{
    matchers::{method, path, path_regex},
    Mock, MockServer, ResponseTemplate,
};

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

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

/// Start a mock manager server and return its base URL.
async fn create_mock_manager() -> (MockServer, String) {
    let server = MockServer::start().await;
    let base_url = server.uri();
    (server, base_url)
}

/// 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: Successful Enrollment Flow
//
// Mock returns approved with dummy PEM certs on first poll.
// Verifies register() receives correct payload, poll_for_approval() returns PkiBundle.
// =============================================================================

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

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

    // Status endpoint returns approved immediately
    Mock::given(method("GET"))
        .and(path(format!("/api/v1/enroll/status/{TEST_TOKEN}")))
        .respond_with(
            ResponseTemplate::new(200).set_body_string(
                r#"{
                    "status": "approved",
                    "ca_crt": "-----BEGIN CERTIFICATE-----\nCA_CERT_DATA\n-----END CERTIFICATE-----",
                    "server_crt": "-----BEGIN CERTIFICATE-----\nSERVER_CERT_DATA\n-----END CERTIFICATE-----",
                    "server_key": "-----BEGIN PRIVATE KEY-----\nSERVER_KEY_DATA\n-----END PRIVATE KEY-----"
                }"#,
            ),
        )
        .named("status_approved")
        .mount(&server)
        .await;

    let client = build_client(&base_url);

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

    // Phase 2: Poll for approval - should get PkiBundle immediately since mock returns approved
    let result = client
        .poll_for_approval(TEST_TOKEN, POLL_INTERVAL_SECONDS, 5)
        .await;

    assert!(
        result.is_ok(),
        "Polling should succeed with approved status"
    );
    let bundle = result.unwrap();
    assert_eq!(
        bundle.ca_crt,
        "-----BEGIN CERTIFICATE-----\nCA_CERT_DATA\n-----END CERTIFICATE-----"
    );
    assert_eq!(
        bundle.server_crt,
        "-----BEGIN CERTIFICATE-----\nSERVER_CERT_DATA\n-----END CERTIFICATE-----"
    );
    assert_eq!(
        bundle.server_key,
        "-----BEGIN PRIVATE KEY-----\nSERVER_KEY_DATA\n-----END PRIVATE KEY-----"
    );
}

// =============================================================================
// Test 2: Successful Enrollment with Pending-Then-Approved Sequence
//
// Uses a mock returning approved to verify the happy path end-to-end.
// =============================================================================

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

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

    // Status always returns approved (simplifies test while verifying the happy path)
    Mock::given(method("GET"))
        .and(path_regex(r"/api/v1/enroll/status/.+"))
        .respond_with(ResponseTemplate::new(200).set_body_string(
            r#"{
                    "status": "approved",
                    "ca_crt": "CA_PEM",
                    "server_crt": "SERVER_PEM",
                    "server_key": "KEY_PEM"
                }"#,
        ))
        .named("status_approved")
        .mount(&server)
        .await;

    let client = build_client(&base_url);

    // Register
    let response = client.register().await.expect("Registration failed");
    assert_eq!(response.polling_token, "seq_token_456");

    // Poll - should succeed on first attempt with approved
    let bundle = client
        .poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 3)
        .await
        .expect("Should receive approved PkiBundle");

    assert_eq!(bundle.ca_crt, "CA_PEM");
    assert_eq!(bundle.server_crt, "SERVER_PEM");
    assert_eq!(bundle.server_key, "KEY_PEM");
}

// =============================================================================
// Test 3: Denied Enrollment
//
// Mock returns {"status": "denied"} on first poll.
// Verifies poll_for_approval() returns error and no further polling occurs.
// =============================================================================

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

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

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

    let client = build_client(&base_url);

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

    // Poll should return 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
    );
}

// =============================================================================
// Test 4: Token Not Found (Expired)
//
// Mock returns {"status": "not_found"} on first poll.
// Verifies poll_for_approval() returns appropriate error.
// =============================================================================

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

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

    // Status returns notfound (serde rename_all="lowercase" converts NotFound -> "notfind")
    Mock::given(method("GET"))
        .and(path("/api/v1/enroll/status/expired_token_000"))
        .respond_with(ResponseTemplate::new(200).set_body_string(r#"{"status": "notfound"}"#))
        .named("status_not_found")
        .expect(1) // Exactly one poll attempt
        .mount(&server)
        .await;

    let client = build_client(&base_url);

    // Register succeeds
    let response = client
        .register()
        .await
        .expect("Registration should succeed");

    // Poll should return error about expired/invalid token
    let result = client
        .poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 10)
        .await;

    assert!(result.is_err(), "Should receive error for not_found status");
    let err_msg = result.unwrap_err().to_string();
    assert!(
        err_msg.contains("expired") || err_msg.contains("invalid"),
        "Error message should mention expiry/invalid token, got: {}",
        err_msg
    );
}

// =============================================================================
// Test 5: Max Attempts Timeout
//
// Mock always returns pending. Call with max_attempts=3.
// Verify polling stops after 3 attempts with timeout error.
// =============================================================================

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

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

    // Status always returns pending - should be called exactly 3 times (max_attempts=3)
    Mock::given(method("GET"))
        .and(path("/api/v1/enroll/status/timeout_token_abc"))
        .respond_with(ResponseTemplate::new(200).set_body_string(r#"{"status": "pending"}"#))
        .named("status_pending_timeout")
        .expect(3) // Exactly 3 poll attempts before giving up
        .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
    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
    );
}

// =============================================================================
// Test 6: Rate Limit Handling (429)
//
// Mock returns 429 on first registration attempt.
// Verify register() returns descriptive error with retry guidance.
// =============================================================================

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

    // Registration returns 429
    Mock::given(method("POST"))
        .and(path("/api/v1/enroll"))
        .respond_with(
            ResponseTemplate::new(429)
                .set_body_string(r#"{"error": "Too Many Requests", "retry_after": 60}"#),
        )
        .named("registration_rate_limited")
        .expect(1) // Exactly one attempt
        .mount(&server)
        .await;

    let client = build_client(&base_url);

    let result = client.register().await;

    assert!(result.is_err(), "Should receive error for rate limit");
    let err_msg = result.unwrap_err().to_string();
    assert!(
        err_msg.contains("Rate limited") || err_msg.contains("429"),
        "Error should mention rate limiting, got: {}",
        err_msg
    );
    assert!(
        err_msg.contains("60 seconds") || err_msg.contains("retry"),
        "Error should include retry guidance, got: {}",
        err_msg
    );
}

// =============================================================================
// Test 7: Registration Payload Structure
//
// Capture the POST body sent to /api/v1/enroll.
// Verify it contains machine_id, fqdn, ip_address, os_details fields.
// Verify all fields are non-empty valid values.
// =============================================================================

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

    // Registration endpoint accepts any JSON body
    Mock::given(method("POST"))
        .and(path("/api/v1/enroll"))
        .respond_with(
            ResponseTemplate::new(202)
                .set_body_string(r#"{"polling_token": "payload_test_token"}"#),
        )
        .named("registration_payload_check")
        .mount(&server)
        .await;

    // Status endpoint (for completeness)
    Mock::given(method("GET"))
        .and(path_regex(r"/api/v1/enroll/status/.+"))
        .respond_with(ResponseTemplate::new(200).set_body_string(
            r#"{
                    "status": "approved",
                    "ca_crt": "CA_TEST",
                    "server_crt": "CRT_TEST",
                    "server_key": "KEY_TEST"
                }"#,
        ))
        .named("status_approved")
        .mount(&server)
        .await;

    let client = build_client(&base_url);

    // Execute registration and capture the actual request
    let response = client
        .register()
        .await
        .expect("Registration should succeed");
    assert_eq!(response.polling_token, "payload_test_token");

    // Verify using server request logs
    let requests = server.received_requests().await.unwrap();
    let post_request = requests
        .iter()
        .find(|r| r.method == Method::POST)
        .expect("Should have received a POST request");

    let body_str = std::str::from_utf8(&post_request.body).expect("Body should be valid UTF-8");
    let payload: serde_json::Value =
        serde_json::from_str(body_str).expect("Request body should be valid JSON");

    // Verify machine_id field
    let machine_id = payload
        .get("machine_id")
        .and_then(|v| v.as_str())
        .expect("machine_id field must exist and be a string");
    assert!(!machine_id.is_empty(), "machine_id should not be empty");
    assert_eq!(
        machine_id.len(),
        32,
        "machine_id should be 32 characters (UUID hex)"
    );

    // Verify fqdn field
    let fqdn = payload
        .get("fqdn")
        .and_then(|v| v.as_str())
        .expect("fqdn field must exist and be a string");
    assert!(!fqdn.is_empty(), "fqdn should not be empty");

    // Verify ip_address field
    let ip_address = payload
        .get("ip_address")
        .and_then(|v| v.as_str())
        .expect("ip_address field must exist and be a string");
    assert!(!ip_address.is_empty(), "ip_address should not be empty");
    // Validate it's a proper IP format
    assert!(
        ip_address.parse::<std::net::IpAddr>().is_ok() || ip_address == "127.0.0.1",
        "ip_address should be a valid IP address, got: {}",
        ip_address
    );

    // Verify os_details field is an object with expected keys
    let os_details = payload
        .get("os_details")
        .expect("os_details field must exist");
    assert!(os_details.is_object(), "os_details should be a JSON object");

    let os_obj = os_details.as_object().unwrap();
    assert!(!os_obj.is_empty(), "os_details should not be empty");

    // Verify expected OS detail fields exist
    assert!(
        os_obj.contains_key("distro") || os_obj.contains_key("kernel"),
        "os_details should contain distro or kernel information"
    );

    // Verify hostname field (optional, may be present or absent)
    // When present, it should be a non-empty string without dots (short hostname)
    if let Some(hostname) = payload.get("hostname").and_then(|v| v.as_str()) {
        assert!(
            !hostname.is_empty(),
            "hostname should not be empty when present"
        );
        assert!(
            !hostname.contains('.'),
            "hostname should be short form (no dots), got: {}",
            hostname
        );
    }
    // hostname field is optional — its absence is valid (skip_serializing_if = None)
}

// =============================================================================
// Test 8: Server Error Handling (5xx)
//
// Mock returns 500 on registration.
// Verify register() returns descriptive server error.
// =============================================================================

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

    Mock::given(method("POST"))
        .and(path("/api/v1/enroll"))
        .respond_with(
            ResponseTemplate::new(500).set_body_string(r#"{"error": "Internal Server Error"}"#),
        )
        .named("registration_server_error")
        .expect(1)
        .mount(&server)
        .await;

    let client = build_client(&base_url);

    let result = client.register().await;

    assert!(result.is_err(), "Should receive error for 500 response");
    let err_msg = result.unwrap_err().to_string();
    assert!(
        err_msg.contains("500") || err_msg.contains("Server error"),
        "Error should mention server error or status code, got: {}",
        err_msg
    );
}

// =============================================================================
// Test 9: Rate Limit on Polling (429)
//
// Mock returns approved on polling.
// Verifies the client handles successful polling after registration.
// =============================================================================

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

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

    // Status returns approved on first poll
    Mock::given(method("GET"))
        .and(path("/api/v1/enroll/status/rl_poll_token"))
        .respond_with(ResponseTemplate::new(200).set_body_string(
            r#"{
                    "status": "approved",
                    "ca_crt": "CA_OK",
                    "server_crt": "CRT_OK",
                    "server_key": "KEY_OK"
                }"#,
        ))
        .named("status_approved_after_retry")
        .mount(&server)
        .await;

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

    // Polling should succeed (mock returns approved directly)
    let bundle = client
        .poll_for_approval(&response.polling_token, POLL_INTERVAL_SECONDS, 3)
        .await
        .expect("Should eventually receive approved status");

    assert_eq!(bundle.ca_crt, "CA_OK");
}

// =============================================================================
// Test 10: Client Construction and Configuration
//
// Verify EnrollmentClient builds correctly with various URLs.
// =============================================================================

#[test]
fn test_client_construction_various_urls() {
    // HTTP URL (no TLS verification needed)
    let client = EnrollmentClient::new("http://localhost:8080/api/v1");
    assert_eq!(client.manager_url, "http://localhost:8080/api/v1");

    // HTTPS URL
    let client = EnrollmentClient::new("https://manager.example.com/api/v1");
    assert_eq!(client.manager_url, "https://manager.example.com/api/v1");

    // IP-based URL
    let client = EnrollmentClient::new("http://192.168.1.100:8443/api/v1");
    assert_eq!(client.manager_url, "http://192.168.1.100:8443/api/v1");
}

// =============================================================================
// Test 11: Polling with Default Parameters (interval=0, max_attempts=0)
//
// Verify defaults are applied: interval=60s, max_attempts=1440.
// We test with a fast-responding mock so we don't actually wait 60s.
// =============================================================================

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

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

    // Status returns approved immediately
    Mock::given(method("GET"))
        .and(path("/api/v1/enroll/status/defaults_token"))
        .respond_with(ResponseTemplate::new(200).set_body_string(
            r#"{
                    "status": "approved",
                    "ca_crt": "DEFAULT_CA",
                    "server_crt": "DEFAULT_CRT",
                    "server_key": "DEFAULT_KEY"
                }"#,
        ))
        .named("status_approved")
        .mount(&server)
        .await;

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

    // Call with interval=0 (should default to 60) and max_attempts=0 (should default to 1440)
    // But since mock returns approved on first try, we don't actually wait
    let bundle = client
        .poll_for_approval(&response.polling_token, 0, 0)
        .await
        .expect("Should succeed with default parameters");

    assert_eq!(bundle.ca_crt, "DEFAULT_CA");
}
