/**
 * @since 2.0.0
 */
import * as Arr from "../Array.js";
import * as Clock from "../Clock.js";
import * as Data from "../Data.js";
import * as Duration from "../Duration.js";
import * as Effect from "../Effect.js";
import * as Fiber from "../Fiber.js";
import { flow } from "../Function.js";
import * as Latch from "../Latch.js";
import * as Layer from "../Layer.js";
import * as Order from "../Order.js";
import * as Semaphore from "../Semaphore.js";
/**
 * The warning message that will be displayed if a test is using time but is
 * not advancing the `TestClock`.
 */
const warningMessage = "A test is using time, but is not advancing the test " + "clock, which may result in the test hanging. Use TestClock.adjust to " + "manually advance the time.";
const defaultOptions = {
  warningDelay: "1 second"
};
const SleepOrder = /*#__PURE__*/Order.flip(/*#__PURE__*/Order.Struct({
  timestamp: Order.Number,
  sequence: Order.Number
}));
/**
 * Creates a `TestClock` with optional configuration.
 *
 * @example
 * ```ts
 * import { Effect } from "effect"
 * import { TestClock } from "effect/testing"
 *
 * const program = Effect.gen(function*() {
 *   // Create a TestClock with default settings
 *   const testClock = yield* TestClock.make()
 *
 *   // Create a TestClock with custom warning delay
 *   const customTestClock = yield* TestClock.make({
 *     warningDelay: "10 seconds"
 *   })
 *
 *   // Use the TestClock to control time in tests
 *   yield* testClock.adjust("1 hour")
 *   const currentTime = testClock.currentTimeMillisUnsafe()
 *   console.log(currentTime) // Time advanced by 1 hour
 * })
 * ```
 *
 * @since 4.0.0
 * @category constructors
 */
export const make = /*#__PURE__*/Effect.fnUntraced(function* (options) {
  const config = Object.assign({}, defaultOptions, options);
  let sequence = 0;
  const sleeps = [];
  const liveClock = yield* Clock.clockWith(Effect.succeed);
  const warningSemaphore = yield* Semaphore.make(1);
  let currentTimestamp = new Date(0).getTime();
  let warningState = WarningState.Start();
  function currentTimeMillisUnsafe() {
    return currentTimestamp;
  }
  function currentTimeNanosUnsafe() {
    return BigInt(Math.floor(currentTimestamp * 1000000));
  }
  const currentTimeMillis = Effect.sync(currentTimeMillisUnsafe);
  const currentTimeNanos = Effect.sync(currentTimeNanosUnsafe);
  function withLive(effect) {
    return Effect.provideService(effect, Clock.Clock, liveClock);
  }
  /**
   * Forks a fiber that will display a warning message if a test is using time
   * but is not advancing the `TestClock`.
   */
  const warningStart = warningSemaphore.withPermits(1)(Effect.suspend(() => {
    if (warningState._tag === "Start") {
      return Effect.logWarning(warningMessage).pipe(Effect.delay(config.warningDelay), withLive, Effect.forkChild, Effect.interruptible, Effect.flatMap(fiber => Effect.sync(() => {
        warningState = WarningState.Pending({
          fiber
        });
      })));
    }
    return Effect.void;
  }));
  /**
   * Cancels the warning message that is displayed if a test is using time but
   * is not advancing the `TestClock`.
   */
  const warningDone = warningSemaphore.withPermits(1)(Effect.suspend(() => {
    switch (warningState._tag) {
      case "Pending":
        {
          return Fiber.interrupt(warningState.fiber).pipe(Effect.andThen(Effect.sync(() => {
            warningState = WarningState.Done();
          })));
        }
      case "Start":
      case "Done":
        {
          warningState = WarningState.Done();
          return Effect.void;
        }
    }
  }));
  const sleep = Effect.fnUntraced(function* (duration) {
    const millis = Duration.toMillis(duration);
    const end = currentTimestamp + millis;
    if (end <= currentTimestamp) return;
    const latch = Latch.makeUnsafe();
    sleeps.push({
      sequence: sequence++,
      timestamp: end,
      latch
    });
    sleeps.sort(SleepOrder);
    yield* warningStart;
    yield* latch.await;
  });
  const runSemaphore = yield* Semaphore.make(1);
  const run = Effect.fnUntraced(function* (step) {
    yield* Fiber.await(yield* Effect.forkScoped(Effect.yieldNow));
    const endTimestamp = step(currentTimestamp);
    while (Arr.isArrayNonEmpty(sleeps)) {
      if (Arr.lastNonEmpty(sleeps).timestamp > endTimestamp) break;
      const entry = sleeps.pop();
      currentTimestamp = entry.timestamp;
      entry.latch.openUnsafe();
      yield* Effect.yieldNow;
    }
    currentTimestamp = endTimestamp;
  }, runSemaphore.withPermits(1));
  function adjust(duration) {
    const millis = Duration.toMillis(Duration.fromInputUnsafe(duration));
    return warningDone.pipe(Effect.andThen(run(timestamp => timestamp + millis)));
  }
  function setTime(timestamp) {
    return warningDone.pipe(Effect.andThen(run(() => timestamp)));
  }
  yield* Effect.addFinalizer(() => warningDone);
  return {
    currentTimeMillisUnsafe,
    currentTimeNanosUnsafe,
    currentTimeMillis,
    currentTimeNanos,
    adjust,
    setTime,
    sleep,
    withLive
  };
});
/**
 * Creates a `Layer` which constructs a `TestClock`.
 *
 * @example
 * ```ts
 * import { Effect } from "effect"
 * import { TestClock } from "effect/testing"
 *
 * // Create a TestClock layer
 * const testClockLayer = TestClock.layer()
 *
 * // Create a TestClock layer with custom options
 * const customTestClockLayer = TestClock.layer({
 *   warningDelay: "5 seconds"
 * })
 *
 * const program = Effect.gen(function*() {
 *   // Use the layer in your program
 *   yield* TestClock.adjust("1 hour")
 * }).pipe(Effect.provide(testClockLayer))
 * ```
 *
 * @since 4.0.0
 * @category layers
 */
export const layer = /*#__PURE__*/flow(make, /*#__PURE__*/Layer.effect(Clock.Clock));
/**
 * Retrieves the `TestClock` service for this test and uses it to run the
 * specified workflow.
 *
 * @example
 * ```ts
 * import { Effect } from "effect"
 * import { TestClock } from "effect/testing"
 *
 * const program = Effect.gen(function*() {
 *   // Use testClockWith to access the TestClock instance
 *   const currentTime = yield* TestClock.testClockWith((testClock) =>
 *     Effect.succeed(testClock.currentTimeMillisUnsafe())
 *   )
 *
 *   // Adjust time using the TestClock instance
 *   yield* TestClock.testClockWith((testClock) => testClock.adjust("2 hours"))
 *
 *   console.log(currentTime) // Initial time
 * })
 * ```
 *
 * @since 2.0.0
 * @category utils
 */
export const testClockWith = f => Effect.withFiber(fiber => f(fiber.getRef(Clock.Clock)));
/**
 * Accesses a `TestClock` instance in the context and increments the time
 * by the specified duration, running any actions scheduled for on or before
 * the new time in order.
 *
 * @example
 * ```ts
 * import { Effect } from "effect"
 * import { TestClock } from "effect/testing"
 *
 * const program = Effect.gen(function*() {
 *   let executed = false
 *
 *   // Fork an effect that sleeps for 30 minutes
 *   const fiber = yield* Effect.gen(function*() {
 *     yield* Effect.sleep("30 minutes")
 *     executed = true
 *   }).pipe(Effect.forkChild)
 *
 *   // Advance the clock by 30 minutes
 *   yield* TestClock.adjust("30 minutes")
 *
 *   // The effect should now be executed
 *   console.log(executed) // true
 * })
 * ```
 *
 * @since 2.0.0
 * @category utils
 */
export const adjust = duration => testClockWith(testClock => testClock.adjust(duration));
/**
 * Sets the current clock time to the specified `timestamp`. Any effects that
 * were scheduled to occur on or before the new time will be run in order.
 *
 * @example
 * ```ts
 * import { Duration, Effect } from "effect"
 * import { TestClock } from "effect/testing"
 *
 * const program = Effect.gen(function*() {
 *   let executed = false
 *
 *   // Fork an effect that sleeps for 2 hours
 *   const fiber = yield* Effect.gen(function*() {
 *     yield* Effect.sleep("2 hours")
 *     executed = true
 *   }).pipe(Effect.forkChild)
 *
 *   // Set the clock to a specific timestamp (2 hours from epoch)
 *   const targetTime = Duration.toMillis(Duration.hours(2))
 *   yield* TestClock.setTime(targetTime)
 *
 *   // The effect should now be executed
 *   console.log(executed) // true
 * })
 * ```
 *
 * @since 2.0.0
 * @category utils
 */
export const setTime = timestamp => testClockWith(testClock => testClock.setTime(timestamp));
/**
 * Executes the specified effect with the live `Clock` instead of the
 * `TestClock`.
 *
 * @example
 * ```ts
 * import { Clock, Effect } from "effect"
 * import { TestClock } from "effect/testing"
 *
 * const program = Effect.gen(function*() {
 *   // Get the current test time (starts at epoch)
 *   const testTime = yield* Clock.currentTimeMillis
 *   console.log(testTime) // 0
 *
 *   // Get the actual system time using withLive
 *   const realTime = yield* TestClock.withLive(Clock.currentTimeMillis)
 *   console.log(realTime) // Actual system timestamp
 *
 *   // Advance test time
 *   yield* TestClock.adjust("1 hour")
 *
 *   // Test time is now 1 hour ahead
 *   const newTestTime = yield* Clock.currentTimeMillis
 *   console.log(newTestTime) // 3600000 (1 hour in milliseconds)
 * })
 * ```
 *
 * @since 2.0.0
 * @category utils
 */
export const withLive = effect => testClockWith(testClock => testClock.withLive(effect));
const WarningState = /*#__PURE__*/Data.taggedEnum();
//# sourceMappingURL=TestClock.js.map