// Copyright 2018-2026 the Deno authors. MIT license.

use std::cell::RefCell;
use std::collections::HashMap;
use std::os::raw::c_char;
use std::os::raw::c_void;
use std::ptr;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;

use napi_sys::ValueType::napi_number;
use napi_sys::*;

use crate::assert_napi_ok;
use crate::napi_get_callback_info;
use crate::napi_new_property;

pub struct NapiObject {
  counter: i32,
}

thread_local! {
  // map from native object ptr to napi reference (this is similar to what napi-rs does)
  static REFS: RefCell<HashMap<*mut c_void, napi_ref>> = RefCell::new(HashMap::new());
}

pub extern "C" fn finalize_napi_object(
  env: napi_env,
  finalize_data: *mut c_void,
  _finalize_hint: *mut c_void,
) {
  let obj = unsafe { Box::from_raw(finalize_data as *mut NapiObject) };
  drop(obj);
  if let Some(reference) =
    REFS.with_borrow_mut(|map| map.remove(&finalize_data))
  {
    unsafe { napi_delete_reference(env, reference) };
  }
}

impl NapiObject {
  fn new_inner(
    env: napi_env,
    info: napi_callback_info,
    finalizer: napi_finalize,
    out_ptr: Option<*mut napi_ref>,
  ) -> napi_value {
    assert!(matches!(
      (finalizer, out_ptr),
      (None, None) | (Some(_), Some(_))
    ));
    let mut new_target: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_get_new_target(env, info, &mut new_target));
    let is_constructor = !new_target.is_null();

    let (args, argc, this) = napi_get_callback_info!(env, info, 1);
    assert_eq!(argc, 1);

    if is_constructor {
      let mut value = 0;

      let mut ty = -1;
      assert_napi_ok!(napi_typeof(env, args[0], &mut ty));
      assert_eq!(ty, napi_number);

      assert_napi_ok!(napi_get_value_int32(env, args[0], &mut value));

      let obj = Box::new(Self { counter: value });
      let obj_raw = Box::into_raw(obj) as *mut c_void;
      assert_napi_ok!(napi_wrap(
        env,
        this,
        obj_raw,
        finalizer,
        ptr::null_mut(),
        out_ptr.unwrap_or(ptr::null_mut())
      ));

      if let Some(p) = out_ptr {
        if finalizer.is_some() {
          REFS.with_borrow_mut(|map| map.insert(obj_raw, unsafe { p.read() }));
        }
      }

      return this;
    }

    unreachable!();
  }

  #[allow(
    clippy::new_ret_no_self,
    reason = "napi constructor returns napi_value"
  )]
  pub extern "C" fn new(env: napi_env, info: napi_callback_info) -> napi_value {
    Self::new_inner(env, info, None, None)
  }

  #[allow(
    clippy::new_ret_no_self,
    reason = "napi constructor returns napi_value"
  )]
  pub extern "C" fn new_with_finalizer(
    env: napi_env,
    info: napi_callback_info,
  ) -> napi_value {
    let mut out = ptr::null_mut();
    Self::new_inner(env, info, Some(finalize_napi_object), Some(&mut out))
  }

  pub extern "C" fn set_value(
    env: napi_env,
    info: napi_callback_info,
  ) -> napi_value {
    let (args, argc, this) = napi_get_callback_info!(env, info, 1);
    assert_eq!(argc, 1);
    let mut obj: *mut Self = ptr::null_mut();
    assert_napi_ok!(napi_unwrap(
      env,
      this,
      &mut obj as *mut _ as *mut *mut c_void
    ));

    assert_napi_ok!(napi_get_value_int32(env, args[0], &mut (*obj).counter));

    ptr::null_mut()
  }

  pub extern "C" fn get_value(
    env: napi_env,
    info: napi_callback_info,
  ) -> napi_value {
    let (_args, argc, this) = napi_get_callback_info!(env, info, 0);
    assert_eq!(argc, 0);
    let mut obj: *mut Self = ptr::null_mut();
    assert_napi_ok!(napi_unwrap(
      env,
      this,
      &mut obj as *mut _ as *mut *mut c_void
    ));

    let mut num: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_create_int32(env, (*obj).counter, &mut num));

    num
  }

  pub extern "C" fn increment(
    env: napi_env,
    info: napi_callback_info,
  ) -> napi_value {
    let (_args, argc, this) = napi_get_callback_info!(env, info, 0);
    assert_eq!(argc, 0);
    let mut obj: *mut Self = ptr::null_mut();
    assert_napi_ok!(napi_unwrap(
      env,
      this,
      &mut obj as *mut _ as *mut *mut c_void
    ));

    unsafe {
      (*obj).counter += 1;
    }

    ptr::null_mut()
  }

  pub extern "C" fn factory(
    env: napi_env,
    info: napi_callback_info,
  ) -> napi_value {
    let (_args, argc, _this) = napi_get_callback_info!(env, info, 0);
    assert_eq!(argc, 0);

    let int64 = 64;
    let mut value: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_create_int64(env, int64, &mut value));
    value
  }
}

/// A class that uses getter/setter accessor properties defined via
/// napi_define_class with napi_default (0) attributes. This tests that
/// napi_writable is correctly ignored for accessor properties — the
/// presence of a setter should make the property writable regardless
/// of whether napi_writable is set.
pub struct NapiAccessorObject {
  value: i32,
}

extern "C" fn accessor_obj_constructor(
  env: napi_env,
  info: napi_callback_info,
) -> napi_value {
  let mut this: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_get_cb_info(
    env,
    info,
    &mut 0,
    ptr::null_mut(),
    &mut this,
    ptr::null_mut(),
  ));

  let obj = Box::new(NapiAccessorObject { value: 0 });
  let obj_raw = Box::into_raw(obj) as *mut c_void;
  assert_napi_ok!(napi_wrap(
    env,
    this,
    obj_raw,
    Some(accessor_obj_release),
    ptr::null_mut(),
    ptr::null_mut(),
  ));

  this
}

extern "C" fn accessor_obj_release(
  _env: napi_env,
  data: *mut c_void,
  _hint: *mut c_void,
) {
  unsafe {
    drop(Box::from_raw(data as *mut NapiAccessorObject));
  }
}

extern "C" fn accessor_getter(
  env: napi_env,
  info: napi_callback_info,
) -> napi_value {
  let (_args, _argc, this) = napi_get_callback_info!(env, info, 0);
  let mut obj: *mut NapiAccessorObject = ptr::null_mut();
  assert_napi_ok!(napi_unwrap(
    env,
    this,
    &mut obj as *mut _ as *mut *mut c_void
  ));

  let mut result: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_create_int32(env, (*obj).value, &mut result));
  result
}

extern "C" fn accessor_setter(
  env: napi_env,
  info: napi_callback_info,
) -> napi_value {
  let (args, argc, this) = napi_get_callback_info!(env, info, 1);
  assert_eq!(argc, 1);
  let mut obj: *mut NapiAccessorObject = ptr::null_mut();
  assert_napi_ok!(napi_unwrap(
    env,
    this,
    &mut obj as *mut _ as *mut *mut c_void
  ));

  assert_napi_ok!(napi_get_value_int32(env, args[0], &mut (*obj).value));

  ptr::null_mut()
}

/// Test napi_remove_wrap: wraps an object, then removes the wrap.
extern "C" fn test_remove_wrap(
  env: napi_env,
  info: napi_callback_info,
) -> napi_value {
  let (args, argc, _) = napi_get_callback_info!(env, info, 1);
  assert_eq!(argc, 1);

  // Wrap the object with some native data
  let data = Box::into_raw(Box::new(42i32)) as *mut std::ffi::c_void;
  assert_napi_ok!(napi_wrap(
    env,
    args[0],
    data,
    None,
    ptr::null_mut(),
    ptr::null_mut(),
  ));

  // Remove the wrap and verify we get the same pointer back
  let mut removed_data: *mut std::ffi::c_void = ptr::null_mut();
  assert_napi_ok!(napi_remove_wrap(env, args[0], &mut removed_data));
  assert_eq!(removed_data, data);

  // Clean up
  unsafe {
    let _ = Box::from_raw(removed_data as *mut i32);
  }

  let mut result: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_get_boolean(env, true, &mut result));
  result
}

/// Raw napi_wrap: wraps a JS object with an i32 value.
/// Used for cross-env wrap/unwrap testing.
extern "C" fn test_raw_wrap(
  env: napi_env,
  info: napi_callback_info,
) -> napi_value {
  let (args, argc, _) = napi_get_callback_info!(env, info, 2);
  assert_eq!(argc, 2);

  let mut value: i32 = 0;
  assert_napi_ok!(napi_get_value_int32(env, args[1], &mut value));

  let data = Box::into_raw(Box::new(value)) as *mut c_void;
  assert_napi_ok!(napi_wrap(
    env,
    args[0],
    data,
    None,
    ptr::null_mut(),
    ptr::null_mut()
  ));

  let mut result: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_get_boolean(env, true, &mut result));
  result
}

/// Raw napi_unwrap: retrieves the i32 value from a wrapped JS object.
/// Used for cross-env wrap/unwrap testing.
extern "C" fn test_raw_unwrap(
  env: napi_env,
  info: napi_callback_info,
) -> napi_value {
  let (args, argc, _) = napi_get_callback_info!(env, info, 1);
  assert_eq!(argc, 1);

  let mut data: *mut c_void = ptr::null_mut();
  let status = unsafe { napi_unwrap(env, args[0], &mut data) };
  if status != napi_sys::Status::napi_ok {
    let mut result: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_get_null(env, &mut result));
    return result;
  }

  let value = unsafe { *(data as *const i32) };
  let mut result: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_create_int32(env, value, &mut result));
  result
}

/// Invokes `napi_new_instance` on the constructor passed as the first
/// argument with the i32 second argument forwarded. Regression coverage
/// for #33924: this verifies the API path still produces a properly
/// wrapped instance on the normal codepath.
extern "C" fn test_call_new_instance(
  env: napi_env,
  info: napi_callback_info,
) -> napi_value {
  let (args, argc, _) = napi_get_callback_info!(env, info, 2);
  assert_eq!(argc, 2);

  let argv = [args[1]];
  let mut result: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_new_instance(
    env,
    args[0],
    argv.len(),
    argv.as_ptr(),
    &mut result,
  ));
  result
}

extern "C" fn js_calling_constructor(
  env: napi_env,
  info: napi_callback_info,
) -> napi_value {
  let (_, _, this) = napi_get_callback_info!(env, info, 0);
  let mut global: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_get_global(env, &mut global));

  let mut object: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_get_named_property(
    env,
    global,
    c"Object".as_ptr(),
    &mut object,
  ));

  let mut seal: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_get_named_property(
    env,
    object,
    c"seal".as_ptr(),
    &mut seal,
  ));

  let mut result: napi_value = ptr::null_mut();
  let args = [this];
  assert_napi_ok!(napi_call_function(
    env,
    global,
    seal,
    args.len(),
    args.as_ptr(),
    &mut result,
  ));

  ptr::null_mut()
}

extern "C" fn test_new_instance_constructor_can_call_js(
  env: napi_env,
  _info: napi_callback_info,
) -> napi_value {
  let mut constructor: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_define_class(
    env,
    c"JsCallingConstructor".as_ptr(),
    usize::MAX,
    Some(js_calling_constructor),
    ptr::null_mut(),
    0,
    ptr::null(),
    &mut constructor,
  ));

  let mut result: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_new_instance(
    env,
    constructor,
    0,
    ptr::null(),
    &mut result,
  ));
  result
}

// Regression test for #33924. The runtime runs with V8's Explicit microtask
// policy, so microtasks are drained deliberately on event loop turns.
// `napi_resolve_deferred` used to restore the isolate to the Auto policy (V8's
// old default) after resolving, which permanently flipped the whole isolate to
// Auto. Under Auto, V8 auto-drains microtasks whenever the call stack unwinds to
// depth zero - including mid-construction inside `napi_new_instance` when it is
// invoked from native event-loop code (e.g. an async-work completion). A drained
// microtask that constructed another napi-rs class while the factory was still
// in flight corrupted object wrapping and produced "Failed to recover
// `TsconfigCache` type from napi value". This test recreates the depth-zero
// context with async work and asserts the queued microtask does NOT drain during
// construction.
static MICROTASK_RAN: AtomicBool = AtomicBool::new(false);

extern "C" fn mark_microtask_ran(
  _env: napi_env,
  _info: napi_callback_info,
) -> napi_value {
  MICROTASK_RAN.store(true, Ordering::SeqCst);
  ptr::null_mut()
}

struct PolicyBaton {
  constructor: napi_ref,
  report: napi_ref,
  task: napi_async_work,
}

unsafe extern "C" fn policy_execute(_env: napi_env, _data: *mut c_void) {}

unsafe extern "C" fn policy_complete(
  env: napi_env,
  status: napi_status,
  data: *mut c_void,
) {
  unsafe {
    assert!(status == napi_sys::Status::napi_ok);
    let baton: Box<PolicyBaton> = Box::from_raw(data as *mut PolicyBaton);

    MICROTASK_RAN.store(false, Ordering::SeqCst);

    // Create a deferred and register a microtask reaction on its promise.
    let mut deferred: napi_deferred = ptr::null_mut();
    let mut promise: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_create_promise(env, &mut deferred, &mut promise));

    let mut mark: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_create_function(
      env,
      c"mark".as_ptr(),
      usize::MAX,
      Some(mark_microtask_ran),
      ptr::null_mut(),
      &mut mark,
    ));

    let mut then: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_get_named_property(
      env,
      promise,
      c"then".as_ptr(),
      &mut then,
    ));
    let then_args = [mark];
    let mut _then_result: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_call_function(
      env,
      promise,
      then,
      then_args.len(),
      then_args.as_ptr(),
      &mut _then_result,
    ));

    // Resolve the deferred. This enqueues the `mark` microtask. Pre-fix this
    // call also left the isolate in the Auto microtask policy.
    let mut undefined: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_get_undefined(env, &mut undefined));
    assert_napi_ok!(napi_resolve_deferred(env, deferred, undefined));

    // Round-trip through napi_new_instance at call-depth-zero. Under Auto, V8
    // drains the pending microtask when NewInstance unwinds to depth zero;
    // under Explicit it stays queued until the next event loop checkpoint.
    let mut constructor: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_get_reference_value(
      env,
      baton.constructor,
      &mut constructor,
    ));
    let mut seven: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_create_int32(env, 7, &mut seven));
    let ctor_args = [seven];
    let mut _instance: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_new_instance(
      env,
      constructor,
      ctor_args.len(),
      ctor_args.as_ptr(),
      &mut _instance,
    ));

    // Report whether the microtask drained during construction.
    let mut drained: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_get_boolean(
      env,
      MICROTASK_RAN.load(Ordering::SeqCst),
      &mut drained,
    ));

    let mut global: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_get_global(env, &mut global));
    let mut report: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_get_reference_value(env, baton.report, &mut report));
    let report_args = [drained];
    let mut _result: napi_value = ptr::null_mut();
    assert_napi_ok!(napi_call_function(
      env,
      global,
      report,
      report_args.len(),
      report_args.as_ptr(),
      &mut _result,
    ));

    assert_napi_ok!(napi_delete_reference(env, baton.constructor));
    assert_napi_ok!(napi_delete_reference(env, baton.report));
    assert_napi_ok!(napi_delete_async_work(env, baton.task));
  }
}

/// Queues async work whose completion (running at call-depth-zero on the event
/// loop) resolves a deferred and then calls `napi_new_instance`, reporting via
/// the second argument whether a microtask drained during construction.
/// Regression coverage for #33924.
extern "C" fn test_resolve_deferred_keeps_microtask_policy(
  env: napi_env,
  info: napi_callback_info,
) -> napi_value {
  let (args, argc, _) = napi_get_callback_info!(env, info, 2);
  assert_eq!(argc, 2);

  let mut constructor: napi_ref = ptr::null_mut();
  assert_napi_ok!(napi_create_reference(env, args[0], 1, &mut constructor));
  let mut report: napi_ref = ptr::null_mut();
  assert_napi_ok!(napi_create_reference(env, args[1], 1, &mut report));

  let mut resource_name: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_create_string_utf8(
    env,
    c"test_microtask_policy".as_ptr(),
    usize::MAX,
    &mut resource_name,
  ));

  let baton = Box::new(PolicyBaton {
    constructor,
    report,
    task: ptr::null_mut(),
  });
  let baton_ptr = Box::into_raw(baton) as *mut c_void;

  let mut async_work: napi_async_work = ptr::null_mut();
  assert_napi_ok!(napi_create_async_work(
    env,
    ptr::null_mut(),
    resource_name,
    Some(policy_execute),
    Some(policy_complete),
    baton_ptr,
    &mut async_work,
  ));
  let baton = unsafe { &mut *(baton_ptr as *mut PolicyBaton) };
  baton.task = async_work;
  assert_napi_ok!(napi_queue_async_work(env, async_work));

  ptr::null_mut()
}

pub fn init(env: napi_env, exports: napi_value) {
  let mut static_prop = napi_new_property!(env, "factory", NapiObject::factory);
  static_prop.attributes = PropertyAttributes::static_;

  let properties = &[
    napi_new_property!(env, "set_value", NapiObject::set_value),
    napi_new_property!(env, "get_value", NapiObject::get_value),
    napi_new_property!(env, "increment", NapiObject::increment),
    static_prop,
  ];

  let mut cons: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_define_class(
    env,
    "NapiObject\0".as_ptr() as *mut c_char,
    usize::MAX,
    Some(NapiObject::new),
    ptr::null_mut(),
    properties.len(),
    properties.as_ptr(),
    &mut cons,
  ));

  assert_napi_ok!(napi_set_named_property(
    env,
    exports,
    "NapiObject\0".as_ptr() as *const c_char,
    cons,
  ));

  let mut cons: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_define_class(
    env,
    c"NapiObjectOwned".as_ptr(),
    usize::MAX,
    Some(NapiObject::new_with_finalizer),
    ptr::null_mut(),
    properties.len(),
    properties.as_ptr(),
    &mut cons,
  ));

  assert_napi_ok!(napi_set_named_property(
    env,
    exports,
    "NapiObjectOwned\0".as_ptr() as *const c_char,
    cons,
  ));

  // Register NapiAccessorObject — class with getter/setter properties
  // using napi_default attributes (napi_writable NOT set).
  let accessor_properties = &[napi_property_descriptor {
    utf8name: c"value".as_ptr(),
    name: ptr::null_mut(),
    method: None,
    getter: Some(accessor_getter),
    setter: Some(accessor_setter),
    data: ptr::null_mut(),
    attributes: PropertyAttributes::default, // napi_writable is NOT set
    value: ptr::null_mut(),
  }];

  let mut cons: napi_value = ptr::null_mut();
  assert_napi_ok!(napi_define_class(
    env,
    c"NapiAccessorObject".as_ptr(),
    usize::MAX,
    Some(accessor_obj_constructor),
    ptr::null_mut(),
    accessor_properties.len(),
    accessor_properties.as_ptr(),
    &mut cons,
  ));

  assert_napi_ok!(napi_set_named_property(
    env,
    exports,
    c"NapiAccessorObject".as_ptr(),
    cons,
  ));

  // Register standalone functions on exports
  let extra_props = &[
    napi_new_property!(env, "test_remove_wrap", test_remove_wrap),
    napi_new_property!(env, "test_raw_wrap", test_raw_wrap),
    napi_new_property!(env, "test_raw_unwrap", test_raw_unwrap),
    napi_new_property!(env, "test_call_new_instance", test_call_new_instance),
    napi_new_property!(
      env,
      "test_new_instance_constructor_can_call_js",
      test_new_instance_constructor_can_call_js
    ),
    napi_new_property!(
      env,
      "test_resolve_deferred_keeps_microtask_policy",
      test_resolve_deferred_keeps_microtask_policy
    ),
  ];
  assert_napi_ok!(napi_define_properties(
    env,
    exports,
    extra_props.len(),
    extra_props.as_ptr()
  ));
}
