/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Boot } from "./boot.js";
class TestTimeoutError extends Error {
  diagnostics;
  constructor(message) {
    super(`Test timeout: ${message}`);
    try {
      this.diagnostics = MatterHooks?.generateDiagnostics?.();
    } catch (e) {
      this.diagnostics = `(diagnostics generation failed: ${e.message}`;
    }
  }
  code;
  timeout;
  file;
}
const macrotaskDependents = /* @__PURE__ */ new Set();
const registry = {
  timers: /* @__PURE__ */ new Set(),
  register(_timer) {
  },
  unregister(_timer) {
  }
};
class MockTimer {
  name = "Test";
  systemId = 0;
  intervalMs = 0;
  isPeriodic = false;
  #mockTime;
  #duration;
  isRunning = false;
  #callback;
  constructor(mockTime, name, duration, callback) {
    this.name = name;
    this.#mockTime = mockTime;
    this.#duration = duration;
    if (this instanceof MockInterval) {
      this.#callback = callback;
    } else {
      this.#callback = () => {
        this.#close();
        callback();
      };
    }
  }
  start() {
    registry.register(this);
    this.#mockTime.callbackAtTime(this.#mockTime.nowMs + this.#duration, this.#callback);
    this.isRunning = true;
    return this;
  }
  stop() {
    this.#close();
    return this;
  }
  #close() {
    registry.unregister(this);
    this.#mockTime.removeCallback(this.#callback);
    this.isRunning = false;
  }
}
class MockInterval extends MockTimer {
  constructor(mockTime, name, duration, callback) {
    const intervalCallback = async () => {
      mockTime.callbackAtTime(mockTime.nowMs + duration, intervalCallback);
      await callback();
    };
    super(mockTime, name, duration, intervalCallback);
  }
}
function isAsync(fn) {
  return fn.constructor.name === "AsyncFunction";
}
let callbacks = new Array();
let nowMs = 0;
let real = void 0;
let enabled = false;
let defaultToMacrotasks = false;
const instrumentedImplementations = /* @__PURE__ */ new WeakSet();
const epoch = /* @__PURE__ */ new Date("2025-01-01 12:34:56Z");
const MockTime = {
  epoch,
  get activeImplementation() {
    return enabled ? this : real ?? this;
  },
  /**
   * Revert to standard time implementation.
   */
  disable() {
    enabled = false;
    installActiveImplementation?.();
  },
  /**
   * Enable time mocking.  Reverts to disabled for each test file.
   */
  enable() {
    enabled = true;
    installActiveImplementation?.();
  },
  /**
   * Sets mock time to specific time and enable the mock.
   */
  reset(time = epoch) {
    callbacks = [];
    nowMs = new Date(time).getTime();
    defaultToMacrotasks = false;
    MockTime.enable();
  },
  /**
   * Enable and reset if not already enabled.
   */
  init() {
    if (!enabled) {
      MockTime.enable();
    }
  },
  /**
   * Enable macrotasks (true) or microtasks (false) for mock time incrementation.
   *
   * Microtasks are the default and are more efficient.  Macrotasks are required for e.g. most of node's crypto.subtle
   * methods to resolve.
   */
  get forceMacrotasks() {
    return defaultToMacrotasks;
  },
  set forceMacrotasks(value) {
    defaultToMacrotasks = value;
  },
  requireMacrotasks(dependent) {
    dependent = dependent.finally(() => {
      macrotaskDependents.delete(dependent);
    });
    macrotaskDependents.add(dependent);
    return dependent;
  },
  atTime(time, actor) {
    const revertTo = nowMs;
    let isAsync2 = false;
    try {
      nowMs = typeof time === "number" ? time : time.getTime();
      const result = actor();
      if (typeof result?.then === "function") {
        isAsync2 = true;
        return Promise.resolve(result).finally(() => {
          nowMs = revertTo;
        });
      }
      return result;
    } finally {
      if (!isAsync2) {
        nowMs = revertTo;
      }
    }
  },
  get now() {
    return new Date(nowMs);
  },
  get nowMs() {
    return nowMs;
  },
  getTimer(name, duration, callback) {
    return new MockTimer(this, name, duration, callback);
  },
  getPeriodicTimer(name, interval, callback) {
    return new MockInterval(this, name, interval, callback);
  },
  /**
   * Time compatible sleep.
   *
   * This passes the sleep call through to the (possibly mocked) underlying time implementation.
   */
  sleep(name, duration) {
    if (real === void 0) {
      throw new Error("Cannot sleep because time implementation is not present");
    }
    return real.sleep(name, duration);
  },
  /**
   * Use the installed time implementation to yield to the next macrotask.
   *
   * We do not mock macrotasks because it keeps tests truer to life and unlike timers it does not have a meaningful
   * impact on test execution.
   */
  get macrotask() {
    if (real === void 0) {
      throw new Error("Cannot create macrotask because time implementation is not present");
    }
    return real.macrotask;
  },
  /**
   * Wait for all registered macrotask dependencies to complete.
   */
  get macrotasks() {
    return (async () => {
      while (macrotaskDependents.size) {
        await MockTime.resolve(this.macrotask);
      }
    })();
  },
  /**
   * Resolve a promise with time dependency.
   *
   * Moves time forward until the promise resolves.
   */
  async resolve(promise, { stepMs, macrotasks } = {}) {
    let resolved = false;
    let result;
    let error;
    if (typeof promise !== "object" || promise === null || !("then" in promise) || typeof promise.then !== "function") {
      return promise;
    }
    promise.then(
      (r) => {
        resolved = true;
        result = r;
      },
      (e) => {
        resolved = true;
        error = e;
      }
    );
    let timeAdvanced = 0;
    while (!resolved) {
      if ((macrotasks ?? defaultToMacrotasks) || macrotaskDependents.size) {
        await MockTime.macrotask;
      } else {
        await MockTime.yield();
      }
      if (resolved) {
        break;
      }
      if (timeAdvanced > 60 * 60 * 1e3) {
        throw new TestTimeoutError(
          "Promise did not resolve within one (virtual) hour, probably not going to happen"
        );
      }
      if (stepMs) {
        await this.advance(stepMs);
        timeAdvanced += stepMs;
      } else {
        await this.advance(100);
        timeAdvanced += 100;
      }
      if (resolved) {
        break;
      }
      await this.yield();
    }
    if (error !== void 0) {
      throw error;
    }
    return result;
  },
  /**
   * Move time forward.  Runs tasks scheduled during this interval.
   */
  async advance(ms) {
    const newTimeMs = nowMs + ms;
    while (callbacks.length) {
      const { atMs, callback } = callbacks[0];
      if (atMs > newTimeMs) break;
      callbacks.shift();
      nowMs = atMs;
      await callback();
    }
    nowMs = newTimeMs;
  },
  /**
   * Yield to scheduled microtasks.  This means that all code paths waiting on resolved promises (including await)
   * will proceed before this method returns.
   */
  async yield() {
    await Promise.resolve();
  },
  /**
   * Due to its implementation, an older version of yield() would actually yield to microtasks three times.  Our tests
   * then depended on this functionality -- one yield could trigger up to three nested awaits.
   *
   * To make this clear, the version of yield() that emulates old behavior is called "yield3".
   */
  async yield3() {
    await Promise.resolve();
    await Promise.resolve();
    await Promise.resolve();
  },
  /**
   * Hook a method and invoke a callback just before the method completes. Unhooks after completion.
   *
   * Handles both synchronous and asynchronous methods.  The responseInterceptor should match the async-ness of the
   * intercepted method.
   *
   * The responseInterceptor can optionally access and/or replace the resolve/reject value.
   * The callInterceptor can be used to manipulate the call parameters of the method
   */
  interceptOnce(obj, method, responseInterceptor, callInterceptor) {
    const original = obj[method];
    if (!original) {
      throw new Error(`Interception method ${method} is not present`);
    }
    let result;
    if (isAsync(responseInterceptor)) {
      obj[method] = async function(...args) {
        if (callInterceptor) {
          args = callInterceptor(args);
        }
        try {
          const resolve = await original.apply(this, args);
          result = { resolve };
        } catch (reject) {
          result = { reject };
        } finally {
          obj[method] = original;
        }
        result = await responseInterceptor(result) ?? result;
        if (result.reject) {
          throw result.reject;
        }
        return result.resolve;
      };
    } else {
      obj[method] = function(...args) {
        if (callInterceptor) {
          args = callInterceptor(args);
        }
        try {
          const resolve = original.apply(this, args);
          result = { resolve };
        } catch (reject) {
          result = { reject };
        } finally {
          obj[method] = original;
        }
        result = responseInterceptor(result) ?? result;
        if (result.reject) {
          throw result.reject;
        }
        return result.resolve;
      };
    }
  },
  /**
   * Count the number of registered timers with a specific name.
   */
  timerCountFor(name) {
    return [...registry.timers].filter((timer) => timer.name === name).length;
  },
  callbackAtTime(atMs, callback) {
    if (!Number.isInteger(atMs)) {
      throw new Error(`Callback registered at non-integer time ${atMs}`);
    }
    callbacks.push({ atMs, callback });
    callbacks.sort(({ atMs: atMsA }, { atMs: atMsB }) => atMsA - atMsB);
  },
  removeCallback(callbackToRemove) {
    const index = callbacks.findIndex(({ callback }) => callbackToRemove === callback);
    if (index === -1) return;
    callbacks.splice(index, 1);
  }
};
let installActiveImplementation;
function timeSetup(Time) {
  registry.register = Time.register;
  registry.unregister = Time.unregister;
  registry.timers = Time.timers;
  Time.startup.systemMs = Time.startup.processMs = 0;
  real = Time.default;
  instrumentImplementation(real);
  installActiveImplementation = () => Time.default = MockTime.activeImplementation;
  installActiveImplementation();
}
function instrumentImplementation(time) {
  if (instrumentedImplementations.has(time)) {
    return;
  }
  let get;
  let obj = time;
  while (obj) {
    get = Object.getOwnPropertyDescriptor(obj.constructor.prototype, "macrotask")?.get;
    if (get) {
      break;
    }
    obj = Object.getPrototypeOf(obj);
  }
  if (!get) {
    throw new Error("Time instance does not define macrotask getter");
  }
  Object.defineProperty(time, "macrotask", {
    get() {
      return MockTime.requireMacrotasks(get.apply(time));
    }
  });
  instrumentedImplementations.add(time);
}
Object.assign(globalThis, { MockTime });
Boot.init((kind) => {
  if (kind === "state") {
    return;
  }
  MockTime.reset();
  MockTime.disable();
});
export {
  MockTime,
  TestTimeoutError,
  timeSetup
};
//# sourceMappingURL=time.js.map
