use anyhow::{Context, Result};
use core::time::Duration;
use futures::{pin_mut, stream::StreamExt, FutureExt};
use std::{env, io::Write};
use tempfile::NamedTempFile;
use tokio::time::sleep;
use tokio_retry::{
    strategy::{jitter, FibonacciBackoff},
    Retry,
};

use crate::utils::{match_media_msg, random_user_with_random_convo};

#[tokio::test]
async fn room_msg_can_download_image() -> Result<()> {
    let _ = env_logger::try_init();

    let (mut user, room_id) = random_user_with_random_convo("room_msg_image_download").await?;
    let state_sync = user.start_sync();
    state_sync.await_has_synced_history().await?;

    // wait for sync to catch up
    let retry_strategy = FibonacciBackoff::from_millis(100).map(jitter).take(10);
    Retry::spawn(retry_strategy, || async {
        user.convo(room_id.to_string()).await
    })
    .await?;

    let convo = user.convo(room_id.to_string()).await?;
    let timeline = convo.timeline_stream();
    let stream = timeline.messages_stream();
    pin_mut!(stream);

    let bytes = include_bytes!("../fixtures/kingfisher.jpg");
    let mut tmp_jpg = NamedTempFile::new()?;
    tmp_jpg.as_file_mut().write_all(bytes)?;
    let jpg_name = tmp_jpg // it is randomly generated by system and not kingfisher.jpg
        .path()
        .file_name()
        .expect("it is not file")
        .to_string_lossy()
        .to_string();

    let bytes = include_bytes!("../fixtures/PNG_transparency_demonstration_1.png");
    let mut tmp_png = NamedTempFile::new()?;
    tmp_png.as_file_mut().write_all(bytes)?;

    let mimetype = "image/jpeg";
    let draft = user
        .image_draft(
            tmp_jpg.path().to_string_lossy().to_string(),
            mimetype.to_owned(),
        )
        .filename(jpg_name.clone())
        .thumbnail_image(
            tmp_png.path().to_string_lossy().to_string(),
            "image/png".to_owned(),
        )
        .clone(); // switch variable from temporary to normal so that send_message can use it
    timeline.send_message(Box::new(draft)).await?;

    // image msg may reach via pushback action or reset action
    let mut i = 30;
    let mut found = None;
    while i > 0 {
        if let Some(diff) = stream.next().now_or_never().flatten() {
            match diff.action().as_str() {
                "PushBack" => {
                    let value = diff
                        .value()
                        .expect("diff pushback action should have valid value");
                    if let Some(msg_content) = match_media_msg(&value, mimetype, &jpg_name) {
                        let event_id = value
                            .event_item()
                            .and_then(|t| t.event_id())
                            .expect("media msg should be timeline event item");
                        found = Some((msg_content, event_id));
                    }
                }
                "Set" => {
                    let value = diff
                        .value()
                        .expect("diff set action should have valid value");
                    if let Some(msg_content) = match_media_msg(&value, mimetype, &jpg_name) {
                        let event_id = value
                            .event_item()
                            .and_then(|t| t.event_id())
                            .expect("media msg should be timeline event item");
                        found = Some((msg_content, event_id));
                    }
                }
                "Reset" => {
                    let values = diff
                        .values()
                        .expect("diff reset action should have valid values");
                    for value in values.iter() {
                        if let Some(msg_content) = match_media_msg(value, mimetype, &jpg_name) {
                            let event_id = value
                                .event_item()
                                .and_then(|t| t.event_id())
                                .expect("media msg should be timeline event item");
                            found = Some((msg_content, event_id));
                        }
                        if found.is_some() {
                            break;
                        }
                    }
                }
                _ => {}
            }
            // yay
            if found.is_some() {
                break;
            }
        }
        i -= 1;
        sleep(Duration::from_secs(1)).await;
    }
    let (msg_content, event_id) = found.context("Even after 30 seconds, image msg not received")?;
    assert_eq!(msg_content.mimetype().as_deref(), Some(mimetype));
    assert_eq!(msg_content.filename(), Some(jpg_name));

    let dir_path = env::temp_dir().to_string_lossy().to_string();
    let downloaded_path = convo
        .download_media(event_id.clone(), None, dir_path)
        .await?;
    assert!(
        downloaded_path.text().is_some(),
        "image msg should be downloadable"
    );

    let media_path = convo.media_path(event_id, false).await?;
    assert!(
        media_path.text().is_some(),
        "media path should be accessible if it was downloaded once"
    );

    Ok(())
}

#[tokio::test]
async fn room_msg_can_download_audio() -> Result<()> {
    let _ = env_logger::try_init();

    let (mut user, room_id) = random_user_with_random_convo("room_msg_audio_download").await?;
    let state_sync = user.start_sync();
    state_sync.await_has_synced_history().await?;

    // wait for sync to catch up
    let retry_strategy = FibonacciBackoff::from_millis(100).map(jitter).take(10);
    Retry::spawn(retry_strategy, || async {
        user.convo(room_id.to_string()).await
    })
    .await?;

    let convo = user.convo(room_id.to_string()).await?;
    let timeline = convo.timeline_stream();
    let stream = timeline.messages_stream();
    pin_mut!(stream);

    let bytes = include_bytes!("../fixtures/sample-3s.mp3");
    let mp3_size = bytes.len() as u64;
    let mut tmp_mp3 = NamedTempFile::new()?;
    tmp_mp3.as_file_mut().write_all(bytes)?;
    let mp3_name = tmp_mp3 // it is randomly generated by system and not sample-3s.mp3
        .path()
        .file_name()
        .expect("it is not file")
        .to_string_lossy()
        .to_string();
    let duration = 3; // in seconds

    let mimetype = "audio/mp3";
    let draft = user
        .audio_draft(
            tmp_mp3.path().to_string_lossy().to_string(),
            mimetype.to_owned(),
        )
        .size(mp3_size)
        .filename(mp3_name.clone())
        .duration(duration)
        .clone(); // switch variable from temporary to normal so that send_message can use it
    timeline.send_message(Box::new(draft)).await?;

    // audio msg may reach via pushback action or reset action
    let mut i = 30;
    let mut found = None;
    while i > 0 {
        if let Some(diff) = stream.next().now_or_never().flatten() {
            match diff.action().as_str() {
                "PushBack" | "Set" => {
                    let value = diff
                        .value()
                        .expect("diff pushback action should have valid value");
                    if let Some(msg_content) = match_media_msg(&value, mimetype, &mp3_name) {
                        let event_id = value
                            .event_item()
                            .and_then(|t| t.event_id())
                            .expect("media msg should be timeline event item");
                        found = Some((msg_content, event_id));
                    }
                }
                "Reset" => {
                    let values = diff
                        .values()
                        .expect("diff reset action should have valid values");
                    for value in values.iter() {
                        if let Some(msg_content) = match_media_msg(value, mimetype, &mp3_name) {
                            let event_id = value
                                .event_item()
                                .and_then(|t| t.event_id())
                                .expect("media msg should be timeline event item");
                            found = Some((msg_content, event_id));
                        }
                        if found.is_some() {
                            break;
                        }
                    }
                }
                _ => {}
            }
            // yay
            if found.is_some() {
                break;
            }
        }
        i -= 1;
        sleep(Duration::from_secs(1)).await;
    }
    let (msg_content, event_id) = found.context("Even after 30 seconds, audio msg not received")?;
    assert_eq!(msg_content.mimetype().as_deref(), Some(mimetype));
    assert_eq!(msg_content.size(), Some(mp3_size));
    assert_eq!(msg_content.filename(), Some(mp3_name));
    assert_eq!(msg_content.duration(), Some(duration));

    let dir_path = env::temp_dir().to_string_lossy().to_string();
    let downloaded_path = convo
        .download_media(event_id.clone(), None, dir_path)
        .await?;
    assert!(
        downloaded_path.text().is_some(),
        "audio msg should be downloadable"
    );

    let media_path = convo.media_path(event_id, false).await?;
    assert!(
        media_path.text().is_some(),
        "media path should be accessible if it was downloaded once"
    );

    Ok(())
}

#[tokio::test]
async fn room_msg_can_download_video() -> Result<()> {
    let _ = env_logger::try_init();

    let (mut user, room_id) = random_user_with_random_convo("room_msg_video_download").await?;
    let state_sync = user.start_sync();
    state_sync.await_has_synced_history().await?;

    // wait for sync to catch up
    let retry_strategy = FibonacciBackoff::from_millis(100).map(jitter).take(10);
    Retry::spawn(retry_strategy, || async {
        user.convo(room_id.to_string()).await
    })
    .await?;

    let convo = user.convo(room_id.to_string()).await?;
    let timeline = convo.timeline_stream();
    let stream = timeline.messages_stream();
    pin_mut!(stream);

    let bytes = include_bytes!("../fixtures/big_buck_bunny.mp4");
    let mp4_size = bytes.len() as u64;
    let mut tmp_mp4 = NamedTempFile::new()?;
    tmp_mp4.as_file_mut().write_all(bytes)?;
    let mp4_name = tmp_mp4 // it is randomly generated by system and not big_buck_bunny.mp4
        .path()
        .file_name()
        .expect("it is not file")
        .to_string_lossy()
        .to_string();
    let duration = 5; // in seconds

    let bytes = include_bytes!("../fixtures/PNG_transparency_demonstration_1.png");
    let png_size = bytes.len() as u64;
    let mut tmp_png = NamedTempFile::new()?;
    tmp_png.as_file_mut().write_all(bytes)?;

    let mimetype = "video/mp4";
    let draft = user
        .video_draft(
            tmp_mp4.path().to_string_lossy().to_string(),
            mimetype.to_owned(),
        )
        .size(mp4_size)
        .filename(mp4_name.clone())
        .duration(duration)
        .thumbnail_image(
            tmp_png.path().to_string_lossy().to_string(),
            "image/png".to_owned(),
        )
        .thumbnail_info(None, None, Some(png_size))
        .clone(); // switch variable from temporary to normal so that send_message can use it
    timeline.send_message(Box::new(draft)).await?;

    // video msg may reach via pushback action or reset action
    let mut i = 30;
    let mut found = None;
    while i > 0 {
        if let Some(diff) = stream.next().now_or_never().flatten() {
            match diff.action().as_str() {
                "PushBack" | "Set" => {
                    let value = diff
                        .value()
                        .expect("diff pushback action should have valid value");
                    if let Some(msg_content) = match_media_msg(&value, mimetype, &mp4_name) {
                        let event_id = value
                            .event_item()
                            .and_then(|t| t.event_id())
                            .expect("media msg should be timeline event item");
                        found = Some((msg_content, event_id));
                    }
                }
                "Reset" => {
                    let values = diff
                        .values()
                        .expect("diff reset action should have valid values");
                    for value in values.iter() {
                        if let Some(msg_content) = match_media_msg(value, mimetype, &mp4_name) {
                            let event_id = value
                                .event_item()
                                .and_then(|t| t.event_id())
                                .expect("media msg should be timeline event item");
                            found = Some((msg_content, event_id));
                        }
                        if found.is_some() {
                            break;
                        }
                    }
                }
                _ => {}
            }
            // yay
            if found.is_some() {
                break;
            }
        }
        i -= 1;
        sleep(Duration::from_secs(1)).await;
    }
    let (msg_content, event_id) = found.context("Even after 30 seconds, video msg not received")?;
    assert_eq!(msg_content.mimetype().as_deref(), Some(mimetype));
    assert_eq!(msg_content.size(), Some(mp4_size));
    assert_eq!(msg_content.filename(), Some(mp4_name));
    assert_eq!(msg_content.duration(), Some(duration));

    let dir_path = env::temp_dir().to_string_lossy().to_string();
    let downloaded_path = convo
        .download_media(event_id.clone(), None, dir_path)
        .await?;
    assert!(
        downloaded_path.text().is_some(),
        "video msg should be downloadable"
    );

    let media_path = convo.media_path(event_id, false).await?;
    assert!(
        media_path.text().is_some(),
        "media path should be accessible if it was downloaded once"
    );

    Ok(())
}

#[tokio::test]
async fn room_msg_can_download_file() -> Result<()> {
    let _ = env_logger::try_init();

    let (mut user, room_id) = random_user_with_random_convo("room_msg_file_download").await?;
    let state_sync = user.start_sync();
    state_sync.await_has_synced_history().await?;

    // wait for sync to catch up
    let retry_strategy = FibonacciBackoff::from_millis(100).map(jitter).take(10);
    Retry::spawn(retry_strategy, || async {
        user.convo(room_id.to_string()).await
    })
    .await?;

    let convo = user.convo(room_id.to_string()).await?;
    let timeline = convo.timeline_stream();
    let stream = timeline.messages_stream();
    pin_mut!(stream);

    let bytes = include_bytes!("../fixtures/sample.pdf");
    let pdf_size = bytes.len() as u64;
    let mut tmp_pdf = NamedTempFile::new()?;
    tmp_pdf.as_file_mut().write_all(bytes)?;
    let pdf_name = tmp_pdf // it is randomly generated by system and not sample.pdf
        .path()
        .file_name()
        .expect("it is not file")
        .to_string_lossy()
        .to_string();

    let bytes = include_bytes!("../fixtures/PNG_transparency_demonstration_1.png");
    let png_size = bytes.len() as u64;
    let mut tmp_png = NamedTempFile::new()?;
    tmp_png.as_file_mut().write_all(bytes)?;

    let mimetype = "application/pdf";
    let draft = user
        .file_draft(
            tmp_pdf.path().to_string_lossy().to_string(),
            mimetype.to_owned(),
        )
        .size(pdf_size)
        .filename(pdf_name.clone())
        .thumbnail_image(
            tmp_png.path().to_string_lossy().to_string(),
            "image/png".to_owned(),
        )
        .thumbnail_info(None, None, Some(png_size))
        .clone(); // switch variable from temporary to normal so that send_message can use it
    timeline.send_message(Box::new(draft)).await?;

    // file msg may reach via pushback action or reset action
    let mut i = 30;
    let mut found = None;
    while i > 0 {
        if let Some(diff) = stream.next().now_or_never().flatten() {
            match diff.action().as_str() {
                "PushBack" | "Set" => {
                    let value = diff
                        .value()
                        .expect("diff pushback action should have valid value");
                    if let Some(msg_content) = match_media_msg(&value, mimetype, &pdf_name) {
                        let event_id = value
                            .event_item()
                            .and_then(|t| t.event_id())
                            .expect("media msg should be timeline event item");
                        found = Some((msg_content, event_id));
                    }
                }
                "Reset" => {
                    let values = diff
                        .values()
                        .expect("diff reset action should have valid values");
                    for value in values.iter() {
                        if let Some(msg_content) = match_media_msg(value, mimetype, &pdf_name) {
                            let event_id = value
                                .event_item()
                                .and_then(|t| t.event_id())
                                .expect("media msg should be timeline event item");
                            found = Some((msg_content, event_id));
                        }
                        if found.is_some() {
                            break;
                        }
                    }
                }
                _ => {}
            }
            // yay
            if found.is_some() {
                break;
            }
        }
        i -= 1;
        sleep(Duration::from_secs(1)).await;
    }
    let (msg_content, event_id) = found.context("Even after 30 seconds, file msg not received")?;
    assert_eq!(msg_content.mimetype().as_deref(), Some(mimetype));
    assert_eq!(msg_content.size(), Some(pdf_size));
    assert_eq!(msg_content.filename(), Some(pdf_name));

    let dir_path = env::temp_dir().to_string_lossy().to_string();
    let downloaded_path = convo
        .download_media(event_id.clone(), None, dir_path)
        .await?;
    assert!(
        downloaded_path.text().is_some(),
        "file msg should be downloadable"
    );

    let media_path = convo.media_path(event_id, false).await?;
    assert!(
        media_path.text().is_some(),
        "media path should be accessible if it was downloaded once"
    );

    Ok(())
}
