import test from 'ava'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'

import { kite, kiteSync, node, edge, prop, optional, Database, bulkWrite } from '../dist/index.js'

const makeDbPath = () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kitedb-schema-'))
  return path.join(dir, 'test.kitedb')
}

// =============================================================================
// Schema Builder Tests
// =============================================================================

test('prop builders create correct specs', (t) => {
  const strProp = prop.string('name')
  t.is(strProp.type, 'string')
  t.is(strProp.optional, undefined)

  const intProp = prop.int('age')
  t.is(intProp.type, 'int')

  const floatProp = prop.float('score')
  t.is(floatProp.type, 'float')

  const boolProp = prop.bool('active')
  t.is(boolProp.type, 'bool')

  const vecProp = prop.vector('embedding', 1536)
  t.is(vecProp.type, 'vector')
})

test('optional() marks props as optional', (t) => {
  const required = prop.int('count')
  t.is(required.optional, undefined)

  const opt = optional(prop.int('count'))
  t.is(opt.optional, true)
  t.is(opt.type, 'int')
})

test('node() creates node spec with key function', (t) => {
  const User = node('user', {
    key: (id: string) => `user:${id}`,
    props: {
      name: prop.string('name'),
      email: prop.string('email'),
    },
  })

  t.is(User.name, 'user')
  t.truthy(User.key)
  t.is(User.key?.kind, 'prefix')
  t.is(User.key?.prefix, 'user:')
  t.truthy(User.props)
  t.is(Object.keys(User.props!).length, 2)
})

test('node() with explicit key spec', (t) => {
  const OrgUser = node('org_user', {
    key: { kind: 'template', template: 'org:{org}:user:{id}' },
    props: {
      name: prop.string('name'),
    },
  })

  t.is(OrgUser.name, 'org_user')
  t.is(OrgUser.key?.kind, 'template')
  t.is(OrgUser.key?.template, 'org:{org}:user:{id}')
})

test('node() without config', (t) => {
  const Simple = node('simple')
  t.is(Simple.name, 'simple')
  t.is(Simple.key, undefined)
  t.is(Simple.props, undefined)
})

test('edge() creates edge spec', (t) => {
  const knows = edge('knows', {
    since: prop.int('since'),
    weight: optional(prop.float('weight')),
  })

  t.is(knows.name, 'knows')
  t.truthy(knows.props)
  t.is(Object.keys(knows.props!).length, 2)
  t.is(knows.props?.since.type, 'int')
  t.is(knows.props?.weight.optional, true)
})

test('edge() without props', (t) => {
  const follows = edge('follows')
  t.is(follows.name, 'follows')
  t.is(follows.props, undefined)
})

test('bulkWrite auto-commits in chunks', (t) => {
  const db = Database.open(makeDbPath(), {
    walSize: 64 * 1024,
    autoCheckpoint: false,
    backgroundCheckpoint: false,
  })

  const ops: Array<(ctx: Database) => void> = Array.from({ length: 1000 }, (_, i) => {
    return (ctx) => ctx.createNode(`n-${i}`)
  })

  bulkWrite(db, ops, { chunkSize: 100 })
  t.is(db.countNodes(), 1000)
  db.close()
})

// =============================================================================
// Async kite() Tests
// =============================================================================

test('kite() opens database asynchronously', async (t) => {
  const User = node('user', {
    key: (id: string) => `user:${id}`,
    props: {
      name: prop.string('name'),
    },
  })

  const follows = edge('follows')

  const db = await kite(makeDbPath(), {
    nodes: [User],
    edges: [follows],
  })

  t.truthy(db)
  t.deepEqual(db.nodeTypes(), ['user'])
  t.deepEqual(db.edgeTypes(), ['follows'])

  db.close()
})

test('upsert inserts and updates', async (t) => {
  const User = node('user', {
    key: (id: string) => `user:${id}`,
    props: {
      name: prop.string('name'),
      age: prop.int('age'),
    },
  })

  const db = await kite(makeDbPath(), {
    nodes: [User],
    edges: [],
  })

  const created = db.upsert('user').values('alice', { name: 'Alice', age: 30 }).returning() as any
  t.is(created.name, 'Alice')
  t.is(created.age, 30)

  const updated = db.upsert('user').values('alice', { age: 31 }).returning() as any
  t.is(updated.age, 31)
  t.is(updated.name, 'Alice')

  const deleted = db.upsert('user').values('alice', { name: null }).returning() as any
  t.is(deleted.name, undefined)

  db.close()
})

test('upsertById inserts and updates', async (t) => {
  const User = node('user', {
    key: (id: string) => `user:${id}`,
    props: {
      name: prop.string('name'),
      age: prop.int('age'),
    },
  })

  const db = await kite(makeDbPath(), {
    nodes: [User],
    edges: [],
  })

  const insert = db.upsertById('user', 42)
  insert.set('name', 'Alice')
  insert.set('age', 30)
  insert.execute()

  const created = db.getById(42) as any
  t.is(created?.name, 'Alice')
  t.is(created?.age, 30)

  const update = db.upsertById('user', 42)
  update.set('age', 31)
  update.unset('name')
  update.execute()

  const updated = db.getById(42) as any
  t.is(updated?.age, 31)
  t.is(updated?.name, undefined)

  db.close()
})

test('upsertEdge creates and updates edge props', async (t) => {
  const User = node('user', {
    key: (id: string) => `user:${id}`,
    props: {
      name: prop.string('name'),
    },
  })

  const follows = edge('follows', {
    since: prop.int('since'),
    weight: optional(prop.float('weight')),
  })

  const db = await kite(makeDbPath(), {
    nodes: [User],
    edges: [follows],
  })

  const alice = db.insert('user').values('alice', { name: 'Alice' }).returning() as any
  const bob = db.insert('user').values('bob', { name: 'Bob' }).returning() as any

  const createEdge = db.upsertEdge(alice.id, 'follows', bob.id)
  createEdge.set('since', 2020)
  createEdge.execute()

  const since = db.getEdgeProp(alice.id, 'follows', bob.id, 'since')
  t.is(since?.floatValue, 2020)

  const updateEdge = db.upsertEdge(alice.id, 'follows', bob.id)
  updateEdge.set('weight', 0.75)
  updateEdge.unset('since')
  updateEdge.execute()

  const updatedSince = db.getEdgeProp(alice.id, 'follows', bob.id, 'since')
  t.is(updatedSince, null)
  const weight = db.getEdgeProp(alice.id, 'follows', bob.id, 'weight')
  t.is(weight?.floatValue, 0.75)

  db.close()
})

test('transaction commits and rolls back', async (t) => {
  const User = node('user', {
    key: (id: string) => `user:${id}`,
    props: {
      name: prop.string('name'),
    },
  })

  const db = await kite(makeDbPath(), {
    nodes: [User],
    edges: [],
  })

  await db.transaction(async (ctx) => {
    ctx.insert('user').values('alice', { name: 'Alice' }).execute()
    await new Promise<void>((resolve) => setTimeout(resolve, 0))
    ctx.insert('user').values('bob', { name: 'Bob' }).execute()
  })

  t.truthy(db.get('user', 'alice'))
  t.truthy(db.get('user', 'bob'))

  await t.throwsAsync(async () => {
    await db.transaction(async (ctx) => {
      ctx.insert('user').values('carol', { name: 'Carol' }).execute()
      throw new Error('boom')
    })
  })

  t.is(db.get('user', 'carol'), null)

  db.close()
})

test('batch executes atomically', async (t) => {
  const User = node('user', {
    key: (id: string) => `user:${id}`,
    props: {
      name: prop.string('name'),
    },
  })

  const db = await kite(makeDbPath(), {
    nodes: [User],
    edges: [],
  })

  const results = await db.batch([
    db.insert('user').values('alice', { name: 'Alice' }),
    db.insert('user').values('bob', { name: 'Bob' }),
  ])

  t.is(results.length, 2)
  t.truthy(db.get('user', 'alice'))
  t.truthy(db.get('user', 'bob'))

  await t.throwsAsync(async () => {
    await db.batch([
      db.insert('user').values('carol', { name: 'Carol' }),
      () => {
        throw new Error('boom')
      },
    ])
  })

  t.is(db.get('user', 'carol'), null)

  db.close()
})

test('kiteSync() opens database synchronously', (t) => {
  const User = node('user', {
    key: (id: string) => `user:${id}`,
    props: {
      name: prop.string('name'),
    },
  })

  const knows = edge('knows', {
    since: prop.int('since'),
  })

  const db = kiteSync(makeDbPath(), {
    nodes: [User],
    edges: [knows],
  })

  t.truthy(db)
  t.deepEqual(db.nodeTypes(), ['user'])
  t.deepEqual(db.edgeTypes(), ['knows'])

  db.close()
})

// =============================================================================
// Full Integration Test
// =============================================================================

test('full schema-based workflow', async (t) => {
  // Define schema
  const Document = node('document', {
    key: (id: string) => `doc:${id}`,
    props: {
      title: prop.string('title'),
      content: prop.string('content'),
    },
  })

  const Topic = node('topic', {
    key: (name: string) => `topic:${name}`,
    props: {
      name: prop.string('name'),
    },
  })

  const discusses = edge('discusses', {
    relevance: prop.float('relevance'),
  })

  // Open database
  const db = await kite(makeDbPath(), {
    nodes: [Document, Topic],
    edges: [discusses],
  })

  // Insert nodes
  const doc = db.insert('document').values('doc1', { title: 'Hello', content: 'World' }).returning() as any
  t.truthy(doc)
  t.is(doc.key, 'doc:doc1')
  t.is(doc.title, 'Hello')

  const topic = db.insert('topic').values('greeting', { name: 'Greetings' }).returning() as any
  t.truthy(topic)
  t.is(topic.key, 'topic:greeting')

  // Link with edge props
  db.link(doc.id, 'discusses', topic.id, { relevance: 0.95 })

  // Verify edge
  t.true(db.hasEdge(doc.id, 'discusses', topic.id))

  // Get edge prop
  const relevance = db.getEdgeProp(doc.id, 'discusses', topic.id, 'relevance')
  t.truthy(relevance)
  t.is(relevance?.floatValue, 0.95)

  // Query
  const allDocs = db.all('document')
  t.is(allDocs.length, 1)

  const allTopics = db.all('topic')
  t.is(allTopics.length, 1)

  db.close()
})

test('async kite() is non-blocking', async (t) => {
  // This test verifies that kite() doesn't block
  // by checking that we can interleave other async operations
  const User = node('user', {
    key: (id: string) => `user:${id}`,
    props: { name: prop.string('name') },
  })

  const dbPath = makeDbPath()

  // Start opening database
  const dbPromise = kite(dbPath, {
    nodes: [User],
    edges: [],
  })

  // This should execute before db open completes (in theory)
  let counter = 0
  const tick = () =>
    new Promise<void>((resolve) => {
      counter++
      setImmediate(resolve)
    })

  await tick()

  // Now wait for db
  const db = await dbPromise
  t.truthy(db)
  t.true(counter > 0)

  db.close()
})
