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, type FieldsAction, type FieldsAuthorization, type FieldsContext, type FieldsPrincipal } from './types.js'
import type { EntityRef } from './model.js'
import { encodeFieldValueNodes } from './values.js'
import { exportFields } from './export.js'
import { parseFieldsImport, type FieldsImportLimits } from './import.js'

const group: FieldGroup = Object.freeze({
  key:'profile', title:'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',settings:Object.freeze({maxLength:100})}),
    Object.freeze({key:'secret',name:'secret',label:'Secret',type:'password',settings:Object.freeze({hasherKey:'argon2id'})}),
  ]),
})
const limits:FieldsImportLimits=Object.freeze({maxBytes:1024*1024,maxDefinitions:100,maxEntities:100,maxRevisions:100,maxBlockNodes:1000,maxDepth:24})
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}
function context():FieldsContext{
  const principal:FieldsPrincipal=Object.freeze({id:'exporter',capabilities:new Set(['fields.manage'])})
  const authorization:FieldsAuthorization=Object.freeze({
    assert:(_principal:FieldsPrincipal,action:FieldsAction,ref:EntityRef)=>{if(action==='read'&&ref.entityType==='content'&&ref.entityId==='entry-denied')throw new Error('denied')},
    assertDefinitionMigration:()=>({policyVersion:'policy-1'}),
  })
  return Object.freeze({principal,authorization})
}
async function insertGroup(db:Awaited<ReturnType<typeof makePgHarness>>['db'],definition:FieldGroup,key=definition.key,hostSessionId:string|null=null){const hash=await canonicalFieldGroupHash(definition),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 (${key},'db',1,true,1,${hash},${JSON.stringify({...definition,key})},${hostSessionId},${now},${now})`);await db.execute(sql`INSERT INTO field_definition_versions (group_key,revision,canonical_hash,definition,origin,host_session_id,created_at) VALUES (${key},1,${hash},${JSON.stringify({...definition,key})},'db',${hostSessionId},${now})`)}
async function insertNodes(db:Awaited<ReturnType<typeof makePgHarness>>['db']){const ids=new Map<string,string>(),rows=encodeFieldValueNodes(group,{title:'Visible title',secret:{configured:true}},{entityType:'content',entityId:'entry-1',definitionRevision:1,nodeIdForPath:(path)=>{let id=ids.get(path);if(!id){id=crypto.randomUUID();ids.set(path,id)}return id}});for(const row of rows)await db.execute(sql`INSERT INTO field_value_nodes (node_id,entity_type,entity_id,group_key,definition_revision,field_key,path,parent_node_id,row_id,layout_key,node_kind,ordinal,is_null,value_text,value_number,value_boolean,value_date_time,value_ref,value_json,created_at,updated_at) VALUES (${row.nodeId},${row.entityType},${row.entityId},${row.groupKey},${row.definitionRevision},${row.fieldKey},${row.path},${row.parentNodeId},${row.rowId},${row.layoutKey},${row.nodeKind},${row.ordinal},${row.isNull},${row.valueText},${row.valueNumber},${row.valueBoolean},${row.valueDateTime},${row.valueRef},${row.valueJson},NOW(),NOW())`)}

describe('native fields export',()=>{
  it('exports published definitions and values, omits sensitive/inaccessible data, and round-trips through the import parser',async()=>{
    const {db}=await fixture();await insertGroup(db,group);await insertNodes(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-session',${'a'.repeat(64)},${'b'.repeat(64)},'staged',1,NULL,0,${JSON.stringify({plan:{}})},NOW(),NOW())`)
    const hidden={...group,key:'hidden_profile',title:'Hidden Profile'} as FieldGroup;await insertGroup(db,hidden,'hidden_profile','hidden-session')
    const manifest=await exportFields(db,{entities:[{entityType:'content',entityId:'entry-1'},{entityType:'content',entityId:'entry-denied'}]},context())
    expect(manifest.definitions.groups.map((item)=>item.key)).toEqual(['profile'])
    expect(manifest.entities).toEqual([expect.objectContaining({sourceRef:{entityType:'content',entityId:'entry-1'},values:{profile:{title:'Visible title'}}})])
    expect(manifest.omissions).toEqual(expect.arrayContaining([
      {sourceRef:{entityType:'content',entityId:'entry-1'},fieldPath:['profile','secret'],reason:'sensitive'},
      {sourceRef:{entityType:'content',entityId:'entry-denied'},fieldPath:['*'],reason:'unauthorized'},
    ]))
    const registry=createFieldTypeRegistry(),parsed=await parseFieldsImport(new TextEncoder().encode(JSON.stringify(manifest)),limits,registry)
    expect(parsed.manifest.definitions.groups.map((item)=>item.key)).toEqual(['profile'])
    expect(parsed.manifest.entities?.[0]?.values).toEqual({profile:{title:'Visible title'}})
  })
})
