use axum::Router;
use once_cell::sync::Lazy;
use portpicker::{pick_unused_port, Port};
use springtime::application;
use springtime::future::{BoxFuture, FutureExt};
use springtime_di::instance_provider::ErrorPtr;
use springtime_di::{component_alias, Component};
use springtime_web_axum::axum::extract::Path;
use springtime_web_axum::config::{ServerConfig, WebConfig, WebConfigProvider};
use springtime_web_axum::controller;
use springtime_web_axum::server::{ShutdownSignalSender, ShutdownSignalSource};
use std::sync::Mutex;
use tokio::sync::Barrier;

#[derive(Component)]
struct TestController;

#[controller(path = "/test", server_names = ["default", "test"])]
impl TestController {
    #[get("/:user_id")]
    async fn hello_world(&self, Path(user_id): Path<u32>) -> String {
        format!("Hello {user_id}!")
    }

    #[post("/")]
    async fn post_something(&self) -> &'static str {
        "Posted!"
    }

    #[fallback]
    async fn fallback(&self) -> &'static str {
        "fallback"
    }

    #[router_source]
    fn create_router(&self) -> Result<Router, ErrorPtr> {
        Ok(Router::new())
    }

    #[router_post_configure]
    fn post_configure_router(&self, router: Router) -> Result<Router, ErrorPtr> {
        Ok(router)
    }
}

#[derive(Component)]
#[component(constructor = "TestWebConfigProvider::new")]
struct TestWebConfigProvider {
    #[component(ignore)]
    config: WebConfig,
}

#[component_alias]
impl WebConfigProvider for TestWebConfigProvider {
    fn config(&self) -> BoxFuture<'_, Result<&WebConfig, ErrorPtr>> {
        async { Ok(&self.config) }.boxed()
    }
}

impl TestWebConfigProvider {
    fn new() -> BoxFuture<'static, Result<Self, ErrorPtr>> {
        async {
            let mut server_config = ServerConfig::default();
            server_config.listen_address = format!("127.0.0.1:{}", *PORT);

            let mut config = WebConfig::default();
            config.servers = [("test".to_string(), server_config)].into_iter().collect();

            Ok(Self { config })
        }
        .boxed()
    }
}

static SHUTDOWN_SIGNAL: Lazy<Mutex<Option<ShutdownSignalSender>>> = Lazy::new(Default::default);
static START_BARRIER: Lazy<Barrier> = Lazy::new(|| Barrier::new(2));
static PORT: Lazy<Port> = Lazy::new(|| pick_unused_port().unwrap());

#[derive(Component)]
struct TestShutdownSignalSource;

#[component_alias]
impl ShutdownSignalSource for TestShutdownSignalSource {
    fn register_shutdown(&self, shutdown_sender: ShutdownSignalSender) -> Result<(), ErrorPtr> {
        SHUTDOWN_SIGNAL.lock().unwrap().replace(shutdown_sender);
        tokio::spawn(async {
            START_BARRIER.wait().await;
        });

        Ok(())
    }
}

#[tokio::test]
async fn should_register_controller() {
    let handle = tokio::spawn(async {
        let mut application = application::create_default().unwrap();
        application.run().await.unwrap();
    });

    let body = reqwest::get(format!("http://localhost:{}/test/42", *PORT))
        .await
        .unwrap()
        .text()
        .await
        .unwrap();
    assert_eq!(body, "Hello 42!");

    let body = reqwest::get(format!("http://localhost:{}/test/invalid/route", *PORT))
        .await
        .unwrap()
        .text()
        .await
        .unwrap();
    assert_eq!(body, "fallback");

    START_BARRIER.wait().await;
    SHUTDOWN_SIGNAL
        .lock()
        .unwrap()
        .as_ref()
        .unwrap()
        .send(())
        .unwrap();

    handle.await.unwrap();
}