//! MATLAB-compatible `gamma` builtin with GPU-aware semantics for RunMat.
//!
//! Provides element-wise evaluation of the gamma function for real and complex inputs while
//! preserving MATLAB broadcasting semantics. GPU execution uses provider hooks when available and
//! falls back to host computation otherwise.

use num_complex::Complex64;
use runmat_accelerate_api::GpuTensorHandle;
use runmat_builtins::{CharArray, ComplexTensor, Tensor, Value};
use runmat_macros::runtime_builtin;

use crate::builtins::common::random_args::{complex_tensor_into_value, keyword_of};
use crate::builtins::common::spec::{
    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
    ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
};
use crate::builtins::common::{gpu_helpers, map_control_flow_with_builtin, tensor};
use crate::builtins::math::type_resolvers::numeric_unary_type;
use crate::{build_runtime_error, BuiltinResult, RuntimeError};

const PI: f64 = std::f64::consts::PI;
const SQRT_TWO_PI: f64 = 2.506_628_274_631_000_5;
const LANCZOS_G: f64 = 7.0;
const EPSILON: f64 = 1e-12;

const LANCZOS_COEFFS: [f64; 8] = [
    676.5203681218851,
    -1259.1392167224028,
    771.3234287776531,
    -176.6150291621406,
    12.507343278686905,
    -0.13857109526572012,
    9.984_369_578_019_572e-6,
    1.5056327351493116e-7,
];

#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::elementwise::gamma")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
    name: "gamma",
    op_kind: GpuOpKind::Elementwise,
    supported_precisions: &[ScalarType::F32, ScalarType::F64],
    broadcast: BroadcastSemantics::Matlab,
    provider_hooks: &[ProviderHook::Unary { name: "unary_gamma" }],
    constant_strategy: ConstantStrategy::InlineLiteral,
    residency: ResidencyPolicy::NewHandle,
    nan_mode: ReductionNaN::Include,
    two_pass_threshold: None,
    workgroup_size: None,
    accepts_nan_mode: false,
    notes:
        "Providers may execute gamma directly on device buffers via unary_gamma; runtimes gather to the host when the hook is unavailable.",
};

#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::elementwise::gamma")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
    name: "gamma",
    shape: ShapeRequirements::BroadcastCompatible,
    constant_strategy: ConstantStrategy::InlineLiteral,
    elementwise: None,
    reduction: None,
    emits_nan: false,
    notes: "Fusion planner currently falls back to host evaluation; providers may supply specialised kernels in the future.",
};

const BUILTIN_NAME: &str = "gamma";

fn builtin_error(message: impl Into<String>) -> RuntimeError {
    build_runtime_error(message)
        .with_builtin(BUILTIN_NAME)
        .build()
}

#[runtime_builtin(
    name = "gamma",
    category = "math/elementwise",
    summary = "Element-wise gamma function for scalars, vectors, matrices, or N-D tensors.",
    keywords = "gamma,factorial,special,gpu",
    accel = "unary",
    type_resolver(numeric_unary_type),
    builtin_path = "crate::builtins::math::elementwise::gamma"
)]
async fn gamma_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
    let output = parse_output_template(&rest)?;
    let base = match value {
        Value::GpuTensor(handle) => gamma_gpu(handle).await?,
        Value::Complex(re, im) => gamma_complex_scalar_value(Complex64::new(re, im)),
        Value::ComplexTensor(ct) => gamma_complex_tensor(ct)?,
        Value::CharArray(ca) => gamma_char_array(ca)?,
        Value::LogicalArray(logical) => {
            let tensor = tensor::logical_to_tensor(&logical)
                .map_err(|e| builtin_error(format!("gamma: {e}")))?;
            gamma_tensor(tensor).map(tensor::tensor_into_value)?
        }
        Value::String(_) | Value::StringArray(_) => {
            return Err(builtin_error("gamma: expected numeric input"))
        }
        Value::Tensor(tensor) => gamma_tensor(tensor).map(tensor::tensor_into_value)?,
        Value::Num(n) => Value::Num(gamma_real_scalar(n)),
        Value::Int(i) => Value::Num(gamma_real_scalar(i.to_f64())),
        Value::Bool(b) => Value::Num(gamma_real_scalar(if b { 1.0 } else { 0.0 })),
        other => {
            return Err(builtin_error(format!(
                "gamma: unsupported input type {:?}; expected numeric or gpuArray input",
                other
            )))
        }
    };
    apply_output_template(base, &output).await
}

async fn gamma_gpu(handle: GpuTensorHandle) -> BuiltinResult<Value> {
    if let Some(provider) = runmat_accelerate_api::provider_for_handle(&handle) {
        if let Ok(out) = provider.unary_gamma(&handle).await {
            return Ok(Value::GpuTensor(out));
        }
    }
    let tensor = gpu_helpers::gather_tensor_async(&handle)
        .await
        .map_err(|flow| map_control_flow_with_builtin(flow, BUILTIN_NAME))?;
    Ok(tensor::tensor_into_value(gamma_tensor(tensor)?))
}

fn gamma_tensor(tensor: Tensor) -> BuiltinResult<Tensor> {
    let mut data = Vec::with_capacity(tensor.data.len());
    for &v in &tensor.data {
        data.push(gamma_real_scalar(v));
    }
    Tensor::new(data, tensor.shape.clone()).map_err(|e| builtin_error(format!("gamma: {e}")))
}

fn gamma_complex_tensor(ct: ComplexTensor) -> BuiltinResult<Value> {
    let mut out = Vec::with_capacity(ct.data.len());
    for &(re, im) in &ct.data {
        let res = gamma_complex_scalar(Complex64::new(re, im));
        out.push((res.re, res.im));
    }
    let tensor = ComplexTensor::new(out, ct.shape.clone())
        .map_err(|e| builtin_error(format!("gamma: {e}")))?;
    Ok(complex_tensor_into_value(tensor))
}

fn gamma_complex_scalar_value(z: Complex64) -> Value {
    let res = gamma_complex_scalar(z);
    if res.im.abs() <= EPSILON * res.re.abs().max(1.0) {
        Value::Num(res.re)
    } else {
        Value::Complex(res.re, res.im)
    }
}

fn gamma_char_array(ca: CharArray) -> BuiltinResult<Value> {
    let data = ca
        .data
        .iter()
        .map(|&ch| gamma_real_scalar(ch as u32 as f64))
        .collect::<Vec<_>>();
    let tensor = Tensor::new(data, vec![ca.rows, ca.cols])
        .map_err(|e| builtin_error(format!("gamma: {e}")))?;
    Ok(tensor::tensor_into_value(tensor))
}

fn gamma_real_scalar(x: f64) -> f64 {
    if x.is_nan() {
        return f64::NAN;
    }
    if x.is_infinite() {
        return if x.is_sign_positive() {
            f64::INFINITY
        } else {
            f64::NAN
        };
    }
    if is_non_positive_integer(x) {
        return f64::INFINITY;
    }
    let result = gamma_complex_scalar(Complex64::new(x, 0.0));
    if result.im.abs() <= EPSILON * result.re.abs().max(1.0) {
        result.re
    } else {
        f64::NAN
    }
}

fn gamma_complex_scalar(z: Complex64) -> Complex64 {
    if z.re.is_nan() || z.im.is_nan() {
        return Complex64::new(f64::NAN, f64::NAN);
    }
    if z.im.abs() <= EPSILON && z.re.is_infinite() {
        return Complex64::new(f64::INFINITY, 0.0);
    }
    if is_complex_pole(z) {
        return Complex64::new(f64::INFINITY, 0.0);
    }
    if z.re < 0.5 {
        let sin_term = (Complex64::new(PI, 0.0) * z).sin();
        if sin_term.norm_sqr() <= EPSILON * EPSILON {
            return Complex64::new(f64::INFINITY, 0.0);
        }
        let gamma_one_minus_z = gamma_complex_scalar(Complex64::new(1.0, 0.0) - z);
        return Complex64::new(PI, 0.0) / (sin_term * gamma_one_minus_z);
    }
    lanczos_gamma(z)
}

fn lanczos_gamma(z: Complex64) -> Complex64 {
    let z_minus_one = z - Complex64::new(1.0, 0.0);
    let mut sum = Complex64::new(0.999_999_999_999_809_9, 0.0);
    for (idx, coeff) in LANCZOS_COEFFS.iter().enumerate() {
        let denom = z_minus_one + Complex64::new((idx + 1) as f64, 0.0);
        sum += Complex64::new(*coeff, 0.0) / denom;
    }
    let t = z_minus_one + Complex64::new(LANCZOS_G + 0.5, 0.0);
    let power = t.powc(z_minus_one + Complex64::new(0.5, 0.0));
    let exponential = (-t).exp();
    Complex64::new(SQRT_TWO_PI, 0.0) * power * exponential * sum
}

fn is_non_positive_integer(x: f64) -> bool {
    x <= 0.0 && is_close_to_integer(x)
}

fn is_complex_pole(z: Complex64) -> bool {
    z.im.abs() <= EPSILON && is_non_positive_integer(z.re)
}

fn is_close_to_integer(x: f64) -> bool {
    if !x.is_finite() {
        return false;
    }
    let nearest = x.round();
    let diff = (x - nearest).abs();
    if nearest == 0.0 {
        diff <= EPSILON * EPSILON
    } else {
        diff <= EPSILON * nearest.abs().max(1.0)
    }
}

#[derive(Clone)]
enum OutputTemplate {
    Default,
    Like(Value),
}

fn parse_output_template(args: &[Value]) -> BuiltinResult<OutputTemplate> {
    match args.len() {
        0 => Ok(OutputTemplate::Default),
        1 => {
            if matches!(keyword_of(&args[0]).as_deref(), Some("like")) {
                Err(builtin_error("gamma: expected prototype after 'like'"))
            } else {
                Err(builtin_error("gamma: unrecognised argument for gamma"))
            }
        }
        2 => {
            if matches!(keyword_of(&args[0]).as_deref(), Some("like")) {
                Ok(OutputTemplate::Like(args[1].clone()))
            } else {
                Err(builtin_error(
                    "gamma: unsupported option; only 'like' is accepted",
                ))
            }
        }
        _ => Err(builtin_error("gamma: too many input arguments")),
    }
}

async fn apply_output_template(value: Value, template: &OutputTemplate) -> BuiltinResult<Value> {
    match template {
        OutputTemplate::Default => Ok(value),
        OutputTemplate::Like(proto) => apply_like_template(value, proto).await,
    }
}

async fn apply_like_template(value: Value, prototype: &Value) -> BuiltinResult<Value> {
    let analysis = analyse_like_prototype(prototype).await?;
    match analysis.class {
        PrototypeClass::Real => match analysis.device {
            DevicePreference::Host => convert_to_host_real(value).await,
            DevicePreference::Gpu => convert_to_gpu_real(value),
        },
        PrototypeClass::Complex => match analysis.device {
            DevicePreference::Host => convert_to_host_complex(value).await,
            DevicePreference::Gpu => Err(builtin_error(
                "gamma: complex GPU prototypes are not supported yet",
            )),
        },
    }
}

fn convert_to_gpu_real(value: Value) -> BuiltinResult<Value> {
    let provider = runmat_accelerate_api::provider().ok_or_else(|| {
        builtin_error(
            "gamma: GPU output requested via 'like' but no acceleration provider is active",
        )
    })?;
    match value {
        Value::GpuTensor(handle) => Ok(Value::GpuTensor(handle)),
        Value::Tensor(tensor) => {
            let view = runmat_accelerate_api::HostTensorView {
                data: &tensor.data,
                shape: &tensor.shape,
            };
            let handle = provider
                .upload(&view)
                .map_err(|e| builtin_error(format!("gamma: {e}")))?;
            Ok(Value::GpuTensor(handle))
        }
        Value::Num(n) => {
            let tensor = Tensor::new(vec![n], vec![1, 1])
                .map_err(|e| builtin_error(format!("gamma: {e}")))?;
            convert_to_gpu_real(Value::Tensor(tensor))
        }
        Value::Int(i) => convert_to_gpu_real(Value::Num(i.to_f64())),
        Value::Bool(b) => convert_to_gpu_real(Value::Num(if b { 1.0 } else { 0.0 })),
        Value::LogicalArray(logical) => {
            let tensor = tensor::logical_to_tensor(&logical)
                .map_err(|e| builtin_error(format!("gamma: {e}")))?;
            convert_to_gpu_real(Value::Tensor(tensor))
        }
        Value::Complex(_, _) | Value::ComplexTensor(_) => Err(builtin_error(
            "gamma: GPU prototypes for 'like' only support real numeric outputs",
        )),
        other => Err(builtin_error(format!(
            "gamma: unsupported result type for GPU output via 'like' ({other:?})"
        ))),
    }
}

async fn convert_to_host_real(value: Value) -> BuiltinResult<Value> {
    match value {
        Value::GpuTensor(handle) => {
            let proxy = Value::GpuTensor(handle);
            gpu_helpers::gather_value_async(&proxy)
                .await
                .map_err(|flow| map_control_flow_with_builtin(flow, BUILTIN_NAME))
        }
        other => Ok(other),
    }
}

#[async_recursion::async_recursion(?Send)]
async fn convert_to_host_complex(value: Value) -> BuiltinResult<Value> {
    match value {
        Value::Complex(_, _) | Value::ComplexTensor(_) => Ok(value),
        Value::Num(n) => Ok(Value::Complex(n, 0.0)),
        Value::Tensor(tensor) => {
            let data = tensor.data.iter().map(|&re| (re, 0.0)).collect::<Vec<_>>();
            let complex = ComplexTensor::new(data, tensor.shape.clone())
                .map_err(|e| builtin_error(format!("gamma: {e}")))?;
            Ok(complex_tensor_into_value(complex))
        }
        Value::GpuTensor(handle) => {
            let gathered = gpu_helpers::gather_tensor_async(&handle)
                .await
                .map_err(|flow| map_control_flow_with_builtin(flow, BUILTIN_NAME))?;
            convert_to_host_complex(Value::Tensor(gathered)).await
        }
        Value::LogicalArray(logical) => {
            let tensor = tensor::logical_to_tensor(&logical)
                .map_err(|e| builtin_error(format!("gamma: {e}")))?;
            convert_to_host_complex(Value::Tensor(tensor)).await
        }
        Value::Bool(b) => convert_to_host_complex(Value::Num(if b { 1.0 } else { 0.0 })).await,
        Value::Int(i) => convert_to_host_complex(Value::Num(i.to_f64())).await,
        other => Err(builtin_error(format!(
            "gamma: cannot convert {other:?} to complex output via 'like'"
        ))),
    }
}

#[derive(Clone, Copy)]
enum DevicePreference {
    Host,
    Gpu,
}

#[derive(Clone, Copy)]
enum PrototypeClass {
    Real,
    Complex,
}

struct LikeAnalysis {
    class: PrototypeClass,
    device: DevicePreference,
}

#[async_recursion::async_recursion(?Send)]
async fn analyse_like_prototype(proto: &Value) -> BuiltinResult<LikeAnalysis> {
    match proto {
        Value::GpuTensor(_) => Ok(LikeAnalysis {
            class: PrototypeClass::Real,
            device: DevicePreference::Gpu,
        }),
        Value::Tensor(_)
        | Value::Num(_)
        | Value::Int(_)
        | Value::Bool(_)
        | Value::LogicalArray(_)
        | Value::CharArray(_) => Ok(LikeAnalysis {
            class: PrototypeClass::Real,
            device: DevicePreference::Host,
        }),
        Value::Complex(_, _) | Value::ComplexTensor(_) => Ok(LikeAnalysis {
            class: PrototypeClass::Complex,
            device: DevicePreference::Host,
        }),
        other => {
            let gathered = crate::dispatcher::gather_if_needed_async(other)
                .await
                .map_err(|flow| map_control_flow_with_builtin(flow, BUILTIN_NAME))?;
            analyse_like_prototype(&gathered).await
        }
    }
}

#[cfg(test)]
pub(crate) mod tests {
    use super::*;
    use crate::builtins::common::test_support;
    use futures::executor::block_on;
    use runmat_accelerate_api::HostTensorView;
    use runmat_builtins::{IntValue, ResolveContext, Tensor, Type};

    fn gamma_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
        block_on(super::gamma_builtin(value, rest))
    }

    #[test]
    fn gamma_type_preserves_tensor_shape() {
        let out = numeric_unary_type(
            &[Type::Tensor {
                shape: Some(vec![Some(2), Some(3)]),
            }],
            &ResolveContext::new(Vec::new()),
        );
        assert_eq!(
            out,
            Type::Tensor {
                shape: Some(vec![Some(2), Some(3)])
            }
        );
    }

    #[test]
    fn gamma_type_scalar_tensor_returns_num() {
        let out = numeric_unary_type(
            &[Type::Tensor {
                shape: Some(vec![Some(1), Some(1)]),
            }],
            &ResolveContext::new(Vec::new()),
        );
        assert_eq!(out, Type::Num);
    }

    fn approx_eq(a: f64, b: f64, tol: f64) {
        assert!((a - b).abs() <= tol, "expected {b}, got {a} (tol {tol})");
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_positive_integer() {
        match gamma_builtin(Value::Num(5.0), Vec::new()).expect("gamma") {
            Value::Num(v) => approx_eq(v, 24.0, 1e-12),
            other => panic!("expected scalar result, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_half_integer() {
        match gamma_builtin(Value::Num(0.5), Vec::new()).expect("gamma") {
            Value::Num(v) => approx_eq(v, PI.sqrt(), 1e-12),
            other => panic!("expected scalar result, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_negative_non_integer() {
        match gamma_builtin(Value::Num(-0.5), Vec::new()).expect("gamma") {
            Value::Num(v) => approx_eq(v, -2.0 * PI.sqrt(), 1e-12),
            other => panic!("expected scalar result, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_matrix() {
        let tensor = Tensor::new(vec![1.0, 3.0, 2.0, 4.0], vec![2, 2]).unwrap();
        match gamma_builtin(Value::Tensor(tensor), Vec::new()).expect("gamma") {
            Value::Tensor(t) => {
                assert_eq!(t.shape, vec![2, 2]);
                approx_eq(t.data[0], 1.0, 1e-12);
                approx_eq(t.data[1], 2.0, 1e-12);
                approx_eq(t.data[2], 1.0, 1e-12);
                approx_eq(t.data[3], 6.0, 1e-12);
            }
            other => panic!("expected tensor result, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_pole_returns_inf() {
        match gamma_builtin(Value::Num(0.0), Vec::new()).expect("gamma") {
            Value::Num(v) => assert!(v.is_infinite() && v.is_sign_positive()),
            other => panic!("expected scalar result, got {other:?}"),
        }
        match gamma_builtin(Value::Num(-3.0), Vec::new()).expect("gamma") {
            Value::Num(v) => assert!(v.is_infinite()),
            other => panic!("expected scalar result, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_small_negative_not_infinite() {
        match gamma_builtin(Value::Num(-1.0e-10), Vec::new()).expect("gamma") {
            Value::Num(v) => {
                assert!(v.is_finite(), "expected finite value, got {v}");
                assert!(v.is_sign_negative(), "expected negative value, got {v}");
                assert!(v.abs() > 1.0e9, "expected large magnitude, got {v}");
            }
            other => panic!("expected scalar result, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_complex_identity() {
        let z = Complex64::new(0.5, 1.0);
        let gamma_z = gamma_complex_scalar(z);
        let gamma_z_plus_one = gamma_complex_scalar(z + Complex64::new(1.0, 0.0));
        let lhs = gamma_z_plus_one;
        let rhs = z * gamma_z;
        approx_eq(lhs.re, rhs.re, 1e-10);
        approx_eq(lhs.im, rhs.im, 1e-10);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_char_array() {
        let chars = CharArray::new("ab".chars().collect(), 1, 2).unwrap();
        match gamma_builtin(Value::CharArray(chars), Vec::new()).expect("gamma") {
            Value::Tensor(t) => {
                assert_eq!(t.shape, vec![1, 2]);
                approx_eq(t.data[0], gamma_real_scalar(97.0), 1e-12);
                approx_eq(t.data[1], gamma_real_scalar(98.0), 1e-12);
            }
            other => panic!("expected tensor result, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_string_input_errors() {
        let err = gamma_builtin(Value::from("hello"), Vec::new()).expect_err("expected error");
        assert!(err.message().contains("expected numeric input"));
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_gpu_provider_fallback() {
        test_support::with_test_provider(|provider| {
            let tensor = Tensor::new(vec![0.5, 1.5, 2.5], vec![1, 3]).unwrap();
            let view = HostTensorView {
                data: &tensor.data,
                shape: &tensor.shape,
            };
            let handle = provider.upload(&view).expect("upload");
            let result = gamma_builtin(Value::GpuTensor(handle), Vec::new()).expect("gamma");
            let gathered = test_support::gather(result).expect("gather");
            approx_eq(gathered.data[0], PI.sqrt(), 1e-12);
            approx_eq(gathered.data[1], 0.8862269254527579, 1e-12);
            approx_eq(gathered.data[2], 1.329340388179137, 1e-12);
        });
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    #[cfg(feature = "wgpu")]
    fn gamma_wgpu_matches_cpu() {
        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
        );
        let tensor = Tensor::new(vec![0.5, 1.5, 2.5, -0.5, 4.2, -1.3], vec![3, 2]).expect("tensor");
        let cpu = gamma_tensor(tensor.clone()).expect("cpu gamma");
        let provider = runmat_accelerate_api::provider().expect("provider");
        let view = HostTensorView {
            data: &tensor.data,
            shape: &tensor.shape,
        };
        let handle = provider.upload(&view).expect("upload");
        let gpu_value = block_on(gamma_gpu(handle)).expect("gamma gpu");
        let gathered = test_support::gather(gpu_value).expect("gather");
        assert_eq!(gathered.shape, cpu.shape);
        let tol = match provider.precision() {
            runmat_accelerate_api::ProviderPrecision::F64 => 1e-10,
            runmat_accelerate_api::ProviderPrecision::F32 => 1e-4,
        };
        for (got, expected) in gathered.data.iter().zip(cpu.data.iter()) {
            approx_eq(*got, *expected, tol);
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_like_gpu_prototype() {
        test_support::with_test_provider(|provider| {
            let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
            let proto_view = HostTensorView {
                data: &[0.0],
                shape: &[1, 1],
            };
            let proto = provider.upload(&proto_view).expect("upload");
            let result = gamma_builtin(
                Value::Tensor(tensor.clone()),
                vec![Value::from("like"), Value::GpuTensor(proto.clone())],
            )
            .expect("gamma");
            match result {
                Value::GpuTensor(handle) => {
                    let gathered = test_support::gather(Value::GpuTensor(handle)).expect("gather");
                    approx_eq(gathered.data[0], 1.0, 1e-12);
                    approx_eq(gathered.data[1], 1.0, 1e-12);
                }
                other => panic!("expected GPU tensor, got {other:?}"),
            }
        });
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_like_gpu_complex_result_errors() {
        test_support::with_test_provider(|provider| {
            let proto_view = HostTensorView {
                data: &[0.0],
                shape: &[1, 1],
            };
            let proto = provider.upload(&proto_view).expect("upload");
            let err = gamma_builtin(
                Value::Complex(0.5, 0.75),
                vec![Value::from("like"), Value::GpuTensor(proto)],
            )
            .expect_err("expected error");
            assert!(err.message().contains("only support real numeric outputs"));
        });
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_like_complex_requires_host() {
        let result = gamma_builtin(
            Value::Num(2.0),
            vec![Value::from("like"), Value::Complex(1.0, 0.0)],
        )
        .expect("gamma");
        match result {
            Value::Complex(re, im) => {
                approx_eq(re, 1.0, 1e-12);
                approx_eq(im, 0.0, 1e-12);
            }
            other => panic!("expected complex result, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_like_rejects_extra_args() {
        let err = gamma_builtin(
            Value::Num(1.0),
            vec![Value::from("like"), Value::Num(0.0), Value::Num(1.0)],
        )
        .expect_err("expected error");
        assert!(err.message().contains("too many input arguments"));
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn gamma_int_promotes() {
        let value = Value::Int(IntValue::I32(4));
        match gamma_builtin(value, Vec::new()).expect("gamma") {
            Value::Num(v) => approx_eq(v, 6.0, 1e-12),
            other => panic!("expected scalar result, got {other:?}"),
        }
    }
}
