/**
 * @module tests/unit/db/whereMatcher
 * @description Unit tests for the in-memory `find()` filter evaluator behind
 * historical and speculative `Db` reads (`src/db/whereMatcher.ts`). The
 * evaluator must mirror the metadata index's operator semantics exactly — an
 * entity matches in-memory if and only if it would have matched through the
 * index — and must throw `UnsupportedWhereOperatorError` for anything it
 * does not recognize (never guess on a historical read).
 */

import { describe, it, expect } from 'vitest'
import {
  entityMatchesFind,
  resolveEntityField,
  whereMatches,
  UnsupportedWhereOperatorError
} from '../../../src/db/whereMatcher.js'
import type { Entity } from '../../../src/types/brainy.types.js'
import { NounType } from '../../../src/types/graphTypes.js'

/** Build a minimal entity for matcher tests. */
function entity(overrides: Partial<Entity> = {}): Entity {
  return {
    id: 'e-1',
    vector: [],
    type: NounType.Document,
    createdAt: 1000,
    updatedAt: 2000,
    metadata: {},
    ...overrides
  }
}

describe('db/whereMatcher — resolveEntityField', () => {
  it('resolves standard top-level fields', () => {
    const e = entity({
      subtype: 'invoice',
      service: 'billing',
      confidence: 0.9,
      weight: 0.5,
      _rev: 3,
      data: 'payload'
    })
    expect(resolveEntityField(e, 'id')).toBe('e-1')
    expect(resolveEntityField(e, 'type')).toBe(NounType.Document)
    expect(resolveEntityField(e, 'noun')).toBe(NounType.Document) // alias
    expect(resolveEntityField(e, 'subtype')).toBe('invoice')
    expect(resolveEntityField(e, 'service')).toBe('billing')
    expect(resolveEntityField(e, 'confidence')).toBe(0.9)
    expect(resolveEntityField(e, 'weight')).toBe(0.5)
    expect(resolveEntityField(e, '_rev')).toBe(3)
    expect(resolveEntityField(e, 'createdAt')).toBe(1000)
    expect(resolveEntityField(e, 'updatedAt')).toBe(2000)
    expect(resolveEntityField(e, 'data')).toBe('payload')
  })

  it('resolves custom fields from the metadata bag', () => {
    const e = entity({ metadata: { status: 'open', priority: 2 } })
    expect(resolveEntityField(e, 'status')).toBe('open')
    expect(resolveEntityField(e, 'priority')).toBe(2)
    expect(resolveEntityField(e, 'absent')).toBeUndefined()
  })

  it('resolves dotted paths against the entity and the metadata bag', () => {
    const e = entity({ metadata: { address: { city: 'Lyon' }, priority: 7 } })
    expect(resolveEntityField(e, 'metadata.priority')).toBe(7)
    expect(resolveEntityField(e, 'address.city')).toBe('Lyon')
    expect(resolveEntityField(e, 'address.zip')).toBeUndefined()
  })
})

describe('db/whereMatcher — operators', () => {
  const e = entity({
    subtype: 'invoice',
    metadata: { amount: 250, status: 'open', tags: ['urgent', 'q3'], city: 'Lyon' }
  })

  it('shorthand equality', () => {
    expect(whereMatches(e, { status: 'open' })).toBe(true)
    expect(whereMatches(e, { status: 'closed' })).toBe(false)
  })

  it('eq / equals aliases', () => {
    expect(whereMatches(e, { amount: { eq: 250 } })).toBe(true)
    expect(whereMatches(e, { amount: { equals: 250 } })).toBe(true)
    expect(whereMatches(e, { amount: { eq: 99 } })).toBe(false)
  })

  it('array-membership equality (index posting semantics)', () => {
    expect(whereMatches(e, { tags: 'urgent' })).toBe(true)
    expect(whereMatches(e, { tags: { contains: 'q3' } })).toBe(true)
    expect(whereMatches(e, { tags: 'missing-tag' })).toBe(false)
  })

  it('ne / notEquals aliases', () => {
    expect(whereMatches(e, { status: { ne: 'closed' } })).toBe(true)
    expect(whereMatches(e, { status: { notEquals: 'open' } })).toBe(false)
  })

  it('in / oneOf set membership', () => {
    expect(whereMatches(e, { status: { in: ['open', 'closed'] } })).toBe(true)
    expect(whereMatches(e, { status: { oneOf: ['archived'] } })).toBe(false)
  })

  it('numeric range operators with aliases', () => {
    expect(whereMatches(e, { amount: { gt: 200 } })).toBe(true)
    expect(whereMatches(e, { amount: { greaterThan: 250 } })).toBe(false)
    expect(whereMatches(e, { amount: { gte: 250 } })).toBe(true)
    expect(whereMatches(e, { amount: { greaterThanOrEqual: 251 } })).toBe(false)
    expect(whereMatches(e, { amount: { lt: 251 } })).toBe(true)
    expect(whereMatches(e, { amount: { lessThan: 250 } })).toBe(false)
    expect(whereMatches(e, { amount: { lte: 250 } })).toBe(true)
    expect(whereMatches(e, { amount: { lessThanOrEqual: 249 } })).toBe(false)
  })

  it('string range comparison is lexicographic', () => {
    expect(whereMatches(e, { city: { gt: 'Aix' } })).toBe(true)
    expect(whereMatches(e, { city: { lt: 'Aix' } })).toBe(false)
  })

  it('mixed-type range comparison never matches', () => {
    expect(whereMatches(e, { city: { gt: 5 } })).toBe(false)
    expect(whereMatches(e, { amount: { lt: 'zzz' } })).toBe(false)
  })

  it('between', () => {
    expect(whereMatches(e, { amount: { between: [200, 300] } })).toBe(true)
    expect(whereMatches(e, { amount: { between: [251, 300] } })).toBe(false)
    expect(whereMatches(e, { amount: { between: [200] } })).toBe(false) // malformed operand
  })

  it('exists / missing', () => {
    expect(whereMatches(e, { status: { exists: true } })).toBe(true)
    expect(whereMatches(e, { nope: { exists: true } })).toBe(false)
    expect(whereMatches(e, { nope: { exists: false } })).toBe(true)
    expect(whereMatches(e, { nope: { missing: true } })).toBe(true)
    expect(whereMatches(e, { status: { missing: true } })).toBe(false)
    expect(whereMatches(e, { status: { missing: false } })).toBe(true)
  })

  it('multiple operators on one field AND together', () => {
    expect(whereMatches(e, { amount: { gte: 200, lte: 300 } })).toBe(true)
    expect(whereMatches(e, { amount: { gte: 200, lte: 249 } })).toBe(false)
  })

  it('allOf / anyOf / not logical composition', () => {
    expect(whereMatches(e, { allOf: [{ status: 'open' }, { amount: { gt: 100 } }] })).toBe(true)
    expect(whereMatches(e, { allOf: [{ status: 'open' }, { amount: { gt: 999 } }] })).toBe(false)
    expect(whereMatches(e, { anyOf: [{ status: 'closed' }, { amount: 250 }] })).toBe(true)
    expect(whereMatches(e, { anyOf: [{ status: 'closed' }, { amount: 9 }] })).toBe(false)
    expect(whereMatches(e, { not: { status: 'closed' } })).toBe(true)
    expect(whereMatches(e, { not: { status: 'open' } })).toBe(false)
  })

  it('throws UnsupportedWhereOperatorError for unknown operators — never guesses', () => {
    expect(() => whereMatches(e, { amount: { approximately: 250 } })).toThrow(
      UnsupportedWhereOperatorError
    )
    try {
      whereMatches(e, { amount: { approximately: 250 } })
      expect.unreachable('should have thrown')
    } catch (err) {
      expect(err).toBeInstanceOf(UnsupportedWhereOperatorError)
      expect((err as UnsupportedWhereOperatorError).operator).toBe('approximately')
    }
  })
})

describe('db/whereMatcher — entityMatchesFind', () => {
  const e = entity({
    subtype: 'invoice',
    service: 'billing',
    metadata: { amount: 250 }
  })

  it('filters by type (single + array)', () => {
    expect(entityMatchesFind(e, { type: NounType.Document })).toBe(true)
    expect(entityMatchesFind(e, { type: [NounType.Person, NounType.Document] })).toBe(true)
    expect(entityMatchesFind(e, { type: NounType.Person })).toBe(false)
  })

  it('filters by subtype (single + array; entities without subtype excluded)', () => {
    expect(entityMatchesFind(e, { subtype: 'invoice' })).toBe(true)
    expect(entityMatchesFind(e, { subtype: ['invoice', 'receipt'] })).toBe(true)
    expect(entityMatchesFind(e, { subtype: 'receipt' })).toBe(false)
    expect(entityMatchesFind(entity(), { subtype: 'invoice' })).toBe(false)
  })

  it('filters by service', () => {
    expect(entityMatchesFind(e, { service: 'billing' })).toBe(true)
    expect(entityMatchesFind(e, { service: 'crm' })).toBe(false)
  })

  it('excludeVFS drops VFS-marked entities', () => {
    const vfsEntity = entity({ metadata: { vfsType: 'file' } })
    const markedEntity = entity({ metadata: { isVFSEntity: true } })
    expect(entityMatchesFind(vfsEntity, { excludeVFS: true })).toBe(false)
    expect(entityMatchesFind(markedEntity, { excludeVFS: true })).toBe(false)
    expect(entityMatchesFind(e, { excludeVFS: true })).toBe(true)
    expect(entityMatchesFind(vfsEntity, {})).toBe(true)
  })

  it('composes type + subtype + where', () => {
    expect(
      entityMatchesFind(e, {
        type: NounType.Document,
        subtype: 'invoice',
        where: { amount: { gte: 200 } }
      })
    ).toBe(true)
    expect(
      entityMatchesFind(e, {
        type: NounType.Document,
        subtype: 'invoice',
        where: { amount: { gte: 999 } }
      })
    ).toBe(false)
  })
})
