import { describe, expect, it } from 'vitest'
import { bookType } from './lifecycle.test-helpers.js'
import {
  createContentHeadlessRuntime,
  generateContentSchema,
  type ContentOperationHandlers,
  type ContentOperationKey,
  type ContentSchemaSources,
} from './headless.js'
import type { ContentPrincipal } from './authz.js'
import type { ResolvedContentType } from './registry.js'
import type { ResolvedContentStatus } from './status.js'

function type(overrides:Partial<ResolvedContentType>={}):ResolvedContentType{
  return {...bookType(),origin:'db',version:1,revision:1,canonicalHash:'a'.repeat(64),...overrides}
}
function status(key:string,published=false,overrides:Partial<ResolvedContentStatus>={}):ResolvedContentStatus{
  return {
    key,label:key,published,internal:false,excludeFromSearch:!published,publiclyQueryable:published,showInAdminAll:true,showInAdminStatusFilter:true,
    dateLabel:published?'published':'lastModified',transitionInput:published?'publishedAt':'none',origin:'db',version:1,revision:1,active:true,canonicalHash:key.padEnd(64,'a').slice(0,64),...overrides,
  }
}
function sources(contentType:ResolvedContentType=type()):ContentSchemaSources{return {types:[contentType],statuses:[status('draft'),status('published',true),status('trashed')],taxonomies:[]}}
const keys:ContentOperationKey[]=['list','count','get','create','update','transition','trash','restore','delete','listRevisions','getRevision','restoreRevision','createAutosave','getAutosave','restoreAutosave']
function handlers(calls:string[]=[],inputs:Record<string,unknown>={}):ContentOperationHandlers{
  const group:Record<string,(input:unknown,principal:ContentPrincipal)=>Promise<never>>={}
  for(const key of keys)group[key]=async(input,principal)=>{calls.push(`${key}:${principal.id}`);inputs[key]=input;return undefined as never}
  return {book:group} as ContentOperationHandlers
}
const reader:ContentPrincipal={id:'reader',capabilities:new Set(['read'])}
const editor:ContentPrincipal={id:'editor',capabilities:new Set(['read','edit_own'])}
const publisher:ContentPrincipal={id:'publisher',capabilities:new Set(['read','edit_own','publish'])}
const creator:ContentPrincipal={id:'creator',capabilities:new Set(['read','create'])}

describe('content headless schema/runtime',()=>{
  it('generates deterministic actor-independent, type-identified operation metadata',async()=>{
    const first=await generateContentSchema(sources())
    const second=await generateContentSchema(sources())
    expect(first).toEqual(second)
    expect(first.version).toMatch(/^[0-9a-f]{64}$/)
    expect(first.operations).toHaveLength(15)
    expect(new Set(first.operations.map((op)=>`${op.typeKey}:${op.key}`)).size).toBe(15)
    expect(first.operations.every((op)=>op.typeKey==='book'&&typeof op.capability==='string')).toBe(true)
    expect(JSON.stringify(first)).not.toContain('reader')
    const changed=await generateContentSchema({...sources(),statuses:[status('draft'),status('published',true,{revision:2,canonicalHash:'b'.repeat(64)}),status('trashed')]})
    expect(changed.version).not.toBe(first.version)
  })

  it('keeps existing inactive types readable/editable but does not advertise create',async()=>{
    const schema=await generateContentSchema(sources(type({active:false})))
    const advertised=schema.operations.map((op)=>op.key)
    expect(advertised).toContain('get')
    expect(advertised).toContain('update')
    expect(advertised).not.toContain('create')
  })

  it('fails schema generation when exposed taxonomy/custom-field metadata has no source',async()=>{
    await expect(generateContentSchema(sources(type({taxonomies:['topic']})))).rejects.toMatchObject({code:'invalid-schema-source',field:'taxonomies'})
    await expect(generateContentSchema(sources(type({supports:[...type().supports,'customFields']})))).rejects.toMatchObject({code:'invalid-schema-source',field:'fields'})
    const withSources:ContentSchemaSources={
      ...sources(type({taxonomies:['topic'],supports:[...type().supports,'customFields']})),
      taxonomies:[{key:'topic',version:'1',schema:{type:'taxonomy'}}],
      fields:{async schemaForType(typeKey){return {typeKey,fields:[{key:'rating'}]}}},
    }
    const schema=await generateContentSchema(withSources)
    expect(JSON.stringify(schema.types[0])).toContain('rating')
  })

  it('requires exact equality between advertised operation pairs and supplied handlers',async()=>{
    const complete=handlers()
    const missing={book:{...(complete.book as object)}} as ContentOperationHandlers
    delete (missing.book as Record<string,unknown>).get
    await expect(createContentHeadlessRuntime(sources(),missing)).rejects.toMatchObject({code:'handler-mismatch'})
    const extra={book:{...(complete.book as object),bogus:async()=>undefined}} as unknown as ContentOperationHandlers
    await expect(createContentHeadlessRuntime(sources(),extra)).rejects.toMatchObject({code:'handler-mismatch'})
    await expect(createContentHeadlessRuntime(sources(),complete)).resolves.toMatchObject({schema:{operations:expect.any(Array)}})
  })

  it('decodes unknown inputs, binds create type, authorizes before dispatch, and decodes JSON dates',async()=>{
    const calls:string[]=[],inputs:Record<string,unknown>={}
    const runtime=await createContentHeadlessRuntime(sources(),handlers(calls,inputs))
    const wrongType=await runtime.invoke('book','create',{slug:'a',type:'story',title:'A',body:'x',operationId:'op'},creator)
    expect(wrongType).toMatchObject({ok:false,error:{category:'validation',fieldPath:'type'}})
    expect(calls).toEqual([])

    const denied=await runtime.invoke('book','create',{slug:'a',type:'book',title:'A',body:'x',operationId:'op'},reader)
    expect(denied).toMatchObject({ok:false,error:{category:'authorization'}})
    expect(calls).toEqual([])

    const update=await runtime.invoke('book','update',{id:'11111111-1111-4111-8111-111111111111',patch:{title:'B'},expectedUpdatedAt:'2026-08-22T00:00:00Z',operationId:'u1'},editor)
    expect(update.ok).toBe(true)
    expect((inputs.update as {expectedUpdatedAt:Date}).expectedUpdatedAt).toBeInstanceOf(Date)
    expect(calls).toEqual(['update:editor'])

    const unknown=await runtime.invoke('book','update',{id:'11111111-1111-4111-8111-111111111111',patch:{title:'B'},expectedUpdatedAt:'2026-08-22T00:00:00Z',operationId:'u2',status:'published'} as never,editor)
    expect(unknown).toMatchObject({ok:false,error:{category:'validation'}})
    expect(calls).toHaveLength(1)
  })

  it('requires publish capability for published transitions and never dispatches unadvertised pairs',async()=>{
    const calls:string[]=[]
    const runtime=await createContentHeadlessRuntime(sources(),handlers(calls))
    const transitionInput={id:'11111111-1111-4111-8111-111111111111',to:'published',expectedUpdatedAt:'2026-08-22T00:00:00Z',operationId:'t1'}
    expect(await runtime.invoke('book','transition',transitionInput,editor)).toMatchObject({ok:false,error:{category:'authorization'}})
    expect(calls).toEqual([])
    expect((await runtime.invoke('book','transition',transitionInput,publisher)).ok).toBe(true)
    expect(calls).toEqual(['transition:publisher'])
    const disabled=await runtime.invoke('missing','get',{id:'11111111-1111-4111-8111-111111111111'},reader)
    expect(disabled).toMatchObject({ok:false,error:{code:'operation-disabled',category:'capability'}})
    expect(calls).toHaveLength(1)
  })
})
