//! Property tests for cancellation safety.
//!
//! These tests verify that async operations can be safely cancelled
//! (dropped) without leaving the client in an inconsistent state.
//!
//! Uses proptest for property-based testing with random inputs.

use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;

use lastid_sdk::{ClientBuilder, RequestId, RequestStatus, SDKConfig};
use proptest::prelude::*;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

/// Generate a random request ID
fn arb_request_id() -> impl Strategy<Value = String> {
    "[a-z]{3,8}_[a-z0-9]{8,16}".prop_map(|s| s)
}

/// Generate a random number of polls before cancellation
fn arb_polls_before_cancel() -> impl Strategy<Value = usize> {
    0usize..10
}

/// Generate a random polling interval in milliseconds
fn arb_poll_interval_ms() -> impl Strategy<Value = u64> {
    10u64..100
}

/// Helper to create a test configuration pointing to mock server
fn test_config(mock_url: &str, poll_interval_ms: u64) -> SDKConfig {
    SDKConfig::builder()
        .client_id("test-client-id")
        .idp_endpoint(mock_url)
        .trust_registry_endpoint(format!("{}/v1/trust-registry", mock_url))
        .polling_initial_interval_ms(poll_interval_ms)
        .polling_max_interval_ms(poll_interval_ms * 2)
        .polling_max_duration_seconds(10)
        .build()
        .expect("Valid test config")
}

/// Create a minimal mock SD-JWT for testing
fn create_mock_sdjwt() -> String {
    use base64::Engine;
    use base64::engine::general_purpose::URL_SAFE_NO_PAD;

    let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256","typ":"vc+sd-jwt"}"#.as_bytes());
    let now = chrono::Utc::now().timestamp();
    let exp = now + 3600;

    let payload = URL_SAFE_NO_PAD.encode(
        format!(
            r#"{{
                "iss": "did:lastid:test-issuer",
                "sub": "did:lastid:test-subject",
                "iat": {},
                "exp": {},
                "vct": "LastID.Base"
            }}"#,
            now, exp
        )
        .as_bytes(),
    );

    let signature = URL_SAFE_NO_PAD.encode("mock-signature".as_bytes());

    format!("{}.{}.{}~", header, payload, signature)
}

/// Property: Cancelling poll_for_completion after N polls should not panic
/// and subsequent operations should work correctly.
#[test]
fn prop_cancellation_during_polling_is_safe() {
    proptest!(ProptestConfig::with_cases(20), |(
        request_id in arb_request_id(),
        polls_before_cancel in arb_polls_before_cancel(),
        poll_interval_ms in arb_poll_interval_ms(),
    )| {
        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .unwrap();

        rt.block_on(async {
            let mock_server = MockServer::start().await;
            let config = test_config(&mock_server.uri(), poll_interval_ms);

            // Track poll count
            let poll_count = Arc::new(AtomicUsize::new(0));
            let poll_count_clone = poll_count.clone();
            let target_polls = polls_before_cancel;
            let request_id_for_mock = request_id.clone();

            // Always return pending
            Mock::given(method("GET"))
                .and(path(format!("/v1/verify/status/{}", request_id)))
                .respond_with(move |_req: &wiremock::Request| {
                    poll_count_clone.fetch_add(1, Ordering::SeqCst);
                    ResponseTemplate::new(200).set_body_json(serde_json::json!({
                        "state": "pending",
                        "request_id": request_id_for_mock,
                        "expires_at": "2026-01-16T11:00:00Z"
                    }))
                })
                .mount(&mock_server)
                .await;

            let client = ClientBuilder::new()
                .with_config(config.clone())
                .build()
                .expect("Client should build");

            let request_id_obj = RequestId::new(request_id);

            // Start polling
            let poll_future = client.poll_for_completion(&request_id_obj);
            tokio::pin!(poll_future);

            // Cancel after target number of polls (or timeout)
            let cancel_timeout = Duration::from_millis(
                poll_interval_ms * (target_polls as u64 + 2) + 100
            );

            let result = tokio::time::timeout(cancel_timeout, &mut poll_future).await;

            // Should either:
            // - Err(_) from tokio::time::timeout - future was cancelled/dropped
            // - Ok(Err(LastIDError::Timeout)) - polling timed out (FR-010)
            // - Ok(Ok(status)) - completed with some status (unlikely in this test)
            // All are valid outcomes for cancellation safety testing
            match result {
                Ok(Ok(status)) => {
                    // Polling completed with a status
                    match status {
                        RequestStatus::Pending { .. } => {
                            // Caught it pending - valid
                        }
                        _ => {
                            // Other status - valid
                        }
                    }
                }
                Ok(Err(_)) => {
                    // Error from client (including LastIDError::Timeout) - valid
                }
                Err(_) => {
                    // Timeout (cancellation) - the future was dropped
                    // This is the expected cancellation case
                }
            }

            // CRITICAL: After cancellation, the client should still be usable
            // Create a new client to verify the system is still functional
            let client2 = ClientBuilder::new()
                .with_config(config)
                .build()
                .expect("Client should still build after cancellation");

            // Single poll should work
            let single_poll_result = client2.poll_request(&request_id_obj).await;
            // Should succeed (we have the mock still running)
            assert!(single_poll_result.is_ok(), "Client should work after cancellation");
        });
    });
}

/// Property: Multiple concurrent poll_for_completion calls with cancellations
/// should not cause deadlocks or panics.
#[test]
fn prop_concurrent_cancellations_no_deadlock() {
    proptest!(ProptestConfig::with_cases(10), |(
        num_concurrent in 2usize..5,
        poll_interval_ms in 20u64..50,
    )| {
        let rt = tokio::runtime::Builder::new_multi_thread()
            .worker_threads(2)
            .enable_all()
            .build()
            .unwrap();

        rt.block_on(async {
            let mock_server = MockServer::start().await;
            let config = test_config(&mock_server.uri(), poll_interval_ms);

            // Setup mocks for all request IDs
            for i in 0..num_concurrent {
                let request_id = format!("req_concurrent_{}", i);
                Mock::given(method("GET"))
                    .and(path(format!("/v1/verify/status/{}", request_id)))
                    .respond_with(
                        ResponseTemplate::new(200).set_body_json(serde_json::json!({
                            "state": "pending",
                            "request_id": request_id,
                            "expires_at": "2026-01-16T11:00:00Z"
                        }))
                    )
                    .mount(&mock_server)
                    .await;
            }

            let client = Arc::new(
                ClientBuilder::new()
                    .with_config(config)
                    .build()
                    .expect("Client should build")
            );

            // Spawn multiple polling tasks with different cancellation times
            let mut handles = Vec::new();
            for i in 0..num_concurrent {
                let client = Arc::clone(&client);
                let request_id = RequestId::new(format!("req_concurrent_{}", i));
                let cancel_after = Duration::from_millis(poll_interval_ms * (i as u64 + 1));

                handles.push(tokio::spawn(async move {
                    let poll_future = client.poll_for_completion(&request_id);
                    let _ = tokio::time::timeout(cancel_after, poll_future).await;
                }));
            }

            // Wait for all tasks with a generous timeout to detect deadlocks
            let overall_timeout = Duration::from_secs(5);

            // Join all handles manually
            let all_complete = tokio::time::timeout(overall_timeout, async {
                for handle in handles {
                    let _ = handle.await;
                }
            })
            .await;

            // Should not timeout (no deadlock)
            assert!(all_complete.is_ok(), "Should not deadlock with concurrent cancellations");
        });
    });
}

/// Property: Dropping poll_request mid-flight should not corrupt state.
#[test]
fn prop_poll_request_drop_is_safe() {
    proptest!(ProptestConfig::with_cases(50), |(
        request_id in arb_request_id(),
    )| {
        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .unwrap();

        rt.block_on(async {
            let mock_server = MockServer::start().await;
            let config = test_config(&mock_server.uri(), 50);

            // Add a small delay to the response to increase chance of mid-flight drop
            Mock::given(method("GET"))
                .and(path(format!("/v1/verify/status/{}", request_id)))
                .respond_with(
                    ResponseTemplate::new(200)
                        .set_delay(Duration::from_millis(20))
                        .set_body_json(serde_json::json!({
                            "state": "pending",
                            "request_id": request_id,
                            "expires_at": "2026-01-16T11:00:00Z"
                        }))
                )
                .mount(&mock_server)
                .await;

            let client = ClientBuilder::new()
                .with_config(config)
                .build()
                .expect("Client should build");

            let request_id_obj = RequestId::new(request_id.clone());

            // Start a poll and immediately cancel it
            let poll_future = client.poll_request(&request_id_obj);

            // Use select to race against a very short timeout
            let _result = tokio::select! {
                biased;
                _ = tokio::time::sleep(Duration::from_millis(5)) => {
                    None // Cancelled
                }
                r = poll_future => {
                    Some(r)
                }
            };

            // Whether cancelled or completed, client should still work
            let second_poll = client.poll_request(&request_id_obj).await;
            assert!(second_poll.is_ok(), "Client should work after potential cancellation");
        });
    });
}

/// Property: Client can be used normally after any sequence of cancellations
#[test]
fn prop_client_usable_after_cancellation_sequence() {
    proptest!(ProptestConfig::with_cases(15), |(
        cancellation_sequence in prop::collection::vec(arb_polls_before_cancel(), 1..5),
    )| {
        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .unwrap();

        rt.block_on(async {
            let mock_server = MockServer::start().await;
            let config = test_config(&mock_server.uri(), 30);

            let fulfilled_id = "req_will_fulfill";
            let poll_id = "req_for_cancel";

            // Mock for cancelled requests
            Mock::given(method("GET"))
                .and(path(format!("/v1/verify/status/{}", poll_id)))
                .respond_with(
                    ResponseTemplate::new(200).set_body_json(serde_json::json!({
                        "state": "pending",
                        "request_id": poll_id,
                        "expires_at": "2026-01-16T11:00:00Z"
                    }))
                )
                .mount(&mock_server)
                .await;

            // Mock for fulfilled request (IDP format: state=verified, vp_token)
            Mock::given(method("GET"))
                .and(path(format!("/v1/verify/status/{}", fulfilled_id)))
                .respond_with(
                    ResponseTemplate::new(200).set_body_json(serde_json::json!({
                        "state": "verified",
                        "request_id": fulfilled_id,
                        "vp_token": [create_mock_sdjwt()],
                        "expires_at": "2026-01-16T12:00:00Z"
                    }))
                )
                .mount(&mock_server)
                .await;

            let client = ClientBuilder::new()
                .with_config(config)
                .build()
                .expect("Client should build");

            let poll_request_id = RequestId::new(poll_id.to_string());

            // Execute cancellation sequence
            for polls_before_cancel in cancellation_sequence {
                let poll_future = client.poll_for_completion(&poll_request_id);
                let cancel_time = Duration::from_millis(30 * (polls_before_cancel as u64 + 1));
                let _ = tokio::time::timeout(cancel_time, poll_future).await;
            }

            // After all cancellations, client should still work
            let fulfilled_request_id = RequestId::new(fulfilled_id.to_string());
            let result = client.poll_request(&fulfilled_request_id).await;

            assert!(result.is_ok(), "Client should work after cancellation sequence");
            match result.unwrap() {
                RequestStatus::Fulfilled { .. } => {}
                s => panic!("Expected Fulfilled, got {:?}", s),
            }
        });
    });
}
