use thiserror::Error;

use crate::ax_backend_lowering::prelude::*;
use crate::ax_backend_parser::{parse_backend_ax, AxBackendParseError};

#[derive(Debug, Error, PartialEq, Eq)]
pub enum AxBackendCodegenError {
    #[error("route path is required to generate a route handler")]
    MissingRoutePath,
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum AxBackendCompileError {
    #[error("backend parse failed: {0}")]
    Parse(#[from] AxBackendParseError),
    #[error("backend lowering failed: {0}")]
    Lower(#[from] AxBackendLowerError),
    #[error("backend code generation failed: {0}")]
    Codegen(#[from] AxBackendCodegenError),
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum AxBackendBundleError {
    #[error("backend source `{name}` failed to compile: {source}")]
    Source {
        name: String,
        #[source]
        source: AxBackendCompileError,
    },
}

pub fn generate_backend_module(plan: &AxBackendPlan) -> Result<String, AxBackendCodegenError> {
    let mut out = String::new();
    out.push_str("// @generated by Axonyx backend draft codegen\n");
    out.push_str("#![allow(dead_code, unused_variables)]\n\n");
    out.push_str("use std::collections::BTreeMap;\n\n");
    out.push_str("use axonyx_runtime::backend_prelude::*;\n");
    out.push_str("use axonyx_runtime::server_prelude::*;\n");
    out.push_str("use serde_json::{json, Value};\n\n");
    out.push_str("fn __ax_finalize_response(mut response: AxHttpResponse, headers: BTreeMap<String, String>, cookies: Vec<AxCookie>) -> AxHttpResponse {\n");
    out.push_str("    for (name, value) in headers {\n");
    out.push_str("        response = response.with_header(name, value);\n");
    out.push_str("    }\n");
    out.push_str("    for cookie in cookies {\n");
    out.push_str("        response = response.with_cookie(cookie);\n");
    out.push_str("    }\n");
    out.push_str("    response\n");
    out.push_str("}\n\n");
    out.push_str(
        "fn __ax_request_input_field(request: &AxHttpRequest, name: &str) -> Option<String> {\n",
    );
    out.push_str("    request.form_value(name).or_else(|| request.json_field_string(name))\n");
    out.push_str("}\n\n");

    for handler in &plan.handlers {
        if let Some(input) = handler_input_fields(handler) {
            out.push_str(&render_input_struct(handler, input));
            out.push('\n');
        }

        out.push_str(&render_handler_fn(handler)?);
        out.push('\n');
    }

    Ok(out)
}

pub fn compile_backend_ax_to_module(input: &str) -> Result<String, AxBackendCompileError> {
    let document = parse_backend_ax(input)?;
    let plan = lower_backend_document(&document)?;
    Ok(generate_backend_module(&plan)?)
}

pub fn compile_backend_sources_to_module(
    sources: &[(&str, &str)],
) -> Result<String, AxBackendBundleError> {
    let mut handlers = Vec::new();

    for (name, input) in sources {
        let document = parse_backend_ax(input).map_err(|source| AxBackendBundleError::Source {
            name: (*name).to_string(),
            source: AxBackendCompileError::Parse(source),
        })?;
        let plan =
            lower_backend_document(&document).map_err(|source| AxBackendBundleError::Source {
                name: (*name).to_string(),
                source: AxBackendCompileError::Lower(source),
            })?;
        handlers.extend(plan.handlers);
    }

    generate_backend_module(&AxBackendPlan::new(handlers)).map_err(|source| {
        AxBackendBundleError::Source {
            name: "bundle".to_string(),
            source: AxBackendCompileError::Codegen(source),
        }
    })
}

fn render_input_struct(handler: &AxHandlerPlan, input: &[AxFieldPlan]) -> String {
    let mut out = String::new();
    out.push_str("#[derive(Debug, Clone)]\n");
    out.push_str(&format!(
        "pub struct {} {{\n",
        input_struct_name(&handler.rust_fn)
    ));

    for field in input {
        out.push_str(&format!("    pub {}: {},\n", field.name, field.rust_ty));
    }

    out.push_str("}\n");
    out
}

fn render_handler_fn(handler: &AxHandlerPlan) -> Result<String, AxBackendCodegenError> {
    let signature = match &handler.kind {
        AxHandlerKind::Action { .. } => format!(
            "pub fn {}(runtime: &impl AxBackendRuntime, input: &{}) -> AxRuntimeResult<Value>",
            handler.rust_fn,
            input_struct_name(&handler.rust_fn)
        ),
        AxHandlerKind::Loader { .. } => format!(
            "pub fn {}(runtime: &impl AxBackendRuntime, context: &AxLoaderContext) -> AxRuntimeResult<Value>",
            handler.rust_fn
        ),
        AxHandlerKind::Route { .. } => format!(
            "pub fn {}(runtime: &impl AxBackendRuntime, request: &AxHttpRequest) -> AxRuntimeResult<AxHttpResponse>",
            handler.rust_fn
        ),
        AxHandlerKind::Job => format!(
            "pub fn {}(runtime: &impl AxBackendRuntime) -> AxRuntimeResult<Value>",
            handler.rust_fn
        ),
    };

    let mut out = String::new();
    out.push_str(&signature);
    out.push_str(" {\n");

    if let AxHandlerKind::Route {
        method,
        path,
        input,
        ..
    } = &handler.kind
    {
        if path.is_empty() {
            return Err(AxBackendCodegenError::MissingRoutePath);
        }
        out.push_str(&format!("    // route {method} {path}\n"));
        if !input.is_empty() {
            out.push_str(&render_route_input_binding(handler, input));
        }
    }

    let route_response = matches!(&handler.kind, AxHandlerKind::Route { .. });
    if route_response {
        out.push_str("    let mut __ax_headers: BTreeMap<String, String> = BTreeMap::new();\n");
        out.push_str("    let mut __ax_cookies: Vec<AxCookie> = Vec::new();\n");
    }
    for step in &handler.steps {
        out.push_str(&render_step(step, route_response));
    }

    if !handler
        .steps
        .iter()
        .any(|step| matches!(step, AxStepPlan::Return(_)))
    {
        if route_response {
            out.push_str("    Ok(__ax_finalize_response(AxHttpResponse::json(200, &ok_payload()).map_err(|error| AxRuntimeError::message(error.to_string()))?, __ax_headers, __ax_cookies))\n");
        } else {
            out.push_str("    Ok(ok_payload())\n");
        }
    }

    out.push_str("}\n");
    Ok(out)
}

fn handler_input_fields(handler: &AxHandlerPlan) -> Option<&[AxFieldPlan]> {
    match &handler.kind {
        AxHandlerKind::Action { input, .. } | AxHandlerKind::Route { input, .. }
            if !input.is_empty() =>
        {
            Some(input)
        }
        _ => None,
    }
}

fn render_route_input_binding(handler: &AxHandlerPlan, input: &[AxFieldPlan]) -> String {
    let mut out = format!(
        "    let input = {} {{\n",
        input_struct_name(&handler.rust_fn)
    );

    for field in input {
        out.push_str(&format!(
            "        {}: {},\n",
            field.name,
            render_route_input_field(field)
        ));
    }

    out.push_str("    };\n");
    out
}

fn render_route_input_field(field: &AxFieldPlan) -> String {
    let raw = format!("__ax_request_input_field(request, {:?})", field.name);
    let missing_error = format!("missing required input `{}`", field.name);

    if !field.optional && field.default.is_none() && field.rust_ty != "bool" {
        return match field.rust_ty.as_str() {
            "String" => format!("{raw}.ok_or_else(|| AxRuntimeError::message({missing_error:?}))?"),
            "i64" | "u64" | "f64" => format!(
                "{raw}.ok_or_else(|| AxRuntimeError::message({missing_error:?}))?.trim().parse::<{}>().map_err(|_| AxRuntimeError::message({:?}))?",
                field.rust_ty,
                format!("input `{}` expected {}", field.name, field.rust_ty)
            ),
            _ => format!("{raw}.ok_or_else(|| AxRuntimeError::message({missing_error:?}))?"),
        };
    }

    let missing = if let Some(default) = &field.default {
        render_owned_expr(default)
    } else if field.optional {
        default_value_for_rust_type(&field.rust_ty)
    } else if field.rust_ty == "bool" {
        "false".to_string()
    } else {
        unreachable!("required non-bool fields return before fallback rendering");
    };

    match field.rust_ty.as_str() {
        "String" => format!("{raw}.unwrap_or_else(|| ({missing}).to_string())"),
        "bool" => format!(
            "{raw}.map(|value| matches!(value.trim().to_ascii_lowercase().as_str(), \"true\" | \"1\" | \"on\" | \"yes\")).unwrap_or({missing})"
        ),
        "i64" | "u64" | "f64" => format!(
            "{raw}.map(|value| value.trim().parse::<{}>().map_err(|_| AxRuntimeError::message(format!(\"input `{}` expected {} but received `{{}}`\", value)))).transpose()?.unwrap_or({missing})",
            field.rust_ty, field.name, field.rust_ty
        ),
        _ => format!("{raw}.unwrap_or_else(|| ({missing}).to_string())"),
    }
}

fn default_value_for_rust_type(rust_ty: &str) -> String {
    match rust_ty {
        "String" => "String::new()".to_string(),
        "bool" => "false".to_string(),
        "i64" | "u64" => "0".to_string(),
        "f64" => "0.0".to_string(),
        _ => "Default::default()".to_string(),
    }
}

fn render_step(step: &AxStepPlan, route_response: bool) -> String {
    match step {
        AxStepPlan::Let { binding, value } => {
            format!("    let {binding} = {};\n", render_value_plan(value))
        }
        AxStepPlan::Insert { collection, fields } => format!(
            "    runtime.insert(&AxInsertRequest {{\n        collection: {:?}.to_string(),\n        fields: {},\n    }})?;\n",
            collection,
            render_fields_map(fields)
        ),
        AxStepPlan::Update {
            collection,
            fields,
            filters,
        } => format!(
            "    runtime.update(&AxUpdateRequest {{\n        collection: {:?}.to_string(),\n        fields: {},\n        filters: {},\n    }})?;\n",
            collection,
            render_fields_map(fields),
            render_query_filters(filters)
        ),
        AxStepPlan::Delete { collection, filters } => format!(
            "    runtime.delete(&AxDeleteRequest {{\n        collection: {:?}.to_string(),\n        filters: {},\n    }})?;\n",
            collection,
            render_query_filters(filters)
        ),
        AxStepPlan::Revalidate { target } => {
            format!("    runtime.revalidate({})?;\n", render_borrowed_expr(target))
        }
        AxStepPlan::Patch { signal, value } => {
            format!("    // patch {} = {}\n", signal.code, value.code)
        }
        AxStepPlan::Hook { phase, value } if route_response => {
            render_route_hook_step(*phase, value)
        }
        AxStepPlan::Hook { phase, value } => {
            let phase = match phase {
                AxHookPhasePlan::Before => "before",
                AxHookPhasePlan::After => "after",
            };
            format!("    // {phase} hook {}\n", value.code)
        }
        AxStepPlan::Header { name, value } if route_response => format!(
            "    __ax_headers.insert({}, {});\n",
            render_string_expr(name),
            render_string_expr(value)
        ),
        AxStepPlan::Header { name, value } => {
            format!("    // header {} = {}\n", name.code, value.code)
        }
        AxStepPlan::Cookie { name, value } if route_response => format!(
            "    __ax_cookies.push(AxCookie::new({}, {}).with_path(\"/\"));\n",
            render_string_expr(name),
            render_string_expr(value)
        ),
        AxStepPlan::Cookie { name, value } => {
            format!("    // cookie {} = {}\n", name.code, value.code)
        }
        AxStepPlan::ClearCookie { name } if route_response => format!(
            "    __ax_cookies.push(AxCookie::new({}, \"\").with_path(\"/\").with_max_age(0));\n",
            render_string_expr(name)
        ),
        AxStepPlan::ClearCookie { name } => format!("    // clearCookie {}\n", name.code),
        AxStepPlan::Require { value, fallback } if route_response => format!(
            "    if {}.is_empty() {{\n{}    }}\n",
            render_string_expr(value),
            render_require_fallback(fallback.as_ref())
        ),
        AxStepPlan::Require { value, .. } => format!("    // require {}\n", value.code),
        AxStepPlan::Return(value) => render_return_step(value, route_response),
        AxStepPlan::Send { target, payload } => format!(
            "    runtime.send(&AxSendRequest {{\n        target: {:?}.to_string(),\n        payload: json!({}),\n    }})?;\n",
            target,
            render_borrowed_expr(payload)
        ),
    }
}

fn render_route_hook_step(phase: AxHookPhasePlan, value: &AxRustExpr) -> String {
    match value.code.as_str() {
        "Security.headers" => {
            "    __ax_headers.insert(\"X-Content-Type-Options\".to_string(), \"nosniff\".to_string());\n    __ax_headers.insert(\"Referrer-Policy\".to_string(), \"strict-origin-when-cross-origin\".to_string());\n".to_string()
        }
        "Cache.noStore" => {
            "    __ax_headers.insert(\"Cache-Control\".to_string(), \"no-store\".to_string());\n"
                .to_string()
        }
        "Auth.session" | "Auth.bearer" | "Auth.signedSession" => format!(
            "    if {}.is_empty() {{\n{}    }}\n",
            render_string_expr(value),
            render_require_fallback(None)
        ),
        _ => {
            let phase = match phase {
                AxHookPhasePlan::Before => "before",
                AxHookPhasePlan::After => "after",
            };
            format!("    // {phase} hook {}\n", value.code)
        }
    }
}

fn render_value_plan(value: &AxValuePlan) -> String {
    match value {
        AxValuePlan::Expr(expr) => format!("json!({})", render_borrowed_expr(expr)),
        AxValuePlan::Query(query) => format!("runtime.load(&{})?", render_query_plan(query)),
    }
}

fn render_query_plan(query: &AxQueryPlan) -> String {
    let filters = if query.filters.is_empty() {
        "vec![]".to_string()
    } else {
        let entries = query
            .filters
            .iter()
            .map(|filter| {
                format!(
                    "AxQueryFilterRequest {{ field: {:?}.to_string(), op: {}, value: json!({}) }}",
                    filter.field,
                    render_filter_op(filter.op),
                    render_borrowed_expr(&filter.value)
                )
            })
            .collect::<Vec<_>>()
            .join(", ");
        format!("vec![{entries}]")
    };

    let orders = if query.orders.is_empty() {
        "vec![]".to_string()
    } else {
        let entries = query
            .orders
            .iter()
            .map(|order| {
                format!(
                    "AxQueryOrderRequest {{ field: {:?}.to_string(), direction: {} }}",
                    order.field,
                    render_order_direction(order.direction)
                )
            })
            .collect::<Vec<_>>()
            .join(", ");
        format!("vec![{entries}]")
    };

    let source = match &query.source {
        AxQuerySourcePlan::Stream { collection } => format!("{collection:?}.to_string()"),
        AxQuerySourcePlan::ContentCollection { collection } => {
            format!("{collection:?}.to_string()")
        }
    };

    format!(
        "AxQueryRequest {{ collection: {source}, filters: {filters}, orders: {orders}, limit: {}, offset: {} }}",
        render_option_u32(query.limit),
        render_option_u32(query.offset)
    )
}

fn render_fields_map(fields: &[AxAssignmentPlan]) -> String {
    if fields.is_empty() {
        return "BTreeMap::new()".to_string();
    }

    let entries = fields
        .iter()
        .map(|field| {
            format!(
                "({:?}.to_string(), json!({}))",
                field.name,
                render_borrowed_expr(&field.value)
            )
        })
        .collect::<Vec<_>>()
        .join(", ");

    format!("BTreeMap::from([{entries}])")
}

fn render_query_filters(filters: &[AxQueryFilterPlan]) -> String {
    if filters.is_empty() {
        return "Vec::new()".to_string();
    }

    let entries = filters
        .iter()
        .map(|filter| {
            format!(
                "AxQueryFilterRequest {{ field: {:?}.to_string(), op: {}, value: json!({}) }}",
                filter.field,
                render_filter_op(filter.op),
                render_borrowed_expr(&filter.value)
            )
        })
        .collect::<Vec<_>>()
        .join(", ");

    format!("vec![{entries}]")
}

fn render_borrowed_expr(expr: &AxRustExpr) -> String {
    if let Some(context_expr) = render_context_lookup(&expr.code) {
        context_expr
    } else {
        format!("&{}", expr.code)
    }
}

fn render_owned_expr(expr: &AxRustExpr) -> String {
    render_context_lookup(&expr.code)
        .map(|value| value.trim_start_matches('&').to_string())
        .unwrap_or_else(|| expr.code.clone())
}

fn render_string_expr(expr: &AxRustExpr) -> String {
    format!("({}).to_string()", render_owned_expr(expr))
}

fn render_return_step(value: &AxReturnPlan, route_response: bool) -> String {
    if route_response {
        return match value {
            AxReturnPlan::Expr(expr) | AxReturnPlan::Json(expr) => format!(
                "    Ok(__ax_finalize_response(AxHttpResponse::json(200, &json!({})).map_err(|error| AxRuntimeError::message(error.to_string()))?, __ax_headers, __ax_cookies))\n",
                render_borrowed_expr(expr)
            ),
            AxReturnPlan::Redirect { target, status } => match status {
                Some(status) => format!(
                    "    Ok(__ax_finalize_response(AxHttpResponse::redirect_with_status({status}, {}), __ax_headers, __ax_cookies))\n",
                    render_owned_expr(target)
                ),
                None => format!(
                    "    Ok(__ax_finalize_response(AxHttpResponse::redirect({}), __ax_headers, __ax_cookies))\n",
                    render_owned_expr(target)
                ),
            },
            AxReturnPlan::NoContent => {
                "    Ok(__ax_finalize_response(AxHttpResponse::no_content(), __ax_headers, __ax_cookies))\n".to_string()
            }
            AxReturnPlan::Ok => {
                "    Ok(__ax_finalize_response(AxHttpResponse::json(200, &ok_payload()).map_err(|error| AxRuntimeError::message(error.to_string()))?, __ax_headers, __ax_cookies))\n".to_string()
            }
        };
    }

    match value {
        AxReturnPlan::Expr(expr) | AxReturnPlan::Json(expr) => {
            format!("    Ok(json!({}))\n", render_borrowed_expr(expr))
        }
        AxReturnPlan::Ok | AxReturnPlan::NoContent | AxReturnPlan::Redirect { .. } => {
            "    Ok(ok_payload())\n".to_string()
        }
    }
}

fn render_require_fallback(fallback: Option<&AxReturnPlan>) -> String {
    let response = match fallback {
        Some(AxReturnPlan::Expr(expr)) | Some(AxReturnPlan::Json(expr)) => format!(
            "AxHttpResponse::json(401, &json!({})).map_err(|error| AxRuntimeError::message(error.to_string()))?",
            render_borrowed_expr(expr)
        ),
        Some(AxReturnPlan::Redirect { target, status }) => match status {
            Some(status) => format!(
                "AxHttpResponse::redirect_with_status({status}, {})",
                render_owned_expr(target)
            ),
            None => format!("AxHttpResponse::redirect({})", render_owned_expr(target)),
        },
        Some(AxReturnPlan::NoContent) => "AxHttpResponse::no_content()".to_string(),
        Some(AxReturnPlan::Ok) => "AxHttpResponse::json(200, &ok_payload()).map_err(|error| AxRuntimeError::message(error.to_string()))?".to_string(),
        None => "AxHttpResponse::json(401, &json!({\"error\":\"unauthorized\"})).map_err(|error| AxRuntimeError::message(error.to_string()))?".to_string(),
    };

    format!("        return Ok(__ax_finalize_response({response}, __ax_headers, __ax_cookies));\n")
}

fn render_context_lookup(code: &str) -> Option<String> {
    let (source, field) = code.split_once('.')?;
    match source {
        "params" => Some(format!("&context.param({field:?})?")),
        "query" => Some(format!("&context.query({field:?})?")),
        "request" => render_request_lookup(field),
        "Auth" => render_auth_lookup(field),
        _ => None,
    }
}

fn render_auth_lookup(field: &str) -> Option<String> {
    match field {
        "bearer" => Some("&AxAuth::bearer(request).unwrap_or_default()".to_string()),
        "session" => Some("&AxAuth::session(request).unwrap_or_default()".to_string()),
        "signedSession" => Some(
            "&AxAuth::signed_session(request, &runtime.env().secret(\"session_key\")?).unwrap_or_default()"
                .to_string(),
        ),
        _ => None,
    }
}

fn render_request_lookup(field: &str) -> Option<String> {
    match field {
        "method" => Some("&request.method".to_string()),
        "target" => Some("&request.target".to_string()),
        "body" => Some("&request.body_text_lossy()".to_string()),
        _ => {
            let (group, key) = field.split_once('.')?;
            match group {
                "cookies" => Some(format!(
                    "&request.cookie_value({key:?}).unwrap_or_default()"
                )),
                "headers" => Some(format!(
                    "&request.header_value({:?}).unwrap_or_default()",
                    denormalize_header_key(key)
                )),
                "form" => Some(format!("&request.form_value({key:?}).unwrap_or_default()")),
                "json" => Some(format!(
                    "&request.json_field_string({key:?}).unwrap_or_default()"
                )),
                _ => None,
            }
        }
    }
}

fn denormalize_header_key(key: &str) -> String {
    key.replace('_', "-")
}

fn render_filter_op(op: AxQueryFilterOpPlan) -> &'static str {
    match op {
        AxQueryFilterOpPlan::Eq => "AxQueryFilterOp::Eq",
    }
}

fn render_order_direction(direction: AxQueryOrderDirectionPlan) -> &'static str {
    match direction {
        AxQueryOrderDirectionPlan::Asc => "AxQueryOrderDirection::Asc",
        AxQueryOrderDirectionPlan::Desc => "AxQueryOrderDirection::Desc",
    }
}

fn render_option_u32(value: Option<u32>) -> String {
    match value {
        Some(value) => format!("Some({value})"),
        None => "None".to_string(),
    }
}

fn input_struct_name(rust_fn: &str) -> String {
    let (prefix, suffix) = rust_fn
        .strip_prefix("action_")
        .map(|suffix| ("Action", suffix))
        .or_else(|| {
            rust_fn
                .strip_prefix("route_")
                .map(|suffix| ("Route", suffix))
        })
        .unwrap_or(("Handler", rust_fn));
    let mut out = String::from(prefix);

    for part in suffix.split('_').filter(|part| !part.is_empty()) {
        let mut chars = part.chars();
        if let Some(first) = chars.next() {
            out.push(first.to_ascii_uppercase());
            out.extend(chars.map(|ch| ch.to_ascii_lowercase()));
        }
    }

    out.push_str("Input");
    out
}

pub mod prelude {
    pub use super::compile_backend_ax_to_module;
    pub use super::compile_backend_sources_to_module;
    pub use super::generate_backend_module;
    pub use super::AxBackendBundleError;
    pub use super::AxBackendCodegenError;
    pub use super::AxBackendCompileError;
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ax_ast::prelude::AxExpr;
    use crate::ax_backend_ast::prelude::*;
    use crate::ax_backend_lowering::lower_backend_document;

    #[test]
    fn generates_loader_and_action_module_against_runtime_traits() {
        let document = AxBackendDocument::new([
            AxBackendBlock::Loader(AxLoader::new(
                "PostsList",
                [
                    AxBackendStmt::data(
                        "posts",
                        crate::ax_query_ast::prelude::AxQuerySpec::new(
                            crate::ax_query_ast::prelude::AxQuerySource::Stream {
                                collection: "posts".to_string(),
                            },
                        )
                        .limit(20),
                    ),
                    AxBackendStmt::r#return(AxExpr::ident("posts")),
                ],
            )),
            AxBackendBlock::Action(
                AxAction::new("CreatePost")
                    .input([
                        AxField::new("title", "string"),
                        AxField::new("featured", "bool"),
                    ])
                    .body([
                        AxBackendStmt::insert(
                            "posts",
                            [
                                AxAssignment::new("title", AxExpr::ident("input").member("title")),
                                AxAssignment::new(
                                    "featured",
                                    AxExpr::ident("input").member("featured"),
                                ),
                            ],
                        ),
                        AxBackendStmt::r#return("ok"),
                    ]),
            ),
        ]);

        let plan = lower_backend_document(&document).expect("document should lower");
        let module = generate_backend_module(&plan).expect("module should generate");

        assert!(module.contains("use axonyx_runtime::backend_prelude::*;"));
        assert!(module.contains("use axonyx_runtime::server_prelude::*;"));
        assert!(module.contains("#![allow(dead_code, unused_variables)]"));
        assert!(module.contains(
            "pub fn loader_posts_list(runtime: &impl AxBackendRuntime, context: &AxLoaderContext)"
        ));
        assert!(module.contains("runtime.load(&AxQueryRequest"));
        assert!(module.contains("pub struct ActionCreatePostInput"));
        assert!(module.contains("pub fn action_create_post(runtime: &impl AxBackendRuntime, input: &ActionCreatePostInput)"));
        assert!(module.contains("runtime.insert(&AxInsertRequest"));
        assert!(module.contains("Ok(ok_payload())"));
    }

    #[test]
    fn compiles_backend_ax_source_directly_into_module() {
        let module = compile_backend_ax_to_module(
            r#"
loader PostsList
  data posts = Db.Stream("posts")
    where status = "published"
    limit 12
  return posts
"#,
        )
        .expect("source should compile");

        assert!(module.contains(
            "pub fn loader_posts_list(runtime: &impl AxBackendRuntime, context: &AxLoaderContext)"
        ));
        assert!(module.contains("field: \"status\".to_string()"));
        assert!(module.contains("limit: Some(12)"));
    }

    #[test]
    fn compiles_loader_params_and_query_into_context_lookups() {
        let module = compile_backend_ax_to_module(
            r#"
loader PostDetail
  data posts = Db.Stream("posts")
    where slug = params.slug
    where status = query.status
    limit 1
  return posts
"#,
        )
        .expect("source should compile");

        assert!(module.contains(
            "pub fn loader_post_detail(runtime: &impl AxBackendRuntime, context: &AxLoaderContext)"
        ));
        assert!(module.contains(r#"value: json!(&context.param("slug")?)"#));
        assert!(module.contains(r#"value: json!(&context.query("status")?)"#));
    }

    #[test]
    fn compiles_runtime_env_access_into_runtime_env_calls() {
        let module = compile_backend_ax_to_module(
            r#"
loader PostsList
  data db_url = Runtime.Env.secret.db_url
  data app_name = Runtime.Env.public.app_name
  return app_name
"#,
        )
        .expect("source should compile");

        assert!(module.contains(r#"let db_url = json!(&runtime.env().secret("db_url")?);"#));
        assert!(module.contains(r#"let app_name = json!(&runtime.env().public("app_name")?);"#));
    }

    #[test]
    fn bundles_multiple_backend_sources_into_single_module() {
        let module = compile_backend_sources_to_module(&[
            (
                "app/posts/loader.ax",
                r#"
loader PostsList
  data posts = Db.Stream("posts")
  return posts
"#,
            ),
            (
                "routes/api/posts.ax",
                r#"
route GET "/api/posts"
  data posts = Db.Stream("posts")
  return posts
"#,
            ),
        ])
        .expect("sources should compile");

        assert!(module.contains(
            "pub fn loader_posts_list(runtime: &impl AxBackendRuntime, context: &AxLoaderContext)"
        ));
        assert!(module
            .contains("pub fn route_get_api_posts(runtime: &impl AxBackendRuntime, request: &AxHttpRequest) -> AxRuntimeResult<AxHttpResponse>"));
    }

    #[test]
    fn compiles_route_http_return_helpers_into_responses() {
        let module = compile_backend_ax_to_module(
            r#"
route GET "/api/posts"
  data posts = Db.Stream("posts")
  return json(posts)

route GET "/go"
  return redirect("/next")

route DELETE "/api/posts"
  return noContent()
"#,
        )
        .expect("source should compile");

        assert!(module.contains("AxRuntimeResult<AxHttpResponse>"));
        assert!(module.contains("AxHttpResponse::json(200, &json!(&posts))"));
        assert!(module.contains(r#"AxHttpResponse::redirect("/next".to_string())"#));
        assert!(module.contains("AxHttpResponse::no_content()"));
    }

    #[test]
    fn compiles_route_response_metadata_into_response_finalizer() {
        let module = compile_backend_ax_to_module(
            r#"
route GET "/api/session"
  require request.cookies.session
  header "Cache-Control" = "no-store"
  cookie "theme" = "gold"
  clearCookie "flash"
  return json("ok")
"#,
        )
        .expect("source should compile");

        assert!(module.contains("let mut __ax_headers: BTreeMap<String, String>"));
        assert!(module.contains(
            "if (request.cookie_value(\"session\").unwrap_or_default()).to_string().is_empty()"
        ));
        assert!(module.contains("__ax_headers.insert((\"Cache-Control\".to_string()).to_string(), (\"no-store\".to_string()).to_string());"));
        assert!(module.contains("__ax_cookies.push(AxCookie::new((\"theme\".to_string()).to_string(), (\"gold\".to_string()).to_string()).with_path(\"/\"));"));
        assert!(module.contains("__ax_cookies.push(AxCookie::new((\"flash\".to_string()).to_string(), \"\").with_path(\"/\").with_max_age(0));"));
        assert!(module.contains("__ax_finalize_response(AxHttpResponse::json"));
    }

    #[test]
    fn compiles_route_hooks_into_known_response_policies() {
        let module = compile_backend_ax_to_module(
            r#"
route GET "/api/admin"
  before Security.headers
  before Auth.session
  after Cache.noStore
  return json("ok")
"#,
        )
        .expect("source should compile");

        assert!(module.contains(
            "__ax_headers.insert(\"X-Content-Type-Options\".to_string(), \"nosniff\".to_string())"
        ));
        assert!(module.contains(
            "__ax_headers.insert(\"Referrer-Policy\".to_string(), \"strict-origin-when-cross-origin\".to_string())"
        ));
        assert!(module
            .contains("if (AxAuth::session(request).unwrap_or_default()).to_string().is_empty()"));
        assert!(module.contains(
            "__ax_headers.insert(\"Cache-Control\".to_string(), \"no-store\".to_string())"
        ));
    }

    #[test]
    fn compiles_require_fallback_into_route_response() {
        let module = compile_backend_ax_to_module(
            r#"
route GET "/api/admin"
  require request.cookies.session else redirect("/login")
  return json("ok")
"#,
        )
        .expect("source should compile");

        assert!(module.contains(
            "if (request.cookie_value(\"session\").unwrap_or_default()).to_string().is_empty()"
        ));
        assert!(module.contains(r#"AxHttpResponse::redirect("/login".to_string())"#));
    }

    #[test]
    fn compiles_route_request_context_into_request_helpers() {
        let module = compile_backend_ax_to_module(
            r#"
route POST "/api/session"
  data theme = request.cookies.theme
  data agent = request.headers.user_agent
  data body = request.body
  data title = request.form.title
  data slug = request.json.slug
  data token = Auth.bearer
  data session = Auth.session
  data signed = Auth.signedSession
  return json(theme)
"#,
        )
        .expect("source should compile");

        assert!(module.contains("request.cookie_value(\"theme\").unwrap_or_default()"));
        assert!(module.contains("request.header_value(\"user-agent\").unwrap_or_default()"));
        assert!(module.contains("request.body_text_lossy()"));
        assert!(module.contains("request.form_value(\"title\").unwrap_or_default()"));
        assert!(module.contains("request.json_field_string(\"slug\").unwrap_or_default()"));
        assert!(module.contains("AxAuth::bearer(request).unwrap_or_default()"));
        assert!(module.contains("AxAuth::session(request).unwrap_or_default()"));
        assert!(module
            .contains("AxAuth::signed_session(request, &runtime.env().secret(\"session_key\")?)"));
    }

    #[test]
    fn compiles_route_typed_input_into_request_coercion() {
        let module = compile_backend_ax_to_module(
            r#"
route POST "/api/posts"
  input:
    title: string
    count: i64
    featured?: bool = false

  return json(input.title)
"#,
        )
        .expect("source should compile");

        assert!(module.contains("pub struct RoutePostApiPostsInput"));
        assert!(module.contains("__ax_request_input_field(request, \"title\")"));
        assert!(module.contains("missing required input `title`"));
        assert!(module.contains("parse::<i64>()"));
        assert!(module.contains("let input = RoutePostApiPostsInput"));
        assert!(module.contains("AxHttpResponse::json(200, &json!(&input.title))"));
    }

    #[test]
    fn compiles_delete_where_into_runtime_delete_request() {
        let module = compile_backend_ax_to_module(
            r#"
action RemovePost
  input:
    id: i64

  delete "posts"
    where id = input.id

  return ok
"#,
        )
        .expect("source should compile");

        assert!(module.contains("runtime.delete(&AxDeleteRequest"));
        assert!(module.contains("filters: vec![AxQueryFilterRequest"));
        assert!(module.contains("field: \"id\".to_string()"));
    }
}
