/**
 * Storage-Level Batch Operations Test Suite v5.12.0
 *
 * Comprehensive testing of storage-level batch APIs:
 * - storage.getNounMetadataBatch() - Batch metadata reads
 * - storage.getVerbsBySourceBatch() - Batch relationship queries
 * - brain.batchGet() - High-level batch entity retrieval
 * - PathResolver.getChildren() - VFS batch operations
 *
 * Coverage:
 * ✅ Type-aware storage compatibility
 * ✅ Sharding preservation
 * ✅ Write-cache coherence
 * ✅ Performance improvements (N+1 → batched)
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../src/brainy'
import { NounType, VerbType } from '../../src/coreTypes'
import { performance } from 'perf_hooks'

describe('Storage-Level Batch Operations v5.12.0', () => {
  let brain: Brainy

  beforeEach(async () => {
    brain = new Brainy({ requireSubtype: false,
      storage: { type: 'memory' },
      enableCOW: true
    })
    await brain.init()
  })

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

  describe('brain.batchGet() - High-Level Batch API', () => {
    it('should batch fetch multiple entities (metadata-only)', async () => {
      // Add test entities
      const id1 = await brain.add({
        type: 'document',
        data: 'Entity 1',
        metadata: { category: 'A' }
      })
      const id2 = await brain.add({
        type: 'thing',
        data: 'Entity 2',
        metadata: { category: 'B' }
      })
      const id3 = await brain.add({
        type: 'person',
        data: 'Entity 3',
        metadata: { category: 'C' }
      })

      // Batch fetch (metadata-only by default)
      const results = await brain.batchGet([id1, id2, id3])

      expect(results.size).toBe(3)
      expect(results.get(id1)?.data).toBe('Entity 1')
      expect(results.get(id2)?.data).toBe('Entity 2')
      expect(results.get(id3)?.data).toBe('Entity 3')

      // Vectors should NOT be included by default (empty array or undefined)
      const vector = results.get(id1)?.vector
      expect(vector === undefined || (Array.isArray(vector) && vector.length === 0)).toBe(true)
    })

    it('should handle missing entities gracefully', async () => {
      const id1 = await brain.add({ type: 'document', data: 'Exists' })
      const fakeId = '12345678-1234-1234-1234-123456789abc'
      const anotherFake = '87654321-4321-4321-4321-abcdef123456'

      const results = await brain.batchGet([id1, fakeId, anotherFake])

      expect(results.size).toBe(1)
      expect(results.get(id1)?.data).toBe('Exists')
      expect(results.has(fakeId)).toBe(false)
    })

    it('should support includeVectors option (fallback)', async () => {
      const id1 = await brain.add({
        type: 'document',
        data: 'With vector',
        metadata: { test: true }
      })

      // With vectors (currently falls back to individual gets)
      const results = await brain.batchGet([id1], { includeVectors: true })

      expect(results.size).toBe(1)
      const entity = results.get(id1)
      expect(entity?.data).toBe('With vector')
      expect(entity?.vector).toBeDefined()
      expect(entity?.vector?.length).toBeGreaterThan(0)
    })

    it('should be faster than individual gets for large batches', async () => {
      // Create 100 entities
      const ids: string[] = []
      for (let i = 0; i < 100; i++) {
        const id = await brain.add({
          type: 'document',
          data: `Entity ${i}`,
          metadata: { index: i }
        })
        ids.push(id)
      }

      // Measure individual gets
      const startIndividual = performance.now()
      for (const id of ids.slice(0, 20)) {
        await brain.get(id)
      }
      const individualTime = performance.now() - startIndividual

      // Measure batch get
      const startBatch = performance.now()
      await brain.batchGet(ids.slice(20, 40))
      const batchTime = performance.now() - startBatch

      // Batch should be faster (or at least comparable)
      console.log(`Individual: ${individualTime.toFixed(2)}ms, Batch: ${batchTime.toFixed(2)}ms`)
      expect(batchTime).toBeLessThan(individualTime * 2) // Allow some overhead
    })
  })

  describe('storage.getNounMetadataBatch() - Storage Layer', () => {
    it('should batch fetch noun metadata across types via ID-first paths', async () => {
      // Add entities of different types
      const id1 = await brain.add({ type: 'document', data: 'Doc' })
      const id2 = await brain.add({ type: 'thing', data: 'Thing' })
      const id3 = await brain.add({ type: 'person', data: 'Person' })

      // Access storage directly (8.0 layout: ID-first paths, no type lookup)
      const storage = brain.storage as any
      const results = await storage.getNounMetadataBatch([id1, id2, id3])

      expect(results.size).toBe(3)
      expect(results.get(id1)?.noun).toBe('document')
      expect(results.get(id2)?.noun).toBe('thing')
      expect(results.get(id3)?.noun).toBe('person')
    })

    it('should omit missing ids from the result map', async () => {
      const id = await brain.add({ type: 'document', data: 'Test' })
      const missing = '00000000-0000-4000-8000-000000000000'

      const storage = brain.storage as any
      const results = await storage.getNounMetadataBatch([id, missing])

      expect(results.size).toBe(1)
      expect(results.get(id)?.noun).toBe('document')
      expect(results.has(missing)).toBe(false)
    })

    it('should preserve sharding in all paths', async () => {
      // Add entity
      const id = await brain.add({ type: 'document', data: 'Sharded' })

      // Check that path includes shard
      const storage = brain.storage as any
      const results = await storage.getNounMetadataBatch([id])

      expect(results.size).toBe(1)

      // Verify shard is in the path used (check internal call)
      // Path should be: entities/nouns/document/metadata/{SHARD}/{ID}.json
      const shard = storage.getShardIdFromUuid?.(id) || id.substring(0, 2)
      expect(shard).toBeDefined()
    })

    it('should handle large batches efficiently', async () => {
      // Create 500 entities
      const ids: string[] = []
      for (let i = 0; i < 500; i++) {
        const id = await brain.add({
          type: 'document',
          data: `Batch ${i}`,
          metadata: { batch: true }
        })
        ids.push(id)
      }

      const startTime = performance.now()
      const storage = brain.storage as any
      const results = await storage.getNounMetadataBatch(ids)
      const duration = performance.now() - startTime

      expect(results.size).toBe(500)
      console.log(`Batched 500 metadata reads in ${duration.toFixed(2)}ms`)

      // Should complete in reasonable time
      expect(duration).toBeLessThan(5000) // < 5 seconds
    })
  })

  describe('Write-cache coherence', () => {
    it('should respect write cache for dirty entities', async () => {
      // Add entity
      const id = await brain.add({ type: 'document', data: 'Original' })

      // Update (may be in write cache before flush)
      await brain.update({ id, data: 'Updated' })

      // Batch get should see updated version
      const results = await brain.batchGet([id])

      expect(results.get(id)?.data).toBe('Updated')
    })
  })

  describe('getVerbsBySourceBatch() - Batch Relationship Queries', () => {
    it('should batch fetch relationships by source IDs', async () => {
      // Create entities
      const source1 = await brain.add({ type: 'person', data: 'Alice' })
      const source2 = await brain.add({ type: 'person', data: 'Bob' })
      const target1 = await brain.add({ type: 'document', data: 'Doc1' })
      const target2 = await brain.add({ type: 'document', data: 'Doc2' })

      // Create relationships
      await brain.relate({ from: source1, to: target1, type: 'creates' })
      await brain.relate({ from: source1, to: target2, type: 'creates' })
      await brain.relate({ from: source2, to: target1, type: 'uses' })

      // Batch query
      const storage = brain.storage as any
      const results = await storage.getVerbsBySourceBatch([source1, source2])

      expect(results.size).toBe(2)

      const source1Verbs = results.get(source1) || []
      const source2Verbs = results.get(source2) || []

      expect(source1Verbs.length).toBe(2) // 2 relationships
      expect(source2Verbs.length).toBe(1) // 1 relationship

      // Check verb types
      expect(source1Verbs.every((v: any) => v.verb === 'creates')).toBe(true)
      expect(source2Verbs[0].verb).toBe('uses')
    })

    it('should filter by verb type', async () => {
      const source = await brain.add({ type: 'person', data: 'User' })
      const target1 = await brain.add({ type: 'document', data: 'Doc1' })
      const target2 = await brain.add({ type: 'document', data: 'Doc2' })

      // Multiple relationship types
      await brain.relate({ from: source, to: target1, type: 'creates' })
      await brain.relate({ from: source, to: target2, type: 'uses' })

      const storage = brain.storage as any

      // Filter by 'creates' type
      const createsResults = await storage.getVerbsBySourceBatch(
        [source],
        'creates'
      )

      const createsVerbs = createsResults.get(source) || []
      expect(createsVerbs.length).toBe(1)
      expect(createsVerbs[0].verb).toBe('creates')
    })

    it('should handle sources with no relationships', async () => {
      const source1 = await brain.add({ type: 'person', data: 'Isolated' })
      const source2 = await brain.add({ type: 'person', data: 'Connected' })
      const target = await brain.add({ type: 'document', data: 'Doc' })

      await brain.relate({ from: source2, to: target, type: 'relatedTo' })

      const storage = brain.storage as any
      const results = await storage.getVerbsBySourceBatch([source1, source2])

      expect(results.get(source1) || []).toHaveLength(0) // No relationships
      expect(results.get(source2) || []).toHaveLength(1) // Has relationship
    })
  })

  describe('VFS Integration - PathResolver.getChildren()', () => {
    it('should use batchGet() for directory children', async () => {
      if (!brain.vfs) {
        await brain.vfs.init()
      }

      // Create directory with files
      await brain.vfs!.mkdir('/batch-test')
      await brain.vfs!.writeFile('/batch-test/file1.txt', 'Content 1')
      await brain.vfs!.writeFile('/batch-test/file2.txt', 'Content 2')
      await brain.vfs!.writeFile('/batch-test/file3.txt', 'Content 3')

      // getChildren() should use batchGet() internally
      const startTime = performance.now()
      const tree = await brain.vfs!.getTreeStructure('/batch-test')
      const duration = performance.now() - startTime

      expect(tree.children).toHaveLength(3)
      console.log(`VFS getTreeStructure with batch: ${duration.toFixed(2)}ms`)

      // Verify all children loaded
      const filenames = tree.children!.map(c => c.name).sort()
      expect(filenames).toEqual(['file1.txt', 'file2.txt', 'file3.txt'])
    })

    it('should handle nested directories with parallel traversal', async () => {
      if (!brain.vfs) {
        await brain.vfs.init()
      }

      // Create nested structure
      await brain.vfs!.mkdir('/root')
      await brain.vfs!.mkdir('/root/dir1')
      await brain.vfs!.mkdir('/root/dir2')
      await brain.vfs!.writeFile('/root/dir1/a.txt', 'A')
      await brain.vfs!.writeFile('/root/dir1/b.txt', 'B')
      await brain.vfs!.writeFile('/root/dir2/c.txt', 'C')

      // Should use breadth-first parallel traversal
      const tree = await brain.vfs!.getTreeStructure('/root', { recursive: true })

      expect(tree.children).toHaveLength(2) // 2 subdirectories

      const dir1 = tree.children!.find(c => c.name === 'dir1')
      const dir2 = tree.children!.find(c => c.name === 'dir2')

      expect(dir1?.children).toHaveLength(2) // 2 files in dir1
      expect(dir2?.children).toHaveLength(1) // 1 file in dir2
    })
  })

  describe('Performance: N+1 Query Elimination', () => {
    it('should eliminate N+1 pattern for directory with 12 files', async () => {
      if (!brain.vfs) {
        await brain.vfs.init()
      }

      // Create directory with 12 files (original bug scenario)
      await brain.vfs!.mkdir('/performance-test')
      for (let i = 1; i <= 12; i++) {
        await brain.vfs!.writeFile(`/performance-test/file${i}.txt`, `Content ${i}`)
      }

      // Measure with batching
      const startBatch = performance.now()
      const treeBatch = await brain.vfs!.getTreeStructure('/performance-test')
      const batchTime = performance.now() - startBatch

      expect(treeBatch.children).toHaveLength(12)
      console.log(`12 files with batching: ${batchTime.toFixed(2)}ms`)

      // Before v5.12.0: ~12.7s (22 sequential calls × 580ms)
      // After v5.12.0: <1s (2-3 batched calls)
      expect(batchTime).toBeLessThan(2000) // Should be < 2 seconds
    })

    it('should scale to 100 entities efficiently', async () => {
      // Create 100 entities
      const ids: string[] = []
      for (let i = 0; i < 100; i++) {
        const id = await brain.add({
          type: 'document',
          data: `Entity ${i}`,
          metadata: { index: i }
        })
        ids.push(id)
      }

      // Batch get all 100
      const startTime = performance.now()
      const results = await brain.batchGet(ids)
      const duration = performance.now() - startTime

      expect(results.size).toBe(100)
      console.log(`100 entities batch: ${duration.toFixed(2)}ms (${(100 / duration * 1000).toFixed(0)} entities/sec)`)

      // Should achieve high throughput
      const throughput = 100 / duration * 1000
      expect(throughput).toBeGreaterThan(50) // > 50 entities/sec
    })
  })

  describe('Error Handling', () => {
    it('should handle partial batch failures gracefully', async () => {
      const id1 = await brain.add({ type: 'document', data: 'Exists' })
      const fakeIds = [
        '11111111-1111-1111-1111-111111111111',
        '22222222-2222-2222-2222-222222222222',
        '33333333-3333-3333-3333-333333333333'
      ]

      // Mix of valid and invalid IDs
      const results = await brain.batchGet([id1, ...fakeIds])

      // Should return only valid entities
      expect(results.size).toBe(1)
      expect(results.get(id1)).toBeDefined()

      // Invalid IDs should be silently skipped
      fakeIds.forEach(fakeId => {
        expect(results.has(fakeId)).toBe(false)
      })
    })

    it('should handle empty batch gracefully', async () => {
      const results = await brain.batchGet([])

      expect(results.size).toBe(0)
    })

    it('should handle duplicate IDs in batch', async () => {
      const id = await brain.add({ type: 'document', data: 'Duplicate test' })

      // Same ID multiple times
      const results = await brain.batchGet([id, id, id])

      // Should return single entry
      expect(results.size).toBe(1)
      expect(results.get(id)?.data).toBe('Duplicate test')
    })
  })

  describe('Type-Aware Storage Verification', () => {
    it('should use correct type-first paths for all types', async () => {
      // Create entities of each major type
      const types: NounType[] = [
        NounType.Document,
        NounType.Thing,
        NounType.Person,
        NounType.File,
        NounType.Event
      ]

      const ids: string[] = []
      for (const type of types) {
        const id = await brain.add({
          type: type as any,
          data: `Type ${type}`,
          metadata: { testType: type }
        })
        ids.push(id)
      }

      // Batch fetch
      const results = await brain.batchGet(ids)

      expect(results.size).toBe(types.length)

      // Verify each entity has correct type
      for (const [id, entity] of results) {
        expect(entity.type).toBeDefined()
        expect(types.includes(entity.type as NounType)).toBe(true)
      }
    })
  })

  describe('Sharding Verification', () => {
    it('should maintain shard distribution in batch operations', async () => {
      // Create entities with known shard distribution
      const entityCount = 256 // One per shard
      const ids: string[] = []

      for (let i = 0; i < entityCount; i++) {
        const id = await brain.add({
          type: 'document',
          data: `Shard test ${i}`,
          metadata: { shardTest: true }
        })
        ids.push(id)
      }

      // Batch fetch all
      const results = await brain.batchGet(ids)

      expect(results.size).toBe(entityCount)

      // All entities should be retrievable
      for (const id of ids) {
        expect(results.has(id)).toBe(true)
      }
    })
  })
})

