import { assert, describe, it } from "@effect/vitest"
import type { Layer } from "effect"
import { Context, Data, Effect, Exit, Result, Schema } from "effect"
import { Persistable, PersistedCache, Persistence } from "effect/unstable/persistence"

class User extends Schema.Class<User>("User")({
  id: Schema.Number,
  name: Schema.String
}) {}

class TTLRequest extends Persistable.Class<{
  payload: { id: number }
}>()("TTLRequest", {
  primaryKey: (req) => `TTLRequest:${req.id}`,
  success: User,
  error: Schema.String
}) {}

export class TransientError extends Data.TaggedError("TransientError") {}

class LookupService extends Context.Service<LookupService, { readonly value: string }>()("LookupService") {}

export const suite = (storeId: string, layer: Layer.Layer<Persistence.Persistence, unknown>) =>
  describe(`PersistedCache (${storeId})`, { timeout: 30_000 }, () => {
    it.effect("smoke test", () =>
      Effect.gen(function*() {
        const persistence = yield* Persistence.Persistence
        const store = yield* persistence.make({ storeId: "users" })
        let invocations = 0
        let cache = yield* PersistedCache.make((req: TTLRequest) =>
          Effect.sync(() => {
            invocations++
            return new User({ id: req.id, name: "John" })
          }), {
          storeId: "users",
          timeToLive: () => 5000
        })
        const user = yield* cache.get(new TTLRequest({ id: 1 }))
        assert.deepStrictEqual(user, new User({ id: 1, name: "John" }))
        assert.deepStrictEqual(
          yield* store.get(new TTLRequest({ id: 1 })),
          Exit.succeed(new User({ id: 1, name: "John" }))
        )
        assert.strictEqual(invocations, 1)
        assert.deepStrictEqual(yield* cache.get(new TTLRequest({ id: 1 })), new User({ id: 1, name: "John" }))
        assert.strictEqual(invocations, 1)

        cache = yield* PersistedCache.make((req: TTLRequest) =>
          Effect.sync(() => {
            invocations++
            return new User({ id: req.id, name: "John" })
          }), {
          storeId: "users",
          timeToLive: (_req, _exit) => 5000
        })
        assert.deepStrictEqual(yield* cache.get(new TTLRequest({ id: 1 })), new User({ id: 1, name: "John" }))
        assert.strictEqual(invocations, 1)

        yield* store.setMany([
          [new TTLRequest({ id: 1 }), Exit.fail("Not found")],
          [new TTLRequest({ id: 2 }), Exit.succeed(new User({ id: 2, name: "Jane" }))]
        ])
        const results = yield* store.getMany([new TTLRequest({ id: 1 }), new TTLRequest({ id: 2 })])
        assert.deepStrictEqual(results, [
          Exit.fail("Not found"),
          Exit.succeed(new User({ id: 2, name: "Jane" }))
        ])
      }).pipe(
        Effect.provide(layer),
        Effect.catchFilter((e) => e instanceof TransientError ? Result.succeed(e) : Result.fail(e), () => Effect.void),
        flakyTest
      ))

    it.effect("requireServicesAt: 'lookup' requires lookup services at get-time", () =>
      Effect.gen(function*() {
        const cache = yield* PersistedCache.make(
          (req: TTLRequest) => Effect.map(LookupService, (service) => new User({ id: req.id, name: service.value })),
          {
            storeId: `${storeId}-require-services-at`,
            timeToLive: () => 5000,
            requireServicesAt: "lookup"
          }
        )

        const result1 = yield* cache.get(new TTLRequest({ id: 1 })).pipe(
          Effect.provideService(LookupService, LookupService.of({ value: "first" }))
        )
        const result2 = yield* cache.get(new TTLRequest({ id: 2 })).pipe(
          Effect.provideService(LookupService, LookupService.of({ value: "second" }))
        )
        const result3 = yield* cache.get(new TTLRequest({ id: 1 })).pipe(
          Effect.provideService(LookupService, LookupService.of({ value: "third" }))
        )

        assert.deepStrictEqual(result1, new User({ id: 1, name: "first" }))
        assert.deepStrictEqual(result2, new User({ id: 2, name: "second" }))
        assert.deepStrictEqual(result3, new User({ id: 1, name: "first" }))
      }).pipe(
        Effect.provide(layer),
        Effect.catchFilter((e) => e instanceof TransientError ? Result.succeed(e) : Result.fail(e), () => Effect.void),
        flakyTest
      ))
  })

const flakyTest = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
  effect.pipe(
    Effect.timeoutOrElse({
      duration: "10 seconds",
      orElse: () => Effect.void
    })
  )
