use std::{cell::OnceCell, str::FromStr, sync::Arc};

use actix_http::Request;
use actix_web::{
    body::MessageBody,
    dev::{Service, ServiceResponse},
    test::init_service,
};
use brass_config::{load_config, Config, Environment};
use lettre::transport::stub::StubTransport;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use regex::{Captures, Regex};
use sqlx::{
    postgres::{PgConnectOptions, PgPoolOptions},
    Connection, Executor, PgConnection, PgPool, Pool, Postgres,
};

mod test_requests;
pub use test_requests::test_delete;
pub use test_requests::test_get;
pub use test_requests::test_post;
pub use test_requests::RequestConfig;
pub use test_requests::read_body;

use crate::create_app;

#[derive(Debug)]
pub struct DbTestContext {
    pub db_pool: PgPool,
    config: Config,
}

impl DbTestContext {
    pub async fn app(
        &self,
    ) -> impl Service<
        Request,
        Response = ServiceResponse<impl MessageBody>,
        Error = actix_web::error::Error,
    > {
        init_service(create_app(
            self.config.clone(),
            self.db_pool.clone(),
            StubTransport::new_ok(),
        ))
        .await
    }
}

#[allow(unused)]
pub async fn setup() -> DbTestContext {
    let init_config: OnceCell<Config> = OnceCell::new();
    let config = init_config.get_or_init(|| load_config(&Environment::Test).unwrap());

    let test_db_pool = setup_db(&config.database_url).await;

    let context = DbTestContext {
        config: config.clone(),
        db_pool: test_db_pool,
    };

    context
}

#[allow(unused)]
pub async fn teardown(context: DbTestContext) {
    //drop(context.app);

    teardown_db(context.db_pool);
}

#[allow(unused)]
pub async fn setup_db(url: &str) -> PgPool {
    let test_db_config = prepare_db(url).await;

    println!("{test_db_config}");
    let pool = PgPoolOptions::new().connect(&test_db_config).await.unwrap();

    pool
}

/// Drops a dedicated database for a test case.
///
/// This function is automatically called by the [`abc-macros::db_test`] macro. It ensures test-specific database are cleaned up after each test run so we don't end up with large numbers of unused databases.
pub async fn teardown_db(pool: PgPool) {
    let mut connect_options = pool.connect_options();
    let db_config = Arc::make_mut(&mut connect_options);

    drop(pool);

    let root_db_config = db_config.clone().database("postgres");
    let mut connection: PgConnection = Connection::connect_with(&root_db_config).await.unwrap();

    let test_db_name = db_config.get_database().unwrap();

    let query = format!("DROP DATABASE IF EXISTS {}", test_db_name);
    connection.execute(query.as_str()).await.unwrap();
}

async fn prepare_db(url: &str) -> String {
    let db_config = PgConnectOptions::from_str(url).expect("Invalid DATABASE_URL!");
    let db_name = db_config.get_database().unwrap();

    let root_db_config = db_config.clone().database("postgres");
    let mut connection: PgConnection = Connection::connect_with(&root_db_config).await.unwrap();

    let test_db_name = build_test_db_name(db_name);

    let query = format!("CREATE DATABASE {} TEMPLATE {}", test_db_name, db_name);
    connection.execute(query.as_str()).await.unwrap();

    let regex = Regex::new(r"(.+)\/(.+$)").unwrap();
    let test_db_url = regex.replace(url, |caps: &Captures| {
        format!("{}/{}", &caps[1], test_db_name)
    });

    println!("{test_db_url}");
    test_db_url.to_string()
}

fn build_test_db_name(base_name: &str) -> String {
    let test_db_suffix: String = thread_rng()
        .sample_iter(&Alphanumeric)
        .take(30)
        .map(char::from)
        .collect();
    format!("{}_{}", base_name, test_db_suffix).to_lowercase()
}

macro_rules! assert_snapshot {
    ($x:expr) => {
        insta::with_settings!({snapshot_path => "../../../snapshots"}, {
            insta::assert_snapshot!($x);
        });
    };
}

pub(crate) use assert_snapshot;
