import { IndexedDb, IndexedDbDatabase, IndexedDbTable, IndexedDbVersion } from "@effect/platform-browser"
import { afterEach, assert, describe, it } from "@effect/vitest"
import {
  Array,
  Context,
  DateTime,
  Effect,
  Fiber,
  Layer,
  Option,
  Schema,
  SchemaGetter,
  SchemaIssue,
  Stream
} from "effect"
import { IDBKeyRange, indexedDB } from "fake-indexeddb"

const databaseName = "db"

const layerFakeIndexedDb = Layer.succeed(
  IndexedDb.IndexedDb,
  IndexedDb.make({ indexedDB, IDBKeyRange })
)

const provideDb = (database: IndexedDbDatabase.Any) =>
  Effect.provide(
    database.layer(databaseName).pipe(Layer.provide(layerFakeIndexedDb))
  )

afterEach(() => {
  indexedDB.deleteDatabase(databaseName)
})

class Table1 extends IndexedDbTable.make({
  name: "todo",
  schema: Schema.Struct({
    id: Schema.Number,
    title: Schema.String,
    count: Schema.Number,
    completed: Schema.Boolean
  }),
  keyPath: "id",
  indexes: { titleIndex: "title", countIndex: "count", titleCount: ["title", "count"] }
}) {}

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

class ProductSchema extends Schema.Class<ProductSchema>("ProductSchema")({
  key: IndexedDb.AutoIncrement,
  name: Schema.String,
  price: Schema.Number
}) {}

class VerifyContext extends Context.Service<
  VerifyContext,
  { readonly maxLength: number }
>()("VerifyContext") {}

const VerifyId = Schema.String.pipe(
  Schema.decodeTo(Schema.String, {
    encode: SchemaGetter.transformOrFail((s) =>
      Effect.gen(function*() {
        const { maxLength } = yield* VerifyContext
        if (s.length > maxLength) {
          return yield* Effect.fail(
            new SchemaIssue.InvalidValue(Option.some(s), {
              message: "Max length exceeded"
            })
          )
        }
        return s
      })
    ),
    decode: SchemaGetter.String()
  })
)

const Table2 = IndexedDbTable.make({
  name: "user",
  schema: User,
  keyPath: "id"
})

const Table3 = IndexedDbTable.make({
  name: "product",
  schema: ProductSchema,
  keyPath: "price"
})

const Table4 = IndexedDbTable.make({
  name: "price",
  schema: Schema.Struct({
    id: IndexedDb.AutoIncrement,
    amount: Schema.Number
  }),
  keyPath: "id",
  autoIncrement: true
})

const Table5 = IndexedDbTable.make({
  name: "person",
  schema: Schema.Struct({
    firstName: Schema.String,
    lastName: Schema.String,
    age: Schema.Number
  }),
  keyPath: ["firstName", "lastName"]
})

const Table6 = IndexedDbTable.make({
  name: "user-verify",
  schema: Schema.Struct({ id: VerifyId }),
  keyPath: "id"
})

const Table7 = IndexedDbTable.make({
  name: "no-keypath",
  schema: Schema.Struct({ username: Schema.String, index: Schema.Number })
})

class V1 extends IndexedDbVersion.make(
  Table1,
  Table2,
  Table3,
  Table4,
  Table5,
  Table6,
  Table7
) {}

describe.sequential("IndexedDbQueryBuilder", () => {
  describe("select", () => {
    it.effect("select", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api
            .from("todo")
            .insert({ id: 1, title: "test", count: 1, completed: false })
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const from = api.from("todo")
        const select = from.select()
        const data = yield* select

        assert.equal(from.table.tableName, "todo")
        assert.equal(select.index, undefined)
        assert.deepStrictEqual(select.from, from)
        assert.deepStrictEqual(select.from.IDBKeyRange, IDBKeyRange)
        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [
          { id: 1, title: "test", count: 1, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("select with index", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api
            .from("todo")
            .insert({ id: 2, title: "test2", count: 2, completed: false })
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db
        const from = api.from("todo")
        const select = from.select("titleIndex")
        const data = yield* select

        assert.equal(from.table.tableName, "todo")
        assert.equal(select.index, "titleIndex")
        assert.deepStrictEqual(select.from, from)
        assert.deepStrictEqual(select.from.IDBKeyRange, IDBKeyRange)
        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [
          { id: 2, title: "test2", count: 2, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("select equals", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false },
            { id: 3, title: "test3", count: 3, completed: false }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        {
          const api = yield* Db
          const from = api.from("todo")
          const select = from.select()
          const equals = select.equals(2)
          const data = yield* equals

          assert.equal(from.table.tableName, "todo")
          assert.equal(equals.index, undefined)
          assert.deepStrictEqual(equals.from, from)
          assert.deepStrictEqual(equals.from.IDBKeyRange, IDBKeyRange)
          assert.equal(equals.only, 2)
          assert.equal(data.length, 1)
          assert.deepStrictEqual(data, [
            { id: 2, title: "test2", count: 2, completed: false }
          ])
        }
      }).pipe(provideDb(Db))
    })

    it.effect("select equals with index", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false },
            { id: 3, title: "test3", count: 3, completed: false }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        {
          const api = yield* Db
          const from = api.from("todo")
          const select = from.select("titleIndex")
          const equals = select.equals("test3")
          const data = yield* equals

          assert.equal(from.table.tableName, "todo")
          assert.equal(equals.index, "titleIndex")
          assert.deepStrictEqual(equals.from, from)
          assert.deepStrictEqual(equals.from.IDBKeyRange, IDBKeyRange)
          assert.equal(equals.only, "test3")
          assert.equal(data.length, 1)
          assert.deepStrictEqual(data, [
            { id: 3, title: "test3", count: 3, completed: false }
          ])
        }
      }).pipe(provideDb(Db))
    })

    it.effect("select gte", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false },
            { id: 3, title: "test3", count: 3, completed: false }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const data = yield* api.from("todo").select().gte(2)

        assert.equal(data.length, 2)
        assert.deepStrictEqual(data, [
          { id: 2, title: "test2", count: 2, completed: false },
          { id: 3, title: "test3", count: 3, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("select gte with index", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false },
            { id: 3, title: "test3", count: 3, completed: false }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const data = yield* api.from("todo").select("countIndex").gte(3)

        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [
          { id: 3, title: "test3", count: 3, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("select lte", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false },
            { id: 3, title: "test3", count: 3, completed: false }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        {
          const api = yield* Db.getQueryBuilder
          const data = yield* api.from("todo").select().lte(2)

          assert.equal(data.length, 2)
          assert.deepStrictEqual(data, [
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false }
          ])
        }
      }).pipe(provideDb(Db))
    })

    it.effect("select gt", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false },
            { id: 3, title: "test3", count: 3, completed: false }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const data = yield* api.from("todo").select().gt(2)

        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [
          { id: 3, title: "test3", count: 3, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("select lt", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false },
            { id: 3, title: "test3", count: 3, completed: false }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const data = yield* api.from("todo").select().lt(2)

        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [
          { id: 1, title: "test1", count: 1, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("select between", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false },
            { id: 3, title: "test3", count: 3, completed: false },
            { id: 4, title: "test4", count: 4, completed: false },
            { id: 5, title: "test5", count: 5, completed: false }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const data = yield* api.from("todo").select().between(2, 3)

        assert.equal(data.length, 2)
        assert.deepStrictEqual(data, [
          { id: 2, title: "test2", count: 2, completed: false },
          { id: 3, title: "test3", count: 3, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("select between with exclude", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false },
            { id: 3, title: "test3", count: 3, completed: false },
            { id: 4, title: "test4", count: 4, completed: false },
            { id: 5, title: "test5", count: 5, completed: false }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const data = yield* api.from("todo").select().between(2, 4, {
          excludeLowerBound: true,
          excludeUpperBound: true
        })

        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [
          { id: 3, title: "test3", count: 3, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("select limit", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false },
            { id: 3, title: "test3", count: 3, completed: false },
            { id: 4, title: "test4", count: 4, completed: false },
            { id: 5, title: "test5", count: 5, completed: false }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const data = yield* api.from("todo").select().limit(2)

        assert.equal(data.length, 2)
        assert.deepStrictEqual(data, [
          { id: 1, title: "test1", count: 1, completed: false },
          { id: 2, title: "test2", count: 2, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("select offset", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false },
            { id: 3, title: "test3", count: 3, completed: false },
            { id: 4, title: "test4", count: 4, completed: false },
            { id: 5, title: "test5", count: 5, completed: false }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const data = yield* api.from("todo").select().limit(2).offset(2)

        assert.equal(data.length, 2)
        assert.deepStrictEqual(data, [
          { id: 3, title: "test3", count: 3, completed: false },
          { id: 4, title: "test4", count: 4, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("select limit with filters", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false },
            { id: 3, title: "test3", count: 3, completed: false },
            { id: 4, title: "test4", count: 4, completed: false },
            { id: 5, title: "test5", count: 5, completed: false }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const data = yield* api
          .from("todo")
          .select("countIndex")
          .gte(2)
          .limit(2)

        assert.equal(data.length, 2)
        assert.deepStrictEqual(data, [
          { id: 2, title: "test2", count: 2, completed: false },
          { id: 3, title: "test3", count: 3, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("select first", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false },
            { id: 3, title: "test3", count: 3, completed: false }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const data = yield* api.from("todo").select().first()

        assert.deepStrictEqual(data, {
          id: 1,
          title: "test1",
          count: 1,
          completed: false
        })
      }).pipe(provideDb(Db))
    })

    it.effect("select first with filters", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 1, title: "test1", count: 1, completed: false },
            { id: 2, title: "test2", count: 2, completed: false },
            { id: 3, title: "test3", count: 3, completed: false }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const data = yield* api
          .from("todo")
          .select("titleIndex")
          .equals("test2")
          .first()

        assert.deepStrictEqual(data, {
          id: 2,
          title: "test2",
          count: 2,
          completed: false
        })
      }).pipe(provideDb(Db))
    })
  })

  describe("modify", () => {
    it.effect("insert", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKey = yield* api
          .from("todo")
          .insert({ id: 10, title: "insert1", count: 10, completed: true })
        const data = yield* api.from("todo").select()

        assert.equal(addedKey, 10)
        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [
          { id: 10, title: "insert1", count: 10, completed: true }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("insert schema with context", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("user-verify")
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKey = yield* api.from("user-verify").insert({ id: "abc" })
        const data = yield* api.from("user-verify").select()

        assert.equal(addedKey, "abc")
        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [{ id: "abc" }])
      }).pipe(
        provideDb(Db),
        Effect.provideService(
          VerifyContext,
          VerifyContext.of({ maxLength: 4 })
        )
      )
    })

    it.effect("insert with manual key required", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createObjectStore("user")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const createdAt = DateTime.nowUnsafe()
        const addedKey = yield* api.from("user").insert({
          id: 10,
          name: "insert1",
          email: "insert1@example.com",
          createdAt
        })
        const addedAllKeys = yield* api.from("user").insertAll([
          {
            id: 11,
            name: "insert2",
            email: "insert2@example.com",
            createdAt
          },
          {
            id: 12,
            name: "insert3",
            email: "insert3@example.com",
            createdAt
          }
        ])
        const data = yield* api.from("user").select()

        assert.equal(addedKey, 10)
        assert.deepStrictEqual(addedAllKeys, [11, 12])
        assert.equal(data.length, 3)
        assert.deepStrictEqual(data, [
          new User({
            id: 10,
            name: "insert1",
            email: "insert1@example.com",
            createdAt
          }),
          new User({
            id: 11,
            name: "insert2",
            email: "insert2@example.com",
            createdAt
          }),
          new User({
            id: 12,
            name: "insert3",
            email: "insert3@example.com",
            createdAt
          })
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("insert with manual multiple keys required", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createObjectStore("user")
          yield* api.createObjectStore("person")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKey = yield* api.from("person").insert({
          firstName: "John",
          lastName: "Doe",
          age: 30
        })
        const data = yield* api.from("person").select()

        assert.deepStrictEqual(addedKey, ["John", "Doe"])
        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [
          { firstName: "John", lastName: "Doe", age: 30 }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("insert with manual key optional", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createObjectStore("user")
          yield* api.createObjectStore("product")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKey = yield* api.from("product").insert({
          key: 10,
          name: "insert1",
          price: 10
        })
        const data = yield* api.from("product").select()

        assert.equal(addedKey, 10)
        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [new ProductSchema({ key: 10, name: "insert1", price: 10 })])
      }).pipe(provideDb(Db))
    })

    it.effect("insert with auto-increment", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createObjectStore("user")
          yield* api.createObjectStore("product")
          yield* api.createObjectStore("price")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKey = yield* api
          .from("price")
          .insertAll([{ amount: 10 }, { amount: 20, id: 10 }, { amount: 30 }])
        const data = yield* api.from("price").select()

        assert.deepStrictEqual(addedKey, [1, 10, 11])
        assert.equal(data.length, 3)
        assert.deepStrictEqual(data, [
          { id: 1, amount: 10 },
          { id: 10, amount: 20 },
          { id: 11, amount: 30 }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("insert with auto-increment and get first", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createObjectStore("user")
          yield* api.createObjectStore("product")
          yield* api.createObjectStore("price")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKey = yield* api.from("price").insert({ amount: 10 })
        const data = yield* api.from("price").select().first()

        assert.equal(addedKey, 1)
        assert.deepStrictEqual(data, { id: 1, amount: 10 })
      }).pipe(provideDb(Db))
    })

    it.effect(
      "insert with auto-increment schema and different key path",
      () => {
        class Db extends IndexedDbDatabase.make(
          V1,
          Effect.fn(function*(api) {
            yield* api.createObjectStore("product")
          })
        ) {}

        return Effect.gen(function*() {
          const api = yield* Db.getQueryBuilder
          const addedKey = yield* api
            .from("product")
            .insert({ name: "insert1", price: 12, key: 10 })
          const data = yield* api.from("product").select()

          assert.equal(addedKey, 12)
          assert.equal(data.length, 1)
          assert.deepStrictEqual(data, [
            new ProductSchema({ key: 10, name: "insert1", price: 12 })
          ])
        }).pipe(provideDb(Db))
      }
    )

    it.effect("insert in no keypath table with manual key", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("no-keypath")
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKey = yield* api
          .from("no-keypath")
          .insert({ username: "insert1", index: 12, key: "key" })
        const data = yield* api.from("no-keypath").select()

        assert.equal(addedKey, "key")
        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [
          { username: "insert1", index: 12, key: "key" }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("insertAll in no keypath table with manual key", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("no-keypath")
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKey = yield* api.from("no-keypath").insertAll([
          { username: "insert1", index: 12, key: "key" },
          {
            username: "insert2",
            index: 13,
            key: "key2"
          }
        ])
        const data = yield* api.from("no-keypath").select()

        assert.deepStrictEqual(addedKey, ["key", "key2"])
        assert.equal(data.length, 2)
        assert.deepStrictEqual(data, [
          { username: "insert1", index: 12, key: "key" },
          {
            username: "insert2",
            index: 13,
            key: "key2"
          }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("insertAll in no keypath table and get first", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("no-keypath")
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKey = yield* api.from("no-keypath").insertAll([
          { username: "insert1", index: 12, key: "key" },
          {
            username: "insert2",
            index: 13,
            key: "key2"
          }
        ])
        const data = yield* api.from("no-keypath").select().first()

        assert.deepStrictEqual(addedKey, ["key", "key2"])
        assert.deepStrictEqual(data, {
          username: "insert1",
          index: 12,
          key: "key"
        })
      }).pipe(provideDb(Db))
    })

    it.effect("insertAll in no keypath table and get limit", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("no-keypath")
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKey = yield* api.from("no-keypath").insertAll([
          { username: "insert1", index: 12, key: "key" },
          {
            username: "insert2",
            index: 13,
            key: "key2"
          }
        ])
        const data = yield* api.from("no-keypath").select().limit(1)

        assert.deepStrictEqual(addedKey, ["key", "key2"])
        assert.deepStrictEqual(data, [
          { username: "insert1", index: 12, key: "key" }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect(
      "insertAll in no keypath table with multiple types of keys",
      () => {
        class Db extends IndexedDbDatabase.make(
          V1,
          Effect.fn(function*(api) {
            yield* api.createObjectStore("no-keypath")
          })
        ) {}

        return Effect.gen(function*() {
          const api = yield* Db.getQueryBuilder
          const date = new Date()
          const arrayBuffer = new ArrayBuffer(10)
          const addedKey = yield* api.from("no-keypath").insertAll([
            { username: "insert1", index: 12, key: "key" },
            {
              username: "insert2",
              index: 13,
              key: 14
            },
            {
              username: "insert3",
              index: 14,
              key: date
            },
            {
              username: "insert4",
              index: 15,
              key: arrayBuffer
            },
            {
              username: "insert5",
              index: 16,
              key: [1, "key", date, arrayBuffer]
            }
          ])
          const data = yield* api.from("no-keypath").select()

          // Returned data order not guaranteed
          data.sort((a, b) => a.index - b.index)

          assert.deepStrictEqual(addedKey, [
            "key",
            14,
            date,
            arrayBuffer,
            [1, "key", date, arrayBuffer]
          ])
          assert.deepStrictEqual(data, [
            { username: "insert1", index: 12, key: "key" },
            {
              username: "insert2",
              index: 13,
              key: 14
            },
            { username: "insert3", index: 14, key: date },
            { username: "insert4", index: 15, key: arrayBuffer },
            {
              username: "insert5",
              index: 16,
              key: [1, "key", date, arrayBuffer]
            }
          ])
        }).pipe(provideDb(Db))
      }
    )

    it.effect("upsert", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api
            .from("todo")
            .insert({ id: 10, title: "insert1", count: 10, completed: true })
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKey = yield* api
          .from("todo")
          .upsert({ id: 10, title: "update1", count: -10, completed: false })
        const data = yield* api.from("todo").select()

        assert.equal(addedKey, 10)
        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [
          { id: 10, title: "update1", count: -10, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("delete", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 10, title: "insert1", count: 10, completed: true },
            { id: 11, title: "insert2", count: 11, completed: true }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        yield* api.from("todo").delete().equals(10)
        const data = yield* api.from("todo").select()

        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [
          { id: 11, title: "insert2", count: 11, completed: true }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("delete with limit", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 10, title: "insert1", count: 10, completed: true },
            { id: 11, title: "insert2", count: 11, completed: true },
            { id: 12, title: "insert3", count: 12, completed: true }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        yield* api.from("todo").delete().limit(2)
        const data = yield* api.from("todo").select()

        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [
          { id: 12, title: "insert3", count: 12, completed: true }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("clear", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 10, title: "insert1", count: 10, completed: true },
            { id: 11, title: "insert2", count: 11, completed: true }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        yield* api.from("todo").clear
        const data = yield* api.from("todo").select()

        assert.equal(data.length, 0)
      }).pipe(provideDb(Db))
    })
  })

  describe("modify all", () => {
    it.effect("insertAll", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKeys = yield* api.from("todo").insertAll([
          { id: 10, title: "insert1", count: 10, completed: true },
          { id: 11, title: "insert2", count: 11, completed: true }
        ])
        const data = yield* api.from("todo").select()

        assert.deepStrictEqual(addedKeys, [10, 11])
        assert.equal(data.length, 2)
        assert.deepStrictEqual(data, [
          { id: 10, title: "insert1", count: 10, completed: true },
          { id: 11, title: "insert2", count: 11, completed: true }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("upsertAll", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 10, title: "insert1", count: 10, completed: true },
            { id: 11, title: "insert2", count: 11, completed: true }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKeys = yield* api.from("todo").upsertAll([
          { id: 10, title: "update1", count: -10, completed: false },
          { id: 11, title: "update2", count: -11, completed: false }
        ])
        const data = yield* api.from("todo").select()

        assert.deepStrictEqual(addedKeys, [10, 11])
        assert.equal(data.length, 2)
        assert.deepStrictEqual(data, [
          { id: 10, title: "update1", count: -10, completed: false },
          { id: 11, title: "update2", count: -11, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("upsertAll withTransaction", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api.from("todo").insertAll([
            { id: 10, title: "insert1", count: 10, completed: true },
            { id: 11, title: "insert2", count: 11, completed: true }
          ])
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKeys = yield* api.withTransaction({ tables: ["todo"], mode: "readwrite" })(
          Effect.gen(function*() {
            return yield* api.from("todo").upsertAll([
              { id: 10, title: "update1", count: -10, completed: false },
              { id: 11, title: "update2", count: -11, completed: false }
            ]).invalidate()
          })
        )
        const data = yield* api.from("todo").select()

        assert.deepStrictEqual(addedKeys, [10, 11])
        assert.equal(data.length, 2)
        assert.deepStrictEqual(data, [
          { id: 10, title: "update1", count: -10, completed: false },
          { id: 11, title: "update2", count: -11, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("upsertAll same key", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api
            .from("todo")
            .insert({ id: 10, title: "insert1", count: 10, completed: true })
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        const addedKeys = yield* api.from("todo").upsertAll([
          { id: 10, title: "update1", count: -10, completed: false },
          { id: 10, title: "update2", count: -11, completed: false }
        ])
        const data = yield* api.from("todo").select()

        assert.deepStrictEqual(addedKeys, [10, 10])
        assert.equal(data.length, 1)
        assert.deepStrictEqual(data, [
          { id: 10, title: "update2", count: -11, completed: false }
        ])
      }).pipe(provideDb(Db))
    })

    it.effect("clearAll", () => {
      class Db extends IndexedDbDatabase.make(
        V1,
        Effect.fn(function*(api) {
          yield* api.createObjectStore("todo")
          yield* api.createIndex("todo", "titleIndex")
          yield* api.createIndex("todo", "countIndex")
          yield* api
            .from("todo")
            .insert({ id: 10, title: "insert1", count: 10, completed: true })
        })
      ) {}

      return Effect.gen(function*() {
        const api = yield* Db.getQueryBuilder
        yield* api.clearAll
        const data = yield* api.from("todo").select()

        assert.equal(data.length, 0)
      }).pipe(provideDb(Db))
    })
  })

  it.effect("count", () => {
    class Db extends IndexedDbDatabase.make(
      V1,
      Effect.fn(function*(api) {
        yield* api.createObjectStore("todo")
        yield* api.createIndex("todo", "titleIndex")
        yield* api.createIndex("todo", "countIndex")
        yield* api.from("todo").insertAll([
          { id: 1, title: "test1", count: 1, completed: false },
          { id: 2, title: "test2", count: 2, completed: false },
          { id: 3, title: "test3", count: 3, completed: false }
        ])
      })
    ) {}

    return Effect.gen(function*() {
      const api = yield* Db.getQueryBuilder
      const data = yield* api.from("todo").count()

      assert.equal(data, 3)
    }).pipe(provideDb(Db))
  })

  it.effect("count with filters", () => {
    class Db extends IndexedDbDatabase.make(
      V1,
      Effect.fn(function*(api) {
        yield* api.createObjectStore("todo")
        yield* api.createIndex("todo", "titleIndex")
        yield* api.createIndex("todo", "countIndex")
        yield* api.from("todo").insertAll([
          { id: 1, title: "test1", count: 1, completed: false },
          { id: 2, title: "test2", count: 2, completed: false },
          { id: 3, title: "test3", count: 3, completed: false }
        ])
      })
    ) {}

    return Effect.gen(function*() {
      const api = yield* Db
      const data = yield* api.from("todo").count("titleIndex").equals("test2")

      assert.equal(data, 1)
    }).pipe(provideDb(Db))
  })

  it.effect("filter", () => {
    class Db extends IndexedDbDatabase.make(
      V1,
      Effect.fn(function*(api) {
        yield* api.createObjectStore("todo")
        yield* api.createIndex("todo", "titleIndex")
        yield* api.createIndex("todo", "countIndex")
        yield* api.from("todo").insertAll([
          { id: 1, title: "test1", count: 1, completed: false },
          { id: 2, title: "test2", count: 2, completed: false },
          { id: 3, title: "test3", count: 3, completed: false }
        ])
      })
    ) {}

    return Effect.gen(function*() {
      const api = yield* Db
      const data = yield* api.from("todo").select().filter((v) => v.count === 2)

      assert.deepStrictEqual(data, [{
        id: 2,
        title: "test2",
        count: 2,
        completed: false
      }])
    }).pipe(provideDb(Db))
  })

  it.effect("stream", () => {
    class Db extends IndexedDbDatabase.make(
      V1,
      Effect.fn(function*(api) {
        yield* api.createObjectStore("todo")
        yield* api.createIndex("todo", "titleIndex")
        yield* api.createIndex("todo", "countIndex")
        yield* api.from("todo").insertAll(
          Array.makeBy(1000, (i) => ({ id: i + 1, title: `test${i}`, count: i, completed: false }))
        )
      })
    ) {}

    return Effect.gen(function*() {
      const api = yield* Db
      const data = yield* api.from("todo").select().stream().pipe(
        Stream.runCollect
      )
      assert.equal(data.length, 1000)

      const data2 = yield* api.from("todo").select().limit(10).stream().pipe(
        Stream.runCollect
      )
      assert.equal(data2.length, 10)
      assert.equal(data2[0].id, 1)

      const data3 = yield* api.from("todo").select().limit(10).offset(50).stream().pipe(
        Stream.runCollect
      )
      assert.equal(data3.length, 10)
      assert.equal(data3[0].id, 51)
    }).pipe(provideDb(Db))
  })

  it.effect("reactive", () => {
    class Db extends IndexedDbDatabase.make(
      V1,
      Effect.fn(function*(api) {
        yield* api.createObjectStore("todo")
        yield* api.createIndex("todo", "titleIndex")
        yield* api.createIndex("todo", "countIndex")
        yield* api.from("todo").insertAll([
          { id: 1, title: "test1", count: 1, completed: false },
          { id: 2, title: "test2", count: 2, completed: false },
          { id: 3, title: "test3", count: 3, completed: false }
        ])
      })
    ) {}

    return Effect.gen(function*() {
      const api = yield* Db
      const data = yield* api.from("todo").select().reactive().pipe(
        Stream.take(2),
        Stream.runCollect,
        Effect.forkChild({ startImmediately: true })
      )
      yield* api.from("todo").insert({ id: 4, title: "test4", count: 4, completed: false }).invalidate()
      yield* Fiber.join(data)
    }).pipe(provideDb(Db))
  })

  it.effect("compound query", () => {
    class Db extends IndexedDbDatabase.make(
      V1,
      Effect.fn(function*(api) {
        yield* api.createObjectStore("todo")
        yield* api.createIndex("todo", "titleIndex")
        yield* api.createIndex("todo", "countIndex")
        yield* api.createIndex("todo", "titleCount")
        yield* api.from("todo").insertAll([
          { id: 1, title: "test1", count: 1, completed: false },
          { id: 2, title: "test2", count: 2, completed: false },
          { id: 3, title: "test2", count: 3, completed: false },
          { id: 4, title: "test3", count: 4, completed: false }
        ])
      })
    ) {}

    return Effect.gen(function*() {
      const api = yield* Db
      const data = yield* api.from("todo").select("titleCount").between(["test2"], ["test2", []])
      assert.deepStrictEqual(data, [
        { id: 2, title: "test2", count: 2, completed: false },
        { id: 3, title: "test2", count: 3, completed: false }
      ])

      const data2 = yield* api.from("todo").select("titleCount").equals(["test2", 3]).first()
      assert.deepStrictEqual(data2, { id: 3, title: "test2", count: 3, completed: false })
    }).pipe(provideDb(Db))
  })
})
