/**
 * Lazy Vector Loading Tests (B2 optimization)
 *
 * Tests for:
 * - Vectors evicted from memory after addItem() in lazy mode
 * - Search returns correct results after vector eviction
 * - Memory mode retains vectors (default behavior)
 * - Lazy mode requires storage adapter
 */

import { describe, it, expect, beforeEach } from 'vitest'
import { v4 as uuidv4 } from 'uuid'
import { JsHnswVectorIndex } from '../../../src/hnsw/hnswIndex.js'
import { euclideanDistance } from '../../../src/utils/index.js'
import { MemoryStorage } from '../../../src/storage/adapters/memoryStorage.js'

// Helper: generate a random vector of given dimension
function randomVector(dim: number): number[] {
  return Array.from({ length: dim }, () => Math.random() * 2 - 1)
}

// Helper: save a vector to storage so lazy loading can retrieve it
// Both noun (vector) and metadata must be saved for getNounVector() to work
async function saveVector(storage: MemoryStorage, id: string, vector: number[]): Promise<void> {
  await storage.saveNoun({
    id,
    vector,
    connections: new Map(),
    level: 0
  })
  await storage.saveNounMetadata(id, {
    noun: 'thing',
    createdAt: Date.now(),
    updatedAt: Date.now()
  })
}

describe('Lazy Vector Loading (B2)', () => {
  const dim = 32

  // =================================================================
  // 1. LAZY MODE: VECTOR EVICTION
  // =================================================================
  describe('vector eviction in lazy mode', () => {
    let index: JsHnswVectorIndex
    let storage: MemoryStorage

    beforeEach(async () => {
      storage = new MemoryStorage()
      index = new JsHnswVectorIndex(
        {
          M: 8,
          efConstruction: 100,
          efSearch: 50,
          ml: 8,
          vectorStorage: 'lazy'
        },
        euclideanDistance,
        { useParallelization: false, storage }
      )
    })

    it('should still be searchable after vector eviction', async () => {
      const targetId = uuidv4()
      const target = randomVector(dim)

      // Store noun in storage so lazy loading can find it
      await saveVector(storage, targetId, target)
      await index.addItem({ id: targetId, vector: target })

      // Add more entities
      for (let i = 0; i < 20; i++) {
        const id = uuidv4()
        const v = randomVector(dim)
        await saveVector(storage, id, v)
        await index.addItem({ id, vector: v })
      }

      // Search should still find the target
      const results = await index.search(target, 5)
      expect(results.length).toBeGreaterThan(0)

      // The closest result should be the target (distance ~0)
      const targetResult = results.find(([id]) => id === targetId)
      expect(targetResult).toBeDefined()
      expect(targetResult![1]).toBeCloseTo(0, 1)
    })

    it('should return correct top-k results in lazy mode', async () => {
      for (let i = 0; i < 50; i++) {
        const id = uuidv4()
        const v = randomVector(dim)
        await saveVector(storage, id, v)
        await index.addItem({ id, vector: v })
      }

      const query = randomVector(dim)
      const results = await index.search(query, 10)

      // With lazy mode and small graph (50 items), HNSW may return slightly fewer
      // than k if some vectors are evicted and unreachable during graph traversal
      expect(results.length).toBeGreaterThanOrEqual(8)
      expect(results.length).toBeLessThanOrEqual(10)

      // Results should be sorted by distance
      for (let i = 1; i < results.length; i++) {
        expect(results[i][1]).toBeGreaterThanOrEqual(results[i - 1][1])
      }
    })
  })

  // =================================================================
  // 2. MEMORY MODE: VECTORS RETAINED (DEFAULT)
  // =================================================================
  describe('memory mode retains vectors (default)', () => {
    it('should keep vectors in memory by default', async () => {
      const storage = new MemoryStorage()
      const index = new JsHnswVectorIndex(
        { M: 4, efConstruction: 50, efSearch: 20 },
        euclideanDistance,
        { useParallelization: false, storage }
      )

      const id = uuidv4()
      const v = randomVector(dim)
      await index.addItem({ id, vector: v })

      // Search should work without needing storage
      const results = await index.search(v, 1)
      expect(results.length).toBe(1)
      expect(results[0][0]).toBe(id)
      expect(results[0][1]).toBeCloseTo(0, 5)
    })

    it('should work without storage adapter in memory mode', async () => {
      // No storage adapter provided
      const index = new JsHnswVectorIndex(
        { M: 4, efConstruction: 50, efSearch: 20 },
        euclideanDistance,
        { useParallelization: false }
      )

      const id = uuidv4()
      const v = randomVector(dim)
      await index.addItem({ id, vector: v })

      const results = await index.search(v, 1)
      expect(results.length).toBe(1)
      expect(results[0][0]).toBe(id)
    })
  })

  // =================================================================
  // 3. LAZY + NO STORAGE: GRACEFUL BEHAVIOR
  // =================================================================
  describe('lazy mode without storage', () => {
    it('should not evict vectors when no storage adapter is configured', async () => {
      // vectorStorage: 'lazy' but no storage — vectors should stay in memory
      const index = new JsHnswVectorIndex(
        {
          M: 4,
          efConstruction: 50,
          efSearch: 20,
          vectorStorage: 'lazy'
        },
        euclideanDistance,
        { useParallelization: false }
      )

      const id = uuidv4()
      const v = randomVector(dim)
      await index.addItem({ id, vector: v })

      // Should still work because vectors aren't evicted without storage
      const results = await index.search(v, 1)
      expect(results.length).toBe(1)
      expect(results[0][0]).toBe(id)
    })
  })

  // =================================================================
  // 4. MULTIPLE SEARCHES IN LAZY MODE
  // =================================================================
  describe('multiple searches in lazy mode', () => {
    it('should handle repeated searches correctly', async () => {
      const storage = new MemoryStorage()
      const index = new JsHnswVectorIndex(
        {
          M: 8,
          efConstruction: 100,
          efSearch: 50,
          ml: 8,
          vectorStorage: 'lazy'
        },
        euclideanDistance,
        { useParallelization: false, storage }
      )

      const entries: Array<{ id: string; vector: number[] }> = []
      for (let i = 0; i < 25; i++) {
        const id = uuidv4()
        const v = randomVector(dim)
        entries.push({ id, vector: v })
        await saveVector(storage, id, v)
        await index.addItem({ id, vector: v })
      }

      // Run multiple searches — each should return results and be consistent
      for (let q = 0; q < 5; q++) {
        const entry = entries[q * 5]
        const results = await index.search(entry.vector, 5)

        expect(results.length).toBe(5)
        // Results should be sorted by distance
        for (let i = 1; i < results.length; i++) {
          expect(results[i][1]).toBeGreaterThanOrEqual(results[i - 1][1])
        }
        // At least the closest result should have a reasonably small distance
        expect(results[0][1]).toBeLessThan(5)
      }
    })
  })
})
