// Copyright 2018-2026 the Deno authors. MIT license.
// This module is browser compatible.

/**
 * Stubs a property on an object, retaining the attributes of the original property descriptor as far as possible.
 *
 * @experimental **UNSTABLE**: New API, yet to be vetted.
 *
 * @typeParam Self The type of the object to stub a property of.
 * @typeParam Prop The property of the instance to stub.
 * @param self The object to stub the property on.
 * @param property The property to stub.
 * @param value The value to stub the property with.
 * @returns A disposable that restores the original property when disposed.
 *
 * @example Usage
 * ```ts
 * import { stubProperty } from "@std/testing/unstable-stub-property";
 * import { assertEquals } from "@std/assert";
 *
 * const obj = { foo: "bar" };
 * {
 *  using stub = stubProperty(obj, "foo", "baz");
 *  assertEquals(obj.foo, "baz");
 * }
 * // After disposing, the property returns to its original value.
 * assertEquals(obj.foo, "bar");
 * ```
 */
export function stubProperty<Self, Prop extends keyof Self>(
  self: Self,
  property: Prop,
  value: Self[Prop],
): Disposable {
  const descriptor = Object.getOwnPropertyDescriptor(self, property);

  if (descriptor == null) {
    Object.defineProperty(self, property, { value, configurable: true });
    return {
      [Symbol.dispose]() {
        delete self[property];
      },
    };
  }

  if (!descriptor.configurable && !descriptor.writable) {
    throw new TypeError(
      `Cannot stub property "${
        String(property)
      }" because it is not configurable or writable.`,
    );
  }

  Object.defineProperty(self, property, {
    ...descriptor,
    ...(isAccessorDescriptor(descriptor) ? { get: () => value } : { value }),
  });

  return {
    [Symbol.dispose]() {
      Object.defineProperty(self, property, descriptor);
    },
  };
}

function isAccessorDescriptor(descriptor: PropertyDescriptor) {
  return Object.hasOwn(descriptor, "get") || Object.hasOwn(descriptor, "set");
}
