// Security coverage for encrypted paste payloads.
//
// Scope: validate that clipboard RPC payloads are encrypted, authenticated,
// and fail closed under key/nonce/ciphertext tampering.
// Targets: `client/src/paste.rs`, `server/src/paste.rs`, and shared paste key
// decoding.
// Why: paste is effectively remote keyboard injection, so corrupted or
// unauthenticated payloads should never fall through to HID writes.

use lesavka_client::paste::build_paste_request;
use lesavka_server::paste::decrypt;
use serial_test::serial;
use temp_env::with_vars;
use tempfile::tempdir;

const KEY_A: &str = "hex:00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";
const KEY_B: &str = "hex:ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100";

#[test]
#[serial]
fn encrypted_paste_round_trips_only_when_keys_match() {
    with_vars(
        [
            ("LESAVKA_PASTE_KEY", Some(KEY_A)),
            ("LESAVKA_PASTE_KEY_FILE", None::<&str>),
        ],
        || {
            let req = build_paste_request("secret paste").expect("build request");
            assert!(req.encrypted);
            assert_eq!(decrypt(&req).expect("decrypt request"), "secret paste");
        },
    );
}

#[test]
#[serial]
fn tampered_ciphertext_is_rejected() {
    with_vars(
        [
            ("LESAVKA_PASTE_KEY", Some(KEY_A)),
            ("LESAVKA_PASTE_KEY_FILE", None::<&str>),
        ],
        || {
            let mut req = build_paste_request("keep this private").expect("build request");
            req.data[0] ^= 0x55;
            let err = decrypt(&req).expect_err("tampered ciphertext must fail auth");
            assert!(err.to_string().contains("paste decrypt failed"));
        },
    );
}

#[test]
#[serial]
fn tampered_nonce_is_rejected() {
    with_vars(
        [
            ("LESAVKA_PASTE_KEY", Some(KEY_A)),
            ("LESAVKA_PASTE_KEY_FILE", None::<&str>),
        ],
        || {
            let mut req = build_paste_request("nonce protected").expect("build request");
            req.nonce[0] ^= 0x33;
            let err = decrypt(&req).expect_err("tampered nonce must fail auth");
            assert!(err.to_string().contains("paste decrypt failed"));
        },
    );
}

#[test]
#[serial]
fn every_nonce_byte_is_authenticated() {
    with_vars(
        [
            ("LESAVKA_PASTE_KEY", Some(KEY_A)),
            ("LESAVKA_PASTE_KEY_FILE", None::<&str>),
        ],
        || {
            let original = build_paste_request("nonce sweep").expect("build request");
            for index in 0..original.nonce.len() {
                let mut req = original.clone();
                req.nonce[index] ^= 0x5a;
                let err = decrypt(&req).expect_err("mutated nonce byte must fail auth");
                assert!(
                    err.to_string().contains("paste decrypt failed"),
                    "nonce byte {index} should fail closed, got {err:#}"
                );
            }
        },
    );
}

#[test]
#[serial]
fn truncated_ciphertexts_fail_without_leaking_plaintext() {
    with_vars(
        [
            ("LESAVKA_PASTE_KEY", Some(KEY_A)),
            ("LESAVKA_PASTE_KEY_FILE", None::<&str>),
        ],
        || {
            let original = build_paste_request("truncation protected").expect("build request");
            for len in [0, 1, 2, 8, 15, original.data.len().saturating_sub(1)] {
                let mut req = original.clone();
                req.data.truncate(len);
                let err = decrypt(&req).expect_err("truncated ciphertext must fail auth");
                let rendered = format!("{err:#}");
                assert!(rendered.contains("paste decrypt failed"));
                assert!(!rendered.contains("truncation protected"));
            }
        },
    );
}

#[test]
#[serial]
fn malformed_nonce_is_rejected_without_panicking() {
    with_vars(
        [
            ("LESAVKA_PASTE_KEY", Some(KEY_A)),
            ("LESAVKA_PASTE_KEY_FILE", None::<&str>),
        ],
        || {
            let mut req = build_paste_request("bad nonce").expect("build request");
            req.nonce.truncate(3);
            let err = decrypt(&req).expect_err("malformed nonce must fail validation");
            assert!(err.to_string().contains("12 bytes"));
        },
    );
}

#[test]
#[serial]
fn missing_key_blocks_client_request_creation() {
    let dir = tempdir().expect("empty home");
    with_vars(
        [
            ("LESAVKA_PASTE_KEY", None::<&str>),
            ("LESAVKA_PASTE_KEY_FILE", None::<&str>),
            ("HOME", Some(dir.path().to_string_lossy().as_ref())),
        ],
        || {
            let err = build_paste_request("no key").expect_err("missing key must fail");
            let rendered = format!("{err:#}");
            assert!(
                rendered.contains("LESAVKA_PASTE_KEY") || rendered.contains("paste key file"),
                "missing key error should point at the env var or default key file, got: {rendered}"
            );
        },
    );
}

#[test]
#[serial]
fn bad_server_key_rejects_client_payload() {
    let req = with_vars(
        [
            ("LESAVKA_PASTE_KEY", Some(KEY_A)),
            ("LESAVKA_PASTE_KEY_FILE", None::<&str>),
        ],
        || build_paste_request("wrong key").expect("build request"),
    );

    with_vars(
        [
            ("LESAVKA_PASTE_KEY", Some(KEY_B)),
            ("LESAVKA_PASTE_KEY_FILE", None::<&str>),
        ],
        || {
            let err = decrypt(&req).expect_err("wrong key must fail auth");
            let rendered = format!("{err:#}");
            assert!(rendered.contains("paste decrypt failed"));
            assert!(!rendered.contains(KEY_A));
            assert!(!rendered.contains("wrong key"));
        },
    );
}

#[test]
#[serial]
fn plaintext_downgrade_is_rejected() {
    with_vars(
        [
            ("LESAVKA_PASTE_KEY", Some(KEY_A)),
            ("LESAVKA_PASTE_KEY_FILE", None::<&str>),
        ],
        || {
            let mut req = build_paste_request("plaintext downgrade").expect("build request");
            req.encrypted = false;
            let err = decrypt(&req).expect_err("plaintext must not be accepted");
            assert!(err.to_string().contains("encrypted"));
        },
    );
}
