import * as tracing_1 from "../../Tracing"
import * as tracing_2 from "../../Tracing"
const fileName_1 = "(@effect-ts/system): _src/Testing/TestClock/index.ts"
import { Tagged } from "../../Case"
import * as Clock from "../../Clock"
import * as Chunk from "../../Collections/Immutable/Chunk"
import * as HashMap from "../../Collections/Immutable/HashMap"
import * as List from "../../Collections/Immutable/List"
import * as SortedSet from "../../Collections/Immutable/SortedSet"
import * as Tuple from "../../Collections/Immutable/Tuple"
import * as T from "../../Effect"
import * as Fiber from "../../Fiber"
import { identity, pipe } from "../../Function"
import { tag } from "../../Has"
import * as L from "../../Layer"
import * as M from "../../Managed"
import * as O from "../../Option"
import * as Ord from "../../Ord"
import * as Promise from "../../Promise"
import * as Ref from "../../Ref"
import * as RefM from "../../RefM"
import * as St from "../../Structural"
import { intersect } from "../../Utils"
import { Annotations } from "../Annotations"
import { fiberSet } from "../FiberSet"
import { Live } from "../Live"
import { fibers } from "../TestAnnotation"
export function Duration(n) {
  return n
}
export const TestClock = /*#__PURE__*/ tag()
/**
 * `Data` represents the state of the `TestClock`, including the clock time
 */

export class Data extends Tagged("Data") {}
export class Start extends Tagged("Start") {}
Start.of = /*#__PURE__*/ new Start()
export class Done extends Tagged("Done") {}
Done.of = /*#__PURE__*/ new Done()
export class Pending extends Tagged("Pending") {}

Pending.of = (fiber) =>
  new Pending({
    fiber
  })

export class Test {
  constructor(clockState, live, annotations, warningState) {
    this.clockState = clockState
    this.live = live
    this.annotations = annotations
    this.warningState = warningState
    /**
     * Increments the current clock time by the specified duration. Any
     * effects that were scheduled to occur on or before the new time will be
     * run in order.
     */

    this.adjust = (duration) => {
      return T.zipRight_(
        this.warningDone,
        this.run((_) => Duration(_ + duration)),
        fileName_1 + ":152:23"
      )
    }
    /**
     * Returns the current clock time.
     */

    this.currentTime = T.map_(
      Ref.get(this.clockState),
      (d) => d.duration,
      fileName_1 + ":163:10"
    )
    /**
     * Saves the `TestClock`'s current state in an effect which, when run,
     * will restore the `TestClock` state to the saved state
     */

    this.save = T.map_(
      T.bind_(
        T.do,
        "clockData",
        () => Ref.get(this.clockState),
        fileName_1 + ":172:11"
      ),
      ({ clockData }) => Ref.set_(this.clockState, clockData),
      fileName_1 + ":173:10"
    )
    /**
     * Sets the current clock time to the specified time in terms of duration
     * since the epoch. Any effects that were scheduled to occur on or before
     * the new time will immediately be run in order.
     */

    this.setTime = (dateTime) =>
      T.zipRight_(
        this.warningDone,
        this.run(() => Duration(dateTime)),
        fileName_1 + ":182:38"
      )
    /**
     * Semantically blocks the current fiber until the clock time is equal
     * to or greater than the specified duration. Once the clock time is
     * adjusted to on or after the duration, the fiber will automatically be
     * resumed.
     */

    this.sleep = (duration) =>
      T.map_(
        T.tap_(
          T.bind_(
            T.bind_(T.do, "promise", () => Promise.make(), fileName_1 + ":193:13"),
            "shouldAwait",
            ({ promise }) =>
              Ref.modify_(this.clockState, (data) => {
                const end = Duration(data.duration + duration)

                if (end > data.duration) {
                  return Tuple.tuple(
                    true,
                    data.copy({
                      sleeps: List.prepend(Tuple.tuple(end, promise))(data.sleeps)
                    })
                  )
                } else {
                  return Tuple.tuple(false, data)
                }
              }),
            fileName_1 + ":194:13"
          ),
          ({ promise, shouldAwait }) =>
            shouldAwait
              ? T.zipRight_(
                  this.warningStart,
                  Promise.await(promise),
                  fileName_1 + ":215:47"
                )
              : Promise.succeed_(promise, void 0),
          fileName_1 + ":213:12"
        ),
        () => void 0,
        fileName_1 + ":218:12"
      )
    /**
     * Returns a list of the times at which all queued effects are scheduled
     * to resume.
     */

    this.sleeps = T.map_(
      Ref.get(this.clockState),
      (d) => List.map((_) => _.get(0))(d.sleeps),
      fileName_1 + ":228:10"
    )
    /**
     * The warning message that will be displayed if a test is using time but
     * is not advancing the `TestClock`.
     */

    this.warning =
      "Warning: 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."
    /**
     * Forks a fiber that will display a warning message if a test is using
     * time but is not advancing the `TestClock`.
     */

    this.warningStart = RefM.updateSome((_) => {
      switch (_._tag) {
        case "Start": {
          return O.some(
            T.map_(
              T.bind_(
                T.do,
                "fiber",
                () =>
                  this.live.provide(
                    T.fork(
                      T.interruptible(
                        T.delay_(
                          T.succeedWith(() => {
                            console.log(this.warning)
                          }, fileName_1 + ":259:32"),
                          5000,
                          fileName_1 + ":262:26"
                        ),
                        fileName_1 + ":263:34"
                      ),
                      fileName_1 + ":264:25"
                    )
                  ),
                fileName_1 + ":256:19"
              ),
              ({ fiber }) => Pending.of(fiber),
              fileName_1 + ":268:18"
            )
          )
        }

        default:
          return O.none
      }
    })(this.warningState)
    /**
     * Cancels the warning message that is displayed if a test is using time
     * but is not advancing the `TestClock`.
     */

    this.warningDone = RefM.updateSome((_) => {
      switch (_._tag) {
        case "Start": {
          return O.some(T.succeed(Done.of, fileName_1 + ":287:34"))
        }

        case "Pending": {
          return O.some(
            T.as_(Fiber.interrupt(_.fiber), Done.of, fileName_1 + ":290:53")
          )
        }

        default:
          return O.none
      }
    })(this.warningState)
    /**
     * Returns a set of all fibers in this test.
     */

    this.supervisedFibers = T.descriptorWith(
      (d) =>
        T.chain_(
          this.annotations.get(fibers),
          (fa) => {
            switch (fa._tag) {
              case "Left": {
                return T.succeed(fiberSet, fileName_1 + ":309:29")
              }

              case "Right": {
                return T.map_(
                  T.map_(
                    T.forEach_(
                      fa.right,
                      (ref) => T.succeedWith(() => ref.get, fileName_1 + ":314:47"),
                      fileName_1 + ":314:24"
                    ),
                    Chunk.reduce(fiberSet, SortedSet.union_),
                    fileName_1 + ":315:20"
                  ),
                  SortedSet.filter((_) => !St.equals(_.id, d.id)),
                  fileName_1 + ":316:20"
                )
              }
            }
          },
          fileName_1 + ":306:14"
        ),
      fileName_1 + ":303:23"
    )
    /**
     * Captures a "snapshot" of the identifier and status of all fibers in
     * this test other than the current fiber. Fails with the `Unit` value if
     * any of these fibers are not done or suspended. Note that because we
     * cannot synchronize on the status of multiple fibers at the same time
     * this snapshot may not be fully consistent.
     */

    this.freeze = T.chain_(
      this.supervisedFibers,
      T.reduce(
        HashMap.make(),
        (map, fiber) =>
          T.chain_(
            fiber.status,
            (status) => {
              switch (status._tag) {
                case "Done": {
                  return T.succeed(
                    HashMap.set_(map, fiber.id, status),
                    fileName_1 + ":340:33"
                  )
                }

                case "Suspended": {
                  return T.succeed(
                    HashMap.set_(map, fiber.id, status),
                    fileName_1 + ":343:33"
                  )
                }

                default:
                  return T.fail(void 0, fileName_1 + ":346:30")
              }
            },
            fileName_1 + ":337:18"
          ),
        fileName_1 + ":334:15"
      ),
      fileName_1 + ":333:12"
    )
    /**
     * Delays for a short period of time.
     */

    this.delay = this.live.provide(T.sleep(5, fileName_1 + ":357:45"))
    /**
     * Returns whether all descendants of this fiber are done or suspended.
     */

    this.suspended = T.chain_(
      T.zip_(
        this.freeze,
        T.zipRight_(this.delay, this.freeze, fileName_1 + ":364:38"),
        fileName_1 + ":364:10"
      ),
      ({ tuple: [first, last] }) =>
        St.equals(first, last)
          ? T.succeed(first, fileName_1 + ":366:41")
          : T.fail(void 0, fileName_1 + ":366:57"),
      fileName_1 + ":365:12"
    )
    /**
     * Polls until all descendants of this fiber are done or suspended.
     */

    this.awaitSuspended = T.asUnit(
      T.eventually(
        T.filterOrFail_(
          T.zipWith_(
            this.suspended,
            T.zipRight_(
              this.live.provide(T.sleep(10, fileName_1 + ":376:37")),
              this.suspended,
              fileName_1 + ":376:54"
            ),
            St.equals,
            fileName_1 + ":375:14"
          ),
          identity,
          () => void 0,
          fileName_1 + ":379:19"
        ),
        fileName_1 + ":380:17"
      ),
      fileName_1 + ":381:13"
    )
    /**
     * Runs all effects scheduled to occur on or before the specified
     * duration, which may depend on the current time, in order.
     */

    this.run = (f) =>
      T.zipRight_(
        this.awaitSuspended,
        T.chain_(
          Ref.modify_(this.clockState, (data) => {
            const end = f(data.duration)
            const sorted = List.sortWith_(
              data.sleeps,
              Ord.contramap_(Ord.number, (_) => _.get(0))
            )

            if (!List.isEmpty(sorted)) {
              const {
                tuple: [duration, promise]
              } = List.unsafeFirst(sorted)
              const sleeps = List.tail(sorted)

              if (duration <= end) {
                return Tuple.tuple(
                  O.some(Tuple.tuple(end, promise)),
                  new Data({
                    duration,
                    sleeps
                  })
                )
              }
            }

            return Tuple.tuple(
              O.none,
              new Data({
                duration: end,
                sleeps: data.sleeps
              })
            )
          }),
          (o) => {
            switch (o._tag) {
              case "None": {
                return T.unit
              }

              case "Some": {
                return T.zipRight_(
                  T.zipRight_(
                    Promise.succeed_(o.value.get(1), void 0),
                    T.yieldNow,
                    fileName_1 + ":424:29"
                  ),
                  this.run(() => o.value.get(0)),
                  fileName_1 + ":425:29"
                )
              }
            }
          },
          fileName_1 + ":416:18"
        ),
        fileName_1 + ":391:17"
      )
  }
}
export function live(data) {
  return L.fromRawManaged(
    M.gen(function* (_) {
      const live = yield* _(Live, fileName_1 + ":437:26")
      const annotations = yield* _(Annotations, fileName_1 + ":438:33")
      const ref = yield* _(Ref.makeRef(data), fileName_1 + ":439:25")
      const refM = yield* _(RefM.makeRefM(Start.of), fileName_1 + ":440:26")
      const test = yield* _(
        M.make_(
          T.succeedWith(
            () => new Test(ref, live, annotations, refM),
            fileName_1 + ":443:20"
          ),
          (_) => _.warningDone,
          fileName_1 + ":444:15"
        ),
        fileName_1 + ":442:26"
      )
      const clock = Clock.HasClock.of({
        currentTime: test.currentTime,
        _tag: Clock.ClockSymbol,
        sleep: (ms) => test.sleep(Duration(ms))
      })
      const testClock = TestClock.of(test)
      return intersect(testClock, clock)
    })
  )
}
export const defaultTestClock = /*#__PURE__*/ live(
  /*#__PURE__*/ new Data({
    duration: /*#__PURE__*/ Duration(0),
    sleeps: /*#__PURE__*/ List.empty()
  })
)
//# sourceMappingURL=index.js.map
