import { assert, describe, it } from "@effect/vitest"
import { Clock, Context, Data, Deferred, Duration, Effect, Exit, Fiber, Option, ScopedCache } from "effect"
import { TestClock } from "effect/testing"

describe("ScopedCache", () => {
  describe("constructors", () => {
    describe("make", () => {
      it.effect("creates cache with fixed capacity", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          assert.strictEqual(cache.capacity, 10)
        }))

      it.effect("creates cache with default infinite TTL", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          // Add value and verify it doesn't expire
          yield* ScopedCache.get(cache, "test")
          yield* TestClock.adjust(Duration.hours(1000))
          assert.isTrue(yield* ScopedCache.has(cache, "test"))
        }))

      it.effect("creates cache with custom TTL", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "test")
          yield* TestClock.adjust("30 minutes")
          assert.isTrue(yield* ScopedCache.has(cache, "test"))

          yield* TestClock.adjust("31 minutes")
          assert.isFalse(yield* ScopedCache.has(cache, "test"))
        }))

      it.effect("lookup function context is preserved", () =>
        Effect.gen(function*() {
          class TestService extends Context.Service<TestService, { value: number }>()("TestService") {}

          const program = Effect.gen(function*() {
            const cache = yield* ScopedCache.make({
              capacity: 10,
              lookup: (_key: string) => Effect.map(TestService, (service) => service.value)
            })
            return yield* ScopedCache.get(cache, "test")
          })

          const result = yield* Effect.scoped(
            program.pipe(
              Effect.provideService(TestService, TestService.of({ value: 42 }))
            )
          )

          assert.strictEqual(result, 42)
        }))

      it.effect("cache resources are released and future gets interrupt when scope closes", () =>
        Effect.gen(function*() {
          const cache = yield* Effect.scoped(
            Effect.gen(function*() {
              const cache = yield* ScopedCache.make({
                capacity: 10,
                lookup: (key: string) => Effect.succeed(key.length)
              })
              yield* ScopedCache.get(cache, "test")
              return cache
            })
          )

          assert.strictEqual(cache.state._tag, "Closed")
          assert.isTrue(Exit.hasInterrupts(yield* Effect.exit(ScopedCache.get(cache, "test"))))
        }))
    })

    describe("makeWith", () => {
      it.effect("creates cache with function-based TTL", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.makeWith({
            capacity: 10,
            lookup: (key: string) => key === "fail" ? Effect.fail("error") : Effect.succeed(key.length),
            timeToLive: (exit, key) => {
              if (Exit.isFailure(exit)) return "1 second"
              return key === "short" ? "1 minute" : "1 hour"
            }
          })

          // Success with long TTL
          yield* ScopedCache.get(cache, "long")
          yield* TestClock.adjust("30 minutes")
          assert.isTrue(yield* ScopedCache.has(cache, "long"))

          // Success with short TTL
          yield* ScopedCache.get(cache, "short")
          assert.isTrue(yield* ScopedCache.has(cache, "short"))
          yield* TestClock.adjust(Duration.sum(Duration.minutes(1), Duration.seconds(1)))
          assert.isFalse(yield* ScopedCache.has(cache, "short"))
          assert.isTrue(yield* ScopedCache.has(cache, "long"))

          // Failure with very short TTL
          yield* Effect.exit(ScopedCache.get(cache, "fail"))
          assert.isTrue(yield* ScopedCache.has(cache, "fail"))
          yield* TestClock.adjust("2 seconds")
          assert.isFalse(yield* ScopedCache.has(cache, "fail"))
        }))

      it.effect("TTL function receives correct parameters", () =>
        Effect.gen(function*() {
          const receivedParams: Array<{ exit: Exit.Exit<number, string>; key: string }> = []

          const cache = yield* ScopedCache.makeWith({
            capacity: 10,
            lookup: (key: string) => key === "fail" ? Effect.fail("error") : Effect.succeed(key.length),
            timeToLive: (exit, key) => {
              receivedParams.push({ exit, key })
              return Duration.infinity
            }
          })

          yield* ScopedCache.get(cache, "test")
          yield* Effect.exit(ScopedCache.get(cache, "fail"))

          assert.strictEqual(receivedParams.length, 2)
          assert.strictEqual(receivedParams[0].key, "test")
          assert.isTrue(Exit.isSuccess(receivedParams[0].exit))
          assert(Exit.isSuccess(receivedParams[0].exit))
          assert.strictEqual(receivedParams[0].exit.value, 4)

          assert.strictEqual(receivedParams[1].key, "fail")
          assert.isTrue(Exit.isFailure(receivedParams[1].exit))
          assert.deepStrictEqual(receivedParams[1].exit, Exit.fail("error"))
        }))

      it.effect("different TTL for success vs failure", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.makeWith({
            capacity: 10,
            lookup: (key: string) => key === "fail" ? Effect.fail("error") : Effect.succeed(key.length),
            timeToLive: (exit) => Exit.isSuccess(exit) ? "1 hour" : "1 minute"
          })

          yield* ScopedCache.get(cache, "success")
          yield* Effect.exit(ScopedCache.get(cache, "fail"))

          yield* TestClock.adjust("30 minutes")
          assert.isTrue(yield* ScopedCache.has(cache, "success"))
          assert.isFalse(yield* ScopedCache.has(cache, "fail"))
        }))

      it.effect("TTL based on key values", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.makeWith({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: (_exit, key) => key === "short" ? "1 minute" : "1 hour"
          })

          yield* ScopedCache.get(cache, "short")
          yield* ScopedCache.get(cache, "long")

          yield* TestClock.adjust("30 minutes")
          assert.isFalse(yield* ScopedCache.has(cache, "short"))
          assert.isTrue(yield* ScopedCache.has(cache, "long"))
        }))

      it.effect("TTL based on result values", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.makeWith({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: (exit) => {
              const value = Exit.isSuccess(exit) ? exit.value : 0
              return value > 3 ? "1 hour" : "1 minute"
            }
          })

          yield* ScopedCache.get(cache, "ab") // length 2
          yield* ScopedCache.get(cache, "abcd") // length 4

          yield* TestClock.adjust("30 minutes")
          assert.isFalse(yield* ScopedCache.has(cache, "ab"))
          assert.isTrue(yield* ScopedCache.has(cache, "abcd"))
        }))

      it.effect("infinite TTL handling", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.makeWith({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: () => Duration.infinity
          })

          yield* ScopedCache.get(cache, "test")
          yield* TestClock.adjust(Duration.hours(1000))
          assert.isTrue(yield* ScopedCache.has(cache, "test"))
        }))

      it.effect("zero duration TTL", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.makeWith({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: () => Duration.zero
          })

          yield* ScopedCache.get(cache, "test")
          // Entry should expire immediately
          assert.isFalse(yield* ScopedCache.has(cache, "test"))
        }))
    })
  })

  describe("basic operations", () => {
    describe("get", () => {
      it.effect("cache hit - multiple different keys", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          const result1 = yield* ScopedCache.get(cache, "a")
          const result2 = yield* ScopedCache.get(cache, "ab")
          const result3 = yield* ScopedCache.get(cache, "abc")

          assert.strictEqual(result1, 1)
          assert.strictEqual(result2, 2)
          assert.strictEqual(result3, 3)
        }))

      it.effect("cache hit - same key multiple times doesn't invoke lookup again", () =>
        Effect.gen(function*() {
          const { cache, lookupCount, setLookupResult } = yield* makeScopedTestCache(10)
          setLookupResult("test", Effect.succeed(4))

          const results = yield* Effect.all([
            ScopedCache.get(cache, "test"),
            ScopedCache.get(cache, "test"),
            ScopedCache.get(cache, "test")
          ])

          assert.deepStrictEqual(results, [4, 4, 4])
          assert.strictEqual(lookupCount(), 1)
        }))

      it.effect("cache miss - invokes lookup for non-existent key", () =>
        Effect.gen(function*() {
          const { cache, lookupCount, setLookupResult } = yield* makeScopedTestCache(10)
          setLookupResult("test", Effect.succeed(42))

          const result = yield* ScopedCache.get(cache, "test")

          assert.strictEqual(result, 42)
          assert.strictEqual(lookupCount(), 1)
        }))

      it.effect("cache miss - invokes lookup again after TTL expiration", () =>
        Effect.gen(function*() {
          let counter = 0
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (_key: string) => Effect.sync(() => ++counter),
            timeToLive: "1 hour"
          })

          const result1 = yield* ScopedCache.get(cache, "test")
          assert.strictEqual(result1, 1)

          yield* TestClock.adjust("30 minutes")
          const result2 = yield* ScopedCache.get(cache, "test")
          assert.strictEqual(result2, 1)

          yield* TestClock.adjust("31 minutes")
          const result3 = yield* ScopedCache.get(cache, "test")
          assert.strictEqual(result3, 2)
        }))

      it.effect("error handling - lookup function fails", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (_key: string) => Effect.fail("lookup error")
          })

          const result = yield* Effect.exit(ScopedCache.get(cache, "test"))

          assert.deepStrictEqual(result, Exit.fail("lookup error"))
        }))

      it.effect("concurrent access - multiple fibers getting same key only invoke lookup once", () =>
        Effect.gen(function*() {
          let lookupCount = 0
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: () => Effect.delay(Duration.millis(100))(Effect.sync(() => ++lookupCount))
          })

          const effects = [ScopedCache.get(cache, "key"), ScopedCache.get(cache, "key"), ScopedCache.get(cache, "key")]
          const resultsFiber = yield* Effect.all(effects, { concurrency: "unbounded" }).pipe(Effect.forkChild)

          yield* TestClock.adjust(Duration.millis(150))
          const results = yield* Fiber.join(resultsFiber)

          assert.strictEqual(lookupCount, 1)
          assert.deepStrictEqual(results, [1, 1, 1])
        }))

      it.effect("concurrent access - race between get and invalidate", () =>
        Effect.gen(function*() {
          let lookupCount = 0
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (_key: string) => Effect.delay(Duration.millis(50))(Effect.sync(() => ++lookupCount))
          })

          // First get starts
          const fiber1 = yield* ScopedCache.get(cache, "test").pipe(Effect.forkChild)
          yield* TestClock.adjust(Duration.millis(25))

          // Invalidate while first get is in progress
          yield* ScopedCache.invalidate(cache, "test")

          // Second get starts after invalidation
          const fiber2 = yield* ScopedCache.get(cache, "test").pipe(Effect.forkChild)

          yield* TestClock.adjust(Duration.millis(100))
          const result1 = yield* Fiber.join(fiber1)
          const result2 = yield* Fiber.join(fiber2)

          assert.strictEqual(result1, 1)
          assert.strictEqual(result2, 2)
          assert.strictEqual(lookupCount, 2)
        }))

      it.effect("resource cleanup when entry is replaced", () =>
        Effect.gen(function*() {
          const { cache, cleanupTracker } = yield* makeManagedResourceCache(2)

          // Fill cache to capacity
          yield* ScopedCache.get(cache, "key1")
          yield* ScopedCache.get(cache, "key2")
          assert.strictEqual(cleanupTracker.cleanedUp.length, 0)

          // Add third entry, should evict first
          yield* ScopedCache.get(cache, "key3")
          assert.strictEqual(cleanupTracker.cleanedUp.length, 1)
          assert.deepStrictEqual(cleanupTracker.cleanedUp, ["key1"])
        }))

      it.effect("scope is properly provided to lookup function", () =>
        Effect.gen(function*() {
          const scopeIds: Array<string> = []

          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) =>
              Effect.gen(function*() {
                // Create a resource in the provided scope
                yield* Effect.acquireRelease(
                  Effect.sync(() => scopeIds.push(`acquired-${key}`)),
                  () => Effect.sync(() => scopeIds.push(`released-${key}`))
                )
                return `value-${key}`
              })
          })

          yield* ScopedCache.get(cache, "test")
          assert.deepStrictEqual(scopeIds, ["acquired-test"])

          // Invalidate should trigger resource cleanup
          yield* ScopedCache.invalidate(cache, "test")
          assert.deepStrictEqual(scopeIds, ["acquired-test", "released-test"])
        }))

      it.effect("cache closed state - get returns interrupt", () =>
        Effect.gen(function*() {
          const cache = yield* Effect.scoped(
            ScopedCache.make({
              capacity: 10,
              lookup: (key: string) => Effect.succeed(key.length)
            })
          )

          // Cache should be closed now
          const result = yield* Effect.exit(ScopedCache.get(cache!, "test"))
          assert.isTrue(Exit.hasInterrupts(result))
        }))
    })

    describe("getOption", () => {
      it.effect("returns Some for existing cached value", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "test")
          const result = yield* ScopedCache.getOption(cache, "test")

          assert.deepStrictEqual(result, Option.some(4))
        }))

      it.effect("returns None for non-existent key", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          const result = yield* ScopedCache.getOption(cache, "test")

          assert.deepStrictEqual(result, Option.none())
        }))

      it.effect("returns None for expired value", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "test")
          yield* TestClock.adjust("2 hours")
          const result = yield* ScopedCache.getOption(cache, "test")

          assert.deepStrictEqual(result, Option.none())
        }))

      it.effect("waits for value being computed and returns result", () =>
        Effect.gen(function*() {
          const deferred = yield* Deferred.make<void>()
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (_key: string) => Deferred.await(deferred).pipe(Effect.as(42))
          })

          const getFiber = yield* ScopedCache.get(cache, "test").pipe(Effect.forkChild)
          const optionFiber = yield* ScopedCache.getOption(cache, "test").pipe(Effect.forkChild)

          yield* Deferred.succeed(deferred, void 0)

          const getResult = yield* Fiber.join(getFiber)
          const optionResult = yield* Fiber.join(optionFiber)

          assert.strictEqual(getResult, 42)
          assert.deepStrictEqual(optionResult, Option.some(42))
        }))

      it.effect("affects LRU order", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 2,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")

          yield* ScopedCache.getOption(cache, "a")

          yield* ScopedCache.get(cache, "c")

          assert.isTrue(yield* ScopedCache.has(cache, "a"))
          assert.isFalse(yield* ScopedCache.has(cache, "b"))
          assert.isTrue(yield* ScopedCache.has(cache, "c"))
        }))
    })

    describe("getSuccess", () => {
      it.effect("returns Some for successfully cached value", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "test")
          const result = yield* ScopedCache.getSuccess(cache, "test")

          assert.deepStrictEqual(result, Option.some(4))
        }))

      it.effect("returns None for failed cached value", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (_key: string) => Effect.fail("error")
          })

          yield* Effect.exit(ScopedCache.get(cache, "test"))
          const result = yield* ScopedCache.getSuccess(cache, "test")

          assert.deepStrictEqual(result, Option.none())
        }))

      it.effect("returns None for non-existent key", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          const result = yield* ScopedCache.getSuccess(cache, "test")

          assert.deepStrictEqual(result, Option.none())
        }))

      it.effect("returns None for expired value", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "test")
          yield* TestClock.adjust("2 hours")
          const result = yield* ScopedCache.getSuccess(cache, "test")

          assert.deepStrictEqual(result, Option.none())
        }))
    })
  })

  describe("modification operations", () => {
    describe("set", () => {
      it.effect("sets new key-value pair", () =>
        Effect.gen(function*() {
          const { cache, lookupCount } = yield* makeScopedTestCache(10)

          yield* ScopedCache.set(cache, "test", 42)
          const result = yield* ScopedCache.get(cache, "test")

          assert.strictEqual(result, 42)
          assert.strictEqual(lookupCount(), 0)
        }))

      it.effect("overwrites existing key", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "test")
          yield* ScopedCache.set(cache, "test", 100)
          const result = yield* ScopedCache.get(cache, "test")

          assert.strictEqual(result, 100)
        }))

      it.effect("set with TTL - value expires", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.set(cache, "test", 42)
          assert.isTrue(yield* ScopedCache.has(cache, "test"))

          yield* TestClock.adjust("2 hours")
          assert.isFalse(yield* ScopedCache.has(cache, "test"))
        }))

      it.effect("set doesn't invoke lookup function", () =>
        Effect.gen(function*() {
          const { cache, lookupCount } = yield* makeScopedTestCache(10)

          yield* ScopedCache.set(cache, "test", 42)
          yield* ScopedCache.set(cache, "test2", 43)
          yield* ScopedCache.set(cache, "test3", 44)

          assert.strictEqual(lookupCount(), 0)
        }))

      it.effect("set enforces capacity constraints", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 2,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.set(cache, "a", 1)
          yield* ScopedCache.set(cache, "b", 2)
          const sizeBefore = yield* ScopedCache.size(cache)
          assert.strictEqual(sizeBefore, 2)

          yield* ScopedCache.set(cache, "c", 3)
          const sizeAfter = yield* ScopedCache.size(cache)
          assert.strictEqual(sizeAfter, 2)
        }))

      it.effect("resource cleanup when overwriting existing entry", () =>
        Effect.gen(function*() {
          const { cache, cleanupTracker } = yield* makeManagedResourceCache(10)

          yield* ScopedCache.get(cache, "test")
          assert.strictEqual(cleanupTracker.cleanedUp.length, 0)

          // Overwrite should clean up old resource
          yield* ScopedCache.set(cache, "test", "new-value")
          assert.strictEqual(cleanupTracker.cleanedUp.length, 1)
          assert.deepStrictEqual(cleanupTracker.cleanedUp, ["test"])
        }))
    })

    describe("has", () => {
      it.effect("returns true for existing key", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "test")
          const result = yield* ScopedCache.has(cache, "test")

          assert.isTrue(result)
        }))

      it.effect("returns false for non-existent key", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          const result = yield* ScopedCache.has(cache, "test")

          assert.isFalse(result)
        }))

      it.effect("returns false for expired key", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "test")
          yield* TestClock.adjust("2 hours")
          const result = yield* ScopedCache.has(cache, "test")

          assert.isFalse(result)
        }))

      it.effect("does not affect LRU order", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 2,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")

          // Check "a" exists without affecting LRU order
          assert.isTrue(yield* ScopedCache.has(cache, "a"))

          // Add "c" - should still evict "a" (oldest) not "b"
          yield* ScopedCache.get(cache, "c")

          assert.isFalse(yield* ScopedCache.has(cache, "a"))
          assert.isTrue(yield* ScopedCache.has(cache, "b"))
          assert.isTrue(yield* ScopedCache.has(cache, "c"))
        }))
    })

    describe("invalidate", () => {
      it.effect("invalidates existing key", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "test")
          yield* ScopedCache.invalidate(cache, "test")
          const result = yield* ScopedCache.has(cache, "test")

          assert.isFalse(result)
        }))

      it.effect("invalidating non-existent key doesn't error", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.invalidate(cache, "test")
          const result = yield* ScopedCache.has(cache, "test")

          assert.isFalse(result)
        }))

      it.effect("get after invalidate invokes lookup again", () =>
        Effect.gen(function*() {
          let counter = 0
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (_key: string) => Effect.sync(() => ++counter)
          })

          const result1 = yield* ScopedCache.get(cache, "test")
          yield* ScopedCache.invalidate(cache, "test")
          const result2 = yield* ScopedCache.get(cache, "test")

          assert.strictEqual(result1, 1)
          assert.strictEqual(result2, 2)
        }))

      it.effect("resource cleanup when invalidating entry", () =>
        Effect.gen(function*() {
          const { cache, cleanupTracker } = yield* makeManagedResourceCache(10)

          yield* ScopedCache.get(cache, "test")
          assert.strictEqual(cleanupTracker.cleanedUp.length, 0)

          yield* ScopedCache.invalidate(cache, "test")
          assert.strictEqual(cleanupTracker.cleanedUp.length, 1)
          assert.deepStrictEqual(cleanupTracker.cleanedUp, ["test"])
        }))
    })

    describe("invalidateWhen", () => {
      it.effect("invalidates when predicate matches", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "test")
          const result = yield* ScopedCache.invalidateWhen(cache, "test", (value) => value === 4)

          assert.isTrue(result)
          assert.isFalse(yield* ScopedCache.has(cache, "test"))
        }))

      it.effect("doesn't invalidate when predicate doesn't match", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "test")
          const result = yield* ScopedCache.invalidateWhen(cache, "test", (value) => value === 5)

          assert.isFalse(result)
          assert.isTrue(yield* ScopedCache.has(cache, "test"))
        }))

      it.effect("returns false for non-existent key", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          const result = yield* ScopedCache.invalidateWhen(cache, "test", () => true)

          assert.isFalse(result)
        }))

      it.effect("returns false for failed cached value", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (_key: string) => Effect.fail("error")
          })

          yield* Effect.exit(ScopedCache.get(cache, "test"))
          const result = yield* ScopedCache.invalidateWhen(cache, "test", () => true)

          assert.isFalse(result)
        }))

      it.effect("resource cleanup when predicate matches", () =>
        Effect.gen(function*() {
          const { cache, cleanupTracker } = yield* makeManagedResourceCache(10)

          yield* ScopedCache.get(cache, "test")
          assert.strictEqual(cleanupTracker.cleanedUp.length, 0)

          const result = yield* ScopedCache.invalidateWhen(cache, "test", (value) => value.includes("resource"))
          assert.isTrue(result)
          assert.strictEqual(cleanupTracker.cleanedUp.length, 1)
          assert.deepStrictEqual(cleanupTracker.cleanedUp, ["test"])
        }))
    })

    describe("refresh", () => {
      it.effect("refresh existing key invokes lookup again", () =>
        Effect.gen(function*() {
          let counter = 0
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (_key: string) => Effect.sync(() => ++counter)
          })

          yield* ScopedCache.get(cache, "test")
          const result = yield* ScopedCache.refresh(cache, "test")

          assert.strictEqual(result, 2)
        }))

      it.effect("refresh non-existent key invokes lookup", () =>
        Effect.gen(function*() {
          let counter = 0
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (_key: string) => Effect.sync(() => ++counter)
          })

          const result = yield* ScopedCache.refresh(cache, "test")

          assert.strictEqual(result, 1)
        }))

      it.effect("refresh updates TTL", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "test")
          yield* TestClock.adjust("30 minutes")
          yield* ScopedCache.refresh(cache, "test")
          yield* TestClock.adjust("40 minutes")

          assert.isTrue(yield* ScopedCache.has(cache, "test"))
        }))

      it.effect("concurrent refresh calls each invoke lookup independently", () =>
        Effect.gen(function*() {
          let lookupCount = 0
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: () => Effect.sync(() => ++lookupCount)
          })

          yield* ScopedCache.get(cache, "test")
          lookupCount = 0

          const results = yield* Effect.all([
            ScopedCache.refresh(cache, "test"),
            ScopedCache.refresh(cache, "test"),
            ScopedCache.refresh(cache, "test")
          ], { concurrency: "unbounded" })

          // Each refresh calls lookup independently
          assert.strictEqual(lookupCount, 3)
          assert.deepStrictEqual(results, [1, 2, 3])
        }))

      it.effect("resource cleanup for old entry after refresh", () =>
        Effect.gen(function*() {
          const { cache, cleanupTracker } = yield* makeManagedResourceCache(10)

          yield* ScopedCache.get(cache, "test")
          assert.strictEqual(cleanupTracker.cleanedUp.length, 0)

          yield* ScopedCache.refresh(cache, "test")
          assert.strictEqual(cleanupTracker.cleanedUp.length, 1)
          assert.deepStrictEqual(cleanupTracker.cleanedUp, ["test"])
        }))
    })

    describe("invalidateAll", () => {
      it.effect("clears all entries", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")
          yield* ScopedCache.get(cache, "c")

          yield* ScopedCache.invalidateAll(cache)

          assert.isFalse(yield* ScopedCache.has(cache, "a"))
          assert.isFalse(yield* ScopedCache.has(cache, "b"))
          assert.isFalse(yield* ScopedCache.has(cache, "c"))
        }))

      it.effect("size becomes 0 after invalidateAll", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")
          yield* ScopedCache.get(cache, "c")

          yield* ScopedCache.invalidateAll(cache)
          const size = yield* ScopedCache.size(cache)

          assert.strictEqual(size, 0)
        }))

      it.effect("all keys return None after invalidateAll", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")

          yield* ScopedCache.invalidateAll(cache)

          const result1 = yield* ScopedCache.getOption(cache, "a")
          const result2 = yield* ScopedCache.getOption(cache, "b")

          assert.deepStrictEqual(result1, Option.none())
          assert.deepStrictEqual(result2, Option.none())
        }))

      it.effect("resource cleanup for all entries", () =>
        Effect.gen(function*() {
          const { cache, cleanupTracker } = yield* makeManagedResourceCache(10)

          yield* ScopedCache.get(cache, "test1")
          yield* ScopedCache.get(cache, "test2")
          yield* ScopedCache.get(cache, "test3")
          assert.strictEqual(cleanupTracker.cleanedUp.length, 0)

          yield* ScopedCache.invalidateAll(cache)
          assert.strictEqual(cleanupTracker.cleanedUp.length, 3)
          assert.deepStrictEqual(cleanupTracker.cleanedUp.sort(), ["test1", "test2", "test3"])
        }))
    })
  })

  describe("utility operations", () => {
    describe("size", () => {
      it.effect("empty cache returns 0", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          const size = yield* ScopedCache.size(cache)

          assert.strictEqual(size, 0)
        }))

      it.effect("returns correct count after adding entries", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")
          yield* ScopedCache.get(cache, "c")

          const size = yield* ScopedCache.size(cache)

          assert.strictEqual(size, 3)
        }))

      it.effect("decreases after invalidation", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")
          yield* ScopedCache.get(cache, "c")
          yield* ScopedCache.invalidate(cache, "b")

          const size = yield* ScopedCache.size(cache)

          assert.strictEqual(size, 2)
        }))

      it.effect("expired entries are counted until accessed", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")

          yield* TestClock.adjust("2 hours")

          // Size still includes expired entries
          const sizeBefore = yield* ScopedCache.size(cache)
          assert.strictEqual(sizeBefore, 2)

          // Accessing expired entries removes them
          yield* ScopedCache.has(cache, "a")
          const sizeAfter = yield* ScopedCache.size(cache)
          assert.strictEqual(sizeAfter, 1)
        }))
    })

    describe("keys", () => {
      it.effect("returns all active keys", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")
          yield* ScopedCache.get(cache, "c")

          const keys = yield* ScopedCache.keys(cache)

          assert.deepStrictEqual(keys.sort(), ["a", "b", "c"])
        }))

      it.effect("excludes keys for expired entries", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")

          yield* TestClock.adjust("2 hours")

          const keys = yield* ScopedCache.keys(cache)

          assert.deepStrictEqual(keys.sort(), [])
        }))

      it.effect("empty after invalidateAll", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")
          yield* ScopedCache.invalidateAll(cache)

          const keys = yield* ScopedCache.keys(cache)

          assert.deepStrictEqual(keys, [])
        }))

      it.effect("resource cleanup for expired entries as side effect", () =>
        Effect.gen(function*() {
          const { cache, cleanupTracker } = yield* Effect.gen(function*() {
            const cleanupTracker: CleanupTracker = {
              cleanedUp: [],
              acquired: []
            }

            const cache = yield* ScopedCache.make({
              capacity: 10,
              lookup: (key: string) =>
                Effect.gen(function*() {
                  yield* Effect.acquireRelease(
                    Effect.sync(() => cleanupTracker.acquired.push(key)),
                    () => Effect.sync(() => cleanupTracker.cleanedUp.push(key))
                  )
                  return `resource-${key}`
                }),
              timeToLive: "1 hour"
            })

            return { cache, cleanupTracker }
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")
          assert.strictEqual(cleanupTracker.cleanedUp.length, 0)

          yield* TestClock.adjust("2 hours")

          // keys() should clean up expired entries as side effect
          const keys = yield* ScopedCache.keys(cache)
          assert.deepStrictEqual(keys, [])
          assert.strictEqual(cleanupTracker.cleanedUp.length, 2)
          assert.deepStrictEqual(cleanupTracker.cleanedUp.sort(), ["a", "b"])
        }))
    })

    describe("values", () => {
      it.effect("returns all successful cached values", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "ab")
          yield* ScopedCache.get(cache, "abc")

          const values = yield* ScopedCache.values(cache)
          const valuesArray = values.sort()

          assert.deepStrictEqual(valuesArray, [1, 2, 3])
        }))

      it.effect("doesn't include failed lookups", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => key === "fail" ? Effect.fail("error") : Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "a")
          yield* Effect.exit(ScopedCache.get(cache, "fail"))
          yield* ScopedCache.get(cache, "ab")

          const values = yield* ScopedCache.values(cache)
          const valuesArray = values.sort()

          assert.deepStrictEqual(valuesArray, [1, 2])
        }))

      it.effect("doesn't include expired values", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")

          yield* TestClock.adjust("2 hours")

          const values = yield* ScopedCache.values(cache)

          assert.deepStrictEqual(values, [])
        }))

      it.effect("resource cleanup for expired entries as side effect", () =>
        Effect.gen(function*() {
          const { cache, cleanupTracker } = yield* Effect.gen(function*() {
            const cleanupTracker: CleanupTracker = {
              cleanedUp: [],
              acquired: []
            }

            const cache = yield* ScopedCache.make({
              capacity: 10,
              lookup: (key: string) =>
                Effect.gen(function*() {
                  yield* Effect.acquireRelease(
                    Effect.sync(() => cleanupTracker.acquired.push(key)),
                    () => Effect.sync(() => cleanupTracker.cleanedUp.push(key))
                  )
                  return `resource-${key}`
                }),
              timeToLive: "1 hour"
            })

            return { cache, cleanupTracker }
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")
          assert.strictEqual(cleanupTracker.cleanedUp.length, 0)

          yield* TestClock.adjust("2 hours")

          // values() should clean up expired entries as side effect
          const values = yield* ScopedCache.values(cache)
          assert.deepStrictEqual(values, [])
          assert.strictEqual(cleanupTracker.cleanedUp.length, 2)
          assert.deepStrictEqual(cleanupTracker.cleanedUp.sort(), ["a", "b"])
        }))
    })

    describe("entries", () => {
      it.effect("returns all key-value pairs", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "ab")
          yield* ScopedCache.get(cache, "abc")

          const entries = yield* ScopedCache.entries(cache)
          const entriesArray = entries.sort(([a], [b]) => a.localeCompare(b))

          assert.deepStrictEqual(entriesArray, [["a", 1], ["ab", 2], ["abc", 3]])
        }))

      it.effect("filters out failed lookups", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => key === "fail" ? Effect.fail("error") : Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "a")
          yield* Effect.exit(ScopedCache.get(cache, "fail"))
          yield* ScopedCache.get(cache, "ab")

          const entries = yield* ScopedCache.entries(cache)
          const entriesArray = entries.sort(([a], [b]) => a.localeCompare(b))

          assert.deepStrictEqual(entriesArray, [["a", 1], ["ab", 2]])
        }))

      it.effect("filters out expired entries", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "a")
          yield* TestClock.adjust("1 hour")
          yield* ScopedCache.get(cache, "b")

          const entries = yield* ScopedCache.entries(cache)

          assert.deepStrictEqual(entries, [["b", 1]])
        }))

      it.effect("removes expired entries as side effect with resource cleanup", () =>
        Effect.gen(function*() {
          const { cache, cleanupTracker } = yield* Effect.gen(function*() {
            const cleanupTracker: CleanupTracker = {
              cleanedUp: [],
              acquired: []
            }

            const cache = yield* ScopedCache.make({
              capacity: 10,
              lookup: (key: string) =>
                Effect.gen(function*() {
                  yield* Effect.acquireRelease(
                    Effect.sync(() => cleanupTracker.acquired.push(key)),
                    () => Effect.sync(() => cleanupTracker.cleanedUp.push(key))
                  )
                  return `resource-${key}`
                }),
              timeToLive: "1 hour"
            })

            return { cache, cleanupTracker }
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")

          yield* TestClock.adjust("2 hours")

          const sizeBefore = yield* ScopedCache.size(cache)
          assert.strictEqual(sizeBefore, 2)

          const entriesResult = yield* ScopedCache.entries(cache)

          const sizeAfter = yield* ScopedCache.size(cache)

          // entries() should remove expired entries as side effect
          assert.strictEqual(sizeAfter, 0)
          assert.strictEqual(entriesResult.length, 0)
          assert.strictEqual(cleanupTracker.cleanedUp.length, 2)
          assert.deepStrictEqual(cleanupTracker.cleanedUp.sort(), ["a", "b"])
        }))
    })
  })

  describe("capacity management", () => {
    describe("LRU Eviction", () => {
      it.effect("oldest entries removed when capacity exceeded", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 3,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "first")
          yield* ScopedCache.get(cache, "second")
          yield* ScopedCache.get(cache, "third")

          // Check all entries are present
          assert.isTrue(yield* ScopedCache.has(cache, "first"))
          assert.isTrue(yield* ScopedCache.has(cache, "second"))
          assert.isTrue(yield* ScopedCache.has(cache, "third"))

          // Add fourth entry, should evict first (oldest)
          yield* ScopedCache.get(cache, "fourth")

          assert.isFalse(yield* ScopedCache.has(cache, "first"))
          assert.isTrue(yield* ScopedCache.has(cache, "second"))
          assert.isTrue(yield* ScopedCache.has(cache, "third"))
          assert.isTrue(yield* ScopedCache.has(cache, "fourth"))
        }))

      it.effect("access order determines eviction", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 3,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")
          yield* ScopedCache.get(cache, "c")

          // Access "a" to make it most recently used
          yield* ScopedCache.get(cache, "a")

          // Add "d" - should evict "b" (now oldest)
          yield* ScopedCache.get(cache, "d")

          assert.isTrue(yield* ScopedCache.has(cache, "a"))
          assert.isFalse(yield* ScopedCache.has(cache, "b"))
          assert.isTrue(yield* ScopedCache.has(cache, "c"))
          assert.isTrue(yield* ScopedCache.has(cache, "d"))
        }))

      it.effect("capacity of 1", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 1,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          yield* ScopedCache.get(cache, "a")
          assert.isTrue(yield* ScopedCache.has(cache, "a"))

          yield* ScopedCache.get(cache, "b")
          assert.isFalse(yield* ScopedCache.has(cache, "a"))
          assert.isTrue(yield* ScopedCache.has(cache, "b"))

          yield* ScopedCache.get(cache, "c")
          assert.isFalse(yield* ScopedCache.has(cache, "b"))
          assert.isTrue(yield* ScopedCache.has(cache, "c"))
        }))

      it.effect("very large capacity", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10000,
            lookup: (key: string) => Effect.succeed(key.length)
          })

          // Add many entries
          for (let i = 0; i < 100; i++) {
            yield* ScopedCache.get(cache, `key-${i}`)
          }

          // All should still be present
          for (let i = 0; i < 100; i++) {
            assert.isTrue(yield* ScopedCache.has(cache, `key-${i}`))
          }

          const size = yield* ScopedCache.size(cache)
          assert.strictEqual(size, 100)
        }))

      it.effect("resource cleanup for evicted entries", () =>
        Effect.gen(function*() {
          const { cache, cleanupTracker } = yield* makeManagedResourceCache(2)

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")
          assert.deepStrictEqual(cleanupTracker.cleanedUp, [])

          // Adding third entry should evict first and clean up its resource
          yield* ScopedCache.get(cache, "c")
          assert.deepStrictEqual(cleanupTracker.cleanedUp, ["a"])

          // Adding fourth entry should evict second
          yield* ScopedCache.get(cache, "d")
          assert.deepStrictEqual(cleanupTracker.cleanedUp, ["a", "b"])
        }))
    })
  })

  describe("TTL (Time To Live) Testing", () => {
    describe("Fixed TTL", () => {
      it.effect("entry expires after specified duration", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "2 hours"
          })

          yield* ScopedCache.get(cache, "test")

          yield* TestClock.adjust("1 hour")
          assert.isTrue(yield* ScopedCache.has(cache, "test"))

          yield* TestClock.adjust(Duration.sum(Duration.hours(1), Duration.seconds(1)))
          assert.isFalse(yield* ScopedCache.has(cache, "test"))
        }))

      it.effect("get after expiration invokes lookup", () =>
        Effect.gen(function*() {
          let lookupCount = 0
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (_key: string) => Effect.sync(() => ++lookupCount),
            timeToLive: "1 hour"
          })

          const result1 = yield* ScopedCache.get(cache, "test")
          assert.strictEqual(result1, 1)
          assert.strictEqual(lookupCount, 1)

          yield* TestClock.adjust("2 hours")

          const result2 = yield* ScopedCache.get(cache, "test")
          assert.strictEqual(result2, 2)
          assert.strictEqual(lookupCount, 2)
        }))

      it.effect("has returns false for expired entries", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "test")
          assert.isTrue(yield* ScopedCache.has(cache, "test"))

          yield* TestClock.adjust("2 hours")
          assert.isFalse(yield* ScopedCache.has(cache, "test"))
        }))

      it.effect("resource cleanup for expired entries", () =>
        Effect.gen(function*() {
          const { cache, cleanupTracker } = yield* Effect.gen(function*() {
            const cleanupTracker: CleanupTracker = {
              cleanedUp: [],
              acquired: []
            }

            const cache = yield* ScopedCache.make({
              capacity: 10,
              lookup: (key: string) =>
                Effect.gen(function*() {
                  yield* Effect.acquireRelease(
                    Effect.sync(() => cleanupTracker.acquired.push(key)),
                    () => Effect.sync(() => cleanupTracker.cleanedUp.push(key))
                  )
                  return `resource-${key}`
                }),
              timeToLive: "1 hour"
            })

            return { cache, cleanupTracker }
          })

          yield* ScopedCache.get(cache, "test")
          assert.deepStrictEqual(cleanupTracker.cleanedUp, [])

          yield* TestClock.adjust("2 hours")

          // Accessing expired entry should trigger cleanup
          yield* ScopedCache.has(cache, "test")
          assert.deepStrictEqual(cleanupTracker.cleanedUp, ["test"])
        }))
    })

    describe("Function-based TTL", () => {
      it.effect("different TTL for success vs failure", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.makeWith({
            capacity: 10,
            lookup: (key: string) => key === "fail" ? Effect.fail("error") : Effect.succeed(key.length),
            timeToLive: (exit) => Exit.isSuccess(exit) ? "2 hours" : "30 minutes"
          })

          yield* ScopedCache.get(cache, "success")
          yield* Effect.exit(ScopedCache.get(cache, "fail"))

          yield* TestClock.adjust("1 hour")
          assert.isTrue(yield* ScopedCache.has(cache, "success"))
          assert.isFalse(yield* ScopedCache.has(cache, "fail"))

          yield* TestClock.adjust("2 hours")
          assert.isFalse(yield* ScopedCache.has(cache, "success"))
        }))

      it.effect("TTL based on key", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.makeWith({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: (_exit, key) => {
              if (key.startsWith("short-")) return "1 minute"
              if (key.startsWith("medium-")) return "1 hour"
              return "1 day"
            }
          })

          yield* ScopedCache.get(cache, "short-item")
          yield* ScopedCache.get(cache, "medium-item")
          yield* ScopedCache.get(cache, "long-item")

          yield* TestClock.adjust("2 minutes")
          assert.isFalse(yield* ScopedCache.has(cache, "short-item"))
          assert.isTrue(yield* ScopedCache.has(cache, "medium-item"))
          assert.isTrue(yield* ScopedCache.has(cache, "long-item"))

          yield* TestClock.adjust("2 hours")
          assert.isFalse(yield* ScopedCache.has(cache, "medium-item"))
          assert.isTrue(yield* ScopedCache.has(cache, "long-item"))
        }))

      it.effect("TTL based on value", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.makeWith({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: (exit) => {
              if (Exit.isSuccess(exit)) {
                const value = exit.value
                return value > 5 ? "1 hour" : "1 minute"
              }
              return Duration.infinity
            }
          })

          yield* ScopedCache.get(cache, "abc") // length 3
          yield* ScopedCache.get(cache, "longkey") // length 7

          yield* TestClock.adjust("2 minutes")
          assert.isFalse(yield* ScopedCache.has(cache, "abc"))
          assert.isTrue(yield* ScopedCache.has(cache, "longkey"))
        }))

      it.effect("infinite TTL", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.makeWith({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: () => Duration.infinity
          })

          yield* ScopedCache.get(cache, "test")
          yield* TestClock.adjust(Duration.days(365))
          assert.isTrue(yield* ScopedCache.has(cache, "test"))
        }))

      it.effect("zero duration TTL", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.makeWith({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: () => Duration.zero
          })

          yield* ScopedCache.get(cache, "test")
          // Should expire immediately
          assert.isFalse(yield* ScopedCache.has(cache, "test"))
        }))

      it.effect("TTL updates on refresh", () =>
        Effect.gen(function*() {
          let refreshCount = 0
          const cache = yield* ScopedCache.makeWith({
            capacity: 10,
            lookup: (_key: string) => Effect.sync(() => ++refreshCount),
            timeToLive: (exit) => {
              if (Exit.isSuccess(exit)) {
                // First lookup gets 1 hour, refresh gets 2 hours
                return refreshCount === 1 ? "1 hour" : "2 hours"
              }
              return Duration.infinity
            }
          })

          yield* ScopedCache.get(cache, "test")
          yield* TestClock.adjust("30 minutes")
          assert.isTrue(yield* ScopedCache.has(cache, "test"))

          // Refresh should update TTL
          yield* ScopedCache.refresh(cache, "test")

          yield* TestClock.adjust("1 hour")
          assert.isTrue(yield* ScopedCache.has(cache, "test")) // Still alive due to 2 hour TTL

          yield* TestClock.adjust("1 hours")
          assert.isFalse(yield* ScopedCache.has(cache, "test"))
        }))
    })
  })

  describe("error scenarios", () => {
    describe("Failed Lookups", () => {
      it.effect("failed lookup caches the failure", () =>
        Effect.gen(function*() {
          let lookupCount = 0
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (_key: string) => Effect.sync(() => ++lookupCount).pipe(Effect.flatMap(() => Effect.fail("error")))
          })

          const result1 = yield* Effect.exit(ScopedCache.get(cache, "test"))
          const result2 = yield* Effect.exit(ScopedCache.get(cache, "test"))

          assert.deepStrictEqual(result1, Exit.fail("error"))
          assert.deepStrictEqual(result2, Exit.fail("error"))
          assert.strictEqual(lookupCount, 1) // Only called once
        }))

      it.effect("subsequent gets return same failure", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (_key: string) => Effect.fail("lookup failed")
          })

          const results = yield* Effect.all([
            Effect.exit(ScopedCache.get(cache, "test")),
            Effect.exit(ScopedCache.get(cache, "test")),
            Effect.exit(ScopedCache.get(cache, "test"))
          ])

          assert.deepStrictEqual(results, [
            Exit.fail("lookup failed"),
            Exit.fail("lookup failed"),
            Exit.fail("lookup failed")
          ])
        }))

      it.effect("can refresh after failure", () =>
        Effect.gen(function*() {
          let shouldFail = true
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (_key: string) => shouldFail ? Effect.fail("error") : Effect.succeed(42)
          })

          const failResult = yield* Effect.exit(ScopedCache.get(cache, "test"))
          assert.isTrue(Exit.isFailure(failResult))

          shouldFail = false
          const successResult = yield* ScopedCache.refresh(cache, "test")
          assert.strictEqual(successResult, 42)
        }))

      it.effect("multiple fibers encountering same error", () =>
        Effect.gen(function*() {
          let lookupCount = 0
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: () =>
              Effect.delay(Duration.millis(50))(
                Effect.sync(() => ++lookupCount).pipe(Effect.flatMap(() => Effect.fail("error")))
              )
          })

          const fibers = yield* Effect.all([
            ScopedCache.get(cache, "test").pipe(Effect.exit, Effect.forkChild),
            ScopedCache.get(cache, "test").pipe(Effect.exit, Effect.forkChild),
            ScopedCache.get(cache, "test").pipe(Effect.exit, Effect.forkChild)
          ])

          yield* TestClock.adjust(Duration.millis(100))

          const results = yield* Effect.all(fibers.map(Fiber.join))

          assert.strictEqual(lookupCount, 1)
          assert.deepStrictEqual(results, [
            Exit.fail("error"),
            Exit.fail("error"),
            Exit.fail("error")
          ])
        }))

      it.effect("resource cleanup for failed entries", () =>
        Effect.gen(function*() {
          const resourceTracker: Array<string> = []

          const cache = yield* ScopedCache.make({
            capacity: 2,
            lookup: (key: string) =>
              Effect.gen(function*() {
                yield* Effect.acquireRelease(
                  Effect.sync(() => resourceTracker.push(`acquired-${key}`)),
                  () => Effect.sync(() => resourceTracker.push(`released-${key}`))
                )
                return yield* Effect.fail(`error-${key}`)
              })
          })

          yield* Effect.exit(ScopedCache.get(cache, "fail1"))
          yield* Effect.exit(ScopedCache.get(cache, "fail2"))
          assert.deepStrictEqual(resourceTracker, ["acquired-fail1", "acquired-fail2"])

          // Adding third failed entry should evict first
          yield* Effect.exit(ScopedCache.get(cache, "fail3"))
          assert.deepStrictEqual(resourceTracker, [
            "acquired-fail1",
            "acquired-fail2",
            "released-fail1",
            "acquired-fail3"
          ])
        }))
    })
  })

  describe("scope integration", () => {
    describe("resource management", () => {
      it.effect("resources are properly scoped to cache entries", () =>
        Effect.gen(function*() {
          const resourceTracker: Array<string> = []

          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: Effect.fnUntraced(function*(key: string) {
              yield* Effect.acquireRelease(
                Effect.sync(() => resourceTracker.push(`acquired-${key}`)),
                () => Effect.sync(() => resourceTracker.push(`released-${key}`))
              )
              return `value-${key}`
            })
          })

          yield* ScopedCache.get(cache, "test")
          assert.deepStrictEqual(resourceTracker, ["acquired-test"])

          // Resource is still alive while cached
          yield* ScopedCache.get(cache, "test")
          assert.deepStrictEqual(resourceTracker, ["acquired-test"])
        }))

      it.effect("resources are cleaned up when entries are evicted", () =>
        Effect.gen(function*() {
          const resourceTracker: Array<string> = []

          const cache = yield* ScopedCache.make({
            capacity: 2,
            lookup: (key: string) =>
              Effect.gen(function*() {
                yield* Effect.acquireRelease(
                  Effect.sync(() => resourceTracker.push(`acquired-${key}`)),
                  () => Effect.sync(() => resourceTracker.push(`released-${key}`))
                )
                return `value-${key}`
              })
          })

          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")
          assert.deepStrictEqual(resourceTracker, ["acquired-a", "acquired-b"])

          // Adding third entry should evict first and clean up its resource
          yield* ScopedCache.get(cache, "c")
          assert.deepStrictEqual(resourceTracker, ["acquired-a", "acquired-b", "released-a", "acquired-c"])
        }))

      it.effect("resources are cleaned up when cache is closed", () =>
        Effect.gen(function*() {
          const resourceTracker: Array<string> = []
          const cache = yield* Effect.scoped(
            Effect.gen(function*() {
              const cache = yield* ScopedCache.make({
                capacity: 10,
                lookup: (key: string) =>
                  Effect.gen(function*() {
                    yield* Effect.acquireRelease(
                      Effect.sync(() => resourceTracker.push(`acquired-${key}`)),
                      () => Effect.sync(() => resourceTracker.push(`released-${key}`))
                    )
                    return `value-${key}`
                  })
              })

              yield* ScopedCache.get(cache, "test1")
              yield* ScopedCache.get(cache, "test2")
              assert.deepStrictEqual(resourceTracker, ["acquired-test1", "acquired-test2"])

              return cache
            })
          )

          // After scope closes, all resources should be cleaned up
          assert.strictEqual(cache.state._tag, "Closed")
          assert.deepStrictEqual(resourceTracker.sort(), [
            "acquired-test1",
            "acquired-test2",
            "released-test1",
            "released-test2"
          ])
        }))

      it.effect("resources are cleaned up when entries expire", () =>
        Effect.gen(function*() {
          const resourceTracker: Array<string> = []

          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) =>
              Effect.gen(function*() {
                yield* Effect.acquireRelease(
                  Effect.sync(() => resourceTracker.push(`acquired-${key}`)),
                  () => Effect.sync(() => resourceTracker.push(`released-${key}`))
                )
                return `value-${key}`
              }),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "test")
          assert.deepStrictEqual(resourceTracker, ["acquired-test"])

          yield* TestClock.adjust("2 hours")

          // Accessing expired entry should clean up resource
          yield* ScopedCache.has(cache, "test")
          assert.deepStrictEqual(resourceTracker, ["acquired-test", "released-test"])
        }))

      it.effect("resources are cleaned up when entries are invalidated", () =>
        Effect.gen(function*() {
          const resourceTracker: Array<string> = []

          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) =>
              Effect.gen(function*() {
                yield* Effect.acquireRelease(
                  Effect.sync(() => resourceTracker.push(`acquired-${key}`)),
                  () => Effect.sync(() => resourceTracker.push(`released-${key}`))
                )
                return `value-${key}`
              })
          })

          yield* ScopedCache.get(cache, "test")
          assert.deepStrictEqual(resourceTracker, ["acquired-test"])

          yield* ScopedCache.invalidate(cache, "test")
          assert.deepStrictEqual(resourceTracker, ["acquired-test", "released-test"])
        }))

      it.effect("resource cleanup is atomic and doesn't leak", () =>
        Effect.gen(function*() {
          const resourceTracker: Array<string> = []
          const cache = yield* ScopedCache.make({
            capacity: 3,
            lookup: (key: string) =>
              Effect.gen(function*() {
                yield* Effect.acquireRelease(
                  Effect.sync(() => resourceTracker.push(`acquired-${key}`)),
                  () => Effect.sync(() => resourceTracker.push(`released-${key}`))
                )
                return `value-${key}`
              })
          })

          // Fill cache
          yield* ScopedCache.get(cache, "a")
          yield* ScopedCache.get(cache, "b")
          yield* ScopedCache.get(cache, "c")

          // Multiple operations that should trigger cleanup
          yield* Effect.all([
            ScopedCache.invalidate(cache, "a"),
            ScopedCache.refresh(cache, "b"),
            ScopedCache.set(cache, "c", "new-value"),
            ScopedCache.get(cache, "d")
          ], { concurrency: "unbounded" })

          // Verify all resources were properly acquired and released
          const acquired = resourceTracker.filter((r) => r.startsWith("acquired")).length
          const released = resourceTracker.filter((r) => r.startsWith("released")).length

          assert.strictEqual(acquired, 5)
          assert.strictEqual(released, 3)
          assert.strictEqual(yield* ScopedCache.size(cache), 3)
        }))
    })

    describe("scope lifecycle", () => {
      it.effect("cache becomes closed when parent scope is closed", () =>
        Effect.gen(function*() {
          const cache = yield* Effect.scoped(
            Effect.gen(function*() {
              const cache = yield* ScopedCache.make({
                capacity: 10,
                lookup: (key: string) => Effect.succeed(key.length)
              })
              assert.strictEqual(cache.state._tag, "Open")
              yield* ScopedCache.get(cache, "test")
              return cache
            })
          )

          // After scope closure, cache should be closed
          assert.strictEqual(cache.state._tag, "Closed")
        }))

      it.effect("resource cleanup happens in correct order during cache closure", () =>
        Effect.gen(function*() {
          const cleanupOrder: Array<string> = []

          yield* Effect.scoped(
            Effect.gen(function*() {
              const cache = yield* ScopedCache.make({
                capacity: 10,
                lookup: (key: string) =>
                  Effect.gen(function*() {
                    yield* Effect.acquireRelease(
                      Effect.sync(() => cleanupOrder.push(`acquired-${key}`)),
                      () => Effect.sync(() => cleanupOrder.push(`released-${key}`))
                    )
                    return `value-${key}`
                  })
              })

              yield* ScopedCache.get(cache, "first")
              yield* ScopedCache.get(cache, "second")
              yield* ScopedCache.get(cache, "third")

              assert.deepStrictEqual(cleanupOrder, ["acquired-first", "acquired-second", "acquired-third"])
            })
          )

          // All resources should be cleaned up after scope closure
          const releases = cleanupOrder.filter((item) => item.startsWith("released"))
          assert.strictEqual(releases.length, 3)
          assert.isTrue(releases.includes("released-first"))
          assert.isTrue(releases.includes("released-second"))
          assert.isTrue(releases.includes("released-third"))
        }))
    })
  })

  // TODO: Service context tests are commented out due to TypeScript issues with Context
  // These tests are failing due to complex service dependency injection features
  // that may not be fully implemented in ScopedCache
  describe("service context", () => {
    describe("Service Dependency Injection", () => {
      it.effect("services are available in lookup functions", () =>
        Effect.gen(function*() {
          class ConfigService extends Context.Service<ConfigService, { multiplier: number }>()("ConfigService") {}

          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) =>
              Effect.gen(function*() {
                const config = yield* ConfigService
                return key.length * config.multiplier
              })
          }).pipe(
            Effect.provideService(ConfigService, ConfigService.of({ multiplier: 10 }))
          )

          const result = yield* ScopedCache.get(cache, "test")

          assert.strictEqual(result, 40) // "test".length * 10
        }))

      it.effect("requireServicesAt: 'lookup' provides services at lookup time", () =>
        Effect.gen(function*() {
          class CounterService extends Context.Service<CounterService, { value: number }>()("CounterService") {}

          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (_key: string) => Effect.map(CounterService, (service) => service.value),
            requireServicesAt: "lookup"
          })

          const result1 = yield* ScopedCache.get(cache, "test").pipe(
            Effect.provideService(CounterService, CounterService.of({ value: 42 }))
          )

          const result2 = yield* ScopedCache.get(cache, "another").pipe(
            Effect.provideService(CounterService, CounterService.of({ value: 100 }))
          )

          // Same key with different service context
          const result3 = yield* ScopedCache.get(cache, "test").pipe(
            Effect.provideService(CounterService, CounterService.of({ value: 200 }))
          )

          assert.strictEqual(result1, 42)
          assert.strictEqual(result2, 100)
          assert.strictEqual(result3, 42) // Cached value from first lookup
        }))
    })
  })

  describe("integration tests", () => {
    describe("TestClock Integration", () => {
      it.effect("multiple time advances work correctly", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.length),
            timeToLive: "1 hour"
          })

          yield* ScopedCache.get(cache, "test")

          // Multiple small advances
          for (let i = 0; i < 10; i++) {
            yield* TestClock.adjust("5 minutes")
            assert.isTrue(yield* ScopedCache.has(cache, "test"))
          }

          // Final advance to expire
          yield* TestClock.adjust("15 minutes")
          assert.isFalse(yield* ScopedCache.has(cache, "test"))
        }))

      it.effect("resource cleanup timing is correct", () =>
        Effect.gen(function*() {
          const cleanupTimes: Array<{ key: string; time: number }> = []

          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) =>
              Effect.gen(function*() {
                yield* Effect.acquireRelease(
                  Effect.void,
                  () =>
                    Effect.gen(function*() {
                      const time = yield* Clock.currentTimeMillis
                      cleanupTimes.push({ key, time })
                    })
                )
                return key.length
              }),
            timeToLive: "1 hour"
          })

          const startTime = yield* Clock.currentTimeMillis

          yield* ScopedCache.get(cache, "a")
          yield* TestClock.adjust("30 minutes")
          yield* ScopedCache.get(cache, "b")

          yield* TestClock.adjust("35 minutes") // "a" should expire
          yield* ScopedCache.has(cache, "a") // Trigger cleanup

          yield* TestClock.adjust("30 minutes") // "b" should expire
          yield* ScopedCache.has(cache, "b") // Trigger cleanup

          assert.strictEqual(cleanupTimes.length, 2)
          assert.strictEqual(cleanupTimes[0].key, "a")
          assert.strictEqual(cleanupTimes[0].time, startTime + 65 * 60 * 1000)
          assert.strictEqual(cleanupTimes[1].key, "b")
          assert.strictEqual(cleanupTimes[1].time, startTime + 95 * 60 * 1000)
        }))
    })

    describe("Different Key Types", () => {
      it.effect("string keys work correctly", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) => Effect.succeed(key.toUpperCase())
          })

          yield* ScopedCache.set(cache, "hello", "HELLO!")
          const result1 = yield* ScopedCache.get(cache, "hello")
          const result2 = yield* ScopedCache.get(cache, "world")

          assert.strictEqual(result1, "HELLO!")
          assert.strictEqual(result2, "WORLD")
        }))

      it.effect("number keys work correctly", () =>
        Effect.gen(function*() {
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: number) => Effect.succeed(key * key)
          })

          yield* ScopedCache.set(cache, 5, 25)
          const result1 = yield* ScopedCache.get(cache, 5)
          const result2 = yield* ScopedCache.get(cache, 7)

          assert.strictEqual(result1, 25)
          assert.strictEqual(result2, 49)
        }))

      it.effect("complex object keys work correctly", () =>
        Effect.gen(function*() {
          class CacheKey extends Data.Class<{
            userId: string
            productId: number
          }> {}

          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: CacheKey) => Effect.succeed(`${key.userId}-${key.productId}`)
          })

          const key1 = new CacheKey({ userId: "user1", productId: 100 })
          const key2 = new CacheKey({ userId: "user2", productId: 200 })

          const result1 = yield* ScopedCache.get(cache, key1)
          const result2 = yield* ScopedCache.get(cache, key2)

          assert.strictEqual(result1, "user1-100")
          assert.strictEqual(result2, "user2-200")

          const key1Copy = new CacheKey({ userId: "user1", productId: 100 })
          const hasCopy = yield* ScopedCache.has(cache, key1Copy)
          assert.isTrue(hasCopy)
        }))
    })
  })

  describe("concurrency tests", () => {
    describe("Concurrent Resource Access", () => {
      it.effect("concurrent gets share same resource", () =>
        Effect.gen(function*() {
          let resourceCreationCount = 0
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) =>
              Effect.delay(Duration.millis(100))(
                Effect.sync(() => {
                  resourceCreationCount++
                  return `resource-${key}-${resourceCreationCount}`
                })
              )
          })

          const fibers = yield* Effect.all([
            ScopedCache.get(cache, "shared").pipe(Effect.forkChild),
            ScopedCache.get(cache, "shared").pipe(Effect.forkChild),
            ScopedCache.get(cache, "shared").pipe(Effect.forkChild),
            ScopedCache.get(cache, "shared").pipe(Effect.forkChild)
          ])

          yield* TestClock.adjust(Duration.millis(150))
          const results = yield* Effect.all(fibers.map(Fiber.join))

          assert.strictEqual(resourceCreationCount, 1)
          assert.isTrue(results.every((r) => r === "resource-shared-1"))
        }))

      it.effect("concurrent invalidations don't cause resource leaks", () =>
        Effect.gen(function*() {
          const activeResources: Set<string> = new Set()

          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) =>
              Effect.gen(function*() {
                const resourceId = `${key}-${Date.now()}-${Math.random()}`

                yield* Effect.acquireRelease(
                  Effect.sync(() => activeResources.add(resourceId)),
                  () => Effect.sync(() => activeResources.delete(resourceId))
                )

                return resourceId
              })
          })

          // Create initial resources
          yield* ScopedCache.get(cache, "key1")
          yield* ScopedCache.get(cache, "key2")
          yield* ScopedCache.get(cache, "key3")
          assert.strictEqual(activeResources.size, 3)

          // Concurrent invalidations
          yield* Effect.all([
            ScopedCache.invalidate(cache, "key1"),
            ScopedCache.invalidate(cache, "key2"),
            ScopedCache.invalidate(cache, "key3"),
            ScopedCache.invalidate(cache, "key1"), // Duplicate
            ScopedCache.invalidate(cache, "key2") // Duplicate
          ], { concurrency: "unbounded" })

          assert.strictEqual(activeResources.size, 0)
        }))

      it.effect("concurrent refresh operations don't interfere", () =>
        Effect.gen(function*() {
          let counter = 0
          const cache = yield* ScopedCache.make({
            capacity: 10,
            lookup: (key: string) =>
              Effect.delay(Duration.millis(50))(
                Effect.sync(() => `${key}-${++counter}`)
              )
          })

          // Initial population
          yield* ScopedCache.get(cache, "a").pipe(Effect.forkChild)
          yield* ScopedCache.get(cache, "b").pipe(Effect.forkChild)
          yield* TestClock.adjust(50)
          counter = 0 // Reset counter

          // Concurrent refreshes
          const fibers = yield* Effect.all([
            ScopedCache.refresh(cache, "a").pipe(Effect.forkChild),
            ScopedCache.refresh(cache, "b").pipe(Effect.forkChild),
            ScopedCache.refresh(cache, "a").pipe(Effect.forkChild),
            ScopedCache.refresh(cache, "b").pipe(Effect.forkChild)
          ])

          yield* TestClock.adjust(Duration.millis(100))
          const results = yield* Effect.all(fibers.map(Fiber.join))

          // Each refresh should get a unique value
          assert.deepStrictEqual(results, [
            "a-1", // First refresh
            "b-2", // Second refresh
            "a-3", // Third refresh
            "b-4" // Fourth refresh
          ])
          assert.strictEqual(counter, 4)
        }))
    })
  })
})

// Helper functions for testing
const makeScopedTestCache = (capacity: number, ttl?: Duration.Input) =>
  Effect.gen(function*() {
    let lookupCount = 0
    const lookupResults = new Map<string, Effect.Effect<number, string>>()

    const cache = yield* ScopedCache.make({
      capacity,
      lookup: (key: string) => {
        lookupCount++
        return lookupResults.get(key) ?? Effect.fail(`Key not found: ${key}`)
      },
      timeToLive: ttl
    })

    return {
      cache,
      lookupCount: () => lookupCount,
      resetLookupCount: () => {
        lookupCount = 0
      },
      setLookupResult: (key: string, result: Effect.Effect<number, string>) => {
        lookupResults.set(key, result)
      }
    }
  })

// Helper for tracking resource cleanup
interface CleanupTracker {
  cleanedUp: Array<string>
  acquired: Array<string>
}

const makeManagedResourceCache = (capacity: number) =>
  Effect.gen(function*() {
    const cleanupTracker: CleanupTracker = {
      cleanedUp: [],
      acquired: []
    }

    const cache = yield* ScopedCache.make({
      capacity,
      lookup: (key: string) =>
        Effect.gen(function*() {
          yield* Effect.acquireRelease(
            Effect.sync(() => cleanupTracker.acquired.push(key)),
            () => Effect.sync(() => cleanupTracker.cleanedUp.push(key))
          )
          return `resource-${key}`
        })
    })

    return { cache, cleanupTracker }
  })
