import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { sql } from 'drizzle-orm'
import { makePgHarness } from './pg-harness.js'
import { fieldsMigrationSql } from './migrate.js'
import { queryEntities } from './query.js'
import { isFieldValidationError } from './errors.js'

const h = await makePgHarness()
beforeAll(async () => {
  for (const s of fieldsMigrationSql()
    .split(';')
    .map((x) => x.trim())
    .filter(Boolean)) {
    await h.db.execute(sql.raw(s))
  }
  await h.db.execute(sql`insert into field_values (entity_type,entity_id,group_id,field_key,value_text) values
    ('product','p1','g','material','leather'),('product','p2','g','material','leather'),('product','p3','g','material','suede')`)
  await h.db.execute(sql`insert into field_values (entity_type,entity_id,group_id,field_key,value_num) values
    ('product','p1','g','price',50),('product','p2','g','price',150),('product','p3','g','price',50)`)
  await h.db.execute(sql`insert into field_values (entity_type,entity_id,group_id,field_key,ref_type,ref_id) values
    ('product','p1','g','brand','vendor','v9'),('product','p2','g','brand','vendor','v9'),('product','p3','g','brand','vendor','v7')`)
})
afterAll(async () => { await h.teardown() })

describe('queryEntities', () => {
  it('eq text facet', async () => {
    const r = await queryEntities(h.db, { entityType: 'product', facets: [{ field: 'material', op: 'eq', value: 'leather' }] })
    expect(new Set(r.entityIds)).toEqual(new Set(['p1', 'p2'])); expect(r.capped).toBe(false)
  })
  it('ANDs facets across fields', async () => {
    const r = await queryEntities(h.db, { entityType: 'product', facets: [
      { field: 'material', op: 'eq', value: 'leather' }, { field: 'price', op: 'lt', value: 100 }] })
    expect(r.entityIds).toEqual(['p1'])
  })
  it('between on numeric', async () => {
    const r = await queryEntities(h.db, { entityType: 'product', facets: [{ field: 'price', op: 'between', min: 40, max: 60 }] })
    expect(new Set(r.entityIds)).toEqual(new Set(['p1', 'p3']))
  })
  it('a number facet does not match a text lane', async () => {
    const r = await queryEntities(h.db, { entityType: 'product', facets: [{ field: 'material', op: 'gt', value: 0 } as never] })
    expect(r.entityIds).toEqual([])
  })
  it('in text facet matches any listed value', async () => {
    const r = await queryEntities(h.db, { entityType: 'product', facets: [{ field: 'material', op: 'in', value: ['suede', 'leather'] }] })
    expect(new Set(r.entityIds)).toEqual(new Set(['p1', 'p2', 'p3']))
  })
  it('in with an empty list yields an empty candidate set', async () => {
    const r = await queryEntities(h.db, { entityType: 'product', facets: [{ field: 'material', op: 'in', value: [] }] })
    expect(r.entityIds).toEqual([])
  })
  it('has-ref facet matches the relationship lane', async () => {
    const r = await queryEntities(h.db, { entityType: 'product', facets: [{ field: 'brand', op: 'has-ref', refType: 'vendor', refId: 'v9' }] })
    expect(new Set(r.entityIds)).toEqual(new Set(['p1', 'p2']))
  })
  it('does not interpolate a quote-bearing value (parameterised, no injection)', async () => {
    const r = await queryEntities(h.db, { entityType: 'product', facets: [{ field: 'material', op: 'eq', value: "leather' OR '1'='1" }] })
    expect(r.entityIds).toEqual([])
  })
  it('binds the field_key sink — a quote-bearing field cannot widen the predicate', async () => {
    // If field_key were interpolated, `field_key = 'x' OR '1'='1'` would return every product row.
    const r = await queryEntities(h.db, { entityType: 'product', facets: [{ field: "material' OR '1'='1", op: 'eq', value: 'leather' }] })
    expect(r.entityIds).toEqual([])
  })
  it('binds each IN-list element — a quote-bearing element cannot inject', async () => {
    const r = await queryEntities(h.db, { entityType: 'product', facets: [{ field: 'material', op: 'in', value: ["leather') OR ('1'='1"] }] })
    expect(r.entityIds).toEqual([])
  })
  it('binds the has-ref refType/refId sinks — quote-bearing refs cannot inject', async () => {
    const r = await queryEntities(h.db, { entityType: 'product', facets: [{ field: 'brand', op: 'has-ref', refType: "vendor' OR '1'='1", refId: "v9' OR '1'='1" }] })
    expect(r.entityIds).toEqual([])
  })
  it('between min/max is parameterised — a hostile string max cannot inject (was the proven P0)', async () => {
    // Pre-fix this raw-interpolated `max` exfiltrated rows (UNION SELECT). The Number.isFinite guard
    // short-circuits a non-numeric `max` to an empty set before any SQL is built (finite values are also bound).
    const r = await queryEntities(h.db, { entityType: 'product', facets: [
      { field: 'price', op: 'between', min: 0, max: "999999 UNION SELECT 'SECRET-1'" as unknown as number }] })
    expect(r.entityIds).toEqual([])
  })
  it('a non-finite numeric facet short-circuits to empty (no invalid SQL)', async () => {
    const r = await queryEntities(h.db, { entityType: 'product', facets: [{ field: 'price', op: 'gt', value: Number.POSITIVE_INFINITY }] })
    expect(r.entityIds).toEqual([])
  })
  it('rejects an over-large facet count fail-loud (DoS guard)', async () => {
    const facets = Array.from({ length: 64 }, () => ({ field: 'material', op: 'eq' as const, value: 'leather' }))
    await expect(queryEntities(h.db, { entityType: 'product', facets })).rejects.toSatisfy(isFieldValidationError)
  })
  it('rejects an over-large IN-list fail-loud (DoS guard)', async () => {
    const value = Array.from({ length: 300 }, (_, i) => `v${i}`)
    await expect(
      queryEntities(h.db, { entityType: 'product', facets: [{ field: 'material', op: 'in', value }] }),
    ).rejects.toSatisfy(isFieldValidationError)
  })
  it('caps the candidate set + flags capped', async () => {
    const r = await queryEntities(h.db, { entityType: 'product', facets: [{ field: 'material', op: 'eq', value: 'leather' }], limit: 1 })
    expect(r.entityIds.length).toBe(1); expect(r.capped).toBe(true)
  })
})
