//! Tests for HttpResponse derive macro.

use mad_hatter::HttpResponse;
use mad_hatter::Sealed;
use serde::Serialize;

/// Helper: convert response to body bytes
async fn body_bytes(resp: axum::response::Response) -> Vec<u8> {
    use http_body_util::BodyExt;
    let (_, body) = resp.into_parts();
    let bytes = body.collect().await.unwrap().to_bytes();
    bytes.to_vec()
}

/// Helper: convert response to JSON value
async fn body_json(resp: axum::response::Response) -> serde_json::Value {
    let bytes = body_bytes(resp).await;
    serde_json::from_slice(&bytes).unwrap()
}

// ── Plain struct (no Sealed fields) ──

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

#[tokio::test]
async fn test_plain_struct_serialization() {
    use axum::response::IntoResponse;
    let result = SimpleResult {
        id: 42,
        name: "alice".to_string(),
    };
    let resp = result.into_response();
    assert_eq!(resp.status(), axum::http::StatusCode::OK);

    let json = body_json(resp).await;
    assert_eq!(json["id"], 42);
    assert_eq!(json["name"], "alice");
}

#[derive(Serialize, HttpResponse)]
struct EmptyResult {}

#[tokio::test]
async fn test_empty_struct() {
    use axum::response::IntoResponse;
    let result = EmptyResult {};
    let resp = result.into_response();
    assert_eq!(resp.status(), axum::http::StatusCode::OK);

    let json = body_json(resp).await;
    assert!(json.as_object().unwrap().is_empty());
}

// ── Struct with Sealed fields ──

#[derive(Serialize, HttpResponse)]
struct SealedResult {
    id: String,
    message: Sealed<String>,
}

#[tokio::test]
async fn test_sealed_field_unwrapped() {
    use axum::response::IntoResponse;
    let result = SealedResult {
        id: "abc123".to_string(),
        message: Sealed::new("Operation successful".to_string()),
    };
    let resp = result.into_response();
    assert_eq!(resp.status(), axum::http::StatusCode::OK);

    let json = body_json(resp).await;
    assert_eq!(json["id"], "abc123");
    assert_eq!(json["message"], "Operation successful");
}

#[derive(Serialize, HttpResponse)]
struct MixedResult {
    count: u32,
    label: Sealed<&'static str>,
    description: String,
}

#[tokio::test]
async fn test_mixed_sealed_and_plain_fields() {
    use axum::response::IntoResponse;
    let result = MixedResult {
        count: 7,
        label: Sealed::new("important"),
        description: "Some description".to_string(),
    };
    let resp = result.into_response();

    let json = body_json(resp).await;
    assert_eq!(json["count"], 7);
    assert_eq!(json["label"], "important");
    assert_eq!(json["description"], "Some description");
}

// ── Sealed<String> with dynamic content ──

#[derive(Serialize, HttpResponse)]
struct DynamicSealedResult {
    msg: Sealed<String>,
}

#[tokio::test]
async fn test_sealed_dynamic_string() {
    use axum::response::IntoResponse;
    let dynamic_msg = format!("Instance '{}' created", "inst-001");
    let result = DynamicSealedResult {
        msg: Sealed::new(dynamic_msg),
    };
    let resp = result.into_response();

    let json = body_json(resp).await;
    assert_eq!(json["msg"], "Instance 'inst-001' created");
}

// ── Content-Type verification ──

#[derive(Serialize, HttpResponse)]
struct TypeCheckResult {
    ok: bool,
}

#[tokio::test]
async fn test_response_content_type_is_json() {
    use axum::response::IntoResponse;
    let result = TypeCheckResult { ok: true };
    let resp = result.into_response();
    let ct = resp.headers().get("content-type").unwrap().to_str().unwrap();
    assert!(ct.contains("application/json"), "Content-Type should be JSON, got: {}", ct);
}

// ── HttpError tests ──

#[tokio::test]
async fn test_http_error_not_found() {
    use axum::response::IntoResponse;
    let err = mad_hatter::HttpError::not_found().with_message(Sealed::new("Resource missing".to_string()));
    assert_eq!(err.status, axum::http::StatusCode::NOT_FOUND);
    assert_eq!(err.error_code, "NOT_FOUND");

    let resp = err.into_response();
    assert_eq!(resp.status(), axum::http::StatusCode::NOT_FOUND);

    let json = body_json(resp).await;
    assert_eq!(json["error"], "NOT_FOUND");
    assert_eq!(json["message"], "Resource missing");
}

#[tokio::test]
async fn test_http_error_bad_request() {
    use axum::response::IntoResponse;
    let err = mad_hatter::HttpError::bad_request().with_message(Sealed::new("Invalid input".to_string()));
    assert_eq!(err.error_code, "BAD_REQUEST");

    let resp = err.into_response();
    assert_eq!(resp.status(), axum::http::StatusCode::BAD_REQUEST);

    let json = body_json(resp).await;
    assert_eq!(json["error"], "BAD_REQUEST");
    assert_eq!(json["message"], "Invalid input");
}

#[tokio::test]
async fn test_http_error_internal() {
    use axum::response::IntoResponse;
    let err = mad_hatter::HttpError::internal().with_message(Sealed::new("Something broke".to_string()));

    let resp = err.into_response();
    assert_eq!(resp.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);

    let json = body_json(resp).await;
    assert_eq!(json["error"], "INTERNAL_SERVER_ERROR");
    assert_eq!(json["message"], "Something broke");
}

#[tokio::test]
async fn test_http_error_custom_error_code() {
    use axum::response::IntoResponse;
    let err = mad_hatter::HttpError::new(
        axum::http::StatusCode::CONFLICT,
        "DUPLICATE_NAME",
    ).with_message(Sealed::new("Name already exists".to_string()));
    assert_eq!(err.error_code, "DUPLICATE_NAME");

    let resp = err.into_response();
    assert_eq!(resp.status(), axum::http::StatusCode::CONFLICT);

    let json = body_json(resp).await;
    assert_eq!(json["error"], "DUPLICATE_NAME");
    assert_eq!(json["message"], "Name already exists");
}

#[test]
fn test_http_error_display() {
    let err = mad_hatter::HttpError::not_found();
    assert_eq!(format!("{}", err), "404: NOT_FOUND");
}

#[tokio::test]
async fn test_http_error_no_message() {
    use axum::response::IntoResponse;
    let err = mad_hatter::HttpError::not_found();
    let resp = err.into_response();
    let json = body_json(resp).await;
    assert_eq!(json["error"], "NOT_FOUND");
    assert!(json.get("message").is_none(), "message field should be absent when None");
}

#[tokio::test]
async fn test_http_error_forbidden() {
    use axum::response::IntoResponse;
    let err = mad_hatter::HttpError::forbidden();
    let resp = err.into_response();
    assert_eq!(resp.status(), axum::http::StatusCode::FORBIDDEN);
    let json = body_json(resp).await;
    assert_eq!(json["error"], "FORBIDDEN");
}

#[tokio::test]
async fn test_http_error_bad_gateway() {
    use axum::response::IntoResponse;
    let err = mad_hatter::HttpError::bad_gateway();
    let resp = err.into_response();
    assert_eq!(resp.status(), axum::http::StatusCode::BAD_GATEWAY);
    let json = body_json(resp).await;
    assert_eq!(json["error"], "BAD_GATEWAY");
}