import { D1Client } from "@effect/sql-d1"
import { assert, describe, it } from "@effect/vitest"
import { Cause, Effect, Iterable } from "effect"
import * as Schema from "effect/Schema"
import { SqlError, SqlResolver } from "effect/unstable/sql"
import { D1Miniflare } from "./utils.ts"

const seededClient = Effect.gen(function*() {
  const sql = yield* D1Client.D1Client
  yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
  for (const id of Iterable.range(1, 100)) {
    yield* sql`INSERT INTO test ${sql.insert({ id, name: `name${id}` })}`
  }
  return sql
})

describe("Resolver", () => {
  describe("ordered", () => {
    it.effect("insert", () =>
      Effect.gen(function*() {
        const batches: Array<Array<string>> = []
        const sql = yield* seededClient
        const Insert = SqlResolver.ordered({
          Request: Schema.String,
          Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
          execute: (names) => {
            batches.push(names)
            return sql`INSERT INTO test ${sql.insert(names.map((name) => ({ name })))} RETURNING *`
          }
        })
        const execute = SqlResolver.request(Insert)
        assert.deepStrictEqual(
          yield* Effect.all({
            one: execute("one"),
            two: SqlResolver.request("two", Insert)
          }, { concurrency: "unbounded" }),
          {
            one: { id: 101, name: "one" },
            two: { id: 102, name: "two" }
          }
        )
        assert.deepStrictEqual(batches, [["one", "two"]])
      }).pipe(Effect.provide(D1Miniflare.layerClient)))

    it.effect("result length mismatch", () =>
      Effect.gen(function*() {
        const batches: Array<Array<number>> = []
        const sql = yield* seededClient
        const Select = SqlResolver.ordered({
          Request: Schema.Number,
          Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
          execute: (ids) => {
            batches.push(ids)
            return sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}`
          }
        })
        const execute = SqlResolver.request(Select)
        const error = yield* Effect.all([
          execute(1),
          execute(2),
          execute(3),
          execute(101)
        ], { concurrency: "unbounded" }).pipe(
          Effect.flip
        )
        assert(error instanceof SqlError.ResultLengthMismatch)
        assert.strictEqual(error.actual, 3)
        assert.strictEqual(error.expected, 4)
        assert.deepStrictEqual(batches, [[1, 2, 3, 101]])
      }).pipe(Effect.provide(D1Miniflare.layerClient)))
  })

  describe("grouped", () => {
    it.effect("find by name", () =>
      Effect.gen(function*() {
        const sql = yield* seededClient
        const FindByName = SqlResolver.grouped({
          Request: Schema.String,
          RequestGroupKey: (name) => name,
          Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
          ResultGroupKey: (result) => result.name,
          execute: (names) => sql`SELECT * FROM test WHERE name IN ${sql.in(names)}`
        })
        const execute = SqlResolver.request(FindByName)
        yield* sql`INSERT INTO test ${sql.insert({ name: "name1" })}`
        assert.deepStrictEqual(
          yield* Effect.all({
            one: execute("name1"),
            two: execute("name2"),
            three: Effect.flip(execute("name0"))
          }, { concurrency: "unbounded" }),
          {
            one: [{ id: 1, name: "name1" }, { id: 101, name: "name1" }],
            two: [{ id: 2, name: "name2" }],
            three: new Cause.NoSuchElementError()
          }
        )
      }).pipe(Effect.provide(D1Miniflare.layerClient)))

    it.effect("using raw rows", () =>
      Effect.gen(function*() {
        const sql = yield* seededClient
        const FindByName = SqlResolver.grouped({
          Request: Schema.String,
          RequestGroupKey: (name) => name,
          Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
          ResultGroupKey: (_, result: any) => result.name,
          execute: (names) => sql`SELECT * FROM test WHERE name IN ${sql.in(names)}`
        })
        const execute = SqlResolver.request(FindByName)
        yield* sql`INSERT INTO test ${sql.insert({ name: "name1" })}`
        assert.deepStrictEqual(
          yield* Effect.all({
            one: execute("name1"),
            two: execute("name2"),
            three: Effect.flip(execute("name0"))
          }, { concurrency: "unbounded" }),
          {
            one: [{ id: 1, name: "name1" }, { id: 101, name: "name1" }],
            two: [{ id: 2, name: "name2" }],
            three: new Cause.NoSuchElementError()
          }
        )
      }).pipe(Effect.provide(D1Miniflare.layerClient)))
  })

  describe("findById", () => {
    it.effect("find by id", () =>
      Effect.gen(function*() {
        const sql = yield* seededClient
        const FindById = SqlResolver.findById({
          Id: Schema.Number,
          Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
          ResultId: (result) => result.id,
          execute: (ids) => sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}`
        })
        const execute = SqlResolver.request(FindById)
        assert.deepStrictEqual(
          yield* Effect.all({
            one: execute(1),
            two: execute(2),
            three: Effect.flip(execute(101))
          }, { concurrency: "unbounded" }),
          {
            one: { id: 1, name: "name1" },
            two: { id: 2, name: "name2" },
            three: new Cause.NoSuchElementError()
          }
        )
      }).pipe(Effect.provide(D1Miniflare.layerClient)))

    it.effect("using raw rows", () =>
      Effect.gen(function*() {
        const sql = yield* seededClient
        const FindById = SqlResolver.findById({
          Id: Schema.Number,
          Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
          ResultId: (_, result: any) => result.id,
          execute: (ids) => sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}`
        })
        const execute = SqlResolver.request(FindById)
        assert.deepStrictEqual(
          yield* Effect.all({
            one: execute(1),
            two: execute(2),
            three: Effect.flip(execute(101))
          }, { concurrency: "unbounded" }),
          {
            one: { id: 1, name: "name1" },
            two: { id: 2, name: "name2" },
            three: new Cause.NoSuchElementError()
          }
        )
      }).pipe(Effect.provide(D1Miniflare.layerClient)))
  })
})
