//! Common utilities used in flora-cli integration tests.

/// Flora test stack (backend + bridge).
pub mod test_stack {
    use anyhow::Result;
    use omry_cli::{App, AppConfig, ServiceBridge};
    use omry_integration::flora_stack::{self, FloraStack, FloraStackOptions, api_container};
    use std::net::SocketAddr;
    use std::sync::Arc;
    use std::time::Duration;
    use tokio::time;
    use tracing::instrument;

    pub(crate) async fn get_config(flora_stack: &FloraStack) -> Result<AppConfig> {
        const MAX_ELEMENTS_TO_PARSE: usize = 15000;

        let (host, ports) = flora_stack::get_host_and_ports(flora_stack.flora_container()).await?;
        let host = dns_lookup::lookup_host(&host)?
            .next()
            .ok_or_else(|| anyhow::anyhow!("failed to look up address for {host}"))?;
        let web_addr = SocketAddr::new(host, ports.web);
        let web_addr = format!("http://{web_addr}");
        Ok(AppConfig {
            auth_token: flora_stack.auth_token().into(),
            web_addr,
            max_elements_to_parse: MAX_ELEMENTS_TO_PARSE,
        })
    }

    #[instrument]
    pub(crate) async fn init_test_app(flora_stack: Arc<FloraStack>) -> Result<App> {
        const MAX_CONN_ATTEMPTS: usize = 5;
        const BACKOFF_MULTIPLIER: u32 = 2;

        tokio::spawn(async move {
            let mut attempts_so_far = 0;
            let mut backoff_interval = Duration::from_secs(1);

            // Attempt to first create a flora RPC client, then use that client
            // to initialize our RPC bridge.
            //
            // We need to recreate the flora client on every iteration of the loop
            // even if we initialized it successfully in a previous iteration
            // because the client's connection to the flora RPC server can shut down
            // when we use tokio::time::sleep.

            loop {
                match api_container(&flora_stack).await {
                    Ok(api) => {
                        let config = get_config(&flora_stack).await?;
                        let app = App::from_api(api, config);
                        match app.ping().await {
                            Ok(_) => break Ok(app),
                            Err(error) => {
                                tracing::warn!(
                                    "error while initializing the tarpc client: {error}"
                                );
                            }
                        }
                    }
                    Err(error) => {
                        tracing::warn!("error while creating the flora client: {error}");
                    }
                }

                if attempts_so_far == MAX_CONN_ATTEMPTS {
                    anyhow::bail!("couldn't succeed after {MAX_CONN_ATTEMPTS} attempts");
                }

                time::sleep(backoff_interval).await;
                backoff_interval *= BACKOFF_MULTIPLIER;
                attempts_so_far += 1;
            }
        })
        .await?
    }

    /// Flora test stack.
    #[derive(Debug)]
    pub struct FloraTestStack {
        flora_stack: Arc<FloraStack>,
        app: App,
    }

    impl FloraTestStack {
        #[instrument]
        pub(crate) async fn new() -> Result<Self> {
            let stack_options = FloraStackOptions {
                search_sync_interval: Duration::from_secs(5),
                ..Default::default()
            };
            let flora_stack = Arc::new(FloraStack::new(Some(stack_options)).await?);
            let app = init_test_app(Arc::clone(&flora_stack)).await?;

            tracing::info!("successfully initialized the test bridge");

            Ok(Self { flora_stack, app })
        }

        /// Returns a reference to the bridge API with tarpc.
        #[must_use]
        pub fn app(&self) -> &App {
            &self.app
        }

        /// Attempts to stop ("kill") this Omry stack, shutting down
        /// the connection(s) with the Omry backend.
        ///
        /// ## Errors
        /// Returns [`anyhow::Error`] if we can't stop the Omry backend.
        pub async fn kill_flora_rpc(&mut self) -> Result<()> {
            self.flora_stack.stop().await?;
            Ok(())
        }
    }
}

/// Module that provides functions to get test data.
pub mod test_data {
    use anyhow::Result;
    use omry_cli::ingest;
    use omry_service::archiving::RecordParams;
    use std::env;
    use std::fs;
    use std::path::PathBuf;

    const WORKSPACE_DIR_KEY: &str = "CARGO_WORKSPACE_DIR";

    const TEST_DATA_PATH: &str = "test_data";
    const TEST_PAGES_DIR: &str = "pages";
    const TEST_PAGE_PATH: &str = "pages/example.html";

    /// Returns the path containing data used for tests.
    fn test_data_path() -> Result<PathBuf> {
        let (_, workspace_dir_path) = env::vars()
            .find(|(k, _)| k == WORKSPACE_DIR_KEY)
            .ok_or_else(|| anyhow::anyhow!("{WORKSPACE_DIR_KEY} is not set"))?;

        let test_data_path = fs::canonicalize(workspace_dir_path)?.join(TEST_DATA_PATH);
        Ok(test_data_path)
    }

    /// Returns the path of the test page ("example.html")
    ///
    /// ## Errors
    /// Returns [`anyhow::Error`] if we can't get the path to the test data.
    pub fn test_page_path() -> Result<PathBuf> {
        let test_data_path = test_data_path()?;
        Ok(test_data_path.join(TEST_PAGE_PATH))
    }

    /// Returns the path containing multiple test pages.
    ///
    /// ## Errors
    /// Returns [`anyhow::Error`] if we can't get the path to the test data.
    pub fn test_pages_dir() -> Result<PathBuf> {
        let test_data_path = test_data_path()?;
        Ok(test_data_path.join(TEST_PAGES_DIR))
    }

    /// Returns a record created from the test HTML.
    ///
    /// ## Errors
    /// Returns [`anyhow::Error`] if we can't get the path to the test data.
    pub fn get_test_record() -> Result<RecordParams> {
        let path = test_page_path()?;
        ingest::record_from_path(&path, None)
    }
}
