/**
 * Integration test for metadata explosion fix
 *
 * Validates that vector embeddings (and array-index-as-object-key payloads) are
 * NEVER indexed as metadata fields, while legitimate small arrays and scalar
 * fields ARE registered for `where` filtering.
 *
 * Original bug class: 825,924 chunk files created for 1,144 entities (721 files
 * per entity) because each of the 384 vector dimensions was being indexed as its
 * own numeric metadata field.
 *
 * 8.0 architecture note: the per-vector-dimension chunk explosion is structurally
 * impossible now — the metadata index is a column store + sparse field index
 * (`_column_index/<field>/…` and `_system/idx/**`), not one `__chunk__*` file per
 * field-value. So this suite no longer counts `_system/__chunk__*` files (there
 * are none). Instead it asserts the real invariant directly via the public
 * `getAvailableFields()` surface: no numeric (vector-dimension) field names, no
 * `vector`/`embedding` field, but the legitimate semantic fields ARE indexed.
 *
 * Field placement (8.0 contract — see AddParams JSDoc): `data` is the content that
 * gets embedded; `metadata` holds the structured, queryable fields used in `where`
 * filters. Queryable fields therefore live under `metadata` here.
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../src/brainy.js'
import { NounType } from '../../src/types/graphTypes.js'
import { existsSync, rmSync } from 'fs'

describe('Metadata Vector Exclusion Fix', () => {
  let brainy: Brainy
  const testDir = '/tmp/brainy-metadata-vector-test'

  beforeEach(async () => {
    // Clean test directory
    if (existsSync(testDir)) {
      rmSync(testDir, { recursive: true, force: true })
    }

    brainy = new Brainy({ requireSubtype: false,
      storage: { type: 'filesystem', path: testDir },
      ai: { provider: 'mock' }
    })
    await brainy.init()
  })

  afterEach(async () => {
    if (brainy) {
      await brainy.clear()
      await brainy.close()
    }
    if (existsSync(testDir)) {
      rmSync(testDir, { recursive: true, force: true })
    }
  })

  it('should NOT index vector embeddings as metadata fields', async () => {
    // Add entity with queryable metadata + a small array (tags SHOULD be indexed)
    await brainy.add({
      type: NounType.Person,
      data: 'Alice the developer',
      metadata: {
        name: 'Alice',
        email: 'alice@example.com',
        tags: ['developer', 'typescript']  // Small array - SHOULD be indexed
      }
    })

    // The indexed-field set is the canonical record of what the metadata index
    // actually tracks. The vector explosion bug manifested as hundreds of numeric
    // field names (one per dimension) appearing here.
    const fields = await brainy.getAvailableFields()

    // CRITICAL: no purely-numeric field names (vector dimension indices like
    // "0", "1", "54716") and no nested numeric dimension keys.
    const numericFields = fields.filter(f => /(^|\.)\d+$/.test(f))
    expect(numericFields).toEqual([])

    // CRITICAL: the raw vector must never be indexed as a metadata field.
    expect(fields).not.toContain('vector')
    expect(fields).not.toContain('embedding')
    expect(fields).not.toContain('embeddings')

    // The legitimate semantic fields ARE indexed.
    expect(fields).toContain('name')
    expect(fields).toContain('email')
    expect(fields).toContain('tags')

    // Field count stays sane (semantic fields + Brainy's own system/VFS fields),
    // nowhere near the hundreds a per-dimension explosion would produce.
    expect(fields.length).toBeLessThan(50)
  })

  it('should NOT index objects with numeric keys (array-as-object guard)', async () => {
    // Add entity with an object that has numeric keys (simulates a vector
    // accidentally serialized as { "0": 0.1, "1": 0.2, ... }).
    await brainy.add({
      type: NounType.Person,
      data: 'NumericTest entity',
      metadata: {
        name: 'NumericTest',
        numericObject: {
          '0': 0.1,
          '1': 0.2,
          '2': 0.3,
          '100': 1.0
        }
      }
    })

    const fields = await brainy.getAvailableFields()

    // CRITICAL: numeric keys (whether top-level or nested under a field like
    // "numericObject.0") must NOT become indexed fields. This is the guard that
    // prevents vectors-as-objects from exploding the index.
    const numericFields = fields.filter(f => /(^|\.)\d+$/.test(f))
    expect(numericFields).toEqual([])

    // The legitimate scalar field IS indexed.
    expect(fields).toContain('name')
  })

  it('should still index small arrays (tags, categories)', async () => {
    // Add entity with a small (<= 10 element) array of tags.
    await brainy.add({
      type: NounType.Person,
      data: 'Bob the engineer',
      metadata: {
        name: 'Bob',
        tags: ['javascript', 'react', 'nodejs']
      }
    })

    // Small arrays must be registered as a multi-value field (not skipped, not
    // exploded). 'tags' should appear in the indexed-field set.
    const fields = await brainy.getAvailableFields()
    expect(fields).toContain('tags')

    // Behavioral check: filtering by a tag value should return the entity. The
    // array was ['javascript', 'react', 'nodejs'] — matching ANY member must work.
    //
    // REAL LIBRARY BUG (left failing on purpose): the ColumnStore ingest path in
    // src/utils/metadataIndex.ts (addToIndex, ~line 1407) does
    // `fieldsMap[field] = value` for every extracted pair, so for a multi-value
    // field it keeps only the LAST array element ('nodejs') and drops the rest.
    // Only the '__words__' field gets the array-accumulation treatment. As a
    // result `where: { tags: 'react' }` (and 'javascript') returns [], while
    // `where: { tags: 'nodejs' }` returns 1. Correct input → wrong output, so
    // this assertion stays as-is to keep the bug visible.
    const results = await brainy.find({
      where: { tags: 'react' }
    })

    expect(results.length).toBeGreaterThan(0)
    expect(results[0].entity.metadata?.name).toBe('Bob')
  })

  it('should skip indexing large arrays (>10 elements)', async () => {
    // Add entity with a large array (not a vector, just bulk data).
    const largeArray = Array.from({ length: 100 }, (_, i) => `item${i}`)

    await brainy.add({
      type: NounType.Document,
      data: 'Doc with large array',
      metadata: {
        name: 'Doc with large array',
        items: largeArray
      }
    })

    // Large arrays (> 10 elements) are deliberately skipped to avoid indexing
    // bulk/vector-like payloads: 'items' must NOT appear, and the 100 elements
    // must NOT have produced 100 indexed fields.
    const fields = await brainy.getAvailableFields()
    expect(fields).not.toContain('items')
    const numericFields = fields.filter(f => /(^|\.)\d+$/.test(f))
    expect(numericFields).toEqual([])

    // The scalar 'name' field IS indexed.
    expect(fields).toContain('name')
  })

  it('should preserve HNSW vector search functionality', async () => {
    // Add entities with semantic content
    const id1 = await brainy.add({
      type: NounType.Concept,
      data: 'AI algorithms that learn from data',
      metadata: {
        name: 'Machine Learning',
        description: 'AI algorithms that learn from data'
      }
    })

    const id2 = await brainy.add({
      type: NounType.Concept,
      data: 'Neural networks with multiple layers',
      metadata: {
        name: 'Deep Learning',
        description: 'Neural networks with multiple layers'
      }
    })

    // Vectors are metadata-only by default; request them explicitly to verify the
    // embedding pipeline actually produced a stored vector for each entity.
    const entity1 = await brainy.get(id1, { includeVectors: true })
    const entity2 = await brainy.get(id2, { includeVectors: true })

    expect(entity1).toBeDefined()
    expect(entity2).toBeDefined()
    expect(Array.isArray(entity1?.vector)).toBe(true)
    expect(Array.isArray(entity2?.vector)).toBe(true)
    expect(entity1!.vector!.length).toBe(384)
    expect(entity2!.vector!.length).toBe(384)

    // Self-retrieval confirms the HNSW index is wired: querying an entity by its
    // OWN content text returns it (deterministic embedder → cosine 1.0).
    const selfHits = await brainy.find({ query: 'AI algorithms that learn from data', limit: 5 })
    expect(selfHits.length).toBeGreaterThan(0)
    expect(selfHits.some(r => r.id === id1)).toBe(true)
  })

  it('should preserve metadata field filtering', async () => {
    // Add entities with various queryable metadata
    await brainy.add({
      type: NounType.Person,
      data: 'Charlie the engineer',
      metadata: {
        name: 'Charlie',
        email: 'charlie@example.com',
        role: 'engineer'
      }
    })

    await brainy.add({
      type: NounType.Person,
      data: 'Dana the designer',
      metadata: {
        name: 'Dana',
        email: 'dana@example.com',
        role: 'designer'
      }
    })

    // Verify scalar metadata filtering works
    const engineers = await brainy.find({
      where: { role: 'engineer' }
    })

    expect(engineers.length).toBe(1)
    expect(engineers[0].entity.metadata?.name).toBe('Charlie')

    const designers = await brainy.find({
      where: { role: 'designer' }
    })

    expect(designers.length).toBe(1)
    expect(designers[0].entity.metadata?.name).toBe('Dana')
  })

  it('should handle nested object metadata correctly', async () => {
    // Add entity with nested metadata
    await brainy.add({
      type: NounType.Person,
      data: 'Eve in New York',
      metadata: {
        name: 'Eve',
        address: {
          city: 'New York',
          state: 'NY'
        }
      }
    })

    // Nested fields are flattened to dot-notation field names; filter by the
    // flattened path.
    const results = await brainy.find({
      where: { 'address.city': 'New York' }
    })

    expect(results.length).toBeGreaterThan(0)
    expect(results[0].entity.metadata?.name).toBe('Eve')
  })

  it('should NOT create exponential index fields for multiple entities', async () => {
    // Add 10 entities (each with a vector embedding + a few metadata fields)
    for (let i = 0; i < 10; i++) {
      await brainy.add({
        type: NounType.Person,
        data: `Person ${i}`,
        metadata: {
          name: `Person ${i}`,
          email: `person${i}@example.com`,
          tags: ['user']
        }
      })
    }

    // The indexed-field set is keyed by field NAME, not by entity — so it must
    // stay bounded regardless of entity count. The original bug turned this into
    // ~721 fields per entity (one per vector dimension); here it must remain a
    // small handful of semantic fields plus Brainy's own system/VFS fields.
    const fields = await brainy.getAvailableFields()
    const numericFields = fields.filter(f => /(^|\.)\d+$/.test(f))
    expect(numericFields).toEqual([])
    expect(fields).toContain('name')
    expect(fields).toContain('email')
    expect(fields).toContain('tags')
    // Nowhere near the thousands a per-dimension explosion would produce.
    expect(fields.length).toBeLessThan(50)
  })
})
