import { IndexedDb, IndexedDbDatabase, IndexedDbTable, IndexedDbVersion } from "@effect/platform-browser"
import { afterEach, assert, describe, it } from "@effect/vitest"
import { Effect, Layer, Schema } from "effect"
import { IDBKeyRange, indexedDB } from "fake-indexeddb"

const databaseName = "db"

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

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

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

describe.sequential("IndexedDbDatabase", () => {
  it.effect("insert and read todos", () => {
    const Table = IndexedDbTable.make({
      name: "todo",
      schema: Schema.Struct({
        id: Schema.Number,
        title: Schema.String,
        completed: Schema.Boolean
      }),
      keyPath: "id",
      indexes: { titleIndex: "title" }
    })

    const V1 = IndexedDbVersion.make(Table)

    class Db extends IndexedDbDatabase.make(V1, (api) =>
      Effect.gen(function*() {
        yield* api.createObjectStore("todo")
        yield* api.createIndex("todo", "titleIndex")
        yield* api
          .from("todo")
          .insert({ id: 1, title: "test", completed: false })
      }))
    {}

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

      const name = yield* api.use((database) => database.name)
      const version = yield* api.use((database) => database.version)
      const objectStoreNames = yield* api.use(
        (database) => database.objectStoreNames
      )
      const indexNames = yield* api.use(
        (database) => database.transaction("todo").objectStore("todo").indexNames
      )
      const index = yield* api.use((database) => database.transaction("todo").objectStore("todo").index("titleIndex"))

      assert.equal(name, "db")
      assert.equal(version, 1)
      assert.deepStrictEqual(todo, [
        { id: 1, title: "test", completed: false }
      ])
      assert.deepStrictEqual(Array.from(objectStoreNames), ["todo"])
      assert.deepStrictEqual(Array.from(indexNames), ["titleIndex"])
      assert.deepStrictEqual(index.keyPath, "title")

      yield* api.from("todo").insert({ id: 2, title: "test2", completed: false })

      yield* db.rebuild
      todo = yield* api.from("todo").select()
      assert.deepStrictEqual(todo, [
        { id: 1, title: "test", completed: false }
      ])
    }).pipe(provideMigration(Db))
  })

  it.effect("transaction", () => {
    const Table = IndexedDbTable.make({
      name: "todo",
      schema: Schema.Struct({
        id: Schema.Number,
        title: Schema.String,
        completed: Schema.Boolean
      }),
      keyPath: "id",
      indexes: { titleIndex: "title" }
    })

    const Table2 = IndexedDbTable.make({
      name: "user",
      schema: Schema.Struct({
        id: Schema.Number,
        name: Schema.String
      }),
      keyPath: "id"
    })

    const Db = IndexedDbVersion.make(Table, Table2)

    class Migration extends IndexedDbDatabase.make(Db, (api) =>
      Effect.gen(function*() {
        yield* api.createObjectStore("todo")
        yield* api.createIndex("todo", "titleIndex")
        yield* api
          .from("todo")
          .insert({ id: 1, title: "test", completed: false })
      }))
    {}

    return Effect.gen(function*() {
      const api = yield* Migration.getQueryBuilder
      yield* api.withTransaction({ tables: ["todo"], mode: "readwrite" })(
        api.from("todo").insert({ id: 2, title: "test2", completed: false })
      )

      const todo = yield* api.from("todo").select()
      assert.deepStrictEqual(todo, [
        { id: 1, title: "test", completed: false },
        {
          id: 2,
          title: "test2",
          completed: false
        }
      ])
    }).pipe(provideMigration(Migration))
  })

  it.effect("migration sequence", () => {
    const Table1 = IndexedDbTable.make({
      name: "todo",
      schema: Schema.Struct({
        id: Schema.Number,
        title: Schema.String,
        completed: Schema.Boolean
      }),
      keyPath: "id",
      indexes: { titleIndex: "title" }
    })

    const Table2 = IndexedDbTable.make({
      name: "todo",
      schema: Schema.Struct({
        uuid: Schema.String.check(Schema.isUUID(4)),
        title: Schema.String,
        completed: Schema.Boolean
      }),
      keyPath: "uuid"
    })

    const V1 = IndexedDbVersion.make(Table1)
    const V2 = IndexedDbVersion.make(Table2)
    const uuid = "9535a059-a61f-42e1-a2e0-35ec87203c24"

    class Db extends IndexedDbDatabase.make(V1, (api) =>
      Effect.gen(function*() {
        yield* api.createObjectStore("todo")
        yield* api.createIndex("todo", "titleIndex")
        yield* api
          .from("todo")
          .insert({ id: 1, title: "test", completed: false })
      })).add(V2, (from, to) =>
        Effect.gen(function*() {
          const todo = yield* from.from("todo").select()
          yield* from.deleteIndex("todo", "titleIndex")
          yield* from.deleteObjectStore("todo")
          yield* to.createObjectStore("todo")
          yield* to.from("todo").insertAll(
            todo.map((t) => ({
              uuid,
              title: t.title,
              completed: t.completed
            }))
          )
        }))
    {}

    return Effect.gen(function*() {
      const api = yield* Db.getQueryBuilder
      const todo = yield* api.from("todo").select()
      const name = yield* api.use((database) => database.name)
      const version = yield* api.use((database) => database.version)
      const objectStoreNames = yield* api.use(
        (database) => database.objectStoreNames
      )
      const indexNames = yield* api.use(
        (database) => database.transaction("todo").objectStore("todo").indexNames
      )

      assert.equal(name, "db")
      assert.equal(version, 2)
      assert.deepStrictEqual(todo, [{ uuid, title: "test", completed: false }])
      assert.deepStrictEqual(Array.from(objectStoreNames), ["todo"])
      assert.deepStrictEqual(Array.from(indexNames), [])
    }).pipe(provideMigration(Db))
  })

  it.effect("delete object store migration", () => {
    const Table1 = IndexedDbTable.make({
      name: "todo",
      schema: Schema.Struct({
        id: Schema.Number,
        title: Schema.String,
        completed: Schema.Boolean
      }),
      keyPath: "id"
    })

    const Table2 = IndexedDbTable.make({
      name: "user",
      schema: Schema.Struct({
        userId: Schema.Number,
        name: Schema.String,
        email: Schema.String
      }),
      keyPath: "userId"
    })

    const V1 = IndexedDbVersion.make(Table1)
    const V2 = IndexedDbVersion.make(Table2, Table1)

    class Migration extends IndexedDbDatabase.make(V1, (api) => api.createObjectStore("todo")).add(
      V2,
      Effect.fnUntraced(function*(from, to) {
        yield* from.deleteObjectStore("todo")
        yield* to.createObjectStore("user")
        yield* to.from("user").insert({
          userId: 1,
          name: "John Doe",
          email: "john.doe@example.com"
        })
      })
    ) {}

    return Effect.gen(function*() {
      const api = yield* Migration.getQueryBuilder
      const user = yield* api.from("user").select()

      const name = yield* api.use((database) => database.name)
      const version = yield* api.use((database) => database.version)
      const objectStoreNames = yield* api.use((database) => database.objectStoreNames)
      assert.equal(name, "db")
      assert.equal(version, 2)
      assert.deepStrictEqual(user, [
        { userId: 1, name: "John Doe", email: "john.doe@example.com" }
      ])
      assert.deepStrictEqual(Array.from(objectStoreNames), ["user"])
    }).pipe(provideMigration(Migration))
  })
})
