//! CLI App integration tests.
pub mod common;

use anyhow::Result;
use common::test_data;
use common::test_stack::FloraTestStack;
use omry_cli::ServiceBridge;
use omry_integration::helpers;
use pretty_assertions::{assert_eq, assert_ne};
use tokio::time::{self, Duration};
use url::Url;

macro_rules! sorted_records {
    ($app:expr, $ids:expr) => {{
        let mut views = $app.records($ids).await?.into_vec().await?;
        views.sort_by_key(|r| r.id());
        views
            .into_iter()
            .map(|v| v.into_record().expect("Should be a Record"))
            .collect::<Vec<_>>()
    }};
}

#[tokio::test]
async fn can_check_connection() -> Result<()> {
    helpers::init_tracing_subscriber()?;
    let test_stack = FloraTestStack::new().await?;
    assert!(test_stack.app().ping().await.is_ok());
    Ok(())
}

#[tokio::test]
async fn can_add_record() -> Result<()> {
    helpers::init_tracing_subscriber()?;
    let test_stack = FloraTestStack::new().await?;

    let path = test_data::test_page_path()?;
    let result = test_stack.app().ingest(path).await?;
    insta::assert_debug_snapshot!(result);

    Ok(())
}

#[tokio::test]
async fn can_add_directory() -> Result<()> {
    helpers::init_tracing_subscriber()?;
    let test_stack = FloraTestStack::new().await?;

    let path = test_data::test_pages_dir()?;
    let result = test_stack.app().ingest(path).await?;
    insta::assert_debug_snapshot!(result);

    Ok(())
}

#[tokio::test]
async fn can_get_added_record() -> Result<()> {
    helpers::init_tracing_subscriber()?;
    let test_stack = FloraTestStack::new().await?;
    let app = test_stack.app();

    let path = test_data::test_page_path()?;
    let ids = app.ingest(path).await?;

    let recs = sorted_records!(app, ids);
    insta::assert_ron_snapshot!(recs, {
        "[0].timestamp_flora" => "[timestamp_flora]",
    });

    Ok(())
}

#[tokio::test]
async fn can_search_for_added_record() -> Result<()> {
    const QUERY: &str = "example";
    const SYNC_WAIT: Duration = Duration::from_secs(1);

    helpers::init_tracing_subscriber()?;
    let test_stack = FloraTestStack::new().await?;
    let app = test_stack.app();

    let path = test_data::test_page_path()?;
    let _id = app.ingest(path).await?;

    // Give Omry time to sync the records to Typesense.
    time::sleep(SYNC_WAIT).await;

    let search_result = app.search(QUERY.to_string(), None).await?;
    let response = search_result.full_response();

    assert_ne!(response.found, 0);
    insta::assert_ron_snapshot!(response, {
        ".search_time_ms" => "[search_time]",
        ".hits[].document.timestamp" => "[timestamp]",
        ".hits[].document.id" => "[uuid]",
    });

    Ok(())
}

#[tokio::test]
async fn can_get_record_count() -> Result<()> {
    const NUM_RECORDS: usize = 13;

    helpers::init_tracing_subscriber()?;
    let test_stack = FloraTestStack::new().await?;
    let app = test_stack.app();

    let count = app.count().await?;
    assert_eq!(count, 0);

    for _ in 0..NUM_RECORDS {
        let path = test_data::test_page_path()?;
        app.ingest(path).await?;
    }

    let count: usize = app.count().await?.try_into()?;
    assert_eq!(count, NUM_RECORDS);

    Ok(())
}

#[tokio::test]
async fn can_delete_records() -> Result<()> {
    const NUM_RECORDS: usize = 5;

    helpers::init_tracing_subscriber()?;
    let test_stack = FloraTestStack::new().await?;
    let app = test_stack.app();

    let mut ids = Vec::with_capacity(NUM_RECORDS);

    #[allow(clippy::expect_used)]
    for _ in 0..NUM_RECORDS {
        let path = test_data::test_page_path()?;
        let id = app.ingest(path).await?;
        let id = id.first().expect("There must be an ID");
        ids.push(*id);
    }

    let recs = sorted_records!(app, ids.clone());
    insta::assert_ron_snapshot!(recs, {
        "[].timestamp_flora" => "[timestamp_flora]",
        "[].document.data" => "[data]",
    });

    let ids_to_delete = vec![ids[1], ids[3]];

    let n_deleted = app.delete(ids_to_delete.clone()).await?;
    assert_eq!(n_deleted, ids_to_delete.len());

    // let views = app.records(ids).await?.into_vec().await?;
    // let recs = views
    //     .into_iter()
    //     .map(|v| v.into_record().expect("Should be a Record"))
    //     .collect::<Vec<_>>();

    let recs = sorted_records!(app, ids);
    insta::assert_ron_snapshot!(recs, {
        "[].timestamp_flora" => "[timestamp_flora]",
        "[].document.data" => "[data]",
    });

    Ok(())
}

#[tokio::test]
async fn can_get_all_records_meta() -> Result<()> {
    const NUM_RECORDS: usize = 81;

    helpers::init_tracing_subscriber()?;
    let test_stack = FloraTestStack::new().await?;
    let app = test_stack.app();

    let mut ids = Vec::with_capacity(NUM_RECORDS);

    #[allow(clippy::expect_used)]
    for _ in 0..NUM_RECORDS {
        let path = test_data::test_page_path()?;
        let id = app.ingest(path).await?;
        let id = id.first().expect("There must be an ID");
        ids.push(*id);
    }

    let recs = app.all_records_meta().await?.into_vec().await?;

    #[allow(clippy::expect_used)]
    let recs = recs
        .into_iter()
        .map(|v| v.into_record_meta().expect("Should be RecordMeta"))
        .collect::<Vec<_>>();
    insta::assert_ron_snapshot!(recs, {
        "[].timestamp_flora" => "[timestamp_flora]",
    });

    Ok(())
}

#[tokio::test]
async fn can_get_most_recent() -> Result<()> {
    helpers::init_tracing_subscriber()?;
    let test_stack = FloraTestStack::new().await?;
    let app = test_stack.app();
    let n_records = 5;
    let mut ids = Vec::with_capacity(n_records);

    #[allow(clippy::expect_used)]
    for _ in 0..n_records {
        let path = test_data::test_page_path()?;
        let id = app.ingest(path).await?;
        let id = id.first().expect("There must be an ID");
        ids.push(*id);
        // Sleep to have different timestamps.
        tokio::time::sleep(Duration::from_secs(1)).await;
    }

    let recs = app.most_recent().await?.into_vec().await?;

    #[allow(clippy::expect_used)]
    let timestamps = recs
        .into_iter()
        .map(|v| {
            v.into_record_meta()
                .expect("Should be RecordMeta")
                .timestamp_flora
        })
        .collect::<Vec<_>>();
    assert!(timestamps.is_sorted_by(|a, b| a > b));
    Ok(())
}

#[tokio::test]
async fn can_get_version() -> Result<()> {
    helpers::init_tracing_subscriber()?;
    let test_stack = FloraTestStack::new().await?;
    let app = test_stack.app();
    assert!(app.version().await.is_ok());
    Ok(())
}

#[tokio::test]
async fn can_paginate() -> Result<()> {
    const NUM_RECORDS: usize = 15;
    const PAGE_TO_GET: usize = 2;
    const QUERY: &str = "example";
    const SYNC_WAIT: Duration = Duration::from_secs(1);

    helpers::init_tracing_subscriber()?;
    let test_stack = FloraTestStack::new().await?;
    let app = test_stack.app();

    for _ in 0..NUM_RECORDS {
        let path = test_data::test_page_path()?;
        let _id = app.ingest(path).await?;
    }

    time::sleep(SYNC_WAIT).await;

    let search_result = app.search(QUERY.to_string(), Some(PAGE_TO_GET)).await?;
    assert!(search_result.is_last());
    assert_eq!(search_result.page_idx(), PAGE_TO_GET);

    let response = search_result.full_response();
    assert_eq!(response.found, NUM_RECORDS);

    let hits_idxs = search_result
        .hits()
        .into_iter()
        .map(|(_hit, idx)| idx)
        .collect::<Vec<_>>();
    insta::assert_debug_snapshot!(hits_idxs);
    insta::assert_ron_snapshot!(response, {
        ".search_time_ms" => "[search_time]",
        ".hits[]" => "[hit]",
    });

    Ok(())
}

#[tokio::test]
async fn can_get_view_url() -> Result<()> {
    const DEFAULT_PORT: u16 = 37374;

    helpers::init_tracing_subscriber()?;
    let test_stack = FloraTestStack::new().await?;
    let app = test_stack.app();
    let path = test_data::test_page_path()?;
    let ids = app.ingest(path).await?;

    let records = sorted_records!(app, ids);
    let mut urls = Vec::with_capacity(records.len());

    for record in &records {
        #[allow(
            clippy::expect_used,
            reason = "We get these records from the server, so they'll have IDs"
        )]
        let mut url = test_stack
            .app()
            .local_url(record.id().expect("This record has an ID"))
            .parse::<Url>()?;

        // Set to hardcoded port to avoid getting a different one on each test run.
        url.set_port(Some(DEFAULT_PORT))
            .map_err(|()| anyhow::anyhow!("can't set the port"))?;

        urls.push(url);
    }

    insta::with_settings!({filters => vec![
    (r"//([^/?#]*)/", "//[host]/")]}, {
        insta::assert_ron_snapshot!(urls);
    });
    Ok(())
}
