// SPDX-License-Identifier: GPL-3-0-or-later
// Copyright (c) 2025 Opinsys Oy
// Copyright (c) 2025 Jarkko Sakkinen

#![deny(clippy::all)]
#![deny(clippy::pedantic)]

use openssl::{
    ec::{EcGroup, EcKey},
    nid::Nid,
    rsa::Rsa,
};
use rstest::rstest;
use std::path::Path;
use tempfile::TempDir;

const TPM2SH_PATH: &str = env!("CARGO_BIN_EXE_tpm2sh");
const SEALED_DATA: &str = "deadbeef";

fn tpm2sh(cache_dir: &Path, args: &[&str]) -> duct::Expression {
    duct::cmd(TPM2SH_PATH, args).env("TPM2SH_CACHE_PATH", cache_dir)
}

#[test]
fn integration() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let cache_path = temp_dir.path();

    tpm2sh(cache_path, &["delete", "vtpm:*"]).run().unwrap();

    let primary_handle = tpm2sh(
        cache_path,
        &["create-primary", "-H", "owner", "ecc-nist-p256:sha256"],
    )
    .read()
    .unwrap();

    let primary_handle = primary_handle.trim();
    assert!(primary_handle.starts_with("vtpm:"),);
    eprintln!("Primary handle: {primary_handle}");

    let policy_str = format!("secret({primary_handle})");

    let create_args = [
        "create",
        primary_handle,
        "keyedhash:sha256",
        "--data",
        SEALED_DATA,
        "--policy",
        &policy_str,
    ];

    let sealed_handle = tpm2sh(cache_path, &create_args)
        .pipe(tpm2sh(cache_path, &["load"]))
        .read()
        .unwrap();
    let sealed_handle = sealed_handle.trim();

    println!("Sealed secret handle: {sealed_handle}");

    let unseal_output = tpm2sh(cache_path, &["unseal", "--hex", sealed_handle])
        .read()
        .unwrap();

    assert_eq!(unseal_output.trim(), SEALED_DATA);

    let create_pcr_args = [
        "create",
        primary_handle,
        "keyedhash:sha256",
        "--data",
        SEALED_DATA,
        "--policy",
        "pcr(sha256:7) or pcr(sha256:15)",
    ];

    let sealed_handle_pcr = tpm2sh(cache_path, &create_pcr_args)
        .pipe(tpm2sh(cache_path, &["load"]))
        .read()
        .unwrap();
    let sealed_handle_pcr = sealed_handle_pcr.trim();

    println!("Sealed PCRs handle: {sealed_handle_pcr}");

    let unseal_pcr_output = tpm2sh(cache_path, &["unseal", "--hex", sealed_handle_pcr])
        .read()
        .unwrap();

    assert_eq!(unseal_pcr_output.trim(), SEALED_DATA);

    let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();
    let ec_key = EcKey::generate(&group).unwrap();
    let ec_pem = ec_key.private_key_to_pem().unwrap();
    let ecc_handle = tpm2sh(cache_path, &["convert", primary_handle])
        .stdin_bytes(ec_pem)
        .pipe(tpm2sh(cache_path, &["load"]))
        .read()
        .unwrap();
    let ecc_handle = ecc_handle.trim();

    assert!(ecc_handle.starts_with("vtpm:"),);
    println!("External ECC handle: {ecc_handle}");

    let rsa = Rsa::generate(2048).unwrap();
    let rsa_pem = rsa.private_key_to_pem().unwrap();
    let rsa_handle = tpm2sh(cache_path, &["convert", primary_handle])
        .stdin_bytes(rsa_pem)
        .pipe(tpm2sh(cache_path, &["load"]))
        .read()
        .unwrap();
    let rsa_handle = rsa_handle.trim();

    assert!(rsa_handle.starts_with("vtpm:"),);
    println!("External RSA handle: {rsa_handle}");

    let delete_output = tpm2sh(cache_path, &["delete", "vtpm:*"]).read().unwrap();

    assert!(delete_output.contains(primary_handle.trim().strip_prefix("vtpm:").unwrap()),);
    assert!(delete_output.contains(sealed_handle.trim().strip_prefix("vtpm:").unwrap()),);
}

/// Tests `convert` and `load` interoperability with keys generated by the external
/// `openssl` binary.
#[rstest]
#[case::rsa("genrsa -out private.pem 2048")]
#[case::ecc("ecparam -name prime256v1 -genkey -noout -out private.pem")]
fn test_convert_openssl(#[case] openssl_args: &str) {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let cache_path = temp_dir.path();
    let private_key_path = temp_dir.path().join("private.pem");

    let args: Vec<&str> = openssl_args.split_whitespace().collect();
    let openssl_status = std::process::Command::new("openssl")
        .args(&args)
        .current_dir(temp_dir.path())
        .status()
        .expect("Failed to execute openssl");
    assert!(openssl_status.success());

    let primary_handle = tpm2sh(
        cache_path,
        &["create-primary", "-H", "owner", "ecc-nist-p256:sha256"],
    )
    .read()
    .expect("Failed to create primary key");
    let primary_handle = primary_handle.trim();

    let convert_args = [
        "convert",
        primary_handle,
        "-I",
        private_key_path.to_str().unwrap(),
    ];

    let load_output = tpm2sh(cache_path, &convert_args)
        .pipe(tpm2sh(cache_path, &["load"]))
        .read()
        .expect("Failed to convert and load key");

    let loaded_handle = load_output.trim();
    assert!(loaded_handle.starts_with("vtpm:"));
}

#[test]
fn test_password_auth() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let cache_path = temp_dir.path();

    let auth_primary = hex::encode("primary");
    let auth_native = hex::encode("native");
    let auth_external = hex::encode("external");

    let primary_handle = tpm2sh(
        cache_path,
        &[
            "create-primary",
            "-H",
            "owner",
            "ecc-nist-p256:sha256",
            "--password",
            &auth_primary,
        ],
    )
    .read()
    .expect("Failed to create primary");
    let primary_handle = primary_handle.trim();

    let native_child = tpm2sh(
        cache_path,
        &[
            "create",
            primary_handle,
            "keyedhash:sha256",
            "--data",
            SEALED_DATA,
            "--auth",
            &auth_primary,
            "--password",
            &auth_native,
        ],
    )
    .pipe(tpm2sh(cache_path, &["load", "--auth", &auth_primary]))
    .read()
    .expect("Failed to create native child");
    let native_child = native_child.trim();

    tpm2sh(
        cache_path,
        &["unseal", native_child, "--auth", &auth_native],
    )
    .run()
    .expect("Failed to unseal with correct auth");

    let rsa = Rsa::generate(2048).unwrap();
    let rsa_pem = rsa.private_key_to_pem().unwrap();

    let ext_handle = tpm2sh(
        cache_path,
        &[
            "convert",
            primary_handle,
            "--auth",
            &auth_primary,
            "--password",
            &auth_external,
        ],
    )
    .stdin_bytes(rsa_pem)
    .pipe(tpm2sh(cache_path, &["load", "--auth", &auth_primary]))
    .read()
    .expect("Failed to import external key");
    let ext_handle = ext_handle.trim();

    println!("Imported external key handle: {ext_handle}");
}

#[test]
fn test_deep_hierarchy_recursion() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let cache_path = temp_dir.path();

    let l1_handle = tpm2sh(
        cache_path,
        &["create-primary", "-H", "owner", "ecc-nist-p256:sha256"],
    )
    .read()
    .unwrap();
    let l1_handle = l1_handle.trim();

    let l2_handle = tpm2sh(cache_path, &["create", l1_handle, "rsa-2048:sha256"])
        .pipe(tpm2sh(cache_path, &["load"]))
        .read()
        .unwrap();
    let l2_handle = l2_handle.trim();

    let deep_data = hex::encode("deep-secret");

    let l3_handle = tpm2sh(
        cache_path,
        &[
            "create",
            l2_handle,
            "keyedhash:sha256",
            "--data",
            &deep_data,
        ],
    )
    .pipe(tpm2sh(cache_path, &["load"]))
    .read()
    .unwrap();
    let l3_handle = l3_handle.trim();

    let output = tpm2sh(cache_path, &["unseal", "--hex", l3_handle])
        .read()
        .expect("Failed to unseal deep object");

    assert_eq!(output.trim(), deep_data);
}
