/**
 * @module column-store.test
 * @description Tests for the ColumnStore coordinator: full lifecycle including
 * add, flush, reload, filter, sort, range query, and removal.
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { ColumnStore } from '../../../../src/indexes/columnStore/ColumnStore.js'
import { MemoryStorage } from '../../../../src/storage/adapters/memoryStorage.js'
import { EntityIdMapper } from '../../../../src/utils/entityIdMapper.js'
import { RoaringBitmap32 } from '../../../../src/utils/roaring/index.js'

describe('ColumnStore', () => {
  let storage: MemoryStorage
  let idMapper: EntityIdMapper
  let store: ColumnStore

  beforeEach(async () => {
    storage = new MemoryStorage()
    await storage.init()
    idMapper = new EntityIdMapper({ storage, storageKey: 'test:idMapper' })
    await idMapper.init()

    store = new ColumnStore({ flushThreshold: 10 }) // low threshold for tests
    await store.init(storage, idMapper)
  })

  afterEach(async () => {
    await store.close()
  })

  // -----------------------------------------------------------------------
  // Basic add + sort
  // -----------------------------------------------------------------------

  describe('sortTopK', () => {
    it('returns entities sorted by numeric field ascending', async () => {
      store.addEntity(BigInt(idMapper.getOrAssign('a')), { createdAt: 300 })
      store.addEntity(BigInt(idMapper.getOrAssign('b')), { createdAt: 100 })
      store.addEntity(BigInt(idMapper.getOrAssign('c')), { createdAt: 200 })
      await store.flush()

      const result = await store.sortTopK('createdAt', 'asc', 10)
      const uuids = result.map(id => idMapper.getUuid(Number(id)))
      expect(uuids).toEqual(['b', 'c', 'a'])
    })

    it('returns entities sorted by numeric field descending', async () => {
      store.addEntity(BigInt(idMapper.getOrAssign('a')), { createdAt: 300 })
      store.addEntity(BigInt(idMapper.getOrAssign('b')), { createdAt: 100 })
      store.addEntity(BigInt(idMapper.getOrAssign('c')), { createdAt: 200 })
      await store.flush()

      const result = await store.sortTopK('createdAt', 'desc', 10)
      const uuids = result.map(id => idMapper.getUuid(Number(id)))
      expect(uuids).toEqual(['a', 'c', 'b'])
    })

    it('respects limit K', async () => {
      for (let i = 0; i < 50; i++) {
        store.addEntity(BigInt(idMapper.getOrAssign(`e${i}`)), { createdAt: i * 1000 })
      }
      await store.flush()

      const result = await store.sortTopK('createdAt', 'desc', 5)
      expect(result).toHaveLength(5)
      // Should be the 5 highest timestamps
      const uuids = result.map(id => idMapper.getUuid(Number(id)))
      expect(uuids).toEqual(['e49', 'e48', 'e47', 'e46', 'e45'])
    })

    it('sorts string fields lexicographically', async () => {
      store.addEntity(BigInt(idMapper.getOrAssign('a')), { status: 'pending' })
      store.addEntity(BigInt(idMapper.getOrAssign('b')), { status: 'active' })
      store.addEntity(BigInt(idMapper.getOrAssign('c')), { status: 'inactive' })
      await store.flush()

      const result = await store.sortTopK('status', 'asc', 10)
      const uuids = result.map(id => idMapper.getUuid(Number(id)))
      expect(uuids).toEqual(['b', 'c', 'a']) // active, inactive, pending
    })

    it('returns empty for unknown field', async () => {
      const result = await store.sortTopK('nonexistent', 'asc', 10)
      expect(result).toEqual([])
    })
  })

  // -----------------------------------------------------------------------
  // Filtered sort
  // -----------------------------------------------------------------------

  describe('filteredSortTopK', () => {
    it('returns only entities in the filter bitmap, sorted', async () => {
      const idA = idMapper.getOrAssign('a')
      const idB = idMapper.getOrAssign('b')
      const idC = idMapper.getOrAssign('c')
      const idD = idMapper.getOrAssign('d')

      store.addEntity(BigInt(idA), { createdAt: 400 })
      store.addEntity(BigInt(idB), { createdAt: 100 })
      store.addEntity(BigInt(idC), { createdAt: 300 })
      store.addEntity(BigInt(idD), { createdAt: 200 })
      await store.flush()

      // Filter: only B and C
      const bitmap = new RoaringBitmap32([idB, idC])
      const result = await store.filteredSortTopK(bitmap, 'createdAt', 'desc', 10)
      const uuids = result.map(id => idMapper.getUuid(Number(id)))
      expect(uuids).toEqual(['c', 'b']) // 300, 100
    })
  })

  // -----------------------------------------------------------------------
  // Point filter
  // -----------------------------------------------------------------------

  describe('filter', () => {
    it('returns matching entities as roaring bitmap', async () => {
      const idA = idMapper.getOrAssign('a')
      const idB = idMapper.getOrAssign('b')
      const idC = idMapper.getOrAssign('c')

      store.addEntity(BigInt(idA), { status: 'active' })
      store.addEntity(BigInt(idB), { status: 'inactive' })
      store.addEntity(BigInt(idC), { status: 'active' })
      await store.flush()

      const bitmap = await store.filter('status', 'active')
      expect(bitmap.has(idA)).toBe(true)
      expect(bitmap.has(idC)).toBe(true)
      expect(bitmap.has(idB)).toBe(false)
    })

    it('returns empty bitmap for no match', async () => {
      store.addEntity(BigInt(idMapper.getOrAssign('a')), { status: 'active' })
      await store.flush()

      const bitmap = await store.filter('status', 'deleted')
      expect(bitmap.size).toBe(0)
    })
  })

  // -----------------------------------------------------------------------
  // Range query
  // -----------------------------------------------------------------------

  describe('rangeQuery', () => {
    it('returns entities within numeric range', async () => {
      const ids: number[] = []
      for (let i = 0; i < 10; i++) {
        ids.push(idMapper.getOrAssign(`e${i}`))
        store.addEntity(BigInt(ids[i]), { price: i * 10 })
      }
      await store.flush()

      const bitmap = await store.rangeQuery('price', 30, 70)
      // price 30, 40, 50, 60, 70 → entities e3, e4, e5, e6, e7
      expect(bitmap.size).toBe(5)
      expect(bitmap.has(ids[3])).toBe(true)
      expect(bitmap.has(ids[7])).toBe(true)
      expect(bitmap.has(ids[2])).toBe(false)
      expect(bitmap.has(ids[8])).toBe(false)
    })

    // Exclusive-bound coverage — regression guard for the column-store bug where
    // includeMin/includeMax were dropped, so strict greaterThan/lessThan silently
    // behaved as gte/lte whenever the column store served the query.
    const seedPrices = async (): Promise<number[]> => {
      const ids: number[] = []
      for (let i = 0; i < 10; i++) {
        ids.push(idMapper.getOrAssign(`x${i}`))
        store.addEntity(BigInt(ids[i]), { price: i * 10 }) // 0,10,...,90
      }
      await store.flush()
      return ids
    }

    it('honors exclusive lower bound — greaterThan (30, 70]', async () => {
      const ids = await seedPrices()
      const bm = await store.rangeQuery('price', 30, 70, false, true)
      // 40,50,60,70 → e4..e7 (30 excluded)
      expect(bm.size).toBe(4)
      expect(bm.has(ids[3])).toBe(false)
      expect(bm.has(ids[4])).toBe(true)
      expect(bm.has(ids[7])).toBe(true)
    })

    it('honors exclusive upper bound — lessThan [30, 70)', async () => {
      const ids = await seedPrices()
      const bm = await store.rangeQuery('price', 30, 70, true, false)
      // 30,40,50,60 → e3..e6 (70 excluded)
      expect(bm.size).toBe(4)
      expect(bm.has(ids[7])).toBe(false)
      expect(bm.has(ids[3])).toBe(true)
      expect(bm.has(ids[6])).toBe(true)
    })

    it('honors both exclusive bounds — (30, 70)', async () => {
      const ids = await seedPrices()
      const bm = await store.rangeQuery('price', 30, 70, false, false)
      // 40,50,60 → e4..e6
      expect(bm.size).toBe(3)
      expect(bm.has(ids[3])).toBe(false)
      expect(bm.has(ids[7])).toBe(false)
      expect(bm.has(ids[4])).toBe(true)
      expect(bm.has(ids[6])).toBe(true)
    })

    it('exclusive lower excludes ALL duplicates of the boundary value', async () => {
      const a = idMapper.getOrAssign('dup_a'); store.addEntity(BigInt(a), { price: 30 })
      const b = idMapper.getOrAssign('dup_b'); store.addEntity(BigInt(b), { price: 30 })
      const c = idMapper.getOrAssign('dup_c'); store.addEntity(BigInt(c), { price: 30 })
      const d = idMapper.getOrAssign('dup_d'); store.addEntity(BigInt(d), { price: 40 })
      await store.flush()
      const bm = await store.rangeQuery('price', 30, 100, false, true) // (30, 100]
      expect(bm.has(a)).toBe(false)
      expect(bm.has(b)).toBe(false)
      expect(bm.has(c)).toBe(false)
      expect(bm.has(d)).toBe(true)
    })

    it('honors exclusive bounds in the unflushed tail buffer', async () => {
      // No flush → values live only in the tail buffer (separate code path from
      // the persisted segment cursor).
      const ids: number[] = []
      for (let i = 0; i < 5; i++) {
        ids.push(idMapper.getOrAssign(`tail${i}`))
        store.addEntity(BigInt(ids[i]), { score: i * 10 }) // 0,10,20,30,40
      }
      const bm = await store.rangeQuery('score', 0, 40, false, false) // (0, 40)
      expect(bm.size).toBe(3) // 10,20,30
      expect(bm.has(ids[0])).toBe(false)
      expect(bm.has(ids[4])).toBe(false)
      expect(bm.has(ids[1])).toBe(true)
      expect(bm.has(ids[3])).toBe(true)
    })
  })

  // -----------------------------------------------------------------------
  // Removal
  // -----------------------------------------------------------------------

  describe('removeEntity', () => {
    it('excluded from sort after removal', async () => {
      const idA = idMapper.getOrAssign('a')
      const idB = idMapper.getOrAssign('b')
      const idC = idMapper.getOrAssign('c')

      store.addEntity(BigInt(idA), { createdAt: 100 })
      store.addEntity(BigInt(idB), { createdAt: 200 })
      store.addEntity(BigInt(idC), { createdAt: 300 })
      await store.flush()

      store.removeEntity(BigInt(idB))
      await store.flush()

      const result = await store.sortTopK('createdAt', 'asc', 10)
      const uuids = result.map(id => idMapper.getUuid(Number(id)))
      expect(uuids).toEqual(['a', 'c'])
    })
  })

  // -----------------------------------------------------------------------
  // Multi-segment merge sort
  // -----------------------------------------------------------------------

  describe('multi-segment', () => {
    it('sorts correctly across multiple L0 segments', async () => {
      // Flush threshold is 10, so 25 entities creates 3 segments
      for (let i = 0; i < 25; i++) {
        store.addEntity(BigInt(idMapper.getOrAssign(`e${i}`)), { ts: i })
      }
      await store.flush()

      const result = await store.sortTopK('ts', 'desc', 5)
      const uuids = result.map(id => idMapper.getUuid(Number(id)))
      expect(uuids).toEqual(['e24', 'e23', 'e22', 'e21', 'e20'])
    })
  })

  // -----------------------------------------------------------------------
  // Persistence (flush + reload)
  // -----------------------------------------------------------------------

  describe('persistence', () => {
    it('survives close + reload', async () => {
      store.addEntity(BigInt(idMapper.getOrAssign('a')), { createdAt: 300 })
      store.addEntity(BigInt(idMapper.getOrAssign('b')), { createdAt: 100 })
      store.addEntity(BigInt(idMapper.getOrAssign('c')), { createdAt: 200 })
      await store.flush()
      await store.close()

      // Create a new store pointing at the same storage
      const store2 = new ColumnStore({ flushThreshold: 10 })
      await store2.init(storage, idMapper)

      const result = await store2.sortTopK('createdAt', 'desc', 10)
      const uuids = result.map(id => idMapper.getUuid(Number(id)))
      expect(uuids).toEqual(['a', 'c', 'b'])

      await store2.close()
    })
  })

  // -----------------------------------------------------------------------
  // Multi-value fields (words)
  // -----------------------------------------------------------------------

  describe('multi-value fields', () => {
    it('indexes multiple values per entity', async () => {
      const idA = idMapper.getOrAssign('docA')
      const idB = idMapper.getOrAssign('docB')

      store.addEntity(BigInt(idA), { __words__: ['machine', 'learning', 'algorithm'] })
      store.addEntity(BigInt(idB), { __words__: ['neural', 'network', 'machine'] })
      await store.flush()

      // Point filter: "machine" should match both
      const machineBitmap = await store.filter('__words__', 'machine')
      expect(machineBitmap.has(idA)).toBe(true)
      expect(machineBitmap.has(idB)).toBe(true)

      // "algorithm" should match only docA
      const algoBitmap = await store.filter('__words__', 'algorithm')
      expect(algoBitmap.has(idA)).toBe(true)
      expect(algoBitmap.has(idB)).toBe(false)

      // AND intersection: "machine" AND "neural" → only docB
      const neuralBitmap = await store.filter('__words__', 'neural')
      const intersection = RoaringBitmap32.and(machineBitmap, neuralBitmap)
      expect(intersection.has(idB)).toBe(true)
      expect(intersection.has(idA)).toBe(false)
    })
  })

  // -----------------------------------------------------------------------
  // getFilterValues
  // -----------------------------------------------------------------------

  describe('getFilterValues', () => {
    it('returns distinct values sorted', async () => {
      store.addEntity(BigInt(idMapper.getOrAssign('a')), { status: 'pending' })
      store.addEntity(BigInt(idMapper.getOrAssign('b')), { status: 'active' })
      store.addEntity(BigInt(idMapper.getOrAssign('c')), { status: 'active' })
      store.addEntity(BigInt(idMapper.getOrAssign('d')), { status: 'inactive' })
      await store.flush()

      const values = await store.getFilterValues('status')
      expect(values).toEqual(['active', 'inactive', 'pending'])
    })
  })

  // -----------------------------------------------------------------------
  // hasField
  // -----------------------------------------------------------------------

  describe('hasField', () => {
    it('returns false for unknown field', () => {
      expect(store.hasField('nonexistent')).toBe(false)
    })

    it('returns true after adding data', async () => {
      store.addEntity(BigInt(idMapper.getOrAssign('a')), { createdAt: 100 })
      expect(store.hasField('createdAt')).toBe(true)
    })
  })
})
