use std::time::Duration;

use super::*;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::sql;
use crate::test_utils::{self, AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext};
use crate::tools::SystemTime;

fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
    tokio::task::block_in_place(move || {
        let img = ImageReader::open(path)
            .expect("failed to open image")
            .with_guessed_format()
            .expect("failed to guess format")
            .decode()
            .expect("failed to decode image");
        assert_eq!(img.width(), width, "invalid width");
        assert_eq!(img.height(), height, "invalid height");
        img
    })
}

const FILE_BYTES: &[u8] = b"hello";
const FILE_DEDUPLICATED: &str = "ea8f163db38682925e4491c5e58d4bb.txt";

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create() {
    let t = TestContext::new().await;
    let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
    let fname = t.get_blobdir().join(FILE_DEDUPLICATED);
    let data = fs::read(fname).await.unwrap();
    assert_eq!(data, FILE_BYTES);
    assert_eq!(blob.as_name(), format!("$BLOBDIR/{FILE_DEDUPLICATED}"));
    assert_eq!(blob.to_abs_path(), t.get_blobdir().join(FILE_DEDUPLICATED));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lowercase_ext() {
    let t = TestContext::new().await;
    let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.TXT").unwrap();
    assert!(
        blob.as_name().ends_with(".txt"),
        "Blob {blob:?} should end with .txt"
    );
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_suffix() {
    let t = TestContext::new().await;
    let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
    assert_eq!(blob.suffix(), Some("txt"));
    let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "bar").unwrap();
    assert_eq!(blob.suffix(), None);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_dup() {
    let t = TestContext::new().await;
    BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
    let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED);
    assert!(foo_path.exists());
    BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.txt").unwrap();
    let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
    while let Ok(Some(dirent)) = dir.next_entry().await {
        let fname = dirent.file_name();
        if fname == foo_path.file_name().unwrap() {
            assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES);
        } else {
            let name = fname.to_str().unwrap();
            assert!(name.ends_with(".txt"));
        }
    }
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_double_ext() {
    let t = TestContext::new().await;
    BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.tar.gz").unwrap();
    let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED).with_extension("gz");
    assert!(foo_path.exists());
    BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.tar.gz").unwrap();
    let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
    while let Ok(Some(dirent)) = dir.next_entry().await {
        let fname = dirent.file_name();
        if fname == foo_path.file_name().unwrap() {
            assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES);
        } else {
            let name = fname.to_str().unwrap();
            println!("{name}");
            assert_eq!(name.starts_with("foo"), false);
            assert_eq!(name.ends_with(".tar.gz"), false);
            assert!(name.ends_with(".gz"));
        }
    }
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_long_names() {
    let t = TestContext::new().await;
    let s = format!("file.{}", "a".repeat(100));
    let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"data", &s).unwrap();
    let blobname = blob.as_name().split('/').next_back().unwrap();
    assert!(blobname.len() < 70);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_from_name_long() {
    let t = TestContext::new().await;
    let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
    fs::write(&src_ext, b"boo").await.unwrap();
    let blob = BlobObject::create_and_deduplicate(&t, &src_ext, &src_ext).unwrap();
    assert_eq!(
        blob.as_name(),
        "$BLOBDIR/06f010b24d1efe57ffab44a8ad20c54.html"
    );
}

#[test]
fn test_is_blob_name() {
    assert!(BlobObject::is_acceptible_blob_name("foo"));
    assert!(BlobObject::is_acceptible_blob_name("foo.txt"));
    assert!(BlobObject::is_acceptible_blob_name(&"f".repeat(128)));
    assert!(!BlobObject::is_acceptible_blob_name("foo/bar"));
    assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
    assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_white_bg() {
    let t = TestContext::new().await;
    let bytes0 = include_bytes!("../../test-data/image/logo.png").as_slice();
    let bytes1 = include_bytes!("../../test-data/image/avatar900x900.png").as_slice();
    for (bytes, color) in [
        (bytes0, [255u8, 255, 255, 255]),
        (bytes1, [253u8, 198, 0, 255]),
    ] {
        let avatar_src = t.dir.path().join("avatar.png");
        fs::write(&avatar_src, bytes).await.unwrap();

        let mut blob = BlobObject::create_and_deduplicate(&t, &avatar_src, &avatar_src).unwrap();
        let img_wh = 128;
        let viewtype = &mut Viewtype::Image;
        let strict_limits = true;
        blob.check_or_recode_to_size(&t, None, viewtype, img_wh, 20_000, strict_limits)
            .unwrap();
        tokio::task::block_in_place(move || {
            let img = ImageReader::open(blob.to_abs_path())
                .unwrap()
                .with_guessed_format()
                .unwrap()
                .decode()
                .unwrap();
            assert!(img.width() == img_wh);
            assert!(img.height() == img_wh);
            assert_eq!(img.get_pixel(0, 0), Rgba(color));
        });
    }
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_selfavatar_outside_blobdir() {
    async fn file_size(path_buf: &Path) -> u64 {
        fs::metadata(path_buf).await.unwrap().len()
    }

    let t = TestContext::new().await;
    let avatar_src = t.dir.path().join("avatar.jpg");
    let avatar_bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
    fs::write(&avatar_src, avatar_bytes).await.unwrap();
    t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
        .await
        .unwrap();
    let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
    let avatar_path = Path::new(&avatar_blob);
    let scaled_avatar_size = file_size(avatar_path).await;
    info!(&t, "Scaled avatar size: {scaled_avatar_size}.");
    assert!(scaled_avatar_size < avatar_bytes.len() as u64);

    check_image_size(avatar_src, 1000, 1000);
    check_image_size(
        &avatar_blob,
        constants::BALANCED_AVATAR_SIZE,
        constants::BALANCED_AVATAR_SIZE,
    );

    assert!(
        avatar_blob.ends_with("2a048b6fcd86448032b854ea1ad7608.jpg"),
        "The avatar filename should be its hash, but instead it's {avatar_blob}"
    );

    let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap();
    let viewtype = &mut Viewtype::Image;
    let strict_limits = true;
    blob.check_or_recode_to_size(&t, None, viewtype, 1000, 3000, strict_limits)
        .unwrap();
    let new_file_size = file_size(&blob.to_abs_path()).await;
    assert!(new_file_size <= 3000);
    assert!(new_file_size > 2000);
    // The new file should be smaller:
    assert!(new_file_size < scaled_avatar_size);
    // And the original file should not be touched:
    assert_eq!(file_size(avatar_path).await, scaled_avatar_size);
    tokio::task::block_in_place(move || {
        let img = ImageReader::open(blob.to_abs_path())
            .unwrap()
            .with_guessed_format()
            .unwrap()
            .decode()
            .unwrap();
        assert!(img.width() > 130);
        assert_eq!(img.width(), img.height());
    });
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_selfavatar_in_blobdir() {
    let t = TestContext::new().await;
    let avatar_src = t.get_blobdir().join("avatar.png");
    fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES)
        .await
        .unwrap();

    check_image_size(&avatar_src, 900, 900);

    t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
        .await
        .unwrap();
    let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
    assert!(
        avatar_cfg.ends_with("d57cb5ce5f371531b6e1fb17b6dd1af.png"),
        "Avatar file name {avatar_cfg} should end with its hash"
    );

    check_image_size(
        avatar_cfg,
        constants::BALANCED_AVATAR_SIZE,
        constants::BALANCED_AVATAR_SIZE,
    );
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_selfavatar_copy_without_recode() {
    let t = TestContext::new().await;
    let avatar_src = t.dir.path().join("avatar.png");
    fs::write(&avatar_src, AVATAR_64x64_BYTES).await.unwrap();
    let avatar_blob = t.get_blobdir().join(AVATAR_64x64_DEDUPLICATED);
    assert!(!avatar_blob.exists());
    t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
        .await
        .unwrap();
    assert!(avatar_blob.exists());
    assert_eq!(
        fs::metadata(&avatar_blob).await.unwrap().len(),
        AVATAR_64x64_BYTES.len() as u64
    );
    let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
    assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_1() {
    let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
    SendImageCheckMediaquality {
        viewtype: Viewtype::Image,
        media_quality_config: "0",
        bytes,
        extension: "jpg",
        has_exif: true,
        original_width: 1000,
        original_height: 1000,
        compressed_width: 1000,
        compressed_height: 1000,
        ..Default::default()
    }
    .test()
    .await
    .unwrap();
    SendImageCheckMediaquality {
        viewtype: Viewtype::Image,
        media_quality_config: "1",
        bytes,
        extension: "jpg",
        has_exif: true,
        original_width: 1000,
        original_height: 1000,
        compressed_width: 1000,
        compressed_height: 1000,
        ..Default::default()
    }
    .test()
    .await
    .unwrap();
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_2() {
    // The "-rotated" files are rotated by 270 degrees using the Exif metadata
    let bytes = include_bytes!("../../test-data/image/rectangle2000x1800-rotated.jpg");
    let img_rotated = SendImageCheckMediaquality {
        viewtype: Viewtype::Image,
        media_quality_config: "0",
        bytes,
        extension: "jpg",
        has_exif: true,
        original_width: 2000,
        original_height: 1800,
        orientation: 270,
        compressed_width: 1800,
        compressed_height: 2000,
        ..Default::default()
    }
    .test()
    .await
    .unwrap();
    assert_correct_rotation(&img_rotated);

    let mut buf = Cursor::new(vec![]);
    img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
    let bytes = buf.into_inner();

    let img_rotated = SendImageCheckMediaquality {
        viewtype: Viewtype::Image,
        media_quality_config: "1",
        bytes: &bytes,
        extension: "jpg",
        original_width: 1800,
        original_height: 2000,
        compressed_width: 1800,
        compressed_height: 2000,
        ..Default::default()
    }
    .test()
    .await
    .unwrap();
    assert_correct_rotation(&img_rotated);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_bad_exif() {
    // `exiftool` reports for this file "Bad offset for IFD0 XResolution", still Exif must be
    // detected and removed.
    let bytes = include_bytes!("../../test-data/image/1000x1000-bad-exif.jpg");
    SendImageCheckMediaquality {
        viewtype: Viewtype::Image,
        media_quality_config: "0",
        bytes,
        extension: "jpg",
        has_exif: true,
        original_width: 1000,
        original_height: 1000,
        compressed_width: 1000,
        compressed_height: 1000,
        ..Default::default()
    }
    .test()
    .await
    .unwrap();
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_balanced_png() {
    let bytes = include_bytes!("../../test-data/image/screenshot.png");

    SendImageCheckMediaquality {
        viewtype: Viewtype::Image,
        media_quality_config: "0",
        bytes,
        extension: "png",
        original_width: 1920,
        original_height: 1080,
        compressed_width: 1920,
        compressed_height: 1080,
        ..Default::default()
    }
    .test()
    .await
    .unwrap();

    SendImageCheckMediaquality {
        viewtype: Viewtype::Image,
        media_quality_config: "1",
        bytes,
        extension: "png",
        original_width: 1920,
        original_height: 1080,
        compressed_width: constants::WORSE_IMAGE_SIZE,
        compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
        ..Default::default()
    }
    .test()
    .await
    .unwrap();

    SendImageCheckMediaquality {
        viewtype: Viewtype::File,
        media_quality_config: "1",
        bytes,
        extension: "png",
        original_width: 1920,
        original_height: 1080,
        compressed_width: 1920,
        compressed_height: 1080,
        ..Default::default()
    }
    .test()
    .await
    .unwrap();

    SendImageCheckMediaquality {
        viewtype: Viewtype::File,
        media_quality_config: "1",
        bytes,
        extension: "png",
        original_width: 1920,
        original_height: 1080,
        compressed_width: 1920,
        compressed_height: 1080,
        set_draft: true,
        ..Default::default()
    }
    .test()
    .await
    .unwrap();

    // This will be sent as Image, see [`BlobObject::check_or_recode_image()`] for explanation.
    SendImageCheckMediaquality {
        viewtype: Viewtype::Sticker,
        media_quality_config: "0",
        bytes,
        extension: "png",
        original_width: 1920,
        original_height: 1080,
        compressed_width: 1920,
        compressed_height: 1080,
        ..Default::default()
    }
    .test()
    .await
    .unwrap();
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sticker_with_exif() {
    let bytes = include_bytes!("../../test-data/image/logo-exif.png");
    SendImageCheckMediaquality {
        viewtype: Viewtype::Sticker,
        bytes,
        extension: "png",
        // TODO: Pretend there's no Exif. Currently `exif` crate doesn't detect Exif in this image,
        // so the test doesn't check all the logic it should.
        has_exif: false,
        original_width: 135,
        original_height: 135,
        res_viewtype: Some(Viewtype::Sticker),
        compressed_width: 135,
        compressed_height: 135,
        ..Default::default()
    }
    .test()
    .await
    .unwrap();
}

/// Tests that RGBA PNG can be recoded into JPEG
/// by dropping alpha channel.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_rgba_png_to_jpeg() {
    let bytes = include_bytes!("../../test-data/image/screenshot-rgba.png");

    SendImageCheckMediaquality {
        viewtype: Viewtype::Image,
        media_quality_config: "1",
        bytes,
        extension: "png",
        original_width: 1920,
        original_height: 1080,
        compressed_width: constants::WORSE_IMAGE_SIZE,
        compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
        ..Default::default()
    }
    .test()
    .await
    .unwrap();
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_huge_jpg() {
    let bytes = include_bytes!("../../test-data/image/screenshot.jpg");
    SendImageCheckMediaquality {
        viewtype: Viewtype::Image,
        media_quality_config: "0",
        bytes,
        extension: "jpg",
        has_exif: true,
        original_width: 1920,
        original_height: 1080,
        compressed_width: constants::BALANCED_IMAGE_SIZE,
        compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
        ..Default::default()
    }
    .test()
    .await
    .unwrap();
}

fn assert_correct_rotation(img: &DynamicImage) {
    // The test images are black in the bottom left corner after correctly applying
    // the EXIF orientation

    let [luma] = img.get_pixel(10, 10).to_luma().0;
    assert_eq!(luma, 255);
    let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0;
    assert_eq!(luma, 255);
    let [luma] = img
        .get_pixel(img.width() - 10, img.height() - 10)
        .to_luma()
        .0;
    assert_eq!(luma, 255);
    let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0;
    assert_eq!(luma, 0);
}

#[derive(Default)]
struct SendImageCheckMediaquality<'a> {
    pub(crate) viewtype: Viewtype,
    pub(crate) media_quality_config: &'a str,
    pub(crate) bytes: &'a [u8],
    pub(crate) extension: &'a str,
    pub(crate) has_exif: bool,
    pub(crate) original_width: u32,
    pub(crate) original_height: u32,
    pub(crate) orientation: i32,
    pub(crate) res_viewtype: Option<Viewtype>,
    pub(crate) compressed_width: u32,
    pub(crate) compressed_height: u32,
    pub(crate) set_draft: bool,
}

impl SendImageCheckMediaquality<'_> {
    pub(crate) async fn test(self) -> anyhow::Result<DynamicImage> {
        let viewtype = self.viewtype;
        let media_quality_config = self.media_quality_config;
        let bytes = self.bytes;
        let extension = self.extension;
        let has_exif = self.has_exif;
        let original_width = self.original_width;
        let original_height = self.original_height;
        let orientation = self.orientation;
        let res_viewtype = self.res_viewtype.unwrap_or(Viewtype::Image);
        let compressed_width = self.compressed_width;
        let compressed_height = self.compressed_height;
        let set_draft = self.set_draft;

        let alice = TestContext::new_alice().await;
        let bob = TestContext::new_bob().await;
        alice
            .set_config(Config::MediaQuality, Some(media_quality_config))
            .await?;
        let file = alice.get_blobdir().join("file").with_extension(extension);
        let file_name = format!("file.{extension}");

        fs::write(&file, &bytes)
            .await
            .context("failed to write file")?;
        check_image_size(&file, original_width, original_height);

        let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
        if has_exif {
            let exif = exif.unwrap();
            assert_eq!(exif_orientation(&exif, &alice), orientation);
        } else {
            assert!(exif.is_none());
        }

        let mut msg = Message::new(viewtype);
        msg.set_file_and_deduplicate(&alice, &file, Some(&file_name), None)?;
        let chat = alice.create_chat(&bob).await;
        if set_draft {
            chat.id.set_draft(&alice, Some(&mut msg)).await.unwrap();
            msg = chat.id.get_draft(&alice).await.unwrap().unwrap();
            assert_eq!(msg.get_viewtype(), Viewtype::File);
        }
        let sent = alice.send_msg(chat.id, &mut msg).await;
        let alice_msg = alice.get_last_msg().await;
        assert_eq!(alice_msg.get_width() as u32, compressed_width);
        assert_eq!(alice_msg.get_height() as u32, compressed_height);
        let file_saved = alice
            .get_blobdir()
            .join("saved-".to_string() + &alice_msg.get_filename().unwrap());
        alice_msg.save_file(&alice, &file_saved).await?;
        check_image_size(file_saved, compressed_width, compressed_height);

        if original_width == compressed_width {
            assert_extension(&alice, alice_msg, extension);
        } else {
            assert_extension(&alice, alice_msg, "jpg");
        }

        let bob_msg = bob.recv_msg(&sent).await;
        assert_eq!(bob_msg.get_viewtype(), res_viewtype);
        assert_eq!(bob_msg.get_width() as u32, compressed_width);
        assert_eq!(bob_msg.get_height() as u32, compressed_height);
        let file_saved = bob
            .get_blobdir()
            .join("saved-".to_string() + &bob_msg.get_filename().unwrap());
        bob_msg.save_file(&bob, &file_saved).await?;
        if viewtype == Viewtype::File {
            assert_eq!(file_saved.extension().unwrap(), extension);
            let bytes1 = fs::read(&file_saved).await?;
            assert_eq!(&bytes1, bytes);
        }

        let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
        assert!(res_viewtype != Viewtype::Image || exif.is_none());

        let img = check_image_size(file_saved, compressed_width, compressed_height);

        if original_width == compressed_width {
            assert_extension(&bob, bob_msg, extension);
        } else {
            assert_extension(&bob, bob_msg, "jpg");
        }

        Ok(img)
    }
}

fn assert_extension(context: &TestContext, msg: Message, extension: &str) {
    assert!(
        msg.param
            .get(Param::File)
            .unwrap()
            .ends_with(&format!(".{extension}"))
    );
    assert!(
        msg.param
            .get(Param::Filename)
            .unwrap()
            .ends_with(&format!(".{extension}"))
    );
    assert!(
        msg.get_filename()
            .unwrap()
            .ends_with(&format!(".{extension}"))
    );
    assert_eq!(
        msg.get_file(context)
            .unwrap()
            .extension()
            .unwrap()
            .to_str()
            .unwrap(),
        extension
    );
    assert_eq!(
        msg.param
            .get_file_blob(context)
            .unwrap()
            .unwrap()
            .suffix()
            .unwrap(),
        extension
    );
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_big_gif_as_image() -> Result<()> {
    let bytes = include_bytes!("../../test-data/image/screenshot.gif");
    let (width, height) = (1920u32, 1080u32);
    let alice = TestContext::new_alice().await;
    let bob = TestContext::new_bob().await;
    alice
        .set_config(
            Config::MediaQuality,
            Some(&(MediaQuality::Worse as i32).to_string()),
        )
        .await?;
    let file = alice.get_blobdir().join("file").with_extension("gif");
    fs::write(&file, &bytes)
        .await
        .context("failed to write file")?;
    let mut msg = Message::new(Viewtype::Image);
    msg.set_file_and_deduplicate(&alice, &file, Some("file.gif"), None)?;
    let chat = alice.create_chat(&bob).await;
    let sent = alice.send_msg(chat.id, &mut msg).await;
    let bob_msg = bob.recv_msg(&sent).await;
    // DC must detect the image as GIF and send it w/o reencoding.
    assert_eq!(bob_msg.get_viewtype(), Viewtype::Gif);
    assert_eq!(bob_msg.get_width() as u32, width);
    assert_eq!(bob_msg.get_height() as u32, height);
    let file_saved = bob
        .get_blobdir()
        .join("saved-".to_string() + &bob_msg.get_filename().unwrap());
    bob_msg.save_file(&bob, &file_saved).await?;
    let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?;
    assert_eq!(file_size, bytes.len() as u64);
    check_image_size(file_saved, width, height);
    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_gif_as_sticker() -> Result<()> {
    let bytes = include_bytes!("../../test-data/image/image100x50.gif");
    let alice = &TestContext::new_alice().await;
    let file = alice.get_blobdir().join("file").with_extension("gif");
    fs::write(&file, &bytes)
        .await
        .context("failed to write file")?;
    let mut msg = Message::new(Viewtype::Sticker);
    msg.set_file_and_deduplicate(alice, &file, None, None)?;
    let chat = alice.get_self_chat().await;
    let sent = alice.send_msg(chat.id, &mut msg).await;
    let msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
    // Message::force_sticker() wasn't used, still Viewtype::Sticker is preserved because of the
    // extension.
    assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_and_deduplicate() -> Result<()> {
    let t = TestContext::new().await;

    let path = t.get_blobdir().join("anyfile.dat");
    fs::write(&path, b"bla").await?;
    let blob = BlobObject::create_and_deduplicate(&t, &path, &path)?;
    assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f.dat");
    assert_eq!(path.exists(), false);

    assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");

    fs::write(&path, b"bla").await?;
    let blob2 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
    assert_eq!(blob2.name, blob.name);

    let path_outside_blobdir = t.dir.path().join("anyfile.dat");
    fs::write(&path_outside_blobdir, b"bla").await?;
    let blob3 =
        BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
    assert!(path_outside_blobdir.exists());
    assert_eq!(blob3.name, blob.name);

    fs::write(&path, b"blabla").await?;
    let blob4 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
    assert_ne!(blob4.name, blob.name);

    fs::remove_dir_all(t.get_blobdir()).await?;
    let blob5 =
        BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
    assert_eq!(blob5.name, blob.name);

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_and_deduplicate_from_bytes() -> Result<()> {
    let t = TestContext::new().await;

    fs::remove_dir(t.get_blobdir()).await?;
    let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
    assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f");

    assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
    let modified1 = blob.to_abs_path().metadata()?.modified()?;

    // Test that the modification time of the file is updated when a new file is created
    // so that it's not deleted during housekeeping.
    // We can't use SystemTime::shift() here because file creation uses the actual OS time,
    // which we can't mock from our code.
    tokio::time::sleep(Duration::from_millis(1100)).await;

    let blob2 = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
    assert_eq!(blob2.name, blob.name);

    let modified2 = blob.to_abs_path().metadata()?.modified()?;
    assert_ne!(modified1, modified2);
    sql::housekeeping(&t).await?;
    assert!(blob2.to_abs_path().exists());

    // If we do shift the time by more than 1h, the blob file will be deleted during housekeeping:
    SystemTime::shift(Duration::from_secs(65 * 60));
    sql::housekeeping(&t).await?;
    assert_eq!(blob2.to_abs_path().exists(), false);

    let blob3 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
    assert_ne!(blob3.name, blob.name);

    {
        // If something goes wrong and the blob file is overwritten,
        // the correct content should be restored:
        fs::write(blob3.to_abs_path(), b"bloblo").await?;

        let blob4 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
        let blob4_content = fs::read(blob4.to_abs_path()).await?;
        assert_eq!(blob4_content, b"blabla");
    }

    Ok(())
}
