import { afterEach, describe, expect, it } from 'vitest'
import { sql } from 'drizzle-orm'
import { makePgHarness } from './pg-harness.js'
import { fieldsMigrationSql, fieldsRecursiveEavForwardMigrationSql } from './migrate.js'
import { fieldsRuntimeMigrationSql } from './revisions.js'
import { canonicalFieldGroupHash, type FieldGroup } from './schema.js'
import { createFieldTypeRegistry } from './types.js'
import { fieldsRequestHash, type CodeFieldsRegistrySnapshot, type CodeFinalFieldGroup } from './definition-lifecycle.js'
import type { CodeOptionsPage } from './options.js'
import type { CodeFieldBlock, FieldBlockDefinition } from './blocks.js'
import { generateFieldsHeadlessSchema } from './headless.js'

const profile:FieldGroup=Object.freeze({
  key:'profile',title:'DB Profile',active:true,
  location:Object.freeze([Object.freeze([{parameter:'entityType',operator:'eq' as const,value:'content'}])]),
  fields:Object.freeze([
    Object.freeze({key:'title',name:'title',label:'Title',type:'text',required:true,settings:Object.freeze({maxLength:100})}),
    Object.freeze({key:'secret',name:'secret',label:'Secret',type:'password',settings:Object.freeze({hasherKey:'argon2id',minLength:12})}),
    Object.freeze({key:'admin_note',name:'admin_note',label:'Admin Note',type:'message',settings:Object.freeze({})}),
    Object.freeze({key:'disabled_value',name:'disabled_value',label:'Disabled',type:'text',disabled:true,settings:Object.freeze({})}),
    Object.freeze({key:'nested',name:'nested',label:'Nested',type:'group',settings:Object.freeze({fields:Object.freeze([
      Object.freeze({key:'nested_text',name:'nested_text',label:'Nested Text',type:'text',defaultValue:'DEFAULT-SECRET',settings:Object.freeze({maxLength:20})}),
      Object.freeze({key:'nested_password',name:'nested_password',label:'Nested Password',type:'password',settings:Object.freeze({hasherKey:'nestedHasher',minLength:16})}),
      Object.freeze({key:'nested_message',name:'nested_message',label:'Nested Message',type:'message',settings:Object.freeze({text:'admin only'})}),
    ])})}),
  ]),
})
const optionGroup:FieldGroup=Object.freeze({
  key:'site_fields',title:'Site Fields',active:true,
  location:Object.freeze([Object.freeze([{parameter:'optionsPage',operator:'eq' as const,value:'site'}])]),
  fields:Object.freeze([Object.freeze({key:'headline',name:'headline',label:'Headline',type:'text',settings:Object.freeze({maxLength:80})})]),
})
const optionDefinition=Object.freeze({key:'site',title:'Site Settings',readCapability:'options.read',writeCapability:'options.write',manageCapability:'options.manage',position:10})
const hero:FieldBlockDefinition=Object.freeze({key:'hero',title:'Hero',fieldGroupKeys:Object.freeze(['profile']),supports:Object.freeze({anchor:true})})
let teardown:(()=>Promise<void>)|undefined
afterEach(async()=>{await teardown?.();teardown=undefined})
async function fixture(){const harness=await makePgHarness();teardown=harness.teardown;for(const statement of fieldsMigrationSql().split(';').map((item)=>item.trim()).filter(Boolean))await harness.db.execute(sql.raw(statement));for(const statement of fieldsRecursiveEavForwardMigrationSql('postgres'))await harness.db.execute(sql.raw(statement));for(const statement of fieldsRuntimeMigrationSql('postgres'))await harness.db.execute(sql.raw(statement));return harness}
async function insertGroup(db:Awaited<ReturnType<typeof makePgHarness>>['db'],group:FieldGroup,version=1,revision=1,hostSessionId:string|null=null){const hash=await canonicalFieldGroupHash(group),now='2026-08-22T00:00:00.000Z';await db.execute(sql`INSERT INTO field_group_definitions (key,origin,version,active,current_revision,canonical_hash,definition,host_session_id,created_at,updated_at) VALUES (${group.key},'db',${version},true,${revision},${hash},${JSON.stringify(group)},${hostSessionId},${now},${now})`)}
async function insertOption(db:Awaited<ReturnType<typeof makePgHarness>>['db']){const hash=await fieldsRequestHash(optionDefinition),now='2026-08-22T00:00:00.000Z';await db.execute(sql`INSERT INTO field_options_definitions (key,origin,version,active,current_revision,canonical_hash,definition,created_at,updated_at) VALUES (${optionDefinition.key},'db',1,true,1,${hash},${JSON.stringify(optionDefinition)},${now},${now})`)}
async function insertBlock(db:Awaited<ReturnType<typeof makePgHarness>>['db']){const hash=await fieldsRequestHash(hero),now='2026-08-22T00:00:00.000Z';await db.execute(sql`INSERT INTO field_block_definitions (key,origin,version,active,current_revision,canonical_hash,definition,created_at,updated_at) VALUES (${hero.key},'db',1,true,1,${hash},${JSON.stringify(hero)},${now},${now})`)}
async function codeRegistry(groups:readonly CodeFinalFieldGroup[]=[],optionsPages:readonly CodeOptionsPage[]=[],blocks:readonly CodeFieldBlock[]=[]):Promise<CodeFieldsRegistrySnapshot>{const version='code-v1',canonicalHash=await fieldsRequestHash({version,groups,optionsPages,blocks});return Object.freeze({version,canonicalHash,groups,optionsPages,blocks})}

describe('fields headless schema',()=>{
  it('is deterministic, uses effective code authority, omits unpublished/admin/sensitive metadata, and describes options/blocks',async()=>{
    const {db}=await fixture();await insertGroup(db,profile,4,3);await insertGroup(db,optionGroup);await insertOption(db);await insertBlock(db)
    await db.execute(sql`INSERT INTO fields_import_journal (import_id,host_session_id,manifest_hash,plan_hash,state,version,next_section,next_offset,payload,created_at,updated_at) VALUES (${crypto.randomUUID()},'hidden-headless',${'a'.repeat(64)},${'b'.repeat(64)},'staged',1,NULL,0,${JSON.stringify({plan:{}})},NOW(),NOW())`)
    await insertGroup(db,Object.freeze({...profile,key:'hidden_group',title:'Hidden'}),1,1,'hidden-headless')
    const codeGroup:CodeFinalFieldGroup=Object.freeze({...profile,title:'Code Profile',origin:'code' as const}),code=await codeRegistry([codeGroup])
    const registry=createFieldTypeRegistry(),first=await generateFieldsHeadlessSchema(db,{registry,codeRegistry:code}),second=await generateFieldsHeadlessSchema(db,{registry,codeRegistry:code})
    expect(first).toEqual(second)
    expect(first.version).toMatch(/^[0-9a-f]{64}$/)
    expect(first.registryVersion).toBe(registry.version)
    expect(first.groups.map((group)=>group.key)).toEqual(['profile','site_fields'])
    const effective=first.groups.find((group)=>group.key==='profile')!
    expect(effective).toMatchObject({title:'Code Profile',origin:'code',version:4,revision:3})
    expect(effective.fields.map((field)=>field.key)).toEqual(['title','secret','nested'])
    expect(effective.fields.find((field)=>field.key==='secret')).toMatchObject({readOnly:true,value:{type:'object',readOnly:true}})
    expect(JSON.stringify(effective)).not.toContain('argon2id')
    expect(JSON.stringify(effective)).not.toContain('admin_note')
    expect(JSON.stringify(effective)).not.toContain('disabled_value')
    expect(JSON.stringify(effective)).not.toContain('nestedHasher')
    expect(JSON.stringify(effective)).not.toContain('DEFAULT-SECRET')
    expect(JSON.stringify(effective)).not.toContain('nested_message')
    const nested=effective.fields.find((field)=>field.key==='nested')!
    expect((nested.value.constraints as {fields?:readonly {key:string}[]}).fields?.map((field)=>field.key)).toEqual(['nested_text','nested_password'])
    expect(first.optionsPages).toEqual([expect.objectContaining({key:'site',fieldGroupKeys:['site_fields'],authorization:{readCapability:'options.read',writeCapability:'options.write',manageCapability:'options.manage'}})])
    expect(first.blocks).toEqual([expect.objectContaining({key:'hero',fieldGroupKeys:['profile'],tree:expect.any(Object)})])
  })

  it('rejects a code registry whose hash does not match its immutable bytes',async()=>{
    const {db}=await fixture(),registry=createFieldTypeRegistry(),valid=await codeRegistry([Object.freeze({...profile,origin:'code' as const})])
    await expect(generateFieldsHeadlessSchema(db,{registry,codeRegistry:{...valid,canonicalHash:'0'.repeat(64)}})).rejects.toMatchObject({code:'invalid-registry'})
  })
})
