//! MATLAB-compatible `fclose` builtin for RunMat.
//!
//! Mirrors MATLAB semantics for closing individual files, vectors of file
//! identifiers, or all open files. The implementation integrates with the
//! shared file registry managed by `fopen` and always executes on the host.

use runmat_builtins::Value;
use runmat_macros::runtime_builtin;

use crate::builtins::common::spec::{
    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
    ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::builtins::io::filetext::{
    helpers::{char_array_value, extract_scalar_string},
    registry,
};
use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};

const INVALID_IDENTIFIER_MESSAGE: &str =
    "Invalid file identifier. Use fopen to generate a valid file ID.";
const BUILTIN_NAME: &str = "fclose";

#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::filetext::fclose")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
    name: "fclose",
    op_kind: GpuOpKind::Custom("file-io"),
    supported_precisions: &[],
    broadcast: BroadcastSemantics::None,
    provider_hooks: &[],
    constant_strategy: ConstantStrategy::InlineLiteral,
    residency: ResidencyPolicy::GatherImmediately,
    nan_mode: ReductionNaN::Include,
    two_pass_threshold: None,
    workgroup_size: None,
    accepts_nan_mode: false,
    notes:
        "Host-only operation: closes identifiers stored in the shared file registry; GPU inputs are gathered automatically.",
};

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

fn map_control_flow(err: RuntimeError) -> RuntimeError {
    let message = err.message().to_string();
    let identifier = err.identifier().map(|value| value.to_string());
    let mut builder = build_runtime_error(format!("{BUILTIN_NAME}: {message}"))
        .with_builtin(BUILTIN_NAME)
        .with_source(err);
    if let Some(identifier) = identifier {
        builder = builder.with_identifier(identifier);
    }
    builder.build()
}

#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::filetext::fclose")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
    name: "fclose",
    shape: ShapeRequirements::Any,
    constant_strategy: ConstantStrategy::InlineLiteral,
    elementwise: None,
    reduction: None,
    emits_nan: false,
    notes: "File I/O is not eligible for fusion; metadata is registered for completeness.",
};

#[runtime_builtin(
    name = "fclose",
    category = "io/filetext",
    summary = "Close one file, multiple files, or all files opened with fopen.",
    keywords = "fclose,file,close,io,identifier",
    accel = "cpu",
    type_resolver(crate::builtins::io::type_resolvers::fclose_type),
    builtin_path = "crate::builtins::io::filetext::fclose"
)]
async fn fclose_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
    let eval = evaluate(&args).await?;
    if let Some(out_count) = crate::output_count::current_output_count() {
        if out_count == 0 {
            return Ok(Value::OutputList(Vec::new()));
        }
        return Ok(crate::output_count::output_list_with_padding(
            out_count,
            eval.outputs(),
        ));
    }
    Ok(eval.first_output())
}

#[derive(Debug, Clone)]
pub struct FcloseEval {
    status: f64,
    message: String,
}

impl FcloseEval {
    fn success() -> Self {
        Self {
            status: 0.0,
            message: String::new(),
        }
    }

    fn failure(message: String) -> Self {
        Self {
            status: -1.0,
            message,
        }
    }

    pub fn first_output(&self) -> Value {
        Value::Num(self.status)
    }

    pub fn outputs(&self) -> Vec<Value> {
        vec![Value::Num(self.status), char_array_value(&self.message)]
    }

    #[cfg(test)]
    pub(crate) fn status(&self) -> f64 {
        self.status
    }

    #[cfg(test)]
    pub(crate) fn message(&self) -> &str {
        &self.message
    }
}

pub async fn evaluate(args: &[Value]) -> BuiltinResult<FcloseEval> {
    let gathered = gather_args(args).await?;
    match gathered.len() {
        0 => Ok(close_all().await),
        1 => handle_single_argument(&gathered[0]).await,
        _ => Err(fclose_error("fclose: too many input arguments")),
    }
}

async fn handle_single_argument(value: &Value) -> BuiltinResult<FcloseEval> {
    if matches_keyword(value, "all") {
        return Ok(close_all().await);
    }
    let fids = collect_file_ids(value).map_err(|err| fclose_error(format!("fclose: {err}")))?;
    Ok(close_fids(&fids).await)
}

async fn close_all() -> FcloseEval {
    let infos = registry::list_infos();
    let mut status_ok = true;
    let mut message = String::new();
    for info in infos {
        if info.id >= 3 {
            if let Err(err) = registry::close_async(info.id).await {
                status_ok = false;
                if message.is_empty() {
                    message = err.to_string();
                }
            }
        }
    }
    if status_ok {
        FcloseEval::success()
    } else {
        FcloseEval::failure(message)
    }
}

async fn close_fids(fids: &[i32]) -> FcloseEval {
    if fids.is_empty() {
        return FcloseEval::success();
    }
    let mut status_ok = true;
    let mut message = String::new();
    for &fid in fids {
        if fid < 0 {
            status_ok = false;
            if message.is_empty() {
                message = INVALID_IDENTIFIER_MESSAGE.to_string();
            }
            continue;
        }
        if fid < 3 {
            continue;
        }
        match registry::close_async(fid).await {
            Ok(Some(_)) => {}
            Ok(None) => {
                status_ok = false;
                if message.is_empty() {
                    message = INVALID_IDENTIFIER_MESSAGE.to_string();
                }
            }
            Err(err) => {
                status_ok = false;
                if message.is_empty() {
                    message = err.to_string();
                }
            }
        }
    }
    if status_ok {
        FcloseEval::success()
    } else {
        FcloseEval::failure(message)
    }
}

fn collect_file_ids(value: &Value) -> BuiltinResult<Vec<i32>> {
    match value {
        Value::Num(_) | Value::Int(_) | Value::Bool(_) => Ok(vec![parse_scalar_fid(value)?]),
        Value::Tensor(t) => {
            let mut ids = Vec::with_capacity(t.data.len());
            for &n in &t.data {
                ids.push(parse_fid_from_f64(n)?);
            }
            Ok(ids)
        }
        Value::LogicalArray(la) => {
            let mut ids = Vec::with_capacity(la.data.len());
            for &b in &la.data {
                let v = if b != 0 { 1 } else { 0 };
                ids.push(v);
            }
            Ok(ids)
        }
        Value::Cell(ca) => {
            let mut ids = Vec::with_capacity(ca.data.len());
            for ptr in &ca.data {
                let nested = collect_file_ids(ptr)?;
                ids.extend(nested);
            }
            Ok(ids)
        }
        Value::CharArray(_) | Value::String(_) | Value::StringArray(_) => {
            Err(fclose_error("file identifier must be numeric or 'all'"))
        }
        _ => Err(fclose_error("file identifier must be numeric or 'all'")),
    }
}

fn parse_scalar_fid(value: &Value) -> BuiltinResult<i32> {
    match value {
        Value::Int(i) => {
            let v = i.to_i64();
            if v < i32::MIN as i64 || v > i32::MAX as i64 {
                return Err(fclose_error("file identifier is out of range"));
            }
            Ok(v as i32)
        }
        Value::Num(n) => parse_fid_from_f64(*n),
        Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
        _ => Err(fclose_error("file identifier must be numeric or 'all'")),
    }
}

fn parse_fid_from_f64(value: f64) -> BuiltinResult<i32> {
    if !value.is_finite() {
        return Err(fclose_error("file identifier must be finite"));
    }
    let rounded = value.round();
    if (rounded - value).abs() > f64::EPSILON {
        return Err(fclose_error("file identifier must be an integer"));
    }
    if rounded < i32::MIN as f64 || rounded > i32::MAX as f64 {
        return Err(fclose_error("file identifier is out of range"));
    }
    Ok(rounded as i32)
}

async fn gather_args(args: &[Value]) -> BuiltinResult<Vec<Value>> {
    let mut gathered = Vec::with_capacity(args.len());
    for value in args {
        gathered.push(
            gather_if_needed_async(value)
                .await
                .map_err(map_control_flow)?,
        );
    }
    Ok(gathered)
}

fn matches_keyword(value: &Value, keyword: &str) -> bool {
    extract_scalar_string(value)
        .map(|text| text.eq_ignore_ascii_case(keyword))
        .unwrap_or(false)
}

#[cfg(test)]
pub(crate) mod tests {
    use super::*;
    use crate::builtins::common::test_support;
    use crate::builtins::io::filetext::{fopen, registry};
    use runmat_builtins::{CellArray, LogicalArray, StringArray, Tensor};
    use runmat_time::system_time_now;
    use std::future::Future;
    use std::io::{self, Cursor, Read, Seek, SeekFrom, Write};
    use std::path::Path;
    use std::path::PathBuf;
    use std::pin::Pin;
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::{Arc, MutexGuard};
    use std::task::{Context, Poll};
    use std::time::UNIX_EPOCH;

    struct FailingFlushProvider {
        fail_flush: Arc<AtomicBool>,
    }

    fn unsupported_provider_op() -> io::Error {
        io::Error::new(io::ErrorKind::Unsupported, "unsupported test provider op")
    }

    #[async_trait::async_trait(?Send)]
    impl runmat_filesystem::FsProvider for FailingFlushProvider {
        fn open(
            &self,
            _path: &Path,
            _flags: &runmat_filesystem::OpenFlags,
        ) -> io::Result<Box<dyn runmat_filesystem::FileHandle>> {
            Ok(Box::new(FailingFlushHandle {
                inner: Cursor::new(Vec::new()),
                fail_flush: self.fail_flush.clone(),
            }))
        }

        async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
            Err(unsupported_provider_op())
        }

        async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
            Err(unsupported_provider_op())
        }

        async fn remove_file(&self, _path: &Path) -> io::Result<()> {
            Err(unsupported_provider_op())
        }

        async fn metadata(&self, _path: &Path) -> io::Result<runmat_filesystem::FsMetadata> {
            Err(unsupported_provider_op())
        }

        async fn symlink_metadata(
            &self,
            _path: &Path,
        ) -> io::Result<runmat_filesystem::FsMetadata> {
            Err(unsupported_provider_op())
        }

        async fn read_dir(&self, _path: &Path) -> io::Result<Vec<runmat_filesystem::DirEntry>> {
            Err(unsupported_provider_op())
        }

        async fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
            Err(unsupported_provider_op())
        }

        async fn create_dir(&self, _path: &Path) -> io::Result<()> {
            Err(unsupported_provider_op())
        }

        async fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
            Err(unsupported_provider_op())
        }

        async fn remove_dir(&self, _path: &Path) -> io::Result<()> {
            Err(unsupported_provider_op())
        }

        async fn remove_dir_all(&self, _path: &Path) -> io::Result<()> {
            Err(unsupported_provider_op())
        }

        async fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
            Err(unsupported_provider_op())
        }

        async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
            Err(unsupported_provider_op())
        }
    }

    struct FailingFlushHandle {
        inner: Cursor<Vec<u8>>,
        fail_flush: Arc<AtomicBool>,
    }

    struct YieldOnce {
        yielded: bool,
    }

    impl Future for YieldOnce {
        type Output = ();

        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
            if self.yielded {
                Poll::Ready(())
            } else {
                self.yielded = true;
                cx.waker().wake_by_ref();
                Poll::Pending
            }
        }
    }

    impl Read for FailingFlushHandle {
        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
            self.inner.read(buf)
        }
    }

    impl Write for FailingFlushHandle {
        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
            self.inner.write(buf)
        }

        fn flush(&mut self) -> io::Result<()> {
            if self.fail_flush.load(Ordering::SeqCst) {
                Err(io::Error::other("flush failed"))
            } else {
                Ok(())
            }
        }
    }

    impl Seek for FailingFlushHandle {
        fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
            self.inner.seek(pos)
        }
    }

    #[async_trait::async_trait(?Send)]
    impl runmat_filesystem::FileHandle for FailingFlushHandle {
        async fn flush_async(&mut self) -> io::Result<()> {
            YieldOnce { yielded: false }.await;
            self.flush()
        }
    }

    fn unwrap_error_message(err: crate::RuntimeError) -> String {
        err.message().to_string()
    }

    fn run_evaluate(args: &[Value]) -> BuiltinResult<FcloseEval> {
        futures::executor::block_on(evaluate(args))
    }

    fn run_fopen(args: &[Value]) -> BuiltinResult<fopen::FopenEval> {
        futures::executor::block_on(fopen::evaluate(args))
    }

    fn registry_guard() -> MutexGuard<'static, ()> {
        registry::test_guard()
    }

    fn unique_path(prefix: &str) -> PathBuf {
        let now = system_time_now()
            .duration_since(UNIX_EPOCH)
            .expect("time went backwards");
        let filename = format!("{}_{}_{}.tmp", prefix, now.as_secs(), now.subsec_nanos());
        std::env::temp_dir().join(filename)
    }

    fn open_temp_file(prefix: &str) -> (f64, PathBuf) {
        let path = unique_path(prefix);
        {
            let mut file = runmat_filesystem::File::create(&path).unwrap();
            writeln!(&mut file, "data").unwrap();
        }
        let eval = run_fopen(&[Value::from(path.to_string_lossy().to_string())]).expect("fopen");
        let fid = eval.as_open().unwrap().fid;
        assert!(fid >= 3.0, "expected valid file identifier");
        (fid, path)
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fclose_closes_single_file() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let (fid, path) = open_temp_file("fclose_single");
        let eval = run_evaluate(&[Value::Num(fid)]).expect("fclose");
        assert_eq!(eval.status(), 0.0);
        assert!(eval.message().is_empty());
        assert!(registry::info_for(fid as i32).is_none());
        test_support::fs::remove_file(path).unwrap();
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fclose_invalid_identifier_returns_error() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let eval = run_evaluate(&[Value::Num(9999.0)]).expect("fclose");
        assert_eq!(eval.status(), -1.0);
        assert_eq!(eval.message(), INVALID_IDENTIFIER_MESSAGE);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fclose_all_closes_everything() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let (fid, path) = open_temp_file("fclose_all");
        let eval = run_evaluate(&[Value::from("all")]).expect("fclose all");
        assert_eq!(eval.status(), 0.0);
        assert!(registry::info_for(fid as i32).is_none());
        let infos = registry::list_infos();
        assert!(infos.iter().all(|info| info.id < 3));
        test_support::fs::remove_file(path).unwrap();
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fclose_no_args_closes_all() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let (fid, path) = open_temp_file("fclose_no_args");
        let eval = run_evaluate(&[]).expect("fclose");
        assert_eq!(eval.status(), 0.0);
        assert!(registry::info_for(fid as i32).is_none());
        test_support::fs::remove_file(path).unwrap();
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fclose_vector_of_fids_closes_each() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let path1 = unique_path("fclose_vec1");
        test_support::fs::write(&path1, "a").unwrap();
        let fid1 = run_fopen(&[Value::from(path1.to_string_lossy().to_string())])
            .expect("open 1")
            .as_open()
            .unwrap()
            .fid;
        let path2 = unique_path("fclose_vec2");
        test_support::fs::write(&path2, "b").unwrap();
        let fid2 = run_fopen(&[Value::from(path2.to_string_lossy().to_string())])
            .expect("open 2")
            .as_open()
            .unwrap()
            .fid;
        let tensor = Tensor::new(vec![fid1, fid2], vec![2, 1]).expect("tensor construction");
        let eval = run_evaluate(&[Value::Tensor(tensor)]).expect("fclose");
        assert_eq!(eval.status(), 0.0);
        assert!(registry::info_for(fid1 as i32).is_none());
        assert!(registry::info_for(fid2 as i32).is_none());
        test_support::fs::remove_file(path1).unwrap();
        test_support::fs::remove_file(path2).unwrap();
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fclose_repeat_returns_error_message() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let (fid, path) = open_temp_file("fclose_repeat");
        let first = run_evaluate(&[Value::Num(fid)]).expect("fclose");
        assert_eq!(first.status(), 0.0);
        let second = run_evaluate(&[Value::Num(fid)]).expect("fclose second");
        assert_eq!(second.status(), -1.0);
        assert_eq!(second.message(), INVALID_IDENTIFIER_MESSAGE);
        test_support::fs::remove_file(path).unwrap();
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fclose_keeps_file_registered_when_flush_fails() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let fail_flush = Arc::new(AtomicBool::new(true));
        let provider = Arc::new(FailingFlushProvider {
            fail_flush: fail_flush.clone(),
        });
        let _provider_guard = runmat_filesystem::replace_provider(provider);

        let eval = run_fopen(&[Value::from("flush_fail.txt"), Value::from("w")]).expect("fopen");
        let fid = eval.as_open().expect("open result").fid;
        assert!(fid >= 3.0);

        let first = run_evaluate(&[Value::Num(fid)]).expect("first fclose");
        assert_eq!(first.status(), -1.0);
        assert!(first.message().contains("flush failed"));
        assert!(registry::info_for(fid as i32).is_some());
        let handle = registry::take_handle(fid as i32).expect("handle remains registered");
        assert!(handle.lock().expect("handle lock").is_some());

        fail_flush.store(false, Ordering::SeqCst);
        let second = run_evaluate(&[Value::Num(fid)]).expect("second fclose");
        assert_eq!(second.status(), 0.0);
        assert!(registry::info_for(fid as i32).is_none());
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn concurrent_fclose_only_one_call_closes_file() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let _provider_guard = runmat_filesystem::replace_provider(Arc::new(FailingFlushProvider {
            fail_flush: Arc::new(AtomicBool::new(false)),
        }));

        let eval =
            run_fopen(&[Value::from("yielding_close.txt"), Value::from("w")]).expect("fopen");
        let fid = eval.as_open().expect("open result").fid as i32;
        assert!(fid >= 3);

        let (first, second) = futures::executor::block_on(futures::future::join(
            registry::close_async(fid),
            registry::close_async(fid),
        ));
        let closed_count = [first.expect("first close"), second.expect("second close")]
            .into_iter()
            .filter(Option::is_some)
            .count();

        assert_eq!(closed_count, 1);
        assert!(registry::info_for(fid).is_none());
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fclose_standard_stream_bool_argument() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let eval = run_evaluate(&[Value::Bool(true)]).expect("fclose stdout");
        assert_eq!(eval.status(), 0.0);
        assert!(eval.message().is_empty());
        let outputs = eval.outputs();
        assert_eq!(outputs.len(), 2);
        assert!(matches!(outputs[1], Value::CharArray(ref ca) if ca.rows == 1 && ca.cols == 0));
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fclose_logical_array_converts_to_numeric_ids() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let logical = LogicalArray::new(vec![1u8, 0u8, 1u8], vec![3]).expect("logical array");
        let eval = run_evaluate(&[Value::LogicalArray(logical)]).expect("fclose logical");
        assert_eq!(eval.status(), 0.0);
        assert!(eval.message().is_empty());
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fclose_cell_array_closes_each_entry() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let (fid1, path1) = open_temp_file("fclose_cell1");
        let (fid2, path2) = open_temp_file("fclose_cell2");
        let cell = CellArray::new(vec![Value::Num(fid1), Value::Num(fid2)], 1, 2).expect("cell");
        let eval = run_evaluate(&[Value::Cell(cell)]).expect("fclose cell");
        assert_eq!(eval.status(), 0.0);
        assert!(registry::info_for(fid1 as i32).is_none());
        assert!(registry::info_for(fid2 as i32).is_none());
        test_support::fs::remove_file(path1).unwrap();
        test_support::fs::remove_file(path2).unwrap();
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fclose_tensor_with_non_integer_entries_errors() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let tensor = Tensor::new(vec![1.5], vec![1, 1]).expect("tensor");
        let err = unwrap_error_message(run_evaluate(&[Value::Tensor(tensor)]).unwrap_err());
        assert_eq!(err, "fclose: file identifier must be an integer");
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fclose_string_array_all_equivalent() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let strings = StringArray::new(vec!["all".to_string()], vec![1]).expect("string array");
        let eval = run_evaluate(&[Value::StringArray(strings)]).expect("fclose all");
        assert_eq!(eval.status(), 0.0);
        assert!(eval.message().is_empty());
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fclose_accepts_empty_tensor() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let tensor = Tensor::new(Vec::new(), vec![0, 0]).expect("tensor");
        let eval = run_evaluate(&[Value::Tensor(tensor)]).expect("fclose");
        assert_eq!(eval.status(), 0.0);
        assert!(eval.message().is_empty());
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fclose_errors_on_non_numeric_input() {
        let _guard = registry_guard();
        registry::reset_for_tests();
        let err = unwrap_error_message(run_evaluate(&[Value::from("not-a-fid")]).unwrap_err());
        assert_eq!(err, "fclose: file identifier must be numeric or 'all'");
    }
}
