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 { encodeFieldValueNodes } from './values.js'
import type { FieldGroup, FieldValueNodeRow } from './schema.js'
import type { FieldsAuthorization, FieldsContext, FieldsPrincipal } from './types.js'
import {
  applyFieldGroupChange,
  applyFieldsCodeReconciliation,
  createFieldGroup,
  defineFinalFieldGroup,
  fieldsRequestHash,
  previewFieldGroupChange,
  previewFieldsCodeReconciliation,
  resolveFinalFieldGroups,
  restoreFieldsBackup,
  fieldsBackupDestinationHash,
  type CodeFieldsRegistrySnapshot,
  type FieldsDefinitionEffects,
  type FieldsBackupManifest,
  type FieldsBackupPayload,
  type FieldsBackupStore,
  type FieldsDestructiveEffects,
} from './definition-lifecycle.js'

const baseGroup: 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 }) }),
  ]),
})
const migratedGroup: FieldGroup = Object.freeze({
  ...baseGroup,
  fields: Object.freeze([
    Object.freeze({ key: 'headline', name: 'headline', label: 'Headline', type: 'text', settings: Object.freeze({ maxLength: 100 }) }),
  ]),
})
let teardown: (() => Promise<void>) | undefined
afterEach(async () => { await teardown?.(); teardown = undefined })

function context(): FieldsContext {
  const principal: FieldsPrincipal = Object.freeze({ id: 'admin-1', capabilities: new Set(['fields.manage']) })
  const authorization: FieldsAuthorization = Object.freeze({
    assert: () => undefined,
    assertDefinitionMigration: () => ({ policyVersion: 'policy-1' }),
  })
  return Object.freeze({ principal, authorization })
}
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 insertNodes(db: Awaited<ReturnType<typeof makePgHarness>>['db'], group: FieldGroup, revision: number, values: Record<string, unknown>) {
  const ids = new Map<string, string>()
  const rows = encodeFieldValueNodes(group, values, {
    entityType: 'content', entityId: 'entry-1', definitionRevision: revision,
    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())
  `)
}
async function emptyCodeVersion() { return fieldsRequestHash({ groups: [], options: [], blocks: [] }) }
async function snapshot(version: string, groups: readonly ReturnType<typeof defineFinalFieldGroup>[]): Promise<CodeFieldsRegistrySnapshot> {
  const optionsPages = Object.freeze([])
  const blocks = Object.freeze([])
  const canonicalHash = await fieldsRequestHash({ version, groups, optionsPages, blocks })
  return Object.freeze({ version, canonicalHash, groups: Object.freeze([...groups]), optionsPages, blocks })
}

describe('final field definition lifecycle', () => {
  it('migrates current recursive values under a new immutable revision while preserving historical snapshots', async () => {
    const { db } = await fixture()
    await db.transaction((tx) => createFieldGroup(tx, baseGroup, context(), { operationId: 'group:create' }))
    await insertNodes(db, baseGroup, 1, { title: 'Old title' })
    const historicalValues = { profile: { title: 'Old title' } }
    await db.execute(sql`INSERT INTO field_revisions (id,entity_type,entity_id,parent_revision_id,kind,definition_versions,values,created_at,created_by,source_version) VALUES (${crypto.randomUUID()},'content','entry-1','content-r1','revision',${JSON.stringify({ profile: 1 })},${JSON.stringify(historicalValues)},NOW(),'admin-1','v1')`)

    const impact = await previewFieldGroupChange(db, 'profile', 1, { fields: migratedGroup.fields }, { kind: 'migrateValues', mappings: [{ from: ['title'], to: ['headline'] }] }, context())
    expect(impact.incompatibilities).toContain('removed:["title"]')
    const applied = await db.transaction((tx) => applyFieldGroupChange(tx, impact.token, { kind: 'migrateValues', mappings: [{ from: ['title'], to: ['headline'] }] }, 'group:migrate', context()))
    expect(applied).toMatchObject({ revision: 2, version: 2 })

    const nodes = await db.transaction((tx) => tx.execute<Pick<FieldValueNodeRow,'fieldKey'|'definitionRevision'|'valueText'>>(sql`SELECT field_key AS "fieldKey",definition_revision AS "definitionRevision",value_text AS "valueText" FROM field_value_nodes WHERE group_key='profile' ORDER BY path`))
    expect(nodes).toEqual([expect.objectContaining({ fieldKey: 'headline', definitionRevision: 2, valueText: 'Old title' })])
    const revisions = await db.transaction((tx) => tx.execute<{ definitionVersions: unknown; values: unknown }>(sql`SELECT definition_versions AS "definitionVersions",values FROM field_revisions WHERE entity_type='content' AND entity_id='entry-1'`))
    expect(revisions[0]).toMatchObject({ definitionVersions: { profile: 1 }, values: historicalValues })
  })

  it('reconciles a DB-owned key to code, preserves the shadowed revision, replays exactly, and rolls back failed effects', async () => {
    const { db } = await fixture()
    await db.transaction((tx) => createFieldGroup(tx, baseGroup, context(), { operationId: 'group:create' }))
    const code = defineFinalFieldGroup(baseGroup)
    const next = await snapshot('deploy-1', [code])
    const effectsSeen: string[] = []
    const effects: FieldsDefinitionEffects = { audit: async (_tx, event) => { effectsSeen.push(`audit:${event.kind}`) }, enqueue: async (_tx, event) => { effectsSeen.push(`outbox:${event.kind}`) } }
    const plan = await previewFieldsCodeReconciliation(db, context(), { expectedCurrentVersion: await emptyCodeVersion(), next, groupStrategies: {}, optionsStrategies: {}, blockStrategies: {} })
    expect(plan.groupImpacts).toHaveLength(1)
    const applied = await db.transaction((tx) => applyFieldsCodeReconciliation(tx, context(), { planToken: plan.token, operationId: 'code:apply', next, groupStrategies: {}, optionsStrategies: {}, blockStrategies: {} }, effects))
    const replay = await db.transaction((tx) => applyFieldsCodeReconciliation(tx, context(), { planToken: plan.token, operationId: 'code:apply', next, groupStrategies: {}, optionsStrategies: {}, blockStrategies: {} }, effects))
    expect(applied).toEqual(next)
    expect(replay).toEqual(next)
    expect(effectsSeen).toEqual(['audit:codeRegistryReconciled','outbox:codeRegistryReconciled'])
    expect(await resolveFinalFieldGroups(db)).toEqual([expect.objectContaining({ key: 'profile', origin: 'code', version: 2, revision: 2, shadowedDbVersion: 1 })])
    const versions = await db.transaction((tx) => tx.execute<{ revision: number; origin: string }>(sql`SELECT revision,origin FROM field_definition_versions WHERE group_key='profile' ORDER BY revision`))
    expect(versions).toEqual([{ revision: 1, origin: 'db' }, { revision: 2, origin: 'code' }])

    const prior = await snapshot('deploy-2', [])
    const removePlan = await previewFieldsCodeReconciliation(db, context(), { expectedCurrentVersion: await fieldsRequestHash({ groups: [{ key: 'profile', version: 2, revision: 2, hash: await fieldsRequestHash(baseGroup), active: true }], options: [], blocks: [] }), next: prior, groupStrategies: {}, optionsStrategies: {}, blockStrategies: {} })
    expect(removePlan.groupImpacts).toHaveLength(1)

    const failingNext = await snapshot('deploy-failing', [defineFinalFieldGroup({ ...baseGroup, title: 'Changed by code' })])
    const failingPlan = await previewFieldsCodeReconciliation(db, context(), { expectedCurrentVersion: removePlan.fromVersion, next: failingNext, groupStrategies: {}, optionsStrategies: {}, blockStrategies: {} })
    await expect(db.transaction((tx) => applyFieldsCodeReconciliation(tx, context(), { planToken: failingPlan.token, operationId: 'code:fail', next: failingNext, groupStrategies: {}, optionsStrategies: {}, blockStrategies: {} }, { audit: async () => undefined, enqueue: async () => { throw new Error('outbox down') } }))).rejects.toThrow('outbox down')
    expect(await resolveFinalFieldGroups(db)).toEqual([expect.objectContaining({ title: 'Profile', version: 2, revision: 2 })])
  })
})


describe('fields backup restore', () => {
  it('restores verified typed values under a new immutable definition revision and replays exactly', async () => {
    const { db } = await fixture()
    await db.transaction((tx) => createFieldGroup(tx, baseGroup, context(), { operationId: 'restore:seed-group' }))
    const payload: FieldsBackupPayload = Object.freeze({
      version: 1,
      groupDefinitions: Object.freeze([Object.freeze({ key: 'profile', definition: baseGroup })]),
      values: Object.freeze([Object.freeze({
        ref: Object.freeze({ entityType: 'content', entityId: 'entry-restore' }),
        groupKey: 'profile', definitionRevision: 1,
        values: Object.freeze({ title: 'Recovered title' }),
      })]),
    })
    const destinationHash = await fieldsBackupDestinationHash(db, payload)
    const manifest: FieldsBackupManifest = Object.freeze({
      id: 'backup-restore-1', scopeHash: 'scope-restore-1', corpusVersion: 'corpus-v1', corpusHash: 'corpus-h1',
      itemCounts: Object.freeze({ groupDefinitions: 1, values: 1 }), byteCount: JSON.stringify(payload).length, immutable: true,
    })
    const backups: FieldsBackupStore = {
      verify: async () => manifest,
      read: async () => Object.freeze({ manifest, payload: payload as unknown as import('./schema.js').FieldStorageValue }),
    }
    const events: string[] = []
    const effects: FieldsDestructiveEffects = {
      audit: { record: async (_tx, event) => { events.push(`audit:${'kind' in event ? event.kind : 'destructive'}`) } },
      outbox: { enqueue: async (_tx, event) => { events.push(`outbox:${'kind' in event ? event.kind : 'destructive'}`) } },
    }
    const restored = await db.transaction((tx) => restoreFieldsBackup(tx, manifest, destinationHash, 'restore:apply', context(), effects, backups))
    const replay = await db.transaction((tx) => restoreFieldsBackup(tx, manifest, destinationHash, 'restore:apply', context(), effects, backups))
    expect(replay).toEqual(restored)
    expect(restored).toMatchObject({ operationId: 'restore:apply', manifestId: manifest.id, destinationCorpusHash: destinationHash })
    expect(events).toEqual(['audit:backupRestored','outbox:backupRestored'])
    expect(await resolveFinalFieldGroups(db)).toEqual([expect.objectContaining({ key: 'profile', version: 2, revision: 2 })])
    const nodes = await db.transaction((tx) => tx.execute<{ definitionRevision: number; fieldKey: string; valueText: string }>(sql`SELECT definition_revision AS "definitionRevision",field_key AS "fieldKey",value_text AS "valueText" FROM field_value_nodes WHERE entity_type='content' AND entity_id='entry-restore' ORDER BY path`))
    expect(nodes).toEqual([expect.objectContaining({ definitionRevision: 2, fieldKey: 'title', valueText: 'Recovered title' })])
  })

  it('rejects destination drift and rolls back definitions, values, and receipts when restore effects fail', async () => {
    const { db } = await fixture()
    await db.transaction((tx) => createFieldGroup(tx, baseGroup, context(), { operationId: 'restore:seed-group' }))
    const payload: FieldsBackupPayload = Object.freeze({
      version: 1,
      groupDefinitions: Object.freeze([Object.freeze({ key: 'profile', definition: { ...baseGroup, title: 'Recovered profile' } })]),
      values: Object.freeze([Object.freeze({ ref: Object.freeze({ entityType: 'content', entityId: 'entry-fail' }), groupKey: 'profile', definitionRevision: 1, values: Object.freeze({ title: 'Recovered' }) })]),
    })
    const destinationHash = await fieldsBackupDestinationHash(db, payload)
    const manifest: FieldsBackupManifest = Object.freeze({ id: 'backup-restore-fail', scopeHash: 'scope-fail', corpusVersion: 'v1', corpusHash: 'h1', itemCounts: Object.freeze({ values: 1 }), byteCount: JSON.stringify(payload).length, immutable: true })
    const backups: FieldsBackupStore = { verify: async () => manifest, read: async () => ({ manifest, payload: payload as unknown as import('./schema.js').FieldStorageValue }) }

    await db.execute(sql`UPDATE field_group_definitions SET version=version+1 WHERE key='profile'`)
    await expect(db.transaction((tx) => restoreFieldsBackup(tx, manifest, destinationHash, 'restore:drift', context(), { audit: { record: async () => undefined }, outbox: { enqueue: async () => undefined } }, backups))).rejects.toMatchObject({ code: 'corpus-drift' })
    await db.execute(sql`UPDATE field_group_definitions SET version=version-1 WHERE key='profile'`)
    const freshHash = await fieldsBackupDestinationHash(db, payload)

    await expect(db.transaction((tx) => restoreFieldsBackup(tx, manifest, freshHash, 'restore:rollback', context(), {
      audit: { record: async () => undefined },
      outbox: { enqueue: async () => { throw new Error('restore outbox down') } },
    }, backups))).rejects.toThrow('restore outbox down')
    expect(await resolveFinalFieldGroups(db)).toEqual([expect.objectContaining({ key: 'profile', title: 'Profile', version: 1, revision: 1 })])
    const nodes = await db.transaction((tx) => tx.execute<{ count: number }>(sql`SELECT COUNT(*)::int AS count FROM field_value_nodes WHERE entity_type='content' AND entity_id='entry-fail'`))
    expect(nodes[0]?.count).toBe(0)
    const receipts = await db.transaction((tx) => tx.execute<{ count: number }>(sql`SELECT COUNT(*)::int AS count FROM fields_operation_receipts WHERE operation_id='restore:rollback'`))
    expect(receipts[0]?.count).toBe(0)
  })
})
