"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultTestClock = exports.live = exports.Test = exports.Pending = exports.Done = exports.Start = exports.Data = exports.TestClock = exports.Duration = void 0;
const tracing_1 = __importStar(require("../../Tracing"));
const fileName_1 = "(@effect-ts/system): _src/Testing/TestClock/index.ts";
const Case_1 = require("../../Case");
const Clock = __importStar(require("../../Clock"));
const ClockId = __importStar(require("../../Clock/id"));
const Chunk = __importStar(require("../../Collections/Immutable/Chunk"));
const HashMap = __importStar(require("../../Collections/Immutable/HashMap"));
const List = __importStar(require("../../Collections/Immutable/List"));
const SortedSet = __importStar(require("../../Collections/Immutable/SortedSet"));
const Tuple = __importStar(require("../../Collections/Immutable/Tuple"));
const T = __importStar(require("../../Effect"));
const Fiber = __importStar(require("../../Fiber"));
const Function_1 = require("../../Function");
const Has_1 = require("../../Has");
const L = __importStar(require("../../Layer"));
const M = __importStar(require("../../Managed"));
const O = __importStar(require("../../Option"));
const Ord = __importStar(require("../../Ord"));
const Promise = __importStar(require("../../Promise"));
const Ref = __importStar(require("../../Ref"));
const RefM = __importStar(require("../../RefM"));
const St = __importStar(require("../../Structural"));
const Annotations_1 = require("../Annotations");
const FiberSet_1 = require("../FiberSet");
const Live_1 = require("../Live");
const TestAnnotation_1 = require("../TestAnnotation");
function Duration(n) {
    return n;
}
exports.Duration = Duration;
exports.TestClock = (0, Has_1.tag)(Clock.ClockId);
/**
 * `Data` represents the state of the `TestClock`, including the clock time
 */
class Data extends (0, Case_1.Tagged)("Data") {
}
exports.Data = Data;
class Start extends (0, Case_1.Tagged)("Start") {
}
exports.Start = Start;
Start.of = new Start();
class Done extends (0, Case_1.Tagged)("Done") {
}
exports.Done = Done;
Done.of = new Done();
class Pending extends (0, Case_1.Tagged)("Pending") {
}
exports.Pending = Pending;
Pending.of = (fiber) => new Pending({ fiber });
class Test {
    constructor(clockState, live, annotations, warningState) {
        this.clockState = clockState;
        this.live = live;
        this.annotations = annotations;
        this.warningState = warningState;
        this.serviceId = ClockId.ClockId;
        /**
         * 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 + ":156:23");
        };
        /**
         * Returns the current clock time.
         */
        this.currentTime = (T.map_(Ref.get(this.clockState), (d) => d.duration, fileName_1 + ":167: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 + ":176:11"), ({ clockData }) => Ref.set_(this.clockState, clockData), fileName_1 + ":177: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 + ":186: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 + ":197: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 + ":198:13"), ({ promise, shouldAwait }) => shouldAwait
            ? (T.zipRight_(this.warningStart, Promise.await(promise), fileName_1 + ":219:47")) : Promise.succeed_(promise, void 0), fileName_1 + ":217:12"), () => void 0, fileName_1 + ":222: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 + ":232: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 + ":263:32"), 5000, fileName_1 + ":266:26"), fileName_1 + ":267:34"), fileName_1 + ":268:25"))), fileName_1 + ":260:19"), ({ fiber }) => Pending.of(fiber), fileName_1 + ":272: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 + ":291:34"));
                }
                case "Pending": {
                    return (O.some(T.as_(Fiber.interrupt(_.fiber), Done.of, fileName_1 + ":294: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(TestAnnotation_1.fibers), (fa) => {
            switch (fa._tag) {
                case "Left": {
                    return T.succeed(FiberSet_1.fiberSet, fileName_1 + ":313:29");
                }
                case "Right": {
                    return (T.map_(T.map_(T.forEach_(fa.right, (ref) => T.succeedWith(() => ref.get, fileName_1 + ":318:47"), fileName_1 + ":318:24"), Chunk.reduce(FiberSet_1.fiberSet, SortedSet.union_), fileName_1 + ":319:20"), SortedSet.filter((_) => !St.equals(_.id, d.id)), fileName_1 + ":320:20"));
                }
            }
        }, fileName_1 + ":310:14")), fileName_1 + ":307: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 + ":344:33");
                }
                case "Suspended": {
                    return T.succeed(HashMap.set_(map, fiber.id, status), fileName_1 + ":347:33");
                }
                default:
                    return T.fail(void 0, fileName_1 + ":350:30");
            }
        }, fileName_1 + ":341:18")), fileName_1 + ":338:15"), fileName_1 + ":337:12"));
        /**
         * Delays for a short period of time.
         */
        this.delay = this.live.provide(T.sleep(5, fileName_1 + ":361: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 + ":368:38")), fileName_1 + ":368:10"), ({ tuple: [first, last] }) => St.equals(first, last) ? T.succeed(first, fileName_1 + ":370:41") : T.fail(void 0, fileName_1 + ":370:57"), fileName_1 + ":369: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 + ":380:37")), this.suspended, fileName_1 + ":380:54")), St.equals, fileName_1 + ":379:14"), Function_1.identity, () => void 0, fileName_1 + ":383:19"), fileName_1 + ":384:17"), fileName_1 + ":385: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 + ":428:29"), this.run(() => o.value.get(0)), fileName_1 + ":429:29"));
                }
            }
        }, fileName_1 + ":420:18")), fileName_1 + ":395:17"));
    }
}
exports.Test = Test;
function live(data) {
    return L.fromRawManaged(M.gen(function* (_) {
        const live = yield* _(Live_1.Live, fileName_1 + ":441:26");
        const annotations = yield* _(Annotations_1.Annotations, fileName_1 + ":442:33");
        const ref = yield* _(Ref.makeRef(data), fileName_1 + ":443:25");
        const refM = yield* _(RefM.makeRefM(Start.of), fileName_1 + ":444:26");
        const test = yield* _(M.make_(T.succeedWith(() => new Test(ref, live, annotations, refM), fileName_1 + ":447:20"), (_) => _.warningDone, fileName_1 + ":448:15"), fileName_1 + ":446:26");
        const testClock = exports.TestClock.has(test);
        return testClock;
    }));
}
exports.live = live;
exports.defaultTestClock = live(new Data({
    duration: Duration(0),
    sleeps: List.empty()
}));
//# sourceMappingURL=index.js.map