import { describe, expect, it } from 'vitest'
import type { AnyFieldDefinition, FieldGroup } from './schema.js'
import {
  authorizeRelationshipReplacement,
  decodeFieldValueNodes,
  encodeFieldValueNodes,
  flexibleRows,
  formatFieldValue,
  repeaterRows,
  validateFieldValue,
} from './values.js'
import {
  createFieldTypeRegistry,
  defineCustomField,
  isSanitizedFieldHtml,
  sanitizeFieldHtml,
  type CustomFieldTypeDescriptor,
  type RuntimeFieldTypeDescriptor,
} from './types.js'

function field<T extends AnyFieldDefinition['type']>(type:T,key:string,settings:Record<string,unknown>={}):AnyFieldDefinition {
  return { type, key, name:key, label:key, settings } as AnyFieldDefinition
}
const group:FieldGroup={
  key:'profile',title:'Profile',active:true,location:[[{parameter:'entityType',operator:'eq',value:'person'}]],
  fields:[
    field('text','title',{maxLength:100}),
    field('group','meta',{fields:[field('number','rating',{min:0,max:10})]}),
    field('repeater','links',{fields:[field('url','url',{schemes:['https']})],min:0,max:5}),
    field('flexible','sections',{layouts:[
      {key:'text',name:'text',label:'Text',fields:[field('textarea','body',{newline:'preserve'})]},
      {key:'metric',name:'metric',label:'Metric',fields:[field('number','amount',{})]},
    ],min:0,max:5}),
  ],
}

describe('recursive field values',()=>{
  it('preserves stable row/layout identity and canonical typed paths across reorder',async()=>{
    const registry=createFieldTypeRegistry()
    const values={
      title:'Hello',meta:{rating:7},
      links:[{rowId:'row/b',values:{url:'https://example.com/b'}},{rowId:'row~a',values:{url:'https://example.com/a'}}],
      sections:[{rowId:'s2',layoutKey:'metric',values:{amount:9}},{rowId:'s1',layoutKey:'text',values:{body:'body'}}],
    }
    for(const definition of group.fields) if(Object.prototype.hasOwnProperty.call(values,definition.key)) await validateFieldValue(definition,(values as Record<string,unknown>)[definition.key],registry)
    const nodes=encodeFieldValueNodes(group,values,{entityType:'person',entityId:'p1',definitionRevision:4,nodeIdForPath:(path)=>`node:${path}`})
    expect(nodes.some((node)=>node.path.includes('/r:row~1b/'))).toBe(true)
    expect(nodes.some((node)=>node.path.includes('/r:row~0a/'))).toBe(true)
    expect(nodes.some((node)=>node.path.includes('/l:metric/'))).toBe(true)
    const decoded=decodeFieldValueNodes(group,nodes,{definitionRevision:4})
    expect(decoded).toEqual(values)
    expect([...repeaterRows(decoded.links as never[] )].map((row)=>row.rowId)).toEqual(['row/b','row~a'])
    expect([...flexibleRows(decoded.sections as never[], 'text')].map((row)=>row.rowId)).toEqual(['s1'])
  })

  it('validates representative contracts from every built-in family',async()=>{
    const registry=createFieldTypeRegistry()
    const cases:Array<[AnyFieldDefinition,unknown,unknown]>=[
      [field('text','t',{maxLength:5}),'abc','abc'],
      [field('number','n',{min:1,max:5,integer:true}),3,3],
      [field('boolean','b',{}),true,true],
      [field('checkbox','c',{choices:{a:'A',b:'B'},min:1,max:2}),['a','a','b'],['a','b']],
      [field('link','l',{schemes:['https']}),{url:'https://example.com',title:'Example'},{url:'https://example.com/',title:'Example'}],
      [field('image','i',{return:'ref'}),{id:'m1',kind:'image'}, {id:'m1',kind:'image'}],
      [field('relationship','r',{entityTypes:['person'],return:'ref',min:1,max:2}),[{entityType:'person',id:'p2'}],[{entityType:'person',id:'p2'}]],
      [field('taxonomy','tax',{taxonomies:['topic'],multiple:true,return:'ref'}),[{taxonomy:'topic',id:'x'}],[{taxonomy:'topic',id:'x'}]],
      [field('user','u',{multiple:false,return:'ref'}),{id:'u1'},{id:'u1'}],
      [field('date','d',{inputFormat:'Y-m-d',displayFormat:'Y-m-d',returnFormat:'Y-m-d'}),'2026-08-22','2026-08-22'],
      [field('dateTime','dt',{inputFormat:'iso',displayFormat:'iso',returnFormat:'iso',timezone:'utc'}),'2026-08-22T01:02:03+00:00','2026-08-22T01:02:03.000Z'],
      [field('color','color',{alpha:true}),'#Aa00ff80','#aa00ff80'],
      [field('icon','icon',{sets:['core']}),{set:'core',name:'star'},{set:'core',name:'star'}],
      [field('map','map',{providerKey:'host'}),{latitude:18.8,longitude:98.9,address:'CM'},{latitude:18.8,longitude:98.9,address:'CM'}],
    ]
    for(const [definition,input,expected] of cases) {
      const context = ['image','file','gallery'].includes(definition.type)
        ? { mediaMetadata: { resolve: async () => ({ mimeType: 'image/png', bytes: 100, width: 200, height: 200 }) } }
        : {}
      expect(await validateFieldValue(definition,input,registry,context)).toEqual(expected)
    }
    await expect(validateFieldValue(field('password','pw',{hasherKey:'argon',minLength:8}),{operation:'set',value:'short'},registry)).rejects.toMatchObject({name:'FieldValidationError'})
    await expect(validateFieldValue(field('message','m',{text:'hi'}),'stored',registry)).rejects.toMatchObject({name:'FieldValidationError'})
  })

  it('requires authoritative media metadata and target authorization',async()=>{
    const registry=createFieldTypeRegistry()
    const image=field('image','hero',{return:'ref',mimeTypes:['image/*'],minBytes:10,minWidth:100})
    await expect(validateFieldValue(image,{id:'m1',kind:'image'},registry)).rejects.toMatchObject({field:'mediaMetadata'})
    await expect(validateFieldValue(image,{id:'m1',kind:'image'},registry,{mediaMetadata:{resolve:async()=>({mimeType:'image/png',bytes:20,width:120,height:80})}})).resolves.toEqual({id:'m1',kind:'image'})

    const checked:string[]=[]
    const relationship=field('relationship','related',{entityTypes:['person'],return:'ref',bidirectional:{targetFieldKey:'related'}})
    await validateFieldValue(relationship,[{entityType:'person',id:'p2'}],registry,{authorization:{assertTarget:(ref,action)=>{checked.push(`${action}:${ref.id}`)}}})
    await authorizeRelationshipReplacement(relationship,[{entityType:'person',id:'old'}],[{entityType:'person',id:'new'}],{assertTarget:(ref,action)=>{checked.push(`${action}:${ref.id}`)}})
    expect(checked).toEqual(['readTarget:p2','readTarget:new','writeReverseTarget:old','writeReverseTarget:new'])
  })


  it('parses built-in definitions through the canonical schema trust boundary and sanitizes rich text',async()=>{
    const registry=createFieldTypeRegistry()
    expect(()=>registry.parseDefinition({
      type:'url',key:'site',name:'site',label:'Site',settings:{schemes:['https'],unexpected:true},
    },{maxBytes:4096,maxDepth:4,maxChildren:16})).toThrowError()

    const rich=field('richText','body',{toolbar:'basic'})
    await expect(validateFieldValue(rich,'<script>x()</script><b>safe</b>',registry)).rejects.toMatchObject({field:'sanitizer'})
    const context={sanitizer:{sanitizeHtml:(html:string)=>html.replace(/<script>.*?<\/script>/g,'')}}
    const canonical=await validateFieldValue(rich,'<script>x()</script><b>safe</b>',registry,context)
    expect(canonical).toBe('<b>safe</b>')
    const formatted=await formatFieldValue(rich,canonical,registry,{media:async()=>'',entity:async()=>'',term:async()=>'',user:async()=>''},context)
    expect(formatted.kind).toBe('sanitizedHtml')
    if(formatted.kind==='sanitizedHtml')expect(isSanitizedFieldHtml(formatted.value)).toBe(true)
  })

  it('authorizes relationship targets before formatted projection',async()=>{
    const registry=createFieldTypeRegistry(),calls:string[]=[]
    const relationship=field('relationship','related',{entityTypes:['person'],return:'object'})
    const formatted=await formatFieldValue(
      relationship,[{entityType:'person',id:'p9'}],registry,
      {media:async()=>'',entity:async(ref)=>({id:ref.id}),term:async()=>'',user:async()=>''},
      {authorization:{assertTarget:(ref,action)=>{calls.push(`${action}:${ref.id}`)}}},
    )
    expect(calls).toEqual(['readTarget:p9'])
    expect(formatted).toEqual({kind:'structured',value:[{id:'p9'}]})
  })

  it('projects refs and rejects unbranded custom HTML presentation',async()=>{
    const registry=createFieldTypeRegistry()
    const image=field('image','hero',{return:'url'})
    const formatted=await formatFieldValue(image,{id:'m1',kind:'image'},registry,{media:async()=>'/media/m1',entity:async()=>'',term:async()=>'',user:async()=>''})
    expect(formatted).toEqual({kind:'text',value:'/media/m1'})

    const descriptor:CustomFieldTypeDescriptor<'acme:unsafe',{},string,string,string>={
      type:'acme:unsafe',contractVersion:'1',schemaHash:'a'.repeat(64),codecHash:'b'.repeat(64),
      parseDefinition:(input)=>input as never,parseValue:(input)=>String(input),parseWrite:(input)=>String(input),serialize:(value)=>String(value),deserialize:(value)=>String(value),
      format:()=>({kind:'sanitizedHtml',value:{html:'<img onerror=1>'}} as never),schema:()=>({type:'string',nullable:false,constraints:{}}),redactForRevision:(value)=>String(value),
    }
    const custom=defineCustomField(descriptor,{key:'unsafe',name:'unsafe',label:'Unsafe',type:'acme:unsafe',settings:{}})
    const customRegistry=createFieldTypeRegistry({extensions:[descriptor as RuntimeFieldTypeDescriptor]})
    await expect(formatFieldValue(custom,'x',customRegistry,{media:async()=>'',entity:async()=>'',term:async()=>'',user:async()=>''})).rejects.toMatchObject({name:'FieldValidationError'})
    const safe=await sanitizeFieldHtml('<b>x</b>',{sanitizer:{sanitizeHtml:()=>'<b>x</b>'}})
    expect(isSanitizedFieldHtml(safe)).toBe(true)
  })
})
