use axum::extract::State;

use axum::Json;
use http_body_util::BodyExt;
use mad_hatter::{bind_http, bind_server, HttpError, Sealed};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tower::ServiceExt;

// ---------------------------------------------------------------------------
// Test state and types
// ---------------------------------------------------------------------------

#[derive(Clone)]
struct AppState {
    prefix: String,
}

#[derive(Serialize, Deserialize)]
struct Item {
    id: u64,
    name: String,
}

#[derive(Deserialize)]
struct CreateItem {
    name: String,
}

// ---------------------------------------------------------------------------
// Handler functions (plain axum handlers)
// ---------------------------------------------------------------------------

async fn list_items(State(state): State<Arc<AppState>>) -> Json<Vec<Item>> {
    Json(vec![Item {
        id: 1,
        name: format!("{}-item", state.prefix),
    }])
}

async fn create_item(
    State(_state): State<Arc<AppState>>,
    Json(body): Json<CreateItem>,
) -> Json<Item> {
    Json(Item {
        id: 42,
        name: body.name,
    })
}

async fn get_item(
    State(_state): State<Arc<AppState>>,
    axum::extract::Path(id): axum::extract::Path<u64>,
) -> Result<Json<Item>, HttpError> {
    if id == 0 {
        return Err(HttpError::not_found().with_message(Sealed::new("item not found".to_string())));
    }
    Ok(Json(Item {
        id,
        name: "found".to_string(),
    }))
}

async fn delete_item(
    State(_state): State<Arc<AppState>>,
    axum::extract::Path(id): axum::extract::Path<u64>,
) -> Result<axum::http::StatusCode, HttpError> {
    if id == 0 {
        return Err(HttpError::not_found().with_message(Sealed::new("item not found".to_string())));
    }
    Ok(axum::http::StatusCode::NO_CONTENT)
}

async fn health() -> &'static str {
    "ok"
}

// ---------------------------------------------------------------------------
// bind_http! declarations (routes only — no config)
// ---------------------------------------------------------------------------

bind_http! {
    TestApi, "/api", Arc<AppState> {
        GET "/items" => list_items -> Json<Vec<Item>>,
        POST "/items" => create_item -> Json<Item>,
        GET "/items/{id}" => get_item -> Json<Item>,
        DELETE "/items/{id}" => delete_item,
    }
}

bind_http! {
    HealthApi, "/health", () {
        GET "" => health,
    }
}

// ---------------------------------------------------------------------------
// Helper
// ---------------------------------------------------------------------------

async fn body_string(response: axum::http::Response<axum::body::Body>) -> String {
    let bytes = response.into_body().collect().await.unwrap().to_bytes();
    String::from_utf8(bytes.to_vec()).unwrap()
}

fn request(method: &str, uri: &str) -> axum::http::Request<axum::body::Body> {
    axum::http::Request::builder()
        .method(method)
        .uri(uri)
        .body(axum::body::Body::empty())
        .unwrap()
}

fn json_request(method: &str, uri: &str, body: &str) -> axum::http::Request<axum::body::Body> {
    axum::http::Request::builder()
        .method(method)
        .uri(uri)
        .header("content-type", "application/json")
        .body(axum::body::Body::from(body.to_string()))
        .unwrap()
}

// ---------------------------------------------------------------------------
// Basic route tests
// ---------------------------------------------------------------------------

#[tokio::test]
async fn test_get_route() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });
    let app = TestApi::router().with_state(state);

    let resp = app.oneshot(request("GET", "/api/items")).await.unwrap();
    assert_eq!(resp.status(), 200);

    let body = body_string(resp).await;
    let items: Vec<Item> = serde_json::from_str(&body).unwrap();
    assert_eq!(items.len(), 1);
    assert_eq!(items[0].name, "test-item");
}

#[tokio::test]
async fn test_post_route() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });
    let app = TestApi::router().with_state(state);

    let resp = app
        .oneshot(json_request(
            "POST",
            "/api/items",
            r#"{"name":"new-item"}"#,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), 200);

    let body = body_string(resp).await;
    let item: Item = serde_json::from_str(&body).unwrap();
    assert_eq!(item.id, 42);
    assert_eq!(item.name, "new-item");
}

#[tokio::test]
async fn test_path_param_ok() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });
    let app = TestApi::router().with_state(state);

    let resp = app.oneshot(request("GET", "/api/items/7")).await.unwrap();
    assert_eq!(resp.status(), 200);

    let body = body_string(resp).await;
    let item: Item = serde_json::from_str(&body).unwrap();
    assert_eq!(item.id, 7);
    assert_eq!(item.name, "found");
}

#[tokio::test]
async fn test_path_param_not_found() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });
    let app = TestApi::router().with_state(state);

    let resp = app.oneshot(request("GET", "/api/items/0")).await.unwrap();
    assert_eq!(resp.status(), 404);

    let body = body_string(resp).await;
    let err: serde_json::Value = serde_json::from_str(&body).unwrap();
    assert_eq!(err["error"], "NOT_FOUND");
}

#[tokio::test]
async fn test_delete_route() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });
    let app = TestApi::router().with_state(state);

    let resp = app
        .oneshot(request("DELETE", "/api/items/5"))
        .await
        .unwrap();
    assert_eq!(resp.status(), 204);
}

#[tokio::test]
async fn test_delete_not_found() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });
    let app = TestApi::router().with_state(state);

    let resp = app
        .oneshot(request("DELETE", "/api/items/0"))
        .await
        .unwrap();
    assert_eq!(resp.status(), 404);
}

#[tokio::test]
async fn test_wrong_method_returns_405() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });
    let app = TestApi::router().with_state(state);

    let resp = app.oneshot(request("PUT", "/api/items")).await.unwrap();
    assert_eq!(resp.status(), 405);
}

#[tokio::test]
async fn test_unknown_path_returns_404() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });
    let app = TestApi::router().with_state(state);

    let resp = app
        .oneshot(request("GET", "/api/unknown"))
        .await
        .unwrap();
    assert_eq!(resp.status(), 404);
}

#[tokio::test]
async fn test_health_endpoint() {
    let app = HealthApi::router().with_state(());

    let resp = app.oneshot(request("GET", "/health")).await.unwrap();
    assert_eq!(resp.status(), 200);

    let body = body_string(resp).await;
    assert_eq!(body, "ok");
}

#[tokio::test]
async fn test_same_path_multi_method() {
    let state = Arc::new(AppState {
        prefix: "x".to_string(),
    });
    let app = TestApi::router().with_state(state.clone());
    let resp = app.oneshot(request("GET", "/api/items")).await.unwrap();
    assert_eq!(resp.status(), 200);

    let app2 = TestApi::router().with_state(state);
    let resp2 = app2
        .oneshot(json_request(
            "POST",
            "/api/items",
            r#"{"name":"y"}"#,
        ))
        .await
        .unwrap();
    assert_eq!(resp2.status(), 200);
}

// ---------------------------------------------------------------------------
// Auth-annotated bind_http! declaration
// ---------------------------------------------------------------------------

async fn handle_login() -> &'static str {
    "login page"
}

async fn handle_status() -> &'static str {
    "status ok"
}

async fn handle_dashboard(State(_state): State<Arc<AppState>>) -> &'static str {
    "dashboard"
}

async fn handle_settings(State(_state): State<Arc<AppState>>) -> &'static str {
    "settings"
}

bind_http! {
    AuthApi, "/app", Arc<AppState> {
        #[auth(none)]
        GET "/login" => handle_login,
        #[auth(none)]
        GET "/status" => handle_status,
        #[auth(session)]
        GET "/dashboard" => handle_dashboard,
        GET "/settings" => handle_settings,
    }
}

// ---------------------------------------------------------------------------
// Auth annotation tests
// ---------------------------------------------------------------------------

#[test]
fn test_auth_none_paths_constant() {
    assert_eq!(AuthApi::AUTH_NONE_PATHS, &["/app/login", "/app/status"]);
}

#[tokio::test]
async fn test_public_router_serves_auth_none_routes() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });
    let app = AuthApi::public_router().with_state(state);

    let resp = app.oneshot(request("GET", "/app/login")).await.unwrap();
    assert_eq!(resp.status(), 200);
    let body = body_string(resp).await;
    assert_eq!(body, "login page");
}

#[tokio::test]
async fn test_public_router_does_not_serve_protected_routes() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });
    let app = AuthApi::public_router().with_state(state);

    let resp = app
        .oneshot(request("GET", "/app/dashboard"))
        .await
        .unwrap();
    assert_eq!(resp.status(), 404);
}

#[tokio::test]
async fn test_protected_router_serves_session_routes() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });
    let app = AuthApi::protected_router().with_state(state);

    let resp = app
        .oneshot(request("GET", "/app/dashboard"))
        .await
        .unwrap();
    assert_eq!(resp.status(), 200);
    let body = body_string(resp).await;
    assert_eq!(body, "dashboard");
}

#[tokio::test]
async fn test_protected_router_serves_default_auth_routes() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });
    let app = AuthApi::protected_router().with_state(state);

    let resp = app
        .oneshot(request("GET", "/app/settings"))
        .await
        .unwrap();
    assert_eq!(resp.status(), 200);
    let body = body_string(resp).await;
    assert_eq!(body, "settings");
}

#[tokio::test]
async fn test_protected_router_does_not_serve_public_routes() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });
    let app = AuthApi::protected_router().with_state(state);

    let resp = app.oneshot(request("GET", "/app/login")).await.unwrap();
    assert_eq!(resp.status(), 404);
}

#[tokio::test]
async fn test_full_router_serves_all_routes() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });

    let app = AuthApi::router().with_state(state.clone());
    let resp = app.oneshot(request("GET", "/app/login")).await.unwrap();
    assert_eq!(resp.status(), 200);

    let app2 = AuthApi::router().with_state(state.clone());
    let resp2 = app2
        .oneshot(request("GET", "/app/dashboard"))
        .await
        .unwrap();
    assert_eq!(resp2.status(), 200);

    let app3 = AuthApi::router().with_state(state);
    let resp3 = app3
        .oneshot(request("GET", "/app/settings"))
        .await
        .unwrap();
    assert_eq!(resp3.status(), 200);
}

#[test]
fn test_no_auth_annotations_empty_none_paths() {
    assert!(TestApi::AUTH_NONE_PATHS.is_empty());
}

// Compile-time test: serve() method exists with correct signature
#[allow(dead_code)]
async fn _serve_compiles() {
    let state = Arc::new(AppState {
        prefix: "test".to_string(),
    });
    let _ = TestApi::serve("127.0.0.1:0", state);
}

// ---------------------------------------------------------------------------
// bind_server! config tests
// ---------------------------------------------------------------------------

bind_server! {
    ConfigServer, Arc<AppState> {
        session_cookie_prefix: "alice_session_",
        public_dir: "apps/",
        auth_paths: {
            login: "/login",
            login_error: "/login?error=1",
            setup_page: "/setup.html",
            root: "/",
        },
        body_limit: 200 * 1024 * 1024,
    }
}

#[test]
fn test_server_session_cookie_prefix_constant() {
    assert_eq!(ConfigServer::SESSION_COOKIE_PREFIX, "alice_session_");
}

#[test]
fn test_server_session_cookie_name_function() {
    assert_eq!(ConfigServer::session_cookie_name(8080), "alice_session_8080");
    assert_eq!(ConfigServer::session_cookie_name(3000), "alice_session_3000");
}

#[test]
fn test_server_public_dir_constant() {
    assert_eq!(ConfigServer::PUBLIC_DIR, "apps/");
}

#[test]
fn test_server_auth_paths_constants() {
    assert_eq!(ConfigServer::LOGIN_PATH, "/login");
    assert_eq!(ConfigServer::LOGIN_ERROR_PATH, "/login?error=1");
    assert_eq!(ConfigServer::SETUP_PAGE_PATH, "/setup.html");
    assert_eq!(ConfigServer::ROOT_PATH, "/");
}

#[test]
fn test_server_body_limit_constant() {
    assert_eq!(ConfigServer::BODY_LIMIT, 200 * 1024 * 1024);
}

// ---------------------------------------------------------------------------
// bind_server! auth middleware tests (reverse design)
// ---------------------------------------------------------------------------

struct TestAuthState {
    do_skip_auth: bool,
    cookie_name: String,
    valid_token: String,
}

impl mad_hatter::AuthPolicy for TestAuthState {
    fn skip_auth(&self) -> bool { self.do_skip_auth }
    fn session_cookie_name(&self) -> &str { &self.cookie_name }
    fn validate_session(&self, token: &str) -> bool { token == self.valid_token }
    fn pre_auth_check(&self, _path: &str, _req: &axum::extract::Request) -> mad_hatter::PreAuthResult {
        mad_hatter::PreAuthResult::Continue
    }
    fn unauthorized_response(&self) -> axum::response::Response {
        use axum::response::IntoResponse;
        axum::http::StatusCode::UNAUTHORIZED.into_response()
    }
    fn session_token(&self) -> &str { &self.valid_token }
}

// Server config with auth_policy
bind_server! {
    AuthServer, Arc<TestAuthState> {
        auth_policy: TestAuthState,
        session_cookie_prefix: "test_",
    }
}

// Route group for auth testing
async fn handle_auth_public() -> &'static str { "public" }
async fn handle_auth_private(State(_): State<Arc<TestAuthState>>) -> &'static str { "private" }

bind_http! {
    AuthRoutes, "/api", Arc<TestAuthState> {
        #[auth(none)]
        GET "/open" => handle_auth_public,
        #[auth(session)]
        GET "/secret" => handle_auth_private,
    }
}

// Helper: build router with reverse design (auth only on protected)
fn build_auth_test_router(state: Arc<TestAuthState>) -> axum::Router {
    let protected = AuthRoutes::protected_router()
        .layer(axum::middleware::from_fn_with_state(state.clone(), AuthServer::auth_middleware));
    let public = AuthRoutes::public_router();
    protected.merge(public).with_state(state)
}

#[tokio::test]
async fn test_auth_middleware_skip_auth() {
    let state = Arc::new(TestAuthState {
        do_skip_auth: true,
        cookie_name: "test_session".to_string(),
        valid_token: "token123".to_string(),
    });
    let app = build_auth_test_router(state);
    let resp = app.oneshot(request("GET", "/api/secret")).await.unwrap();
    assert_eq!(resp.status(), 200);
    assert_eq!(body_string(resp).await, "private");
}

#[tokio::test]
async fn test_auth_middleware_unauthorized() {
    let state = Arc::new(TestAuthState {
        do_skip_auth: false,
        cookie_name: "test_session".to_string(),
        valid_token: "token123".to_string(),
    });
    let app = build_auth_test_router(state);
    // No cookie → unauthorized
    let resp = app.oneshot(request("GET", "/api/secret")).await.unwrap();
    assert_eq!(resp.status(), 401);
}

#[tokio::test]
async fn test_auth_middleware_public_route_no_auth_needed() {
    let state = Arc::new(TestAuthState {
        do_skip_auth: false,
        cookie_name: "test_session".to_string(),
        valid_token: "token123".to_string(),
    });
    let app = build_auth_test_router(state);
    // Public route bypasses auth middleware entirely (reverse design)
    let resp = app.oneshot(request("GET", "/api/open")).await.unwrap();
    assert_eq!(resp.status(), 200);
    assert_eq!(body_string(resp).await, "public");
}

#[tokio::test]
async fn test_auth_middleware_valid_cookie() {
    let state = Arc::new(TestAuthState {
        do_skip_auth: false,
        cookie_name: "test_session".to_string(),
        valid_token: "token123".to_string(),
    });
    let app = build_auth_test_router(state);
    let req = axum::http::Request::builder()
        .method("GET")
        .uri("/api/secret")
        .header("cookie", "test_session=token123")
        .body(axum::body::Body::empty())
        .unwrap();
    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), 200);
    assert_eq!(body_string(resp).await, "private");
}

#[tokio::test]
async fn test_auth_middleware_invalid_cookie() {
    let state = Arc::new(TestAuthState {
        do_skip_auth: false,
        cookie_name: "test_session".to_string(),
        valid_token: "token123".to_string(),
    });
    let app = build_auth_test_router(state);
    let req = axum::http::Request::builder()
        .method("GET")
        .uri("/api/secret")
        .header("cookie", "test_session=wrong_token")
        .body(axum::body::Body::empty())
        .unwrap();
    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), 401);
}

#[tokio::test]
async fn test_auth_middleware_pre_auth_check_redirect() {
    struct RedirectAuthState;
    impl mad_hatter::AuthPolicy for RedirectAuthState {
        fn skip_auth(&self) -> bool { false }
        fn session_cookie_name(&self) -> &str { "s" }
        fn validate_session(&self, _token: &str) -> bool { false }
        fn pre_auth_check(&self, path: &str, _req: &axum::extract::Request) -> mad_hatter::PreAuthResult {
            if path == "/api/secret" {
                mad_hatter::PreAuthResult::Redirect("/login".to_string())
            } else {
                mad_hatter::PreAuthResult::Continue
            }
        }
        fn unauthorized_response(&self) -> axum::response::Response {
            use axum::response::IntoResponse;
            axum::http::StatusCode::UNAUTHORIZED.into_response()
        }
        fn session_token(&self) -> &str { "tok" }
    }

    let state = RedirectAuthState;
    use mad_hatter::AuthPolicy as _;
    let result = state.pre_auth_check("/api/secret", &axum::extract::Request::default());
    match result {
        mad_hatter::PreAuthResult::Redirect(url) => assert_eq!(url, "/login"),
        _ => panic!("expected redirect"),
    }
}

// ---------------------------------------------------------------------------
// Absolute path and ANY method tests
// ---------------------------------------------------------------------------

async fn handle_abs_login() -> &'static str { "login page" }
async fn handle_abs_public() -> &'static str { "public" }
async fn handle_abs_any(State(_): State<Arc<TestAuthState>>) -> &'static str { "any method" }

bind_http! {
    AbsApi, "/api", Arc<TestAuthState> {
        #[auth(session)]
        GET "/items" => handle_auth_private,

        #[auth(none)]
        GET "!/login" => handle_abs_login,
        #[auth(none)]
        GET "!/public/files" => handle_abs_public,
        #[auth(session)]
        ANY "!/proxy/{*path}" => handle_abs_any,
    }
}

#[test]
fn test_abs_auth_none_paths_includes_absolute() {
    assert!(AbsApi::AUTH_NONE_PATHS.contains(&"/login"));
    assert!(AbsApi::AUTH_NONE_PATHS.contains(&"/public/files"));
    assert!(!AbsApi::AUTH_NONE_PATHS.contains(&"/proxy/{*path}"));
}

#[tokio::test]
async fn test_abs_route_serves_at_absolute_path() {
    let state = Arc::new(TestAuthState {
        do_skip_auth: true,
        cookie_name: "abs_session".to_string(),
        valid_token: "t".to_string(),
    });
    let app = AbsApi::router().with_state(state);
    let resp = app.oneshot(request("GET", "/login")).await.unwrap();
    assert_eq!(resp.status(), 200);
    assert_eq!(body_string(resp).await, "login page");
}

#[tokio::test]
async fn test_abs_prefixed_route_still_works() {
    let state = Arc::new(TestAuthState {
        do_skip_auth: true,
        cookie_name: "abs_session".to_string(),
        valid_token: "t".to_string(),
    });
    let app = AbsApi::router().with_state(state);
    let resp = app.oneshot(request("GET", "/api/items")).await.unwrap();
    assert_eq!(resp.status(), 200);
}

#[tokio::test]
async fn test_any_method_get() {
    let state = Arc::new(TestAuthState {
        do_skip_auth: true,
        cookie_name: "abs_session".to_string(),
        valid_token: "t".to_string(),
    });
    let app = AbsApi::router().with_state(state);
    let resp = app.oneshot(request("GET", "/proxy/test")).await.unwrap();
    assert_eq!(resp.status(), 200);
    assert_eq!(body_string(resp).await, "any method");
}

#[tokio::test]
async fn test_any_method_post() {
    let state = Arc::new(TestAuthState {
        do_skip_auth: true,
        cookie_name: "abs_session".to_string(),
        valid_token: "t".to_string(),
    });
    let app = AbsApi::router().with_state(state);
    let resp = app.oneshot(request("POST", "/proxy/test")).await.unwrap();
    assert_eq!(resp.status(), 200);
    assert_eq!(body_string(resp).await, "any method");
}

#[tokio::test]
async fn test_any_method_delete() {
    let state = Arc::new(TestAuthState {
        do_skip_auth: true,
        cookie_name: "abs_session".to_string(),
        valid_token: "t".to_string(),
    });
    let app = AbsApi::router().with_state(state);
    let resp = app.oneshot(request("DELETE", "/proxy/test")).await.unwrap();
    assert_eq!(resp.status(), 200);
    assert_eq!(body_string(resp).await, "any method");
}

// ---------------------------------------------------------------------------
// Reverse design integration: multi-group merge
// ---------------------------------------------------------------------------

async fn handle_api_list() -> &'static str { "api list" }
async fn handle_page_home() -> &'static str { "home" }

bind_http! {
    ApiGroup, "/api", () {
        #[auth(session)]
        GET "/list" => handle_api_list,
    }
}

bind_http! {
    PageGroup, "", () {
        #[auth(none)]
        GET "/home" => handle_page_home,
    }
}

#[tokio::test]
async fn test_multi_group_merge() {
    // Simulate reverse design: protected routes from all groups + public routes from all groups
    let app = axum::Router::new()
        .merge(ApiGroup::protected_router())
        .merge(PageGroup::protected_router())
        .merge(ApiGroup::public_router())
        .merge(PageGroup::public_router())
        .with_state(());

    // Protected route works (no auth middleware in this test — just routing)
    let resp = app.clone().oneshot(request("GET", "/api/list")).await.unwrap();
    assert_eq!(resp.status(), 200);
    assert_eq!(body_string(resp).await, "api list");

    // Public route works
    let resp2 = app.oneshot(request("GET", "/home")).await.unwrap();
    assert_eq!(resp2.status(), 200);
    assert_eq!(body_string(resp2).await, "home");
}