/**
 * @module tests/unit/indexes/columnStore/segment-load-fault
 * @description Pattern-B acceptance for the ColumnStore (finding 4): a segment
 * the manifest LISTS but that cannot be loaded must never be silently skipped —
 * doing so dropped every entity in that segment out of `filter`/`rangeQuery`/
 * `sortTopK` with no error, so a corrupt index looked like a merely short result.
 *
 * The three failure classes and their required behaviour:
 *  - a real storage IO fault (EIO) PROPAGATES verbatim — a present-but-unreadable
 *    segment is not "absent", so it must not read as an empty result;
 *  - a manifest-listed segment with undecodable bytes throws `ColumnSegmentLoadError`;
 *  - a manifest-listed segment with NO bytes (gone on disk) throws `ColumnSegmentLoadError`.
 * Only genuine absence stays benign: querying a field that has no manifest at all
 * returns empty (nothing was ever written for it) — that is not a fault.
 */
import { describe, it, expect, beforeEach } from 'vitest'
import {
  ColumnStore,
  ColumnSegmentLoadError
} from '../../../../src/indexes/columnStore/ColumnStore.js'
import { MemoryStorage } from '../../../../src/storage/adapters/memoryStorage.js'
import { EntityIdMapper } from '../../../../src/utils/entityIdMapper.js'

type FaultMode = 'none' | 'io' | 'corrupt' | 'missing'

/**
 * A MemoryStorage that can fault reads of persisted column SEGMENTS only
 * (keys/paths containing the `L0-` segment marker). Manifest and DELETED-bitmap
 * reads pass through untouched so a fresh store still initialises normally — the
 * fault is isolated to the exact seam finding 4 hardened.
 */
class FaultInjectingStorage extends MemoryStorage {
  public faultMode: FaultMode = 'none'

  private eio(): Error {
    const e = new Error('simulated disk read fault') as Error & { code: string }
    e.code = 'EIO'
    return e
  }

  public async loadBinaryBlob(key: string): Promise<Buffer | null> {
    if (this.faultMode !== 'none' && key.includes('/L0-')) {
      if (this.faultMode === 'io') throw this.eio()
      // Too small to hold even a header → readSegmentFromBuffer throws → wrapped.
      if (this.faultMode === 'corrupt') return Buffer.from([1, 2, 3, 4, 5])
      if (this.faultMode === 'missing') return null
    }
    return super.loadBinaryBlob(key)
  }
}

describe('ColumnStore segment-load faults surface loudly, absence stays benign (finding 4)', () => {
  let storage: FaultInjectingStorage
  let idMapper: EntityIdMapper

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

    // Write one persisted L0 segment for `createdAt`, then close the writer.
    const writer = new ColumnStore({ flushThreshold: 10 })
    await writer.init(storage, idMapper)
    for (let i = 0; i < 5; i++) {
      writer.addEntity(BigInt(idMapper.getOrAssign(`e${i}`)), {
        createdAt: (i + 1) * 100
      })
    }
    await writer.flush()
    await writer.close()
    storage.faultMode = 'none'
  })

  // Fresh reader over the same storage: empty segment cache, so every query is
  // forced to actually load the persisted segment (that is the seam under test).
  const reopen = async (): Promise<ColumnStore> => {
    const s = new ColumnStore({ flushThreshold: 10 })
    await s.init(storage, idMapper)
    return s
  }

  it('propagates a storage IO fault verbatim — not [] and not a ColumnSegmentLoadError', async () => {
    storage.faultMode = 'io'
    const store = await reopen()
    await expect(store.filter('createdAt', 300)).rejects.toMatchObject({
      code: 'EIO'
    })
    await store.close()
  })

  it('throws ColumnSegmentLoadError when a manifest-listed segment is undecodable', async () => {
    storage.faultMode = 'corrupt'
    const store = await reopen()
    await expect(
      store.sortTopK('createdAt', 'desc', 10)
    ).rejects.toBeInstanceOf(ColumnSegmentLoadError)
    await store.close()
  })

  it('throws ColumnSegmentLoadError when a manifest-listed segment has no loadable bytes', async () => {
    storage.faultMode = 'missing'
    const store = await reopen()
    await expect(
      store.rangeQuery('createdAt', 100, 500)
    ).rejects.toBeInstanceOf(ColumnSegmentLoadError)
    await store.close()
  })

  it('a field with no manifest is genuine absence — returns empty, never throws', async () => {
    storage.faultMode = 'none'
    const store = await reopen()
    const bitmap = await store.filter('no_such_field', 'x')
    expect(bitmap.size).toBe(0)
    const sorted = await store.sortTopK('no_such_field', 'asc', 10)
    expect(sorted).toEqual([])
    await store.close()
  })

  it('with no fault, the persisted segment still loads and answers queries', async () => {
    storage.faultMode = 'none'
    const store = await reopen()
    const sorted = await store.sortTopK('createdAt', 'desc', 10)
    const uuids = sorted.map((id) => idMapper.getUuid(Number(id)))
    expect(uuids).toEqual(['e4', 'e3', 'e2', 'e1', 'e0'])
    await store.close()
  })
})
