import { assert, describe, it, vi } from "@effect/vitest"
import { assertExitFailure } from "@effect/vitest/utils"
import {
  Cause,
  Context,
  Data,
  Deferred,
  Duration,
  Effect,
  Exit,
  Fiber,
  type Filter,
  Layer,
  Logger,
  type LogLevel,
  Option,
  References,
  Result,
  Schedule,
  Scope,
  TxRef
} from "effect"
import { constFalse, constTrue, pipe } from "effect/Function"
import { TestClock } from "effect/testing"
import { assertCauseFail } from "./utils/assert.ts"

class ATag extends Context.Service<ATag, "A">()("ATag") {}

const assertExitDefect = <A, E>(exit: Exit.Exit<A, E>, defect: unknown) => {
  assert.isTrue(Exit.hasDies(exit))
  const result = Exit.findDefect(exit)
  assert.isTrue(Result.isSuccess(result))
  if (Result.isSuccess(result)) {
    assert.strictEqual(result.success, defect)
  }
}

const assertUnknownError = <A>(exit: Exit.Exit<A, Cause.UnknownError>, cause: unknown, message: string) => {
  assert.isTrue(Exit.isFailure(exit))
  if (Exit.isFailure(exit)) {
    const result = Cause.findError(exit.cause)
    assert.isTrue(Result.isSuccess(result))
    if (Result.isSuccess(result)) {
      assert.isTrue(Cause.isUnknownError(result.success))
      assert.strictEqual((result.success as Error).cause, cause)
      assert.strictEqual(result.success.message, message)
    }
  }
}

describe("Effect", () => {
  it("isEffect", () => {
    assert.isTrue(Effect.isEffect(Effect.succeed(0)))
    assert.isFalse(Effect.isEffect([0]))
  })

  describe("structural compare", () => {
    it("should pass structural comparison", () => {
      assert.deepEqual(Effect.succeed(0), Effect.succeed(0))
    })
    it("should fail structural comparison", () => {
      assert.notDeepEqual(Effect.succeed(0), Effect.succeed(1))
    })
  })

  describe("tracing", () => {
    it.effect("failCause captures stack frame", () =>
      Effect.gen(function*() {
        const cause = yield* Effect.failCause(Cause.die(new Error("boom"))).pipe(
          Effect.withSpan("test span"),
          Effect.sandbox,
          Effect.flip
        )
        const annotations = Cause.annotations(cause)
        const trace = Context.getUnsafe(annotations, Cause.StackTrace)
        assert.strictEqual(trace.name, "test span")
      }))
  })

  it("callback can branch over sync/async", async () => {
    const program = Effect.callback<number>(function(resume) {
      if (this.executionMode === "sync") {
        resume(Effect.succeed(1))
      } else {
        Promise.resolve().then(() => resume(Effect.succeed(2)))
      }
    })

    const isSync = Effect.runSync(program)
    const isAsync = await Effect.runPromise(program)

    assert.strictEqual(isSync, 1)
    assert.strictEqual(isAsync, 2)
  })

  it("runPromise", async () => {
    const result = await Effect.runPromise(Effect.succeed(1))
    assert.strictEqual(result, 1)
  })

  it("acquireUseRelease interrupt", async () => {
    let acquire = false
    let use = false
    let release = false
    const fiber = Effect.acquireUseRelease(
      Effect.sync(() => {
        acquire = true
        return 123
      }).pipe(Effect.delay(100)),
      () =>
        Effect.sync(() => {
          use = true
        }),
      (_) =>
        Effect.sync(() => {
          assert.strictEqual(_, 123)
          release = true
        })
    ).pipe(Effect.runFork)
    fiber.interruptUnsafe()
    const result = await Effect.runPromise(Fiber.await(fiber))
    assert.deepStrictEqual(result, Exit.failCause(Cause.interrupt()))
    assert.isTrue(acquire)
    assert.isFalse(use)
    assert.isTrue(release)
  })

  it("acquireUseRelease uninterruptible", async () => {
    let acquire = false
    let use = false
    let release = false
    const fiber = Effect.acquireUseRelease(
      Effect.sync(() => {
        acquire = true
        return 123
      }).pipe(Effect.delay(100)),
      (_) =>
        Effect.sync(() => {
          use = true
          return _
        }),
      (_) =>
        Effect.sync(() => {
          assert.strictEqual(_, 123)
          release = true
        })
    ).pipe(Effect.uninterruptible, Effect.runFork)
    fiber.interruptUnsafe()
    const result = await Effect.runPromise(Fiber.await(fiber))
    assert.deepStrictEqual(result, Exit.failCause(Cause.interrupt()))
    assert.isTrue(acquire)
    assert.isTrue(use)
    assert.isTrue(release)
  })

  it("Context.Service", () =>
    ATag.pipe(
      Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A"))),
      Effect.provideService(ATag, "A"),
      Effect.runPromise
    ))

  describe("fromOption", () => {
    it("from a some", () =>
      Option.some("A").pipe(
        Effect.fromOption,
        Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A"))),
        Effect.runPromise
      ))

    it("from a none", () =>
      Option.none().pipe(
        Effect.fromOption,
        Effect.flip,
        Effect.tap((error) => Effect.sync(() => assert.ok(error instanceof Cause.NoSuchElementError))),
        Effect.runPromise
      ))

    it.effect("from a none with a custom error", () =>
      Effect.gen(function*() {
        const error = new Error("Missing value")
        const cause = yield* Effect.fromOption(Option.none(), () => error).pipe(Effect.flip)
        assert.strictEqual(cause, error)
      }))

    it.effect("from a none with a custom error data-last", () =>
      Effect.gen(function*() {
        const error = new Error("Missing value")
        const cause = yield* Option.none().pipe(
          Effect.fromOption(() => error),
          Effect.flip
        )
        assert.strictEqual(cause, error)
      }))
  })

  describe("transposeOption", () => {
    it.effect("transposes a none", () =>
      Effect.gen(function*() {
        const result = yield* Effect.transposeOption(Option.none())
        assert.deepStrictEqual(result, Option.none())
      }))

    it.effect("transposes a some containing a success", () =>
      Effect.gen(function*() {
        const result = yield* Effect.transposeOption(Option.some(Effect.succeed("A")))
        assert.deepStrictEqual(result, Option.some("A"))
      }))

    it.effect("transposes a some containing a failure", () =>
      Effect.gen(function*() {
        const error = yield* Effect.transposeOption(Option.some(Effect.fail("error"))).pipe(Effect.flip)
        assert.strictEqual(error, "error")
      }))
  })

  describe("fromResult", () => {
    it("from a success", () =>
      Result.succeed("A").pipe(
        Effect.fromResult,
        Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A"))),
        Effect.runPromise
      ))

    it("from a failure", () =>
      Result.fail("error").pipe(
        Effect.fromResult,
        Effect.flip,
        Effect.tap((error) => Effect.sync(() => assert.strictEqual(error, "error"))),
        Effect.runPromise
      ))
  })

  describe("try", () => {
    it.effect("succeeds with the returned value in direct-thunk form", () =>
      Effect.gen(function*() {
        const result = yield* Effect.try(() => 1)
        assert.strictEqual(result, 1)
      }))

    it.effect("maps thrown values to UnknownError in direct-thunk form", () =>
      Effect.gen(function*() {
        const thrown = new Error("try")
        const exit = yield* Effect.try<number>(() => {
          throw thrown
        }).pipe(Effect.exit)
        assertUnknownError(exit, thrown, "An error occurred in Effect.try")
      }))

    it.effect("succeeds with the returned value", () =>
      Effect.gen(function*() {
        let catchCalled = false
        const result = yield* Effect.try({
          try: () => 1,
          catch: () => {
            catchCalled = true
            return "error" as const
          }
        })
        assert.strictEqual(result, 1)
        assert.isFalse(catchCalled)
      }))

    it.effect("maps thrown values into typed failures", () =>
      Effect.gen(function*() {
        const thrown = new Error("try")
        const mapped = { cause: thrown }
        const exit = yield* Effect.try({
          try: () => {
            throw thrown
          },
          catch: () => mapped
        }).pipe(Effect.exit)
        assertExitFailure(exit, Cause.fail(mapped))
      }))

    it.effect("turns a throwing catch mapper into a defect", () =>
      Effect.gen(function*() {
        const thrown = new Error("try")
        const defect = new Error("catch")
        const exit = yield* Effect.try({
          try: () => {
            throw thrown
          },
          catch: () => {
            throw defect
          }
        }).pipe(Effect.exit)
        assertExitDefect(exit, defect)
      }))
  })

  describe("tryPromise", () => {
    it.effect("succeeds with the resolved value in direct-thunk form", () =>
      Effect.gen(function*() {
        const result = yield* Effect.tryPromise(() => Promise.resolve(1))
        assert.strictEqual(result, 1)
      }))

    it.effect("does not allocate AbortController for zero-argument thunks", () =>
      Effect.gen(function*() {
        const originalAbortController = globalThis.AbortController
        let allocations = 0
        class TestAbortController extends originalAbortController {
          constructor() {
            allocations += 1
            super()
          }
        }

        yield* Effect.acquireUseRelease(
          Effect.sync(() => {
            globalThis.AbortController = TestAbortController
          }),
          () =>
            Effect.gen(function*() {
              const direct = yield* Effect.tryPromise(() => Promise.resolve(1))
              const options = yield* Effect.tryPromise({
                try: () => Promise.resolve(2),
                catch: () => "error" as const
              })
              assert.strictEqual(direct, 1)
              assert.strictEqual(options, 2)
              assert.strictEqual(allocations, 0)
            }),
          () =>
            Effect.sync(() => {
              globalThis.AbortController = originalAbortController
            })
        )
      }))

    it.effect("maps synchronous throws to UnknownError in direct-thunk form", () =>
      Effect.gen(function*() {
        const thrown = new Error("try")
        const exit = yield* Effect.tryPromise<number>(() => {
          throw thrown
        }).pipe(Effect.exit)
        assertUnknownError(exit, thrown, "An error occurred in Effect.tryPromise")
      }))

    it.effect("maps promise rejections to UnknownError in direct-thunk form", () =>
      Effect.gen(function*() {
        const rejected = new Error("reject")
        const exit = yield* Effect.tryPromise<number>(() => Promise.reject(rejected)).pipe(Effect.exit)
        assertUnknownError(exit, rejected, "An error occurred in Effect.tryPromise")
      }))

    it.effect("succeeds with the resolved value in options form", () =>
      Effect.gen(function*() {
        let catchCalled = false
        const result = yield* Effect.tryPromise({
          try: () => Promise.resolve(1),
          catch: () => {
            catchCalled = true
            return "error" as const
          }
        })
        assert.strictEqual(result, 1)
        assert.isFalse(catchCalled)
      }))

    it.effect("maps synchronous throws with catch in options form", () =>
      Effect.gen(function*() {
        const thrown = new Error("try")
        const mapped = { cause: thrown }
        const exit = yield* Effect.tryPromise({
          try: () => {
            throw thrown
          },
          catch: () => mapped
        }).pipe(Effect.exit)
        assertExitFailure(exit, Cause.fail(mapped))
      }))

    it.effect("maps promise rejections with catch in options form", () =>
      Effect.gen(function*() {
        const rejected = new Error("reject")
        const mapped = { cause: rejected }
        const exit = yield* Effect.tryPromise({
          try: () => Promise.reject(rejected),
          catch: () => mapped
        }).pipe(Effect.exit)
        assertExitFailure(exit, Cause.fail(mapped))
      }))

    it.effect("turns a throwing catch mapper for a synchronous throw into a defect", () =>
      Effect.gen(function*() {
        const thrown = new Error("try")
        const defect = new Error("catch")
        const exit = yield* Effect.tryPromise({
          try: () => {
            throw thrown
          },
          catch: () => {
            throw defect
          }
        }).pipe(Effect.exit)
        assertExitDefect(exit, defect)
      }))

    it.effect("turns a throwing catch mapper for a promise rejection into a defect", () =>
      Effect.gen(function*() {
        const rejected = new Error("reject")
        const defect = new Error("catch")
        const exit = yield* Effect.tryPromise({
          try: () => Promise.reject(rejected),
          catch: () => {
            throw defect
          }
        }).pipe(Effect.exit)
        assertExitDefect(exit, defect)
      }))

    it.effect("aborts the provided AbortSignal on interruption", () =>
      Effect.gen(function*() {
        let signal: AbortSignal | undefined
        const fiber = yield* Effect.tryPromise((signal_) => {
          signal = signal_
          return new Promise<never>(() => {})
        }).pipe(Effect.forkChild({ startImmediately: true }))
        yield* Fiber.interrupt(fiber)
        assert.strictEqual(signal?.aborted, true)
      }))
  })

  describe("gen", () => {
    it("gen", () =>
      Effect.gen(function*() {
        const result = yield* Effect.succeed(1)
        assert.strictEqual(result, 1)
        return result
      }).pipe(Effect.runPromise).then((_) => assert.deepStrictEqual(_, 1)))

    it("gen with context", () =>
      Effect.gen({ self: { a: 1, b: 2 } }, function*() {
        const result = yield* Effect.succeed(this.a)
        assert.strictEqual(result, 1)
        return result + this.b
      }).pipe(Effect.runPromise).then((_) => assert.deepStrictEqual(_, 3)))
  })

  describe("forEach", () => {
    it("sequential", () =>
      Effect.gen(function*() {
        const results = yield* Effect.forEach([1, 2, 3], (_) => Effect.succeed(_))
        assert.deepStrictEqual(results, [1, 2, 3])
      }).pipe(Effect.runPromise))

    it("unbounded", () =>
      Effect.gen(function*() {
        const results = yield* Effect.forEach([1, 2, 3], (_) => Effect.succeed(_), { concurrency: "unbounded" })
        assert.deepStrictEqual(results, [1, 2, 3])
      }).pipe(Effect.runPromise))

    it("bounded", () =>
      Effect.gen(function*() {
        const results = yield* Effect.forEach([1, 2, 3, 4, 5], (_) => Effect.succeed(_), { concurrency: 2 })
        assert.deepStrictEqual(results, [1, 2, 3, 4, 5])
      }).pipe(Effect.runPromise))

    it.effect("inherit unbounded", () =>
      Effect.gen(function*() {
        const handle = yield* Effect.forEach([1, 2, 3], (_) => Effect.succeed(_).pipe(Effect.delay(50)), {
          concurrency: "inherit"
        }).pipe(
          Effect.withConcurrency("unbounded"),
          Effect.forkChild
        )
        yield* TestClock.adjust(90)
        assert.deepStrictEqual(handle.pollUnsafe(), Exit.succeed([1, 2, 3]))
      }))

    it.effect("sequential interrupt", () =>
      Effect.gen(function*() {
        const done: Array<number> = []
        const fiber = yield* Effect.forEach([1, 2, 3, 4, 5, 6], (i) =>
          Effect.sync(() => {
            done.push(i)
            return i
          }).pipe(Effect.delay(300))).pipe(Effect.forkChild)
        yield* TestClock.adjust(800)
        yield* Fiber.interrupt(fiber)
        const result = yield* Fiber.await(fiber)
        assert.isTrue(Exit.hasInterrupts(result))
        assert.deepStrictEqual(done, [1, 2])
      }))

    it.effect("unbounded interrupt", () =>
      Effect.gen(function*() {
        const done: Array<number> = []
        const fiber = yield* Effect.forEach([1, 2, 3], (i) =>
          Effect.sync(() => {
            done.push(i)
            return i
          }).pipe(Effect.delay(150)), { concurrency: "unbounded" }).pipe(Effect.forkChild)
        yield* TestClock.adjust(50)
        yield* Fiber.interrupt(fiber)
        const result = yield* Fiber.await(fiber)
        assert.isTrue(Exit.hasInterrupts(result))
        assert.deepStrictEqual(done, [])
      }))

    it.effect("bounded interrupt", () =>
      Effect.gen(function*() {
        const done: Array<number> = []
        const fiber = yield* Effect.forEach([1, 2, 3, 4, 5, 6], (i) =>
          Effect.sync(() => {
            done.push(i)
            return i
          }).pipe(Effect.delay(200)), { concurrency: 2 }).pipe(Effect.forkChild)
        yield* TestClock.adjust(350)
        yield* Fiber.interrupt(fiber)
        const result = yield* Fiber.await(fiber)
        assert.isTrue(Exit.hasInterrupts(result))
        assert.deepStrictEqual(done, [1, 2])
      }))

    it.effect("unbounded fail", () =>
      Effect.gen(function*() {
        const done: Array<number> = []
        const handle = yield* Effect.forEach([1, 2, 3, 4, 5], (i) =>
          Effect.suspend(() => {
            done.push(i)
            return i === 3 ? Effect.fail("error") : Effect.succeed(i)
          }).pipe(Effect.delay(i * 100)), {
          concurrency: "unbounded"
        }).pipe(Effect.forkChild)
        yield* TestClock.adjust(500)
        const result = yield* Fiber.await(handle)
        assert.deepStrictEqual(result, Exit.fail("error"))
        assert.deepStrictEqual(done, [1, 2, 3])
      }))

    it("length = 0", () =>
      Effect.gen(function*() {
        const results = yield* Effect.forEach([], (_) => Effect.succeed(_))
        assert.deepStrictEqual(results, [])
      }).pipe(Effect.runPromise))

    it("string", () =>
      Effect.gen(function*() {
        const results = yield* Effect.forEach("abc", (_) => Effect.succeed(_))
        assert.deepStrictEqual(results, ["a", "b", "c"])
      }).pipe(Effect.runPromise))
  })

  describe("all", () => {
    it("tuple", () =>
      Effect.gen(function*() {
        const results = (yield* Effect.all([
          Effect.succeed(1),
          Effect.succeed(2),
          Effect.succeed(3)
        ])) satisfies [
          number,
          number,
          number
        ]
        assert.deepStrictEqual(results, [1, 2, 3])
      }).pipe(Effect.runPromise))

    it("record", () =>
      Effect.gen(function*() {
        const results = (yield* Effect.all({
          a: Effect.succeed(1),
          b: Effect.succeed("2"),
          c: Effect.succeed(true)
        })) satisfies {
          a: number
          b: string
          c: boolean
        }
        assert.deepStrictEqual(results, {
          a: 1,
          b: "2",
          c: true
        })
      }).pipe(Effect.runPromise))

    it.effect("record discard", () =>
      Effect.gen(function*() {
        const results = (yield* Effect.all({
          a: Effect.succeed(1),
          b: Effect.succeed("2"),
          c: Effect.succeed(true)
        }, { discard: true })) satisfies void
        assert.deepStrictEqual(results, void 0)
      }))

    it.effect("iterable", () =>
      Effect.gen(function*() {
        const results = (yield* Effect.all(
          new Set([
            Effect.succeed(1),
            Effect.succeed(2),
            Effect.succeed(3)
          ])
        )) satisfies Array<number>
        assert.deepStrictEqual(results, [1, 2, 3])
      }))

    it.effect("tuple result mode", () =>
      Effect.gen(function*() {
        const executed: Array<number> = []
        const results = (yield* Effect.all(
          [
            Effect.sync(() => {
              executed.push(1)
              return 1
            }),
            Effect.sync(() => {
              executed.push(2)
            }).pipe(Effect.andThen(Effect.fail("boom"))),
            Effect.sync(() => {
              executed.push(3)
              return 3
            })
          ] as const,
          { mode: "result" }
        )) satisfies [
          Result.Result<number, never>,
          Result.Result<never, string>,
          Result.Result<number, never>
        ]
        assert.deepStrictEqual(executed, [1, 2, 3])
        assert.deepStrictEqual(results, [
          Result.succeed(1),
          Result.fail("boom"),
          Result.succeed(3)
        ])
      }))

    it.effect("record result mode", () =>
      Effect.gen(function*() {
        const results = (yield* Effect.all({
          a: Effect.succeed(1),
          b: Effect.fail("boom"),
          c: Effect.succeed(true)
        }, { mode: "result" })) satisfies {
          a: Result.Result<number, never>
          b: Result.Result<never, string>
          c: Result.Result<boolean, never>
        }
        assert.deepStrictEqual(results, {
          a: Result.succeed(1),
          b: Result.fail("boom"),
          c: Result.succeed(true)
        })
      }))
  })

  describe("partition", () => {
    it.effect("collects only successes", () =>
      Effect.gen(function*() {
        const values = [0, 1, 2, 3, 4]
        const [excluded, satisfying] = yield* Effect.partition(values, Effect.succeed)
        assert.deepStrictEqual(excluded, [])
        assert.deepStrictEqual(satisfying, values)
      }))

    it.effect("collects only failures", () =>
      Effect.gen(function*() {
        const values = [0, 1, 2, 3, 4]
        const [excluded, satisfying] = yield* Effect.partition(values, Effect.fail)
        assert.deepStrictEqual(excluded, values)
        assert.deepStrictEqual(satisfying, [])
      }))

    it.effect("collects failures and successes", () =>
      Effect.gen(function*() {
        const values = [0, 1, 2, 3, 4, 5]
        const [excluded, satisfying] = yield* Effect.partition(values, (n) =>
          n % 2 === 0 ? Effect.fail(n) : Effect.succeed(n))
        assert.deepStrictEqual(excluded, [0, 2, 4])
        assert.deepStrictEqual(satisfying, [1, 3, 5])
      }))

    it.effect("supports concurrency option", () =>
      Effect.gen(function*() {
        const values = [0, 1, 2, 3, 4, 5]
        const [excluded, satisfying] = yield* Effect.partition(
          values,
          (n) => n % 2 === 0 ? Effect.fail(n) : Effect.succeed(n),
          { concurrency: "unbounded" }
        )
        assert.deepStrictEqual(excluded, [0, 2, 4])
        assert.deepStrictEqual(satisfying, [1, 3, 5])
      }))
  })

  describe("validate", () => {
    it.effect("collects successes when all effects succeed", () =>
      Effect.gen(function*() {
        const values = [0, 1, 2, 3, 4]
        const satisfying = yield* Effect.validate(values, Effect.succeed)
        assert.deepStrictEqual(satisfying, values)
      }))

    it.effect("accumulates all failures", () =>
      Effect.gen(function*() {
        const values = [0, 1, 2, 3, 4, 5]
        const errors = yield* Effect.validate(values, (n) => n % 2 === 0 ? Effect.fail(n) : Effect.succeed(n)).pipe(
          Effect.flip
        )
        assert.deepStrictEqual(errors, [0, 2, 4])
      }))

    it.effect("supports discard option", () =>
      Effect.gen(function*() {
        const result = yield* Effect.validate([1, 2, 3], Effect.succeed, { discard: true })
        assert.strictEqual(result, undefined)
      }))
  })

  describe("findFirst", () => {
    it.effect("returns first matching element", () =>
      Effect.gen(function*() {
        const result = yield* Effect.findFirst([1, 2, 3, 4], (n) => Effect.succeed(n > 2))
        assert.deepStrictEqual(result, Option.some(3))
      }))

    it.effect("returns none when no element matches", () =>
      Effect.gen(function*() {
        const result = yield* Effect.findFirst([1, 2, 3, 4], (n) => Effect.succeed(n > 10))
        assert.deepStrictEqual(result, Option.none())
      }))

    it.effect("short-circuits on first match", () =>
      Effect.gen(function*() {
        const seen: Array<number> = []
        const result = yield* Effect.findFirst([1, 2, 3, 4], (n) =>
          Effect.sync(() => {
            seen.push(n)
            return n > 2
          }))
        assert.deepStrictEqual(result, Option.some(3))
        assert.deepStrictEqual(seen, [1, 2, 3])
      }))
  })

  describe("findFirstFilter", () => {
    it.effect("returns first successful transformed value", () =>
      Effect.gen(function*() {
        const result = yield* Effect.findFirstFilter([1, 2, 3, 4], (n) =>
          Effect.succeed(n % 2 === 0 ? Result.succeed(`n=${n}`) : Result.failVoid))
        assert.deepStrictEqual(result, Option.some("n=2"))
      }))

    it.effect("passes index to filter", () =>
      Effect.gen(function*() {
        const result = yield* Effect.findFirstFilter([10, 11, 12], (n, i) =>
          Effect.succeed(i === 2 ? Result.succeed(`${n}:${i}`) : Result.failVoid))
        assert.deepStrictEqual(result, Option.some("12:2"))
      }))

    it.effect("returns none when filter never succeeds", () =>
      Effect.gen(function*() {
        const result = yield* Effect.findFirstFilter([1, 2, 3], () => Effect.succeed(Result.failVoid))
        assert.deepStrictEqual(result, Option.none())
      }))
  })

  describe("filter", () => {
    it.live("odd numbers", () =>
      Effect.gen(function*() {
        const results = yield* Effect.filter([1, 2, 3, 4, 5], (value) => Effect.succeed(value % 2 === 1))
        assert.deepStrictEqual(results, [1, 3, 5])
      }))

    it.live("iterable", () =>
      Effect.gen(function*() {
        const results = yield* Effect.filter(new Set([1, 2, 3, 4, 5]), (value) => Effect.succeed(value % 2 === 1))
        assert.deepStrictEqual(results, [1, 3, 5])
      }))
  })

  describe("acquireDisposable", () => {
    it.effect("releases disposables", ({ expect }) =>
      Effect.gen(function*() {
        const acquire = Effect.sync((): Disposable => ({ [Symbol.dispose]: release }))
        const release = vi.fn(() => void 0)

        yield* Effect.scoped(Effect.acquireDisposable(acquire))
        expect(release).toHaveBeenCalledTimes(1)
      }))

    it.effect("releases async disposables", ({ expect }) =>
      Effect.gen(function*() {
        const acquire = Effect.sync((): AsyncDisposable => ({ [Symbol.asyncDispose]: release }))
        const release = vi.fn(async () => void 0)

        yield* Effect.scoped(Effect.acquireDisposable(acquire))
        expect(release).toHaveBeenCalledTimes(1)
      }))
  })

  describe("acquireRelease", () => {
    it("releases on interrupt", () =>
      Effect.gen(function*() {
        let release = false
        const fiber = yield* Effect.acquireRelease(
          Effect.delay(Effect.succeed("foo"), 100),
          () =>
            Effect.sync(() => {
              release = true
            })
        ).pipe(
          Effect.scoped,
          Effect.forkChild({ startImmediately: true })
        )
        fiber.interruptUnsafe()
        yield* Fiber.await(fiber)
        assert.strictEqual(release, true)
      }).pipe(Effect.runPromise))

    it.effect("supports release dependencies", () =>
      Effect.gen(function*() {
        let release = false
        const scope = yield* Scope.make()
        yield* Scope.provide(scope)(
          Effect.provideService(
            Effect.acquireRelease(
              Effect.succeed("foo"),
              () =>
                Effect.flatMap(Effect.service(ATag), () =>
                  Effect.sync(() => {
                    release = true
                  }))
            ),
            ATag,
            "A"
          )
        )
        yield* Scope.close(scope, Exit.void)
        assert.strictEqual(release, true)
      }))
  })

  it.effect("raceAll", () =>
    Effect.gen(function*() {
      const interrupted: Array<number> = []
      const fiber = yield* Effect.raceAll([500, 300, 200, 0, 100].map((ms) =>
        (ms === 0 ? Effect.fail("boom") : Effect.succeed(ms)).pipe(
          Effect.delay(ms),
          Effect.onInterrupt(() =>
            Effect.sync(() => {
              interrupted.push(ms)
            })
          )
        )
      )).pipe(Effect.forkChild)
      yield* TestClock.adjust("500 millis")
      const result = yield* Fiber.join(fiber)
      assert.strictEqual(result, 100)
      assert.deepStrictEqual(interrupted, [500, 300, 200])
    }))

  it.effect("raceAllFirst", () =>
    Effect.gen(function*() {
      const interrupted: Array<number> = []
      const fiber = yield* Effect.raceAllFirst([500, 300, 200, 0, 100].map((ms) =>
        (ms === 0 ? Effect.fail("boom") : Effect.succeed(ms)).pipe(
          Effect.delay(ms),
          Effect.onInterrupt(() =>
            Effect.sync(() => {
              interrupted.push(ms)
            })
          )
        )
      )).pipe(Effect.exit, Effect.forkChild)
      yield* TestClock.adjust("500 millis")
      const result = yield* Fiber.join(fiber)
      assert.deepStrictEqual(result, Exit.fail("boom"))
      // 100 doesn't start because 0 finishes the race first
      assert.deepStrictEqual(interrupted, [500, 300, 200])
    }))

  describe("repeat", () => {
    it.effect("is interruptible", () =>
      Effect.gen(function*() {
        const fiber = yield* Effect.void.pipe(
          Effect.forever,
          Effect.timeoutOption(50),
          Effect.forkChild
        )
        yield* TestClock.adjust(50)
        const result = yield* Fiber.join(fiber)
        assert.deepStrictEqual(result, Option.none())
      }))

    it.effect("repeat/until - repeats until a condition is true", () =>
      Effect.gen(function*() {
        let input = 10
        let output = 0
        const decrement = Effect.sync(() => --input)
        const increment = Effect.sync(() => output++)
        const result = yield* decrement.pipe(
          Effect.tap(increment),
          Effect.repeat({ until: (n) => n === 0 })
        )
        assert.strictEqual(result, 0)
        assert.strictEqual(output, 10)
      }))

    it.effect("repeat/until - repeats until an effectful condition is true", () =>
      Effect.gen(function*() {
        let input = 10
        let output = 0
        const decrement = Effect.sync(() => --input)
        const increment = Effect.sync(() => output++)
        const result = yield* decrement.pipe(
          Effect.tap(increment),
          Effect.repeat({ until: (n) => Effect.succeed(n === 0) })
        )
        assert.strictEqual(result, 0)
        assert.strictEqual(output, 10)
      }))

    it.effect("repeat/until - always evaluates at least once", () =>
      Effect.gen(function*() {
        let n = 0
        const increment = Effect.sync(() => n++)
        yield* Effect.repeat(increment, { until: constTrue })
        assert.strictEqual(n, 1)
      }))

    it.effect("repeat/while - repeats while a condition is true", () =>
      Effect.gen(function*() {
        let input = 10
        let output = 0
        const decrement = Effect.sync(() => --input)
        const increment = Effect.sync(() => output++)
        const result = yield* decrement.pipe(
          Effect.tap(increment),
          Effect.repeat({ while: (n) => n > 0 })
        )
        assert.strictEqual(result, 0)
        assert.strictEqual(output, 10)
      }))

    it.effect("repeat/while - repeats while an effectful condition is true", () =>
      Effect.gen(function*() {
        let input = 10
        let output = 0
        const decrement = Effect.sync(() => --input)
        const increment = Effect.sync(() => output++)
        const result = yield* decrement.pipe(
          Effect.tap(increment),
          Effect.repeat({ while: (n) => Effect.succeed(n > 0) })
        )
        assert.strictEqual(result, 0)
        assert.strictEqual(output, 10)
      }))

    it.effect("repeat/while - always evaluates at least once", () =>
      Effect.gen(function*() {
        let n = 0
        const increment = Effect.sync(() => n++)
        yield* Effect.repeat(increment, { while: constFalse })
        assert.strictEqual(n, 1)
      }))

    it.effect("repeat/times", () =>
      Effect.gen(function*() {
        let n = 0
        const increment = Effect.sync(() => ++n)
        const result = yield* Effect.repeat(increment, {
          times: 2
        })
        assert.strictEqual(n, 3)
        assert.strictEqual(result, 3)
      }))

    it.effect("repeat/schedule - repeats according to the specified schedule", () =>
      Effect.gen(function*() {
        let n = 0
        const increment = Effect.sync(() => ++n)
        const result = yield* Effect.repeat(increment, Schedule.recurs(3))
        assert.strictEqual(result, 3)
      }))

    it.effect("repeat/schedule - with until", () =>
      Effect.gen(function*() {
        let n = 0
        const increment = Effect.sync(() => ++n)
        const result = yield* Effect.repeat(increment, {
          schedule: Schedule.recurs(3),
          until: (n) => n === 3
        })
        assert.strictEqual(n, 3)
        assert.strictEqual(result, 3)
      }))

    it.effect("repeat/schedule - with while", () =>
      Effect.gen(function*() {
        let n = 0
        const increment = Effect.sync(() => ++n)
        const result = yield* Effect.repeat(increment, {
          schedule: Schedule.recurs(3),
          while: (n) => n < 3
        })
        assert.strictEqual(n, 3)
        assert.strictEqual(result, 3) // schedule result
      }))
  })

  describe("retry", () => {
    it.live("nothing on success", () =>
      Effect.gen(function*() {
        let count = 0
        yield* Effect.sync(() => count++).pipe(
          Effect.retry({ times: 10000 })
        )
        assert.strictEqual(count, 1)
      }))

    it.effect("retry/until - retries until a condition is true", () =>
      Effect.gen(function*() {
        let input = 10
        let output = 0
        const decrement = Effect.sync(() => --input)
        const increment = Effect.sync(() => output++)
        const result = yield* decrement.pipe(
          Effect.tap(increment),
          Effect.flip,
          Effect.retry({ until: (n) => n === 0 }),
          Effect.flip
        )
        assert.strictEqual(result, 0)
        assert.strictEqual(output, 10)
      }))

    it.effect("retry/until - retries until an effectful condition is true", () =>
      Effect.gen(function*() {
        let input = 10
        let output = 0
        const decrement = Effect.sync(() => --input)
        const increment = Effect.sync(() => output++)
        const result = yield* decrement.pipe(
          Effect.tap(increment),
          Effect.flip,
          Effect.retry({ until: (n) => Effect.succeed(n === 0) }),
          Effect.flip
        )
        assert.strictEqual(result, 0)
        assert.strictEqual(output, 10)
      }))

    it.effect("retry/until - always evaluates at least once", () =>
      Effect.gen(function*() {
        let n = 0
        const increment = Effect.failSync(() => n++)
        yield* increment.pipe(
          Effect.retry({ until: constTrue }),
          Effect.flip
        )
        assert.strictEqual(n, 1)
      }))

    it.effect("retry/while - retries while a condition is true", () =>
      Effect.gen(function*() {
        let input = 10
        let output = 0
        const decrement = Effect.sync(() => --input)
        const increment = Effect.sync(() => output++)
        const result = yield* decrement.pipe(
          Effect.tap(increment),
          Effect.flip,
          Effect.retry({ while: (n) => n > 0 }),
          Effect.flip
        )
        assert.strictEqual(result, 0)
        assert.strictEqual(output, 10)
      }))

    it.effect("retry/while - retries while an effectful condition is true", () =>
      Effect.gen(function*() {
        let input = 10
        let output = 0
        const decrement = Effect.sync(() => --input)
        const increment = Effect.sync(() => output++)
        const result = yield* decrement.pipe(
          Effect.tap(increment),
          Effect.flip,
          Effect.retry({ while: (n) => Effect.succeed(n > 0) }),
          Effect.flip
        )
        assert.strictEqual(result, 0)
        assert.strictEqual(output, 10)
      }))

    it.effect("retry/while - always evaluates at least once", () =>
      Effect.gen(function*() {
        let n = 0
        const increment = Effect.failSync(() => n++)
        yield* increment.pipe(
          Effect.retry({ while: constFalse }),
          Effect.flip
        )
        assert.strictEqual(n, 1)
      }))

    it.effect("retry/schedule - retries according to the specified schedule", () =>
      Effect.gen(function*() {
        let n = 0
        const increment = Effect.failSync(() => n++)
        yield* increment.pipe(
          Effect.retry(Schedule.recurs(3)),
          Effect.flip
        )
        assert.strictEqual(n, 4)
      }))

    it.effect("retry/schedule - with until", () =>
      Effect.gen(function*() {
        let n = 0
        const increment = Effect.failSync(() => ++n)
        yield* increment.pipe(
          Effect.retry({
            schedule: Schedule.recurs(3),
            until: (n) => n === 3
          }),
          Effect.flip
        )
        assert.strictEqual(n, 3)
      }))

    it.effect("retry/schedule - until errors", () =>
      Effect.gen(function*() {
        let n = 0
        const increment = Effect.failSync(() => ++n)
        const result = yield* increment.pipe(
          Effect.retry({
            schedule: Schedule.recurs(3),
            until: () => Effect.fail("boom")
          }),
          Effect.flip
        )
        assert.strictEqual(n, 1)
        assert.strictEqual(result, "boom")
      }))

    it.effect("retry/schedule - with while", () =>
      Effect.gen(function*() {
        let n = 0
        const increment = Effect.failSync(() => ++n)
        yield* increment.pipe(
          Effect.retry({
            schedule: Schedule.recurs(3),
            while: (n) => n < 3
          }),
          Effect.flip
        )
        assert.strictEqual(n, 3)
      }))

    it.effect("retry/schedule - while errors", () =>
      Effect.gen(function*() {
        let n = 0
        const increment = Effect.failSync(() => ++n)
        const result = yield* increment.pipe(
          Effect.retry({
            schedule: Schedule.recurs(3),
            while: () => Effect.fail("boom")
          }),
          Effect.flip
        )
        assert.strictEqual(n, 1)
        assert.strictEqual(result, "boom")
      }))

    it.effect("retry/schedule - CurrentMetadata", () =>
      Effect.gen(function*() {
        const metadata: Array<Schedule.Metadata> = []
        yield* pipe(
          Effect.gen(function*() {
            const meta = yield* Schedule.CurrentMetadata
            metadata.push(meta)
          }),
          Effect.flip,
          Effect.retry(Schedule.recurs(3)),
          Effect.flip
        )
        assert.deepStrictEqual(metadata, [
          {
            elapsed: 0,
            elapsedSincePrevious: 0,
            attempt: 0,
            input: undefined,
            output: undefined,
            now: 0,
            start: 0,
            duration: Duration.zero
          },
          {
            elapsed: 0,
            elapsedSincePrevious: 0,
            attempt: 1,
            input: undefined,
            output: 0,
            now: 0,
            start: 0,
            duration: Duration.zero
          },
          {
            elapsed: 0,
            elapsedSincePrevious: 0,
            attempt: 2,
            input: undefined,
            output: 1,
            now: 0,
            start: 0,
            duration: Duration.zero
          },
          {
            elapsed: 0,
            elapsedSincePrevious: 0,
            attempt: 3,
            input: undefined,
            output: 2,
            now: 0,
            start: 0,
            duration: Duration.zero
          }
        ])
      }))
  })

  describe("timeoutOption", () => {
    it.live("timeout a long computation", () =>
      Effect.gen(function*() {
        const result = yield* pipe(
          Effect.sleep(60_000),
          Effect.andThen(Effect.succeed(true)),
          Effect.timeoutOption(10)
        )
        assert.deepStrictEqual(result, Option.none())
      }))
    it.live("timeout a long computation with a failure", () =>
      Effect.gen(function*() {
        const error = new Error("boom")
        const result = yield* pipe(
          Effect.sleep(5000),
          Effect.andThen(Effect.succeed(true)),
          Effect.timeoutOrElse({
            orElse: () => Effect.die(error),
            duration: 10
          }),
          Effect.sandbox,
          Effect.flip
        )
        assert.deepStrictEqual(result, Cause.die(error))
      }))
    it.effect("timeout repetition of uninterruptible effect", () =>
      Effect.gen(function*() {
        const fiber = yield* pipe(
          Effect.void,
          Effect.uninterruptible,
          Effect.forever,
          Effect.timeoutOption(10),
          Effect.forkChild
        )
        yield* TestClock.adjust(10)
        const result = yield* Fiber.join(fiber)
        assert.deepStrictEqual(result, Option.none())
      }))
    it.effect("timeout in uninterruptible region", () =>
      Effect.void.pipe(
        Effect.timeoutOption(20_000),
        Effect.uninterruptible
      ), { timeout: 1000 })
  })

  describe("timeout", () => {
    it.live("timeout a long computation", () =>
      Effect.gen(function*() {
        const result = yield* pipe(
          Effect.sleep(60_000),
          Effect.andThen(Effect.succeed(true)),
          Effect.timeout(10),
          Effect.flip
        )
        assert.deepStrictEqual(result, new Cause.TimeoutError())
      }))
  })

  describe("interruption", () => {
    it.effect("sync forever is interruptible", () =>
      Effect.gen(function*() {
        const fiber = yield* pipe(Effect.succeed(1), Effect.forever, Effect.forkChild)
        yield* Fiber.interrupt(fiber)
        assert(Exit.hasInterrupts(fiber.pollUnsafe()!))
      }))

    it.effect("interrupt of never is interrupted with cause", () =>
      Effect.gen(function*() {
        const fiber = yield* Effect.forkChild(Effect.never)
        yield* Fiber.interrupt(fiber)
        assert(Exit.hasInterrupts(fiber.pollUnsafe()!))
      }))

    it.effect("catch + ensuring + interrupt", () =>
      Effect.gen(function*() {
        let catchFailure = false
        let ensuring = false
        const handle = yield* Effect.never.pipe(
          Effect.catchCause((_) =>
            Effect.sync(() => {
              catchFailure = true
            })
          ),
          Effect.ensuring(Effect.sync(() => {
            ensuring = true
          })),
          Effect.forkChild({ startImmediately: true })
        )
        yield* Fiber.interrupt(handle)
        assert.isFalse(catchFailure)
        assert.isTrue(ensuring)
      }))

    it.effect("run of interruptible", () =>
      Effect.gen(function*() {
        let recovered = false
        const fiber = yield* Effect.never.pipe(
          Effect.interruptible,
          Effect.exit,
          Effect.flatMap((result) =>
            Effect.sync(() => {
              recovered = result._tag === "Failure" && Cause.hasInterruptsOnly(result.cause)
            })
          ),
          Effect.uninterruptible,
          Effect.forkChild({ startImmediately: true })
        )
        yield* Fiber.interrupt(fiber)
        assert.isTrue(recovered)
      }))

    it.effect("alternating interruptibility", () =>
      Effect.gen(function*() {
        let counter = 0
        const fiber = yield* Effect.never.pipe(
          Effect.interruptible,
          Effect.exit,
          Effect.andThen(Effect.sync(() => {
            counter++
          })),
          Effect.uninterruptible,
          Effect.interruptible,
          Effect.exit,
          Effect.andThen(Effect.sync(() => {
            counter++
          })),
          Effect.uninterruptible,
          Effect.forkChild({ startImmediately: true })
        )
        yield* Fiber.interrupt(fiber)
        assert.strictEqual(counter, 2)
      }))

    it.live("acquireUseRelease use inherits interrupt status", () =>
      Effect.gen(function*() {
        let ref = false
        const fiber = yield* Effect.acquireUseRelease(
          Effect.succeed(123),
          (_) =>
            Effect.sync(() => {
              ref = true
            }).pipe(
              Effect.delay(10)
            ),
          () => Effect.void
        ).pipe(
          Effect.uninterruptible,
          Effect.forkChild({ startImmediately: true })
        )
        yield* Fiber.interrupt(fiber)
        assert.isTrue(ref)
      }))

    it.live("async can be uninterruptible", () =>
      Effect.gen(function*() {
        let ref = false
        const fiber = yield* Effect.sleep(10).pipe(
          Effect.andThen(Effect.sync(() => {
            ref = true
          })),
          Effect.uninterruptible,
          Effect.forkChild({ startImmediately: true })
        )
        yield* Fiber.interrupt(fiber)
        assert.isTrue(ref)
      }))

    it.live("callback cannot resume on interrupt", () =>
      Effect.gen(function*() {
        const fiber = yield* Effect.callback<string>((resume) => {
          setTimeout(() => {
            resume(Effect.succeed("foo"))
          }, 10)
        }).pipe(
          Effect.onInterrupt(() => Effect.sleep(30)),
          Effect.forkChild({ startImmediately: true })
        )
        yield* Fiber.interrupt(fiber)
        assert.isTrue(Exit.hasInterrupts(fiber.pollUnsafe()!))
      }))

    it.live("closing scope is uninterruptible", () =>
      Effect.gen(function*() {
        let ref = false
        const child = pipe(
          Effect.sleep(10),
          Effect.andThen(Effect.sync(() => {
            ref = true
          }))
        )
        const fiber = yield* child.pipe(Effect.uninterruptible, Effect.forkChild({ startImmediately: true }))
        yield* Fiber.interrupt(fiber)
        assert.isTrue(ref)
      }))

    it.effect("AbortSignal is aborted", () =>
      Effect.gen(function*() {
        let signal: AbortSignal
        const fiber = yield* Effect.callback<void>((_cb, signal_) => {
          signal = signal_
        }).pipe(Effect.forkChild({ startImmediately: true }))
        yield* Fiber.interrupt(fiber)
        assert.strictEqual(signal!.aborted, true)
      }))
  })

  describe("awaitAllChildren", () => {
    it.effect("awaits children forked by the wrapped effect", () =>
      Effect.gen(function*() {
        const latch = yield* Deferred.make<void>()
        const fiber = yield* Effect.gen(function*() {
          yield* Deferred.await(latch).pipe(Effect.forkChild)
          return 1
        }).pipe(
          Effect.awaitAllChildren,
          Effect.forkChild({ startImmediately: true })
        )
        yield* Effect.yieldNow
        assert.strictEqual(fiber.pollUnsafe(), undefined)
        yield* Deferred.succeed(latch, void 0)
        const result = yield* Fiber.join(fiber)
        assert.strictEqual(result, 1)
      }))

    it.effect("does not await children forked outside the wrapped effect", () =>
      Effect.gen(function*() {
        const preexisting = yield* Effect.never.pipe(Effect.forkChild({ startImmediately: true }))
        const fiber = yield* Effect.succeed(1).pipe(
          Effect.awaitAllChildren,
          Effect.forkChild({ startImmediately: true })
        )
        yield* Effect.yieldNow
        assert.deepStrictEqual(fiber.pollUnsafe(), Exit.succeed(1))
        yield* Fiber.interrupt(preexisting)
      }))

    it.effect("does not await preexisting children in the same fiber", () =>
      Effect.gen(function*() {
        const preexistingLatch = yield* Deferred.make<void>()
        const scopedLatch = yield* Deferred.make<void>()
        const fiber = yield* Effect.gen(function*() {
          yield* Deferred.await(preexistingLatch).pipe(Effect.forkChild)
          return yield* Effect.gen(function*() {
            yield* Deferred.await(scopedLatch).pipe(Effect.forkChild)
            return 1
          }).pipe(Effect.awaitAllChildren)
        }).pipe(
          Effect.forkChild({ startImmediately: true })
        )
        yield* Effect.yieldNow
        assert.strictEqual(fiber.pollUnsafe(), undefined)
        yield* Deferred.succeed(scopedLatch, void 0)
        yield* Effect.yieldNow
        assert.deepStrictEqual(fiber.pollUnsafe(), Exit.succeed(1))
      }))
  })

  describe("fork", () => {
    it.effect("is interrupted with parent", () =>
      Effect.gen(function*() {
        let child = false
        let parent = false
        const fiber = yield* Effect.never.pipe(
          Effect.onInterrupt(() =>
            Effect.sync(() => {
              child = true
            })
          ),
          Effect.forkChild({ startImmediately: true }),
          Effect.andThen(Effect.never),
          Effect.onInterrupt(() =>
            Effect.sync(() => {
              parent = true
            })
          ),
          Effect.forkChild({ startImmediately: true })
        )
        yield* Fiber.interrupt(fiber)
        assert.isTrue(child)
        assert.isTrue(parent)
      }))
  })

  describe("forkDaemon", () => {
    it.effect("is not interrupted with parent", () =>
      Effect.gen(function*() {
        let child = false
        let parent = false
        const handle = yield* Effect.never.pipe(
          Effect.onInterrupt(() =>
            Effect.sync(() => {
              child = true
            })
          ),
          Effect.forkDetach,
          Effect.andThen(Effect.never),
          Effect.onInterrupt(() =>
            Effect.sync(() => {
              parent = true
            })
          ),
          Effect.forkChild({ startImmediately: true })
        )
        yield* Fiber.interrupt(handle)
        assert.isFalse(child)
        assert.isTrue(parent)
      }))
  })

  describe("forkIn", () => {
    it.effect("is interrupted when scope is closed", () =>
      Effect.gen(function*() {
        let interrupted = false
        const scope = yield* Scope.make()
        yield* Effect.never.pipe(
          Effect.onInterrupt(() =>
            Effect.sync(() => {
              interrupted = true
            })
          ),
          Effect.forkIn(scope, { startImmediately: true })
        )
        yield* Scope.close(scope, Exit.void)
        assert.isTrue(interrupted)
      }))
  })

  describe("forkScoped", () => {
    it.effect("is interrupted when scope is closed", () =>
      Effect.gen(function*() {
        let interrupted = false
        const scope = yield* Scope.make()
        yield* Effect.never.pipe(
          Effect.onInterrupt(() =>
            Effect.sync(() => {
              interrupted = true
            })
          ),
          Effect.forkScoped({ startImmediately: true }),
          Scope.provide(scope)
        )
        yield* Scope.close(scope, Exit.void)
        assert.isTrue(interrupted)
      }))
  })

  describe("do notation", () => {
    it.effect("works", () =>
      Effect.succeed(1).pipe(
        Effect.bindTo("a"),
        Effect.let("b", ({ a }) => a + 1),
        Effect.bind("b", ({ b }) => Effect.succeed(b.toString())),
        Effect.tap((value) =>
          Effect.sync(() => {
            assert.deepStrictEqual(value, {
              a: 1,
              b: "2"
            })
          })
        )
      ))
  })

  describe("stack safety", () => {
    it.live("recursion", () => {
      const loop: Effect.Effect<void> = Effect.void.pipe(
        Effect.flatMap((_) => loop)
      )
      return loop.pipe(
        Effect.timeoutOption(50)
      )
    })
  })

  describe("finalization", () => {
    const ExampleError = new Error("Oh noes!")

    it.effect("fail ensuring", () =>
      Effect.gen(function*() {
        let finalized = false
        const result = yield* Effect.fail(ExampleError).pipe(
          Effect.ensuring(Effect.sync(() => {
            finalized = true
          })),
          Effect.exit
        )
        assert.deepStrictEqual(result, Exit.fail(ExampleError))
        assert.isTrue(finalized)
      }))

    it.effect("fail on error", () =>
      Effect.gen(function*() {
        let finalized = false
        const result = yield* Effect.fail(ExampleError).pipe(
          Effect.onError(() =>
            Effect.sync(() => {
              finalized = true
            })
          ),
          Effect.exit
        )
        assert.deepStrictEqual(result, Exit.fail(ExampleError))
        assert.isTrue(finalized)
      }))

    it.effect("finalizer errors not caught", () =>
      Effect.gen(function*() {
        const e2 = new Error("e2")
        const e3 = new Error("e3")
        const result = yield* pipe(
          Effect.fail(ExampleError),
          Effect.ensuring(Effect.die(e2)),
          Effect.ensuring(Effect.die(e3)),
          Effect.sandbox,
          Effect.flip,
          Effect.map((cause) => cause)
        )
        assert.deepStrictEqual(result, Cause.die(e3))
      }))

    it.effect("finalizer errors reported", () =>
      Effect.gen(function*() {
        let reported: Exit.Exit<number> | undefined
        const result = yield* pipe(
          Effect.succeed(42),
          Effect.ensuring(Effect.die(ExampleError)),
          Effect.forkChild,
          Effect.flatMap((fiber) =>
            pipe(
              Fiber.await(fiber),
              Effect.flatMap((e) =>
                Effect.sync(() => {
                  reported = e
                })
              )
            )
          )
        )
        assert.isUndefined(result)
        assert.isFalse(reported !== undefined && Exit.isSuccess(reported))
      }))

    it.effect("acquireUseRelease usage result", () =>
      Effect.gen(function*() {
        const result = yield* Effect.acquireUseRelease(
          Effect.void,
          () => Effect.succeed(42),
          () => Effect.void
        )
        assert.strictEqual(result, 42)
      }))

    it.effect("error in just acquisition", () =>
      Effect.gen(function*() {
        const result = yield* pipe(
          Effect.acquireUseRelease(
            Effect.fail(ExampleError),
            () => Effect.void,
            () => Effect.void
          ),
          Effect.exit
        )
        assert.deepStrictEqual(result, Exit.fail(ExampleError))
      }))

    it.effect("error in just release", () =>
      Effect.gen(function*() {
        const result = yield* pipe(
          Effect.acquireUseRelease(
            Effect.void,
            () => Effect.void,
            () => Effect.die(ExampleError)
          ),
          Effect.exit
        )
        assert.deepStrictEqual(result, Exit.die(ExampleError))
      }))

    it.effect("error in just usage", () =>
      Effect.gen(function*() {
        const result = yield* pipe(
          Effect.acquireUseRelease(
            Effect.void,
            () => Effect.fail(ExampleError),
            () => Effect.void
          ),
          Effect.exit
        )
        assert.deepStrictEqual(result, Exit.fail(ExampleError))
      }))

    it.effect("rethrown caught error in acquisition", () =>
      Effect.gen(function*() {
        const result = yield* Effect.acquireUseRelease(
          Effect.fail(ExampleError),
          () => Effect.void,
          () => Effect.void
        ).pipe(Effect.flip)
        assert.deepEqual(result, ExampleError)
      }))

    it.effect("rethrown caught error in release", () =>
      Effect.gen(function*() {
        const result = yield* pipe(
          Effect.acquireUseRelease(
            Effect.void,
            () => Effect.void,
            () => Effect.die(ExampleError)
          ),
          Effect.exit
        )
        assert.deepStrictEqual(result, Exit.die(ExampleError))
      }))

    it.effect("rethrown caught error in usage", () =>
      Effect.gen(function*() {
        const result = yield* Effect.acquireUseRelease(
          Effect.void,
          () => Effect.fail(ExampleError),
          () => Effect.void
        ).pipe(Effect.exit)
        assert.deepEqual(result, Exit.fail(ExampleError))
      }))

    it.effect("onResult - ensures that a cleanup function runs when an effect fails", () =>
      Effect.gen(function*() {
        let ref = false
        yield* Effect.die("boom").pipe(
          Effect.onExit((result) =>
            Exit.hasDies(result) ?
              Effect.sync(() => {
                ref = true
              }) :
              Effect.void
          ),
          Effect.sandbox,
          Effect.ignore
        )
        assert.isTrue(ref)
      }))
  })

  describe("Effect.ignore", () => {
    type IgnoreOptions = { readonly log?: boolean | LogLevel.Severity; readonly message?: string }

    const makeTestLogger = () => {
      const capturedLogs: Array<{
        readonly logLevel: LogLevel.LogLevel
        readonly cause: Cause.Cause<unknown>
        readonly message: unknown
      }> = []
      const testLogger = Logger.make<unknown, void>((options) => {
        capturedLogs.push({ logLevel: options.logLevel, cause: options.cause, message: options.message })
      })
      return { capturedLogs, testLogger }
    }

    const runIgnore = (options?: IgnoreOptions, currentLogLevel: LogLevel.Severity = "Info") =>
      Effect.gen(function*() {
        const { capturedLogs, testLogger } = makeTestLogger()
        const program = options === undefined
          ? Effect.fail("boom").pipe(Effect.ignore)
          : Effect.fail("boom").pipe(Effect.ignore(options))
        yield* program.pipe(
          Effect.provide(Logger.layer([testLogger])),
          Effect.provideService(References.MinimumLogLevel, "Trace"),
          Effect.provideService(References.CurrentLogLevel, currentLogLevel)
        )
        return capturedLogs
      })

    it.effect("does not log when log is omitted", () =>
      Effect.gen(function*() {
        const logs = yield* runIgnore()
        assert.strictEqual(logs.length, 0)
      }))

    it.effect("does not log when log is false", () =>
      Effect.gen(function*() {
        const logs = yield* runIgnore({ log: false })
        assert.strictEqual(logs.length, 0)
      }))

    it.effect("logs with the current level when log is true", () =>
      Effect.gen(function*() {
        const logs = yield* runIgnore({ log: true }, "Warn")
        assert.strictEqual(logs.length, 1)
        assert.strictEqual(logs[0].logLevel, "Warn")
        assertCauseFail(logs[0].cause, "boom")
      }))

    it.effect("logs with the provided level when log is a LogLevel", () =>
      Effect.gen(function*() {
        const logs = yield* runIgnore({ log: "Error" }, "Warn")
        assert.strictEqual(logs.length, 1)
        assert.strictEqual(logs[0].logLevel, "Error")
        assertCauseFail(logs[0].cause, "boom")
      }))

    it.effect("prepends the provided message when logging", () =>
      Effect.gen(function*() {
        const logs = yield* runIgnore({ log: true, message: "Ignoring failure" })
        assert.strictEqual(logs.length, 1)
        assert.deepStrictEqual(logs[0].message, ["Ignoring failure"])
        assertCauseFail(logs[0].cause, "boom")
      }))
  })

  describe("Effect.ignoreCause", () => {
    type IgnoreCauseOptions = { readonly log?: boolean | LogLevel.Severity; readonly message?: string }

    const makeTestLogger = () => {
      const capturedLogs: Array<{
        readonly logLevel: LogLevel.LogLevel
        readonly cause: Cause.Cause<unknown>
        readonly message: unknown
      }> = []
      const testLogger = Logger.make<unknown, void>((options) => {
        capturedLogs.push({ logLevel: options.logLevel, cause: options.cause, message: options.message })
      })
      return { capturedLogs, testLogger }
    }

    const runIgnoreCause = (options?: IgnoreCauseOptions, currentLogLevel: LogLevel.Severity = "Info") =>
      Effect.gen(function*() {
        const { capturedLogs, testLogger } = makeTestLogger()
        const program = options === undefined
          ? Effect.fail("boom").pipe(Effect.ignoreCause)
          : Effect.fail("boom").pipe(Effect.ignoreCause(options))
        yield* program.pipe(
          Effect.provide(Logger.layer([testLogger])),
          Effect.provideService(References.MinimumLogLevel, "Trace"),
          Effect.provideService(References.CurrentLogLevel, currentLogLevel)
        )
        return capturedLogs
      })

    it.effect("ignores defects", () =>
      Effect.gen(function*() {
        const exit = yield* Effect.die("boom").pipe(Effect.ignoreCause, Effect.exit)
        assert.deepStrictEqual(exit, Exit.void)
      }))

    it.effect("ignores interrupts", () =>
      Effect.gen(function*() {
        const ignored = yield* Effect.interrupt.pipe(Effect.ignoreCause, Effect.exit)
        assert.deepStrictEqual(ignored, Exit.void)
      }))

    it.effect("does not log when log is omitted", () =>
      Effect.gen(function*() {
        const logs = yield* runIgnoreCause()
        assert.strictEqual(logs.length, 0)
      }))

    it.effect("does not log when log is false", () =>
      Effect.gen(function*() {
        const logs = yield* runIgnoreCause({ log: false })
        assert.strictEqual(logs.length, 0)
      }))

    it.effect("logs with the current level when log is true", () =>
      Effect.gen(function*() {
        const logs = yield* runIgnoreCause({ log: true }, "Warn")
        assert.strictEqual(logs.length, 1)
        assert.strictEqual(logs[0].logLevel, "Warn")
        assertCauseFail(logs[0].cause, "boom")
      }))

    it.effect("logs with the provided level when log is a LogLevel", () =>
      Effect.gen(function*() {
        const logs = yield* runIgnoreCause({ log: "Error" }, "Warn")
        assert.strictEqual(logs.length, 1)
        assert.strictEqual(logs[0].logLevel, "Error")
        assertCauseFail(logs[0].cause, "boom")
      }))

    it.effect("prepends the provided message when logging", () =>
      Effect.gen(function*() {
        const logs = yield* runIgnoreCause({ log: true, message: "Ignoring cause" })
        assert.strictEqual(logs.length, 1)
        assert.deepStrictEqual(logs[0].message, ["Ignoring cause"])
        assertCauseFail(logs[0].cause, "boom")
      }))
  })

  describe("error handling", () => {
    class ErrorA extends Data.TaggedError("A") {}
    class ErrorB extends Data.TaggedError("B") {}
    class ErrorC extends Data.Error {}

    it.effect("catchTag", () =>
      Effect.gen(function*() {
        let error: ErrorA | ErrorB | ErrorC = new ErrorA()
        const effect = Effect.failSync(() => error).pipe(
          Effect.catchTag("A", (_) => Effect.succeed(1)),
          Effect.catchTag("B", (_) => Effect.succeed(2)),
          Effect.orElseSucceed(() => 3)
        )
        assert.strictEqual(yield* effect, 1)
        error = new ErrorB()
        assert.strictEqual(yield* effect, 2)
        error = new ErrorC()
        assert.strictEqual(yield* effect, 3)
      }))

    it.effect("catchTag orElse", () =>
      Effect.gen(function*() {
        let error: ErrorA | ErrorB | ErrorC = new ErrorA()
        const effect = Effect.failSync(() => error).pipe(
          Effect.catchTag(["A", "B"], (_) => Effect.succeed(1), (_) => {
            return Effect.succeed(2)
          })
        )
        assert.strictEqual(yield* effect, 1)
        error = new ErrorB()
        assert.strictEqual(yield* effect, 1)
        error = new ErrorC()
        assert.strictEqual(yield* effect, 2)
      }))

    it.effect("catchTags orElse", () =>
      Effect.gen(function*() {
        let error: ErrorA | ErrorB | ErrorC = new ErrorA()
        const effect = Effect.failSync(() => error).pipe(
          Effect.catchTags(
            {
              A: (_) => Effect.succeed(1),
              B: (_) => Effect.succeed(2)
            },
            (_) => Effect.succeed(3)
          )
        )
        assert.strictEqual(yield* effect, 1)
        error = new ErrorB()
        assert.strictEqual(yield* effect, 2)
        error = new ErrorC()
        assert.strictEqual(yield* effect, 3)
      }))

    it.effect("tapErrorTag", () =>
      Effect.gen(function*() {
        let error: ErrorA | ErrorB | ErrorC = new ErrorA()
        const tapped: Array<string> = []
        const effect = Effect.failSync(() => error).pipe(
          Effect.tapErrorTag("A", () =>
            Effect.sync(() => {
              tapped.push("A")
            })),
          Effect.tapErrorTag("B", () =>
            Effect.sync(() => {
              tapped.push("B")
            })),
          Effect.exit
        )
        assert.deepStrictEqual(yield* effect, Exit.fail(error))
        assert.deepStrictEqual(tapped, ["A"])

        tapped.length = 0
        error = new ErrorB()
        assert.deepStrictEqual(yield* effect, Exit.fail(error))
        assert.deepStrictEqual(tapped, ["B"])

        tapped.length = 0
        error = new ErrorC()
        assert.deepStrictEqual(yield* effect, Exit.fail(error))
        assert.deepStrictEqual(tapped, [])
      }))

    it.effect("catchIf with refinement", () =>
      Effect.gen(function*() {
        interface ErrorA {
          readonly _tag: "ErrorA"
        }
        interface ErrorB {
          readonly _tag: "ErrorB"
        }
        const effect: Effect.Effect<never, ErrorA | ErrorB> = Effect.fail({ _tag: "ErrorB" as const })
        const result = yield* pipe(
          effect,
          Effect.catchIf((e): e is ErrorA => e._tag === "ErrorA", Effect.succeed),
          Effect.exit
        )
        assert.deepStrictEqual(result, Exit.fail({ _tag: "ErrorB" as const }))
      }))

    it.effect("catchIf with refinement orElse", () =>
      Effect.gen(function*() {
        interface ErrorA {
          readonly _tag: "ErrorA"
        }
        interface ErrorB {
          readonly _tag: "ErrorB"
        }
        const effect: Effect.Effect<never, ErrorA | ErrorB> = Effect.fail({ _tag: "ErrorB" as const })
        const result = yield* pipe(
          effect,
          Effect.catchIf((e): e is ErrorA => e._tag === "ErrorA", Effect.succeed, (_) => {
            return Effect.succeed(1)
          })
        )
        assert.deepStrictEqual(result, 1)
      }))

    it.effect("catchNoSuchElement", () =>
      Effect.gen(function*() {
        const some = yield* Effect.fromNullishOr("value").pipe(Effect.catchNoSuchElement)
        assert.deepStrictEqual(some, Option.some("value"))

        const none = yield* Effect.fromNullishOr(null as string | null).pipe(Effect.catchNoSuchElement)
        assert.deepStrictEqual(none, Option.none())
      }))

    it.effect("catchNoSuchElement preserves other errors", () =>
      Effect.gen(function*() {
        const error = new ErrorA()
        const result = yield* Effect.fail(error).pipe(Effect.catchNoSuchElement, Effect.exit)
        assert.deepStrictEqual(result, Exit.fail(error))
      }))
  })

  describe("zip", () => {
    it.effect("concurrent: false", () => {
      const executionOrder: Array<string> = []
      const task1 = Effect.succeed("a").pipe(
        Effect.delay(50),
        Effect.tap(() => Effect.sync(() => executionOrder.push("task1")))
      )
      const task2 = Effect.succeed(1).pipe(
        Effect.delay(1),
        Effect.tap(() => Effect.sync(() => executionOrder.push("task2")))
      )
      return Effect.gen(function*() {
        const fiber = yield* Effect.forkChild(Effect.zip(task1, task2))
        yield* TestClock.adjust(51)
        const result = yield* Fiber.join(fiber)
        assert.deepStrictEqual(result, ["a", 1])
        assert.deepStrictEqual(executionOrder, ["task1", "task2"])
      })
    })
    it.effect("concurrent: true", () => {
      const executionOrder: Array<string> = []
      const task1 = Effect.succeed("a").pipe(
        Effect.delay(50),
        Effect.tap(() => Effect.sync(() => executionOrder.push("task1")))
      )
      const task2 = Effect.succeed(1).pipe(
        Effect.delay(1),
        Effect.tap(() => Effect.sync(() => executionOrder.push("task2")))
      )
      return Effect.gen(function*() {
        const fiber = yield* Effect.forkChild(Effect.zip(task1, task2, { concurrent: true }))
        yield* TestClock.adjust(50)
        const result = yield* Fiber.join(fiber)
        assert.deepStrictEqual(result, ["a", 1])
        assert.deepStrictEqual(executionOrder, ["task2", "task1"])
      })
    })
  })

  describe("zipWith", () => {
    it.effect("concurrent: false", () => {
      const executionOrder: Array<string> = []
      const task1 = Effect.succeed("a").pipe(
        Effect.delay(50),
        Effect.tap(() => Effect.sync(() => executionOrder.push("task1")))
      )
      const task2 = Effect.succeed(1).pipe(
        Effect.delay(1),
        Effect.tap(() => Effect.sync(() => executionOrder.push("task2")))
      )
      return Effect.gen(function*() {
        const fiber = yield* Effect.forkChild(Effect.zipWith(task1, task2, (a, b) => a + b))
        yield* TestClock.adjust(51)
        const result = yield* Fiber.join(fiber)
        assert.deepStrictEqual(result, "a1")
        assert.deepStrictEqual(executionOrder, ["task1", "task2"])
      })
    })
    it.effect("concurrent: true", () => {
      const executionOrder: Array<string> = []
      const task1 = Effect.succeed("a").pipe(
        Effect.delay(50),
        Effect.tap(() => Effect.sync(() => executionOrder.push("task1")))
      )
      const task2 = Effect.succeed(1).pipe(
        Effect.delay(1),
        Effect.tap(() => Effect.sync(() => executionOrder.push("task2")))
      )
      return Effect.gen(function*() {
        const fiber = yield* Effect.forkChild(Effect.zipWith(task1, task2, (a, b) => a + b, { concurrent: true }))
        yield* TestClock.adjust(50)
        const result = yield* Fiber.join(fiber)
        assert.deepStrictEqual(result, "a1")
        assert.deepStrictEqual(executionOrder, ["task2", "task1"])
      })
    })
  })

  describe("catchCauseFilter", () => {
    it.effect("first argument as success", () =>
      Effect.gen(function*() {
        const result = yield* Effect.catchCauseFilter(Effect.succeed(1), (_) => Result.fail(_), () => Effect.fail("e2"))
        assert.deepStrictEqual(result, 1)
      }))
    it.effect("first argument as failure and predicate return false", () =>
      Effect.gen(function*() {
        const result = yield* Effect.flip(
          Effect.catchCauseFilter(Effect.fail("e1" as const), (_) => Result.fail(_), () => Effect.fail("e2" as const))
        )
        assert.deepStrictEqual(result, "e1")
      }))
    it.effect("first argument as failure and predicate return true", () =>
      Effect.gen(function*() {
        const result = yield* Effect.flip(
          Effect.catchCauseFilter(
            Effect.fail("e1" as const),
            (e) => Result.succeed(e),
            () => Effect.fail("e2" as const)
          )
        )
        assert.deepStrictEqual(result, "e2")
      }))
  })

  describe("catchIf with predicate", () => {
    it.effect("predicate match recovers", () =>
      Effect.gen(function*() {
        const result = yield* Effect.catchIf(
          Effect.fail("e1"),
          (e) => typeof e === "string",
          (e) => Effect.succeed(`recovered: ${e}`)
        )
        assert.deepStrictEqual(result, "recovered: e1")
      }))
    it.effect("predicate no match preserves error", () =>
      Effect.gen(function*() {
        const result = yield* Effect.exit(
          Effect.catchIf(
            Effect.fail("e1" as const),
            (_e) => false,
            () => Effect.succeed("recovered")
          )
        )
        assert.deepStrictEqual(result, Exit.fail("e1"))
      }))
  })

  describe("catchCauseIf with predicate", () => {
    it.effect("predicate match recovers", () =>
      Effect.gen(function*() {
        const result = yield* Effect.catchCauseIf(
          Effect.fail("e1"),
          Cause.hasFails,
          (cause) => Effect.succeed(`recovered: ${Cause.squash(cause)}`)
        )
        assert.deepStrictEqual(result, "recovered: e1")
      }))
    it.effect("predicate no match preserves error", () =>
      Effect.gen(function*() {
        const result = yield* Effect.exit(
          Effect.catchCauseIf(
            Effect.fail("e1" as const),
            constFalse,
            () => Effect.succeed("recovered")
          )
        )
        assert.deepStrictEqual(result, Exit.fail("e1"))
      }))
  })

  describe("tapCauseIf", () => {
    it.effect("filter match taps", () =>
      Effect.gen(function*() {
        const tapped: Array<string> = []
        const result = yield* Effect.exit(
          Effect.tapCauseFilter(
            Effect.fail("e1"),
            (cause) => Result.succeed(cause),
            (cause) => Effect.sync(() => tapped.push(Cause.squash(cause) as string))
          )
        )
        assert.deepStrictEqual(tapped, ["e1"])
        assert.deepStrictEqual(result, Exit.fail("e1"))
      }))
    it.effect("filter no match skips tap", () =>
      Effect.gen(function*() {
        const tapped: Array<string> = []
        const result = yield* Effect.exit(
          Effect.tapCauseFilter(
            Effect.fail("e1"),
            (cause) => Result.fail(cause),
            () => Effect.sync(() => tapped.push("tapped"))
          )
        )
        assert.deepStrictEqual(tapped, [])
        assert.deepStrictEqual(result, Exit.fail("e1"))
      }))
    it.effect("predicate match taps", () =>
      Effect.gen(function*() {
        const tapped: Array<string> = []
        const result = yield* Effect.exit(
          Effect.tapCauseIf(
            Effect.fail("e1"),
            Cause.hasFails,
            (cause) => Effect.sync(() => tapped.push(Cause.squash(cause) as string))
          )
        )
        assert.deepStrictEqual(tapped, ["e1"])
        assert.deepStrictEqual(result, Exit.fail("e1"))
      }))
    it.effect("predicate no match skips tap", () =>
      Effect.gen(function*() {
        const tapped: Array<string> = []
        const result = yield* Effect.exit(
          Effect.tapCauseIf(
            Effect.fail("e1"),
            constFalse,
            () => Effect.sync(() => tapped.push("tapped"))
          )
        )
        assert.deepStrictEqual(tapped, [])
        assert.deepStrictEqual(result, Exit.fail("e1"))
      }))
    it.effect("success skips tap", () =>
      Effect.gen(function*() {
        const tapped: Array<string> = []
        const result = yield* Effect.tapCauseIf(
          Effect.succeed(42),
          constTrue,
          () => Effect.sync(() => tapped.push("tapped"))
        )
        assert.deepStrictEqual(tapped, [])
        assert.deepStrictEqual(result, 42)
      }))
  })

  describe("onErrorIf", () => {
    it.effect("predicate match runs finalizer", () =>
      Effect.gen(function*() {
        const finalized: Array<string> = []
        const result = yield* Effect.exit(
          Effect.onErrorIf(
            Effect.fail("e1"),
            Cause.hasFails,
            (cause) => Effect.sync(() => finalized.push(Cause.squash(cause) as string))
          )
        )
        assert.deepStrictEqual(finalized, ["e1"])
        assert.deepStrictEqual(result, Exit.fail("e1"))
      }))
    it.effect("predicate no match skips finalizer", () =>
      Effect.gen(function*() {
        const finalized: Array<string> = []
        const result = yield* Effect.exit(
          Effect.onErrorIf(
            Effect.fail("e1"),
            constFalse,
            () => Effect.sync(() => finalized.push("finalized"))
          )
        )
        assert.deepStrictEqual(finalized, [])
        assert.deepStrictEqual(result, Exit.fail("e1"))
      }))
    it.effect("filter match runs finalizer", () =>
      Effect.gen(function*() {
        const finalized: Array<string> = []
        const result = yield* Effect.exit(
          Effect.onErrorFilter(
            Effect.fail("e1"),
            Cause.findError as Filter.Filter<Cause.Cause<string>, string>,
            (error: string) => Effect.sync(() => finalized.push(error))
          )
        )
        assert.deepStrictEqual(finalized, ["e1"])
        assert.deepStrictEqual(result, Exit.fail("e1"))
      }))
    it.effect("success skips finalizer", () =>
      Effect.gen(function*() {
        const finalized: Array<string> = []
        const result = yield* Effect.onErrorIf(
          Effect.succeed(42),
          constTrue,
          () => Effect.sync(() => finalized.push("finalized"))
        )
        assert.deepStrictEqual(finalized, [])
        assert.deepStrictEqual(result, 42)
      }))
  })

  describe("onExitIf", () => {
    it.effect("predicate match on success runs finalizer", () =>
      Effect.gen(function*() {
        const finalized: Array<string> = []
        const result = yield* Effect.onExitIf(
          Effect.succeed(42),
          Exit.isSuccess,
          (exit) => {
            if (Exit.isSuccess(exit)) {
              return Effect.sync(() => finalized.push(`success:${exit.value}`))
            }
            return Effect.void
          }
        )
        assert.deepStrictEqual(finalized, ["success:42"])
        assert.deepStrictEqual(result, 42)
      }))
    it.effect("predicate no match on failure skips finalizer", () =>
      Effect.gen(function*() {
        const finalized: Array<string> = []
        const result = yield* Effect.exit(
          Effect.onExitIf(
            Effect.fail("e1"),
            Exit.isSuccess,
            () => Effect.sync(() => finalized.push("finalized"))
          )
        )
        assert.deepStrictEqual(finalized, [])
        assert.deepStrictEqual(result, Exit.fail("e1"))
      }))
    it.effect("predicate match on failure runs finalizer", () =>
      Effect.gen(function*() {
        const finalized: Array<string> = []
        const result = yield* Effect.exit(
          Effect.onExitIf(
            Effect.fail("e1"),
            Exit.isFailure,
            (exit) => {
              if (Exit.isFailure(exit)) {
                return Effect.sync(() => finalized.push(`failure:${Cause.squash(exit.cause)}`))
              }
              return Effect.void
            }
          )
        )
        assert.deepStrictEqual(finalized, ["failure:e1"])
        assert.deepStrictEqual(result, Exit.fail("e1"))
      }))
    it.effect("filter match receives pass value and exit", () =>
      Effect.gen(function*() {
        const finalized: Array<string> = []
        const result = yield* Effect.onExitFilter(
          Effect.succeed(42),
          (exit) => Exit.isSuccess(exit) ? Result.succeed(`value:${exit.value}`) : Result.fail(exit),
          (value, exit) =>
            Effect.sync(() => finalized.push(`${value}:${Exit.isSuccess(exit) ? `success:${exit.value}` : "failure"}`))
        )
        assert.deepStrictEqual(finalized, ["value:42:success:42"])
        assert.deepStrictEqual(result, 42)
      }))
  })

  describe("filter with predicate/refinement", () => {
    it.effect("predicate filters iterable", () =>
      Effect.gen(function*() {
        const result = yield* Effect.filter(
          [1, 2, 3, 4, 5],
          (n: number) => n % 2 === 0
        )
        assert.deepStrictEqual(result, [2, 4])
      }))
    it.effect("refinement narrows iterable", () =>
      Effect.gen(function*() {
        const result = yield* Effect.filter(
          [1, "a", 2, "b", 3] as Array<number | string>,
          (x): x is number => typeof x === "number"
        )
        assert.deepStrictEqual(result, [1, 2, 3])
      }))
    it.effect("Filter.Filter overload maps pass values", () =>
      Effect.gen(function*() {
        const filter: Filter.Filter<number, string, number> = (n) =>
          n % 2 === 0 ? Result.succeed(`n=${n}`) : Result.fail(n)
        const result = yield* Effect.filterMap(
          [1, 2, 3, 4],
          filter
        )
        assert.deepStrictEqual(result, ["n=2", "n=4"])
      }))
  })

  describe("catch", () => {
    it.effect("first argument as success", () =>
      Effect.gen(function*() {
        const result = yield* Effect.catch(Effect.succeed(1), () => Effect.fail("e2" as const))
        assert.deepStrictEqual(result, 1)
      }))
    it.effect("first argument as failure", () =>
      Effect.gen(function*() {
        const result = yield* Effect.flip(Effect.catch(Effect.fail("e1" as const), () => Effect.fail("e2" as const)))
        assert.deepStrictEqual(result, "e2")
      }))
  })

  describe("firstSuccessOf", () => {
    it.effect("returns the first success and does not run later effects", () =>
      Effect.gen(function*() {
        const executed: Array<string> = []
        const result = yield* Effect.firstSuccessOf([
          Effect.sync(() => executed.push("first")).pipe(
            Effect.flatMap(() => Effect.fail("e1" as const))
          ),
          Effect.sync(() => executed.push("second")).pipe(
            Effect.as("success" as const)
          ),
          Effect.sync(() => executed.push("third")).pipe(
            Effect.as("unreachable" as const)
          )
        ])

        assert.strictEqual(result, "success")
        assert.deepStrictEqual(executed, ["first", "second"])
      }))

    it.effect("fails with the last failure when all effects fail", () =>
      Effect.gen(function*() {
        const result = yield* Effect.firstSuccessOf([
          Effect.fail("e1" as const),
          Effect.fail("e2" as const),
          Effect.fail("e3" as const)
        ]).pipe(Effect.flip)

        assert.strictEqual(result, "e3")
      }))

    it.effect("defects on an empty collection", () =>
      Effect.gen(function*() {
        const result = yield* Effect.firstSuccessOf([]).pipe(Effect.sandbox, Effect.flip)
        const reason = result.reasons[0]

        assert.isTrue(Cause.isDieReason(reason))
        if (Cause.isDieReason(reason)) {
          assert.instanceOf(reason.defect, Error)
          assert.strictEqual(reason.defect.message, "Received an empty collection of effects")
        }
      }))
  })

  describe("catchCause", () => {
    it.effect("first argument as success", () =>
      Effect.gen(function*() {
        const result = yield* Effect.catchCause(Effect.succeed(1), () => Effect.fail("e2" as const))
        assert.deepStrictEqual(result, 1)
      }))
    it.effect("first argument as failure", () =>
      Effect.gen(function*() {
        const result = yield* Effect.flip(
          Effect.catchCause(Effect.fail("e1" as const), () => Effect.fail("e2" as const))
        )
        assert.deepStrictEqual(result, "e2")
      }))
  })

  describe("transaction (composable transactions)", () => {
    describe("basic behavior", () => {
      it.effect("should create transaction boundaries when no transaction is active", () =>
        Effect.gen(function*() {
          const ref1 = TxRef.makeUnsafe(0)
          const ref2 = TxRef.makeUnsafe(100)

          yield* Effect.tx(TxRef.set(ref1, 10))

          yield* Effect.tx(TxRef.set(ref2, 200))

          const val1 = yield* Effect.tx(TxRef.get(ref1))
          const val2 = yield* Effect.tx(TxRef.get(ref2))

          assert.strictEqual(val1, 10)
          assert.strictEqual(val2, 200)
        }))

      it.effect("should allow TxRef.modify outside an existing transaction", () =>
        Effect.gen(function*() {
          const ref = TxRef.makeUnsafe(0)

          const result = yield* TxRef.modify(ref, (current) => [current + 1, current + 10])
          const value = yield* Effect.tx(TxRef.get(ref))

          assert.strictEqual(result, 1)
          assert.strictEqual(value, 10)
        }))

      it.effect("should roll back nested changes when the outer transaction fails", () =>
        Effect.gen(function*() {
          const ref1 = TxRef.makeUnsafe(0)
          const ref2 = TxRef.makeUnsafe(100)

          const parentError = yield* Effect.tx(Effect.gen(function*() {
            yield* TxRef.set(ref1, 10)

            yield* Effect.tx(TxRef.set(ref2, 200))

            return yield* Effect.fail("parent failed")
          })).pipe(Effect.flip)

          const val1 = yield* Effect.tx(TxRef.get(ref1))
          const val2 = yield* Effect.tx(TxRef.get(ref2))

          assert.strictEqual(parentError, "parent failed")
          assert.strictEqual(val1, 0)
          assert.strictEqual(val2, 100)
        }))

      it.effect("should allow catching nested failures in the same transaction", () =>
        Effect.gen(function*() {
          const ref1 = TxRef.makeUnsafe(0)
          const ref2 = TxRef.makeUnsafe(100)

          yield* Effect.tx(Effect.gen(function*() {
            yield* TxRef.set(ref1, 10)

            const childResult = yield* Effect.tx(Effect.gen(function*() {
              yield* TxRef.set(ref2, 200)
              return yield* Effect.fail("child failed")
            })).pipe(Effect.result)

            yield* TxRef.set(ref1, 20)

            assert.strictEqual(Result.isFailure(childResult), true)
          }))

          const val1 = yield* Effect.tx(TxRef.get(ref1))
          const val2 = yield* Effect.tx(TxRef.get(ref2))

          assert.strictEqual(val1, 20)
          assert.strictEqual(val2, 200)
        }))
    })

    describe("transaction nesting", () => {
      it.effect("should handle multiple levels of nested composed transactions", () =>
        Effect.gen(function*() {
          const ref1 = TxRef.makeUnsafe(0)
          const ref2 = TxRef.makeUnsafe(0)
          const ref3 = TxRef.makeUnsafe(0)

          yield* Effect.tx(Effect.gen(function*() {
            yield* TxRef.set(ref1, 1)

            yield* Effect.tx(Effect.gen(function*() {
              yield* TxRef.set(ref2, 2)

              yield* Effect.tx(TxRef.set(ref3, 3))
            }))
          }))

          const val1 = yield* Effect.tx(TxRef.get(ref1))
          const val2 = yield* Effect.tx(TxRef.get(ref2))
          const val3 = yield* Effect.tx(TxRef.get(ref3))

          assert.strictEqual(val1, 1)
          assert.strictEqual(val2, 2)
          assert.strictEqual(val3, 3)
        }))
    })

    describe("transaction composition behavior", () => {
      it.effect("should compose nested tx calls into the same transaction", () =>
        Effect.gen(function*() {
          const ref1 = TxRef.makeUnsafe(0)
          const ref2 = TxRef.makeUnsafe(0)

          yield* Effect.tx(Effect.gen(function*() {
            yield* TxRef.set(ref1, 10)

            yield* Effect.tx(TxRef.set(ref2, 20))
          }))

          const val1 = yield* Effect.tx(TxRef.get(ref1))
          const val2 = yield* Effect.tx(TxRef.get(ref2))

          assert.strictEqual(val1, 10)
          assert.strictEqual(val2, 20)
        }))

      it.effect("should rollback the entire composed transaction on failure", () =>
        Effect.gen(function*() {
          const ref = TxRef.makeUnsafe(0)

          const txError = yield* Effect.tx(Effect.gen(function*() {
            yield* TxRef.set(ref, 10)

            return yield* Effect.tx(Effect.gen(function*() {
              yield* TxRef.set(ref, 20)
              return yield* Effect.fail("nested failure")
            }))
          })).pipe(Effect.flip)

          const valueAfterFailure = yield* Effect.tx(TxRef.get(ref))

          assert.strictEqual(txError, "nested failure")
          assert.strictEqual(valueAfterFailure, 0)
        }))

      it.effect("should preserve nested writes when nested failure is caught", () =>
        Effect.gen(function*() {
          const ref = TxRef.makeUnsafe(0)

          yield* Effect.tx(Effect.gen(function*() {
            yield* TxRef.set(ref, 10)

            const childError = yield* Effect.tx(Effect.gen(function*() {
              yield* TxRef.set(ref, 20)
              return yield* Effect.fail("transaction nested failure")
            })).pipe(Effect.flip)

            assert.strictEqual(childError, "transaction nested failure")
          }))

          const transactionValue = yield* Effect.tx(TxRef.get(ref))

          assert.strictEqual(transactionValue, 20)
        }))
    })
  })

  describe("Effect.fn", () => {
    it.effect("should support pipeable arguments", () => {
      const fn = Effect.fn(function*(s: string) {
        return s.length
      }, (effect, ...args) => effect.pipe(Effect.map((result) => [result, ...args])))
      return Effect.gen(function*() {
        const result = yield* fn("a")
        assert.deepStrictEqual(result, [1, "a"])
      })
    })

    it("should proxy body length", () => {
      const traced = Effect.fn(function*(a: string, b: number) {
        return a.length + b
      })
      const named = Effect.fn("named")(function*(a: string, b: number, c: boolean) {
        return c ? a.length + b : b
      })
      const untraced = Effect.fnUntraced(function*(a: string, b: number) {
        return a.length + b
      }, Effect.map((n) => n))

      assert.strictEqual(traced.length, 2)
      assert.strictEqual(named.length, 3)
      assert.strictEqual(untraced.length, 2)
    })
  })

  describe("catchReason", () => {
    class RateLimitError extends Data.TaggedError("RateLimitError")<{
      readonly retryAfter: number
    }> {}

    class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{
      readonly limit: number
    }> {}

    class AiError extends Data.TaggedError("AiError")<{
      readonly reason: RateLimitError | QuotaExceededError
    }> {}

    class OtherError extends Data.TaggedError("OtherError")<{
      readonly message: string
    }> {}

    it.effect("catches matching reason - handler succeeds", () =>
      Effect.gen(function*() {
        const result = yield* Effect.fail(
          new AiError({ reason: new RateLimitError({ retryAfter: 60 }) })
        ).pipe(
          Effect.catchReason("AiError", "RateLimitError", (r) => Effect.succeed(`retry: ${r.retryAfter}`))
        )
        assert.strictEqual(result, "retry: 60")
      }))

    it.effect("orElse", () =>
      Effect.gen(function*() {
        const result = yield* Effect.fail(
          new AiError({ reason: new QuotaExceededError({ limit: 100 }) })
        ).pipe(
          Effect.catchReason(
            "AiError",
            "RateLimitError",
            (r) => Effect.succeed(`retry: ${r.retryAfter}`),
            (_) => Effect.succeed("quota")
          )
        )
        assert.strictEqual(result, "quota")
      }))

    it.effect("catches matching reason - handler fails", () =>
      Effect.gen(function*() {
        const reason = new RateLimitError({ retryAfter: 60 })
        const error = new OtherError({ message: "handled" })
        const exit = yield* Effect.fail(new AiError({ reason })).pipe(
          Effect.catchReason("AiError", "RateLimitError", () => Effect.fail(error)),
          Effect.exit
        )
        assertExitFailure(exit, Cause.fail(error))
      }))

    it.effect("ignores non-matching reason", () =>
      Effect.gen(function*() {
        const reason = new QuotaExceededError({ limit: 100 })
        const exit = yield* Effect.fail(new AiError({ reason })).pipe(
          Effect.catchReason("AiError", "RateLimitError", () => Effect.succeed("no")),
          Effect.exit
        )
        assertExitFailure(exit, Cause.fail(new AiError({ reason })))
      }))

    it.effect("ignores non-matching parent tag", () =>
      Effect.gen(function*() {
        const error = new OtherError({ message: "test" })
        const exit = yield* (Effect.fail(error) as Effect.Effect<never, AiError | OtherError>).pipe(
          Effect.catchReason("AiError", "RateLimitError", () => Effect.succeed("no")),
          Effect.exit
        )
        assertExitFailure(exit, Cause.fail(error))
      }))
  })

  describe("catchReasons", () => {
    class RateLimitError extends Data.TaggedError("RateLimitError")<{
      readonly retryAfter: number
    }> {}

    class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{
      readonly limit: number
    }> {}

    class AiError extends Data.TaggedError("AiError")<{
      readonly reason: RateLimitError | QuotaExceededError
    }> {}

    it.effect("catches with object handlers", () =>
      Effect.gen(function*() {
        const result = yield* Effect.fail(
          new AiError({ reason: new RateLimitError({ retryAfter: 60 }) })
        ).pipe(
          Effect.catchReasons("AiError", {
            RateLimitError: (r) => Effect.succeed(`rate: ${r.retryAfter}`),
            QuotaExceededError: (r) => Effect.succeed(`quota: ${r.limit}`)
          })
        )
        assert.strictEqual(result, "rate: 60")
      }))

    it.effect("orElse", () =>
      Effect.gen(function*() {
        const result = yield* Effect.fail(
          new AiError({ reason: new RateLimitError({ retryAfter: 60 }) })
        ).pipe(
          Effect.catchReasons("AiError", {
            QuotaExceededError: (r) => Effect.succeed(`quota: ${r.limit}`)
          }, (_) => Effect.succeed("orElse"))
        )
        assert.strictEqual(result, "orElse")
      }))

    it.effect("catches second reason type", () =>
      Effect.gen(function*() {
        const result = yield* Effect.fail(
          new AiError({ reason: new QuotaExceededError({ limit: 100 }) })
        ).pipe(
          Effect.catchReasons("AiError", {
            RateLimitError: (r) => Effect.succeed(`rate: ${r.retryAfter}`),
            QuotaExceededError: (r) => Effect.succeed(`quota: ${r.limit}`)
          })
        )
        assert.strictEqual(result, "quota: 100")
      }))

    it.effect("partial handlers - unhandled passes through", () =>
      Effect.gen(function*() {
        const reason = new QuotaExceededError({ limit: 100 })
        const exit = yield* Effect.fail(new AiError({ reason })).pipe(
          Effect.catchReasons("AiError", {
            RateLimitError: () => Effect.succeed("handled")
          }),
          Effect.exit
        )
        assertExitFailure(exit, Cause.fail(new AiError({ reason })))
      }))
  })

  describe("unwrapReason", () => {
    class RateLimitError extends Data.TaggedError("RateLimitError")<{
      readonly retryAfter: number
    }> {}

    class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{
      readonly limit: number
    }> {}

    class AiError extends Data.TaggedError("AiError")<{
      readonly reason: RateLimitError | QuotaExceededError
    }> {}

    class OtherError extends Data.TaggedError("OtherError")<{
      readonly message: string
    }> {}

    it.effect("extracts reason into error channel", () =>
      Effect.gen(function*() {
        const reason = new RateLimitError({ retryAfter: 60 })
        const exit = yield* Effect.fail(new AiError({ reason })).pipe(
          Effect.unwrapReason("AiError"),
          Effect.exit
        )
        assertExitFailure(exit, Cause.fail(reason))
      }))

    it.effect("extracts second reason type", () =>
      Effect.gen(function*() {
        const reason = new QuotaExceededError({ limit: 100 })
        const exit = yield* Effect.fail(new AiError({ reason })).pipe(
          Effect.unwrapReason("AiError"),
          Effect.exit
        )
        assertExitFailure(exit, Cause.fail(reason))
      }))

    it.effect("preserves other errors", () =>
      Effect.gen(function*() {
        const error = new OtherError({ message: "test" })
        const exit = yield* (Effect.fail(error) as Effect.Effect<never, AiError | OtherError>).pipe(
          Effect.unwrapReason("AiError"),
          Effect.exit
        )
        assertExitFailure(exit, Cause.fail(error))
      }))
  })

  describe("cachedWithTTL", () => {
    it.effect("starts ttl from value creation", () =>
      Effect.gen(function*() {
        let count = 0
        const cached = yield* Effect.cachedWithTTL(
          Effect.sleep("2 seconds").pipe(
            Effect.flatMap(() =>
              Effect.sync(() => {
                count += 1
                return count
              })
            )
          ),
          "1 second"
        )

        const firstFiber = yield* Effect.forkChild(cached)
        yield* Effect.yieldNow
        yield* TestClock.adjust("2 seconds")
        assert.strictEqual(yield* Fiber.join(firstFiber), 1)

        const secondFiber = yield* Effect.forkChild(cached)
        yield* Effect.yieldNow
        yield* TestClock.adjust("2 seconds")
        assert.strictEqual(yield* Fiber.join(secondFiber), 1)
        assert.strictEqual(count, 1)
      }))
  })

  describe("provide", () => {
    class MyNumber extends Context.Service<MyNumber, number>()("MyNumber") {}

    it.effect("subsequent calls share MemoMap", () =>
      Effect.gen(function*() {
        let buildCount = 0
        const layer = Layer.sync(MyNumber, () => {
          buildCount += 1
          return 42
        })

        // @effect-diagnostics multipleEffectProvide:off
        yield* Effect.void.pipe(
          Effect.provide(layer, { local: true }), // local always builds the layer
          Effect.provide(layer),
          Effect.provide(layer)
        )

        assert.strictEqual(buildCount, 2)
      }))
  })
})
