use chrono::{DateTime, Utc};
use serde_json::{json, Value};
use tsafe_core::age_crypto;
use tsafe_core::errors::SafeError;
use tsafe_core::run_evidence;
use tsafe_mobile_core::envelopes::{
    assemble_encrypted_qr_chunks, classify_desktop_vault_profile,
    parse_desktop_vault_profile_envelope, parse_encrypted_qr_payload_envelope,
    parse_enrollment_response_envelope, parse_enrollment_start_envelope,
    parse_mobile_profile_return_envelope, parse_public_qr_receive_envelope,
    parse_qr_receive_envelope, parse_qr_share_payload_envelope,
    validate_enrollment_response_matches_start,
    validate_mobile_profile_return_matches_desktop_vault, MobileDesktopVaultProfileAction,
};

const NOW: &str = "2026-05-24T12:00:00Z";
const CREATED_AT: &str = "2026-05-24T11:59:00Z";
const EXPIRES_AT: &str = "2026-05-24T12:10:00Z";
const EXPIRED_AT: &str = "2026-05-24T11:59:30Z";
const CIPHERTEXT_B64: &str = "c2hhcmVkLXNlY3JldA";

#[test]
fn parses_enrollment_start_envelope_without_public_secret_material() {
    let envelope = parse_enrollment_start_envelope(json_bytes(enrollment_start()), now()).unwrap();

    assert_eq!(envelope.version, "1.0");
    assert_eq!(envelope.session_id, "enroll-1");
    assert_eq!(envelope.profile, "default");
    assert_eq!(envelope.repo.owner, "0ryant");
    assert!(envelope
        .requested_capabilities
        .contains(&"vault_pull".to_string()));
    let vault_identity = envelope.vault_identity.as_ref().unwrap();
    assert_eq!(vault_identity.vault_id, "tsafe-vault-win-default-001");
    assert_eq!(vault_identity.origin_platform, "windows");
}

#[test]
fn parses_enrollment_response_with_public_age_recipient() {
    let (_, recipient) = age_crypto::generate_identity();
    let mut raw = enrollment_response(&recipient);
    raw["nonce"] = json!("challenge-1");

    let envelope = parse_enrollment_response_envelope(json_bytes(raw), now()).unwrap();

    assert_eq!(envelope.session_id, "enroll-1");
    assert_eq!(envelope.mobile_recipient, recipient);
    assert_eq!(envelope.profile, "default");
    assert_eq!(
        envelope.vault_identity.as_ref().unwrap().vault_fingerprint,
        "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    );
}

#[test]
fn parses_desktop_vault_profile_and_deduplicates_known_vaults() {
    let envelope =
        parse_desktop_vault_profile_envelope(json_bytes(desktop_vault_profile("macos")), now())
            .unwrap();

    assert_eq!(envelope.schema, "tsafe/mobile/desktop-vault-profile/v1");
    assert_eq!(envelope.desktop_platform, "macos");
    assert_eq!(envelope.profile, "default");
    assert_eq!(envelope.vault_id, "vault-mac-default-20260525");
    assert_eq!(
        envelope.profile_binding_key(),
        "0ryant/tsafe#vault-mac-default-20260525"
    );

    let create = classify_desktop_vault_profile(&envelope, &[]);
    assert_eq!(
        create.action,
        MobileDesktopVaultProfileAction::CreateProfile
    );

    let existing =
        classify_desktop_vault_profile(&envelope, std::slice::from_ref(&envelope.vault_id));
    assert_eq!(
        existing.action,
        MobileDesktopVaultProfileAction::ExistingProfile
    );
}

#[test]
fn validates_mobile_profile_return_matches_desktop_vault() {
    let (_, recipient) = age_crypto::generate_identity();
    let start =
        parse_desktop_vault_profile_envelope(json_bytes(desktop_vault_profile("linux")), now())
            .unwrap();
    let response =
        parse_mobile_profile_return_envelope(json_bytes(mobile_profile_return(&recipient)), now())
            .unwrap();

    validate_mobile_profile_return_matches_desktop_vault(&start, &response).unwrap();

    let mut wrong_vault = mobile_profile_return(&recipient);
    wrong_vault["vault_id"] = json!("vault-windows-default-20260525");
    let wrong_vault = parse_mobile_profile_return_envelope(json_bytes(wrong_vault), now()).unwrap();
    assert_invalid(
        validate_mobile_profile_return_matches_desktop_vault(&start, &wrong_vault),
        "vault mismatch",
    );
}

#[test]
fn rejects_bad_desktop_vault_profile_fields() {
    let mut bad_platform = desktop_vault_profile("ios");
    assert_invalid(
        parse_desktop_vault_profile_envelope(json_bytes(bad_platform.clone()), now()),
        "desktop_platform",
    );

    bad_platform["desktop_platform"] = json!("windows");
    bad_platform["vault_fingerprint"] = json!("sha256:nothex");
    assert_invalid(
        parse_desktop_vault_profile_envelope(json_bytes(bad_platform), now()),
        "vault_fingerprint",
    );
}

#[test]
fn parses_public_qr_receive_envelope() {
    let (_, recipient) = age_crypto::generate_identity();
    let mut raw = public_receive(&recipient);
    raw["session_id"] = json!("share-1");

    let envelope = parse_public_qr_receive_envelope(json_bytes(raw), now()).unwrap();

    assert_eq!(envelope.session_id, "share-1");
    assert_eq!(envelope.public_key_id, "epk-1");
    assert_eq!(envelope.recipient_public_key, recipient);
}

#[test]
fn rejects_expired_public_envelopes() {
    let mut raw = enrollment_start();
    raw["expires_at"] = json!(EXPIRED_AT);

    assert_invalid(
        parse_enrollment_start_envelope(json_bytes(raw), now()),
        "expired",
    );
}

#[test]
fn rejects_unknown_major_version() {
    let mut raw = enrollment_start();
    raw["version"] = json!("2.0");

    assert_invalid(
        parse_enrollment_start_envelope(json_bytes(raw), now()),
        "unsupported major version",
    );
}

#[test]
fn rejects_enrollment_response_wrong_profile_binding() {
    let (_, recipient) = age_crypto::generate_identity();
    let start = parse_enrollment_start_envelope(json_bytes(enrollment_start()), now()).unwrap();
    let mut raw = enrollment_response(&recipient);
    raw["nonce"] = json!("challenge-1");
    raw["profile"] = json!("other-profile");
    raw["vault_identity"]["profile"] = json!("other-profile");
    let response = parse_enrollment_response_envelope(json_bytes(raw), now()).unwrap();

    assert_invalid(
        validate_enrollment_response_matches_start(&start, &response),
        "profile mismatch",
    );
}

#[test]
fn rejects_enrollment_response_nonce_and_flow_mismatch() {
    let (_, recipient) = age_crypto::generate_identity();
    let start = parse_enrollment_start_envelope(json_bytes(enrollment_start()), now()).unwrap();

    let mut wrong_nonce = enrollment_response(&recipient);
    wrong_nonce["nonce"] = json!("different-challenge");
    let response = parse_enrollment_response_envelope(json_bytes(wrong_nonce), now()).unwrap();
    assert_invalid(
        validate_enrollment_response_matches_start(&start, &response),
        "nonce mismatch",
    );

    let mut wrong_flow = enrollment_response(&recipient);
    wrong_flow["session_id"] = json!("enroll-2");
    wrong_flow["nonce"] = json!("challenge-1");
    let response = parse_enrollment_response_envelope(json_bytes(wrong_flow), now()).unwrap();
    assert_invalid(
        validate_enrollment_response_matches_start(&start, &response),
        "flow binding mismatch",
    );

    let mut wrong_vault = enrollment_response(&recipient);
    wrong_vault["vault_identity"]["vault_id"] = json!("tsafe-vault-linux-other-001");
    let response = parse_enrollment_response_envelope(json_bytes(wrong_vault), now()).unwrap();
    assert_invalid(
        validate_enrollment_response_matches_start(&start, &response),
        "vault identity mismatch",
    );
}

#[test]
fn rejects_missing_nonce_session_or_profile() {
    for missing in ["nonce", "session_id", "profile"] {
        let (_, recipient) = age_crypto::generate_identity();
        let mut raw = public_receive(&recipient);
        raw.as_object_mut().unwrap().remove(missing);

        assert_invalid(
            parse_public_qr_receive_envelope(json_bytes(raw), now()),
            missing,
        );
    }
}

#[test]
fn rejects_secret_like_material_in_public_envelopes() {
    let mut raw = enrollment_start();
    raw["display_name"] = json!("AGE-SECRET-KEY-1Q8EXAMPLE");

    assert_invalid(
        parse_enrollment_start_envelope(json_bytes(raw), now()),
        "secret-like material",
    );

    let mut receive = public_receive(&age_recipient_fixture());
    receive["github_token"] = json!("fixture-only-no-real-token");
    assert_invalid(
        parse_public_qr_receive_envelope(json_bytes(receive), now()),
        "secret-like material",
    );
}

#[test]
fn contract_qr_envelopes_reject_bad_frame_and_digest() {
    let mut receive = qr_receive_contract();
    receive["frame"]["count"] = json!(1);
    receive["frame"]["index"] = json!(1);
    assert_invalid(
        parse_qr_receive_envelope(json_bytes(receive), now()),
        "frame index",
    );

    let mut payload = qr_share_payload_contract();
    payload["digest"]["value"] = json!("not-a-digest");
    assert_invalid(
        parse_qr_share_payload_envelope(json_bytes(payload), now()),
        "digest.value",
    );

    let mut payload = qr_share_payload_contract();
    payload["frame"]["payload_digest_sha256"] =
        json!("z333333333333333333333333333333333333333333333333333333333333333");
    assert_invalid(
        parse_qr_share_payload_envelope(json_bytes(payload), now()),
        "frame.payload_digest_sha256",
    );
}

#[test]
fn encrypted_payload_rejects_digest_shape_and_mismatch() {
    let mut malformed = encrypted_payload();
    malformed["digest"] = json!("not-a-sha256");
    assert_invalid(
        parse_encrypted_qr_payload_envelope(json_bytes(malformed), now()),
        "digest",
    );

    let mut mismatch = encrypted_payload();
    mismatch["digest"] =
        json!("blake3:0000000000000000000000000000000000000000000000000000000000000000");
    assert_invalid(
        parse_encrypted_qr_payload_envelope(json_bytes(mismatch), now()),
        "digest mismatch",
    );
}

#[test]
fn assembles_ordered_encrypted_qr_chunks() {
    let chunks = encrypted_chunks();
    let refs = chunks.iter().map(Vec::as_slice).collect::<Vec<_>>();
    let payload = assemble_encrypted_qr_chunks(&refs, now()).unwrap();

    assert_eq!(payload.session_id, "share-1");
    assert_eq!(payload.profile, "default");
    assert_eq!(payload.digest, ciphertext_digest());
}

#[test]
fn rejects_bad_chunk_frame_count_and_order() {
    let mut bad_count = encrypted_chunks();
    let mut first: Value = serde_json::from_slice(&bad_count[0]).unwrap();
    first["frame_count"] = json!(1);
    bad_count[0] = json_bytes(first);
    let bad_count_refs = bad_count.iter().map(Vec::as_slice).collect::<Vec<_>>();
    assert_invalid(
        assemble_encrypted_qr_chunks(&bad_count_refs, now()),
        "frame count",
    );

    let mut out_of_order = encrypted_chunks();
    out_of_order.swap(0, 1);
    let out_of_order_refs = out_of_order.iter().map(Vec::as_slice).collect::<Vec<_>>();
    assert_invalid(
        assemble_encrypted_qr_chunks(&out_of_order_refs, now()),
        "frame order",
    );
}

fn enrollment_start() -> Value {
    json!({
        "schema": "tsafe/mobile/enrollment-start/v1",
        "version": "1.0",
        "session_id": "enroll-1",
        "created_at": CREATED_AT,
        "expires_at": EXPIRES_AT,
        "desktop_public_key": "desktop-ed25519-pubkey",
        "nonce": "challenge-1",
        "repo": {
            "owner": "0ryant",
            "name": "tsafe"
        },
        "branch": "main",
        "profile": "default",
        "team_keys_path": ".tsafe/team-keys.json",
        "vault_path": ".tsafe/vaults/default.vault",
        "vault_identity": {
            "vault_id": "tsafe-vault-win-default-001",
            "vault_fingerprint": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
            "profile": "default",
            "origin_device_id": "desktop-win-nucbox",
            "origin_platform": "windows",
            "label": "Windows default vault"
        },
        "requested_capabilities": ["vault_pull", "totp", "reveal"],
        "display_name": "Ryan desktop"
    })
}

fn enrollment_response(recipient: &str) -> Value {
    json!({
        "schema": "tsafe/mobile/enrollment-response/v1",
        "version": "1.0",
        "session_id": "enroll-1",
        "created_at": CREATED_AT,
        "expires_at": EXPIRES_AT,
        "nonce": "challenge-1",
        "repo": {
            "owner": "0ryant",
            "name": "tsafe"
        },
        "profile": "default",
        "vault_identity": {
            "vault_id": "tsafe-vault-win-default-001",
            "vault_fingerprint": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
            "profile": "default",
            "origin_device_id": "desktop-win-nucbox",
            "origin_platform": "windows",
            "label": "Windows default vault"
        },
        "mobile_recipient": recipient,
        "device_label": "iPhone"
    })
}

fn desktop_vault_profile(platform: &str) -> Value {
    json!({
        "schema": "tsafe/mobile/desktop-vault-profile/v1",
        "version": "1.0",
        "session_id": "desktop-vault-1",
        "created_at": CREATED_AT,
        "expires_at": EXPIRES_AT,
        "nonce": "challenge-1",
        "repo": {
            "owner": "0ryant",
            "name": "tsafe"
        },
        "branch": "main",
        "profile": "default",
        "team_keys_path": ".tsafe/team-keys.json",
        "vault_path": ".tsafe/vaults/default.vault",
        "desktop_platform": platform,
        "vault_id": "vault-mac-default-20260525",
        "vault_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
        "desktop_public_key": "desktop-ed25519-pubkey",
        "requested_capabilities": [
            "vault_ciphertext_pull",
            "mobile_profile_return_qr"
        ],
        "display_name": "Ryan desktop"
    })
}

fn mobile_profile_return(recipient: &str) -> Value {
    json!({
        "schema": "tsafe/mobile/mobile-profile-return/v1",
        "version": "1.0",
        "session_id": "desktop-vault-1",
        "created_at": CREATED_AT,
        "expires_at": EXPIRES_AT,
        "nonce": "challenge-1",
        "repo": {
            "owner": "0ryant",
            "name": "tsafe"
        },
        "profile": "default",
        "vault_id": "vault-mac-default-20260525",
        "mobile_profile_id": "mobile-profile-ios-default-20260525",
        "mobile_recipient": recipient,
        "device_label": "iPhone"
    })
}

fn public_receive(recipient: &str) -> Value {
    json!({
        "schema": "tsafe/mobile/qr-receive/v1",
        "version": "1.0",
        "session_id": "share-1",
        "created_at": CREATED_AT,
        "expires_at": EXPIRES_AT,
        "profile": "default",
        "nonce": "challenge-1",
        "public_key_id": "epk-1",
        "recipient_public_key": recipient,
        "requested_capabilities": ["encrypted_qr_receive"]
    })
}

fn age_recipient_fixture() -> String {
    let (_, recipient) = age_crypto::generate_identity();
    recipient
}

fn qr_receive_contract() -> Value {
    json!({
        "schema": "tsafe/mobile/qr-receive-envelope/v1",
        "version": {
            "major": 1,
            "minor": 0
        },
        "flow_id": "46a53da7-a80e-4456-b765-348cf18843d4",
        "created_at": CREATED_AT,
        "expires_at": EXPIRES_AT,
        "challenge": "receiveChallengeExample001",
        "recipient_ephemeral_key": {
            "key_id": "recv-key-example-001",
            "algorithm": "x25519",
            "public_key": "ReceiverEphemeralPublicKeyExample0012345"
        },
        "supported_algorithms": {
            "kem": ["x25519-hkdf-sha256"],
            "aead": ["xchacha20-poly1305"],
            "digest": ["sha256"]
        },
        "max_frame": {
            "count": 16,
            "bytes_per_frame": 1024
        },
        "frame": {
            "encoding": "json",
            "index": 0,
            "count": 1
        },
        "digest": {
            "algorithm": "sha256",
            "value": "2222222222222222222222222222222222222222222222222222222222222222"
        }
    })
}

fn qr_share_payload_contract() -> Value {
    json!({
        "schema": "tsafe/mobile/qr-share-payload/v1",
        "version": {
            "major": 1,
            "minor": 0
        },
        "flow_id": "46a53da7-a80e-4456-b765-348cf18843d4",
        "created_at": CREATED_AT,
        "expires_at": EXPIRES_AT,
        "recipient_key_id": "recv-key-example-001",
        "challenge": "receiveChallengeExample001",
        "encryption": {
            "kem": "x25519-hkdf-sha256",
            "aead": "xchacha20-poly1305",
            "aead_nonce": "aeadNonceExample001",
            "sender_ephemeral_public_key": "SenderEphemeralPublicKeyExample00123456"
        },
        "ciphertext": "EncryptedCiphertextFixtureOnly0000000000000000000000000000000000000000000000",
        "secret_metadata": {
            "key_name_suggestion": "shared-login",
            "namespace_suggestion": "received",
            "content_kind": "password"
        },
        "frame": {
            "encoding": "json",
            "index": 0,
            "count": 1,
            "payload_digest_sha256": "3333333333333333333333333333333333333333333333333333333333333333"
        },
        "digest": {
            "algorithm": "sha256",
            "value": "4444444444444444444444444444444444444444444444444444444444444444"
        }
    })
}

fn encrypted_payload() -> Value {
    json!({
        "schema": "tsafe/mobile/qr-encrypted-payload/v1",
        "version": "1.0",
        "session_id": "share-1",
        "created_at": CREATED_AT,
        "expires_at": EXPIRES_AT,
        "profile": "default",
        "nonce": "challenge-1",
        "recipient_public_key_id": "epk-1",
        "sender_protocol": "tsafe-mobile/0.1",
        "aead_nonce": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
        "ciphertext": CIPHERTEXT_B64,
        "digest": ciphertext_digest(),
        "frame_count": 1,
        "frame_index": 0,
        "key_name_suggestion": "WIFI_PASSWORD"
    })
}

fn encrypted_payload_json() -> String {
    let digest = ciphertext_digest();
    format!(
        "{{\"schema\":\"tsafe/mobile/qr-encrypted-payload/v1\",\"version\":\"1.0\",\"session_id\":\"share-1\",\"created_at\":\"{CREATED_AT}\",\"expires_at\":\"{EXPIRES_AT}\",\"profile\":\"default\",\"nonce\":\"challenge-1\",\"recipient_public_key_id\":\"epk-1\",\"sender_protocol\":\"tsafe-mobile/0.1\",\"aead_nonce\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",\"ciphertext\":\"{CIPHERTEXT_B64}\",\"digest\":\"{digest}\",\"frame_count\":1,\"frame_index\":0}}"
    )
}

fn encrypted_chunks() -> Vec<Vec<u8>> {
    let payload = encrypted_payload_json();
    let parts = [&payload[..160], &payload[160..320], &payload[320..]];
    parts
        .iter()
        .enumerate()
        .map(|(index, chunk)| {
            json_bytes(json!({
                "schema": "tsafe/mobile/qr-encrypted-chunk/v1",
                "version": "1.0",
                "session_id": "share-1",
                "created_at": CREATED_AT,
                "expires_at": EXPIRES_AT,
                "profile": "default",
                "nonce": "challenge-1",
                "frame_count": parts.len(),
                "frame_index": index,
                "payload_digest": payload_digest(&payload),
                "chunk": chunk
            }))
        })
        .collect()
}

fn ciphertext_digest() -> String {
    run_evidence::blake3_hash(CIPHERTEXT_B64)
}

fn payload_digest(payload: &str) -> String {
    run_evidence::blake3_hash(payload)
}

fn json_bytes(value: Value) -> Vec<u8> {
    serde_json::to_vec(&value).unwrap()
}

fn now() -> DateTime<Utc> {
    DateTime::parse_from_rfc3339(NOW)
        .unwrap()
        .with_timezone(&Utc)
}

fn assert_invalid<T>(result: Result<T, SafeError>, expected: &str) {
    match result {
        Err(SafeError::InvalidVault { reason }) => {
            assert!(
                reason.contains(expected),
                "expected reason to contain {expected:?}, got {reason:?}"
            );
        }
        Err(other) => panic!("expected InvalidVault, got {other:?}"),
        Ok(_) => panic!("expected validation failure"),
    }
}
