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, queryFieldEntities, validateFieldsEntityQuery } 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)
  })
})


const finalGroups = [{
  key:'catalog',title:'Catalog',active:true,location:[[{parameter:'entityType',operator:'eq',value:'product'}]],
  fields:[
    {type:'text',key:'material',name:'material',label:'Material',settings:{}},
    {type:'number',key:'price',name:'price',label:'Price',settings:{}},
    {type:'relationship',key:'brand',name:'brand',label:'Brand',settings:{entityTypes:['vendor'],return:'ref'}},
    {type:'group',key:'dimensions',name:'dimensions',label:'Dimensions',settings:{fields:[{type:'number',key:'width',name:'width',label:'Width',settings:{}}]}},
  ],
}] as const satisfies readonly import('./schema.js').FieldGroup[]

describe('authoritative field query',()=>{
  it('passes validated query through authorization scope and preserves authoritative total count without caps',async()=>{
    let scoped=false,received:unknown
    const scope={policyVersion:'policy-7',applyTo<T>(query:T){scoped=true;return query}}
    const adapter={async query(input:unknown,authorized:unknown){received={input,authorized};return {items:[{id:'p3'},{id:'p4'}],totalItems:12_345}}}
    const result=await queryFieldEntities(adapter,{
      entityTypes:['product','product'],predicate:{op:'and',children:[
        {op:'eq',path:['catalog','material'],value:'leather'},
        {op:'gte',path:['catalog','price'],value:40},
      ]},orderBy:{path:['catalog','price'],direction:'desc',nulls:'last'},page:2,pageSize:2,
    },finalGroups,scope)
    expect(scoped).toBe(true)
    expect(result).toEqual({items:[{id:'p3'},{id:'p4'}],totalItems:12_345})
    expect((received as {input:{entityTypes:string[]}}).input.entityTypes).toEqual(['product'])
  })

  it('rejects unsupported field/operator combinations and bounded-input violations before adapter execution',async()=>{
    expect(()=>validateFieldsEntityQuery({entityTypes:['product'],predicate:{op:'contains',path:['catalog','price'],value:'4'},page:1,pageSize:20},finalGroups)).toThrowError(expect.objectContaining({name:'FieldValidationError'}))
    expect(()=>validateFieldsEntityQuery({entityTypes:['product'],predicate:{op:'in',path:['catalog','brand'],values:Array.from({length:300},(_,i)=>`v${i}`)},page:1,pageSize:20},finalGroups)).toThrowError(expect.objectContaining({field:'query.predicate.values'}))
    expect(()=>validateFieldsEntityQuery({entityTypes:['product'],predicate:{op:'eq',path:['catalog','dimensions','width'],value:2},page:1,pageSize:201},finalGroups)).toThrowError(expect.objectContaining({field:'query.pageSize'}))
  })
})
