use bytes::Bytes;
use chris::{types::*, AnonChrisClient, BaseChrisClient, Downloadable};
use futures::{future, pin_mut, StreamExt, TryStreamExt};
use rstest::*;
use std::collections::{HashMap, HashSet};

mod helpers;
use helpers::{AnyResult, TESTING_URL};

#[fixture]
fn cube_url() -> CubeUrl {
    TESTING_URL.to_string().parse().unwrap()
}

#[fixture]
#[once]
fn chris_client(cube_url: CubeUrl) -> AnonChrisClient {
    futures::executor::block_on(async {
        AnonChrisClient::build(cube_url)
            .unwrap()
            .connect()
            .await
            .unwrap()
    })
}

#[rstest]
#[tokio::test(flavor = "multi_thread")]
async fn test_filebrowser_subdirs(chris_client: &AnonChrisClient) -> AnyResult {
    let fb = chris_client.filebrowser();
    let entry = fb
        .readdir("chrisui")
        .await?
        .expect("Filebrowser path not found");
    let subdirs = entry.subfolders();
    let expected_subdirs = ["feed_307", "feed_310"];
    for expected in expected_subdirs {
        assert!(subdirs.contains(&expected.to_string()))
    }
    Ok(())
}

#[rstest]
#[tokio::test(flavor = "multi_thread")]
async fn test_filebrowser_download_file(chris_client: &AnonChrisClient) -> AnyResult {
    let fb = chris_client.filebrowser();
    let entry = fb
        .readdir("chrisui/feed_310/pl-dircopy_313/pl-unstack-folders_314/pl-mri-preview_875/data")
        .await?
        .expect("Filebrowser path not found");
    let iter_files = entry.iter_files();
    let search = iter_files.stream_connected().try_filter(|f| {
        future::ready(
            f.object
                .fname()
                .as_str()
                .ends_with("/fetal-template-22.txt"),
        )
    });
    pin_mut!(search);
    let file = search
        .next()
        .await
        .expect("No files found in filebrowser path")?;
    let stream = file.stream().await?;
    let chunks: Vec<Bytes> = stream.try_collect().await?;
    let bytes: Vec<u8> = chunks
        .into_iter()
        .flat_map(|chunk| chunk.into_iter())
        .collect();
    let actual = String::from_utf8(bytes)?;
    let expected = "1961680 voxels\n1245019.9508666992 mm^3".to_string();
    assert_eq!(actual, expected);
    Ok(())
}

#[rstest]
#[tokio::test(flavor = "multi_thread")]
async fn test_get_plugin_parameters(chris_client: &AnonChrisClient) -> AnyResult {
    let plugin = chris_client
        .plugin()
        .name("pl-mri-preview")
        .version("1.2.0")
        .search()
        .get_only()
        .await?;
    let params: Vec<_> = plugin.parameters().stream().try_collect().await?;
    let expected = HashSet::from(["--inputs", "--outputs", "--background", "--units-fallback"]);
    let actual = HashSet::from_iter(params.iter().map(|p| p.flag.as_str()));
    assert_eq!(expected, actual);
    Ok(())
}

#[rstest]
#[tokio::test(flavor = "multi_thread")]
async fn test_search_public_feeds(chris_client: &AnonChrisClient) -> AnyResult {
    let feed = chris_client
        .public_feeds()
        .name_exact("Fetal Brain Atlases")
        .search()
        .get_first()
        .await?
        .expect("Feed not found");
    assert_eq!(&feed.object.name, "Fetal Brain Atlases");
    Ok(())
}

#[rstest]
#[tokio::test(flavor = "multi_thread")]
async fn test_get_feed(chris_client: &AnonChrisClient) -> AnyResult {
    let id = FeedId(307);
    let feed = chris_client.get_feed(id).await?;
    assert_eq!(feed.object.id, id);

    let actual: HashSet<_> = feed
        .get_plugin_instances()
        .stream()
        .map_ok(|p| p.id)
        .try_collect()
        .await?;
    let expected_ids = [
        369, 368, 367, 366, 331, 327, 326, 325, 323, 321, 312, 311, 310, 307,
    ];
    let expected = HashSet::from_iter(expected_ids.into_iter().map(PluginInstanceId));
    assert_eq!(actual, expected);
    Ok(())
}

#[rstest]
#[tokio::test(flavor = "multi_thread")]
async fn test_get_plugin_instance(chris_client: &AnonChrisClient) -> AnyResult {
    let id = PluginInstanceId(380);
    let pi = chris_client.get_plugin_instance(id).await?;
    assert_eq!(pi.object.id, id);
    assert_eq!(pi.object.plugin_name.as_str(), "pl-bulk-rename");

    let plugin = pi.plugin().get().await?;
    assert_eq!(pi.object.plugin_id, plugin.object.id);

    let actual: HashMap<_, _> = pi
        .parameters()
        .stream()
        .map_ok(|p| (p.param_name, p.value.to_string()))
        .try_collect()
        .await?;
    let expected_values = &[
        ("filter", ".*\\.dcm"),
        ("expression", ".*/(.+\\.dcm)"),
        ("replacement", "$1"),
    ];
    let expected: HashMap<_, _> = expected_values
        .iter()
        .map(|(k, v)| (k.to_string(), v.to_string()))
        .collect();
    assert_eq!(actual, expected);
    Ok(())
}

#[rstest]
#[case(3)]
#[case(5)]
#[tokio::test(flavor = "multi_thread")]
async fn test_search_max_items(chris_client: &AnonChrisClient, #[case] count: usize) -> AnyResult {
    let items: Vec<_> = chris_client
        .plugin()
        .search()
        .max_items(count)
        .stream()
        .try_collect()
        .await?;
    assert_eq!(items.len(), count);
    Ok(())
}
