import { sql } from 'drizzle-orm'
import { describe, expect, it } from 'vitest'
import { lifecycleFixture, writeDeps } from './lifecycle.test-helpers.js'
import { createContentEntry } from './lifecycle.js'
import { resolveContentType } from './registry.js'
import { resolveContentStatuses } from './status.js'
import { buildContentQueryPredicate, count, countByStatus, listPage, type ContentQueryContext, type ContentSearchAdapter } from './query.js'
import { createContentPostgresSearchAdapter } from './search.js'
import type { ContentPrincipal } from './authz.js'

async function context(fx: Awaited<ReturnType<typeof lifecycleFixture>>, principal: ContentPrincipal): Promise<ContentQueryContext> {
  const [type,statuses]=await Promise.all([
    fx.db.transaction((tx)=>resolveContentType(tx,'book')),
    fx.db.transaction((tx)=>resolveContentStatuses(tx)),
  ])
  return {principal,types:[type],statuses}
}

const reader:ContentPrincipal={id:'reader',capabilities:new Set(['read'])}
const member:ContentPrincipal={id:'member',member:true,capabilities:new Set(['read'])}
const privateReader:ContentPrincipal={id:'private',capabilities:new Set(['read','read_private'])}

describe('canonical content query',()=>{
  it('applies authorization before count/order/pagination and binds exact definition revisions',async()=>{
    const fx=await lifecycleFixture()
    try{
      const deps=writeDeps()
      const admin={id:'admin',capabilities:new Set(['create','read','read_private','edit_own','edit_others','edit_private','edit_published','publish','delete_own','delete_others','delete_private','delete_published','manage_terms'])}
      const pub=await fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'q:public',slug:'public',type:'book',title:'Zulu',body:'x',visibility:'public'},admin,deps))
      const priv=await fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'q:private',slug:'private',type:'book',title:'Alpha',body:'x',visibility:'private'},admin,deps))
      const mem=await fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'q:members',slug:'members',type:'book',title:'Middle',body:'x',visibility:'members'},admin,deps))

      const readerCtx=await context(fx,reader)
      const memberCtx=await context(fx,member)
      const privateCtx=await context(fx,privateReader)
      expect(await fx.db.transaction((tx)=>count(tx,{},readerCtx))).toBe(1)
      expect(await fx.db.transaction((tx)=>count(tx,{},memberCtx))).toBe(2)
      expect(await fx.db.transaction((tx)=>count(tx,{},privateCtx))).toBe(2)

      const page=await fx.db.transaction((tx)=>listPage(tx,{orderBy:'title',direction:'asc',page:1,pageSize:2},privateCtx))
      expect(page).toMatchObject({page:1,pageSize:2,totalItems:2,totalPages:1})
      expect(page.items.map((item)=>item.id)).toEqual([priv.id,pub.id])

      await fx.db.transaction((tx)=>tx.execute(sql`UPDATE content_entries SET status_definition_revision = 999 WHERE id = ${pub.id}`))
      expect(await fx.db.transaction((tx)=>count(tx,{},readerCtx))).toBe(0)
      expect(await fx.db.transaction((tx)=>count(tx,{includeIds:[mem.id]},memberCtx))).toBe(1)
    }finally{await fx.stop()}
  },90_000)

  it('computes every status lane from a status-free query and rejects runtime status injection',async()=>{
    const fx=await lifecycleFixture()
    try{
      const deps=writeDeps()
      const principal={id:'admin',capabilities:new Set(['create','read','read_private'])}
      await fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'q:lane',slug:'lane',type:'book',title:'Lane',body:'x'},principal,deps))
      const ctx=await context(fx,principal)
      const counts=await fx.db.transaction((tx)=>countByStatus(tx,{types:['book']},ctx))
      expect(counts).toMatchObject({draft:1,published:0,trashed:0})
      await expect(fx.db.transaction((tx)=>countByStatus(tx,{types:['book'],statuses:['draft']} as never,ctx))).rejects.toMatchObject({code:'validation',field:'statuses'})
    }finally{await fx.stop()}
  },90_000)

  it('delegates search pagination/count authoritatively without capped candidate intersections',async()=>{
    const fx=await lifecycleFixture()
    try{
      const deps=writeDeps()
      const principal={id:'admin',capabilities:new Set(['create','read'])}
      const first=await fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'q:s1',slug:'s1',type:'book',title:'Search One',body:'x'},principal,deps))
      await fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'q:s2',slug:'s2',type:'book',title:'Search Two',body:'x'},principal,deps))
      const base=await context(fx,principal)
      let calls=0,receivedPageSize=0,predicateSeen=false
      const search:ContentSearchAdapter={
        async searchPage(_db,input,authorizedPredicate){
          calls+=1
          receivedPageSize=input.pageSize ?? 0
          predicateSeen=Boolean(authorizedPredicate)
          return {ids:[first.id],totalItems:12_345,rank:{[first.id]:9}}
        },
      }
      const ctx={...base,capabilities:{search}}
      const page=await fx.db.transaction((tx)=>listPage(tx,{search:'needle',page:7,pageSize:1},ctx))
      expect(page).toMatchObject({page:7,pageSize:1,totalItems:12_345,totalPages:12_345})
      expect(page.items.map((item)=>item.id)).toEqual([first.id])
      expect(calls).toBe(1)
      expect(receivedPageSize).toBe(1)
      expect(predicateSeen).toBe(true)
      expect(await fx.db.transaction((tx)=>count(tx,{search:'needle'},ctx))).toBe(12_345)
      await expect(fx.db.transaction((tx)=>listPage(tx,{search:'needle'},base))).rejects.toMatchObject({code:'capability-unavailable'})
    }finally{await fx.stop()}
  },90_000)


  it('uses the canonical PostgreSQL adapter for title/slug/excerpt/body and preserves total on an empty later page',async()=>{
    const fx=await lifecycleFixture()
    try{
      const deps=writeDeps()
      const principal={id:'admin',capabilities:new Set(['create','read'])}
      await fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'q:p1',slug:'first-needle',type:'book',title:'One',body:'plain',excerpt:'x'},principal,deps))
      await fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'q:p2',slug:'second',type:'book',title:'Needle Two',body:'plain',excerpt:'x'},principal,deps))
      await fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'q:p3',slug:'third',type:'book',title:'Three',body:'<p>needle body</p>',excerpt:'x'},principal,deps))
      const base=await context(fx,principal)
      const ctx={...base,capabilities:{search:createContentPostgresSearchAdapter()}}
      const page=await fx.db.transaction((tx)=>listPage(tx,{search:'needle',page:1,pageSize:2},ctx))
      expect(page.totalItems).toBe(3)
      expect(page.items).toHaveLength(2)
      const empty=await fx.db.transaction((tx)=>listPage(tx,{search:'needle',page:9,pageSize:2},ctx))
      expect(empty.items).toEqual([])
      expect(empty.totalItems).toBe(3)
      expect(empty.totalPages).toBe(2)
    }finally{await fx.stop()}
  },90_000)

  it('builds a fail-closed predicate for principals with no mapped read capability',async()=>{
    const fx=await lifecycleFixture()
    try{
      const ctx=await context(fx,{id:'none',capabilities:new Set()})
      const predicate=buildContentQueryPredicate({},ctx)
      const rows=await fx.db.transaction((tx)=>tx.execute<{count:number|string}>(sql`SELECT COUNT(*) AS count FROM content_entries WHERE ${predicate}`))
      expect(Number(rows[0]?.count ?? 0)).toBe(0)
    }finally{await fx.stop()}
  },90_000)
})
