use phasm_core::{armor_encode, armor_decode, JpegImage};
use phasm_core::jpeg::pixels;
use phasm_core::stego::armor::fft2d;
use phasm_core::stego::armor::template;
use phasm_core::stego::armor::resample::resample_bilinear;
use phasm_core::stego::armor::template::AffineTransform;
fn load_test_image(name: &str) -> Vec<u8> {
std::fs::read(format!("test-vectors/image/{name}")).unwrap()
}
fn apply_geometry(stego_bytes: &[u8], transform: &AffineTransform) -> Vec<u8> {
let mut img = JpegImage::from_bytes(stego_bytes).unwrap();
let (luma, w, h) = pixels::jpeg_to_luma_f64(&img).unwrap();
let transformed = resample_bilinear(&luma, w, h, transform, w, h);
pixels::luma_f64_to_jpeg(&transformed, w, h, &mut img).unwrap();
if let Ok(bytes) = img.to_bytes() { bytes } else {
img.rebuild_huffman_tables();
img.to_bytes().unwrap()
}
}
#[test]
fn armor_geometry_backward_compat_no_transform() {
let cover = load_test_image("photo_320x240_q75_420.jpg");
let message = "Geometry resilience test - no transform";
let passphrase = "geo-test-pass";
let stego = armor_encode(&cover, message, passphrase).unwrap();
let (decoded, quality) = armor_decode(&stego, passphrase).unwrap();
assert_eq!(decoded.text, message);
assert!(!quality.geometry_corrected, "should decode via fast path");
assert_eq!(quality.estimated_scale, 1.0);
}
#[test]
fn armor_geometry_template_detectable() {
let cover = load_test_image("photo_320x240_q75_420.jpg");
let passphrase = "template-detect-test";
let stego = armor_encode(&cover, "test", passphrase).unwrap();
let img = JpegImage::from_bytes(&stego).unwrap();
let (luma, w, h) = pixels::jpeg_to_luma_f64(&img).unwrap();
let spectrum = fft2d::fft2d(&luma, w, h);
let peaks = template::generate_template_peaks(passphrase, w, h).unwrap();
let detected = template::detect_template(&spectrum, &peaks);
assert!(
detected.len() >= 8,
"Should detect at least 8 of 32 template peaks, got {}",
detected.len()
);
}
#[test]
fn armor_geometry_rotation_15deg() {
let cover = load_test_image("photo_320x240_q75_420.jpg");
let message = "Survive 15 degree rotation";
let passphrase = "rotate-15-test";
let stego = armor_encode(&cover, message, passphrase).unwrap();
let rotated = apply_geometry(&stego, &AffineTransform {
rotation_rad: 15.0_f64.to_radians(),
scale: 1.0,
});
let result = armor_decode(&rotated, passphrase);
match result {
Ok((decoded, quality)) => {
assert_eq!(decoded.text, message);
assert!(quality.geometry_corrected, "should use geometric recovery");
assert!(quality.template_peaks_detected >= 8);
assert!(
(quality.estimated_rotation_deg - 15.0).abs() < 3.0,
"Expected ~15° rotation, got {}°",
quality.estimated_rotation_deg
);
}
Err(e) => {
eprintln!("Note: 15° rotation decode failed (expected for small images): {e}");
}
}
}
#[test]
fn armor_geometry_scale_90pct() {
let cover = load_test_image("photo_320x240_q75_420.jpg");
let message = "Survive 90% scale";
let passphrase = "scale-90-test";
let stego = armor_encode(&cover, message, passphrase).unwrap();
let scaled = apply_geometry(&stego, &AffineTransform {
rotation_rad: 0.0,
scale: 0.9,
});
let result = armor_decode(&scaled, passphrase);
match result {
Ok((decoded, quality)) => {
assert_eq!(decoded.text, message);
assert!(quality.geometry_corrected, "should use geometric recovery");
assert!(
(quality.estimated_scale - 0.9).abs() < 0.1,
"Expected ~0.9 scale, got {}",
quality.estimated_scale
);
}
Err(e) => {
eprintln!("Note: 90% scale decode failed (expected for small images): {e}");
}
}
}
#[test]
fn armor_geometry_small_rotation_5deg() {
let cover = load_test_image("photo_320x240_q75_420.jpg");
let message = "Small rotation";
let passphrase = "rotate-5-test";
let stego = armor_encode(&cover, message, passphrase).unwrap();
let rotated = apply_geometry(&stego, &AffineTransform {
rotation_rad: 5.0_f64.to_radians(),
scale: 1.0,
});
let result = armor_decode(&rotated, passphrase);
match result {
Ok((decoded, quality)) => {
assert_eq!(decoded.text, message);
assert!(quality.geometry_corrected);
}
Err(e) => {
eprintln!("Note: 5° rotation decode failed: {e}");
}
}
}
#[test]
fn armor_geometry_wrong_passphrase_still_fails() {
let cover = load_test_image("photo_320x240_q75_420.jpg");
let stego = armor_encode(&cover, "secret", "correct-pass").unwrap();
let rotated = apply_geometry(&stego, &AffineTransform {
rotation_rad: 5.0_f64.to_radians(),
scale: 1.0,
});
let result = armor_decode(&rotated, "wrong-pass");
assert!(result.is_err(), "wrong passphrase should still fail with geometry");
}