import { afterEach, describe, expect, it } from 'vitest'
import { sql } from 'drizzle-orm'
import { getTransactionIdentity, type TransactionIdentity } from '@platform-modules/db'
import { makePgHarness } from './pg-harness.js'
import { canonicalFieldGroupHash, type FieldGroup } from './schema.js'
import { fieldsMigrationSql, fieldsRecursiveEavForwardMigrationSql } from './migrate.js'
import { fieldsRuntimeMigrationSql } from './revisions.js'
import {
  EMPTY_BLOCK_DOCUMENT_VERSION,
  applyFieldBlockChange,
  createFieldBlock,
  getBlockDocument,
  previewFieldBlockChange,
  resolveFieldBlocks,
  setBlockDocument,
  validateBlockDocument,
  type BlockDocument,
  type FieldBlockDefinition,
  type ReusableBlockResolver,
} from './blocks.js'
import type { FieldsAuthorization, FieldsContext, FieldsPrincipal } from './types.js'

const group: FieldGroup = Object.freeze({
  key: 'block_fields',
  title: 'Block Fields',
  active: true,
  location: Object.freeze([Object.freeze([{ parameter: 'surface', operator: 'eq' as const, value: 'block' }])]),
  fields: Object.freeze([
    Object.freeze({ key: 'heading', name: 'heading', label: 'Heading', type: 'text', required: true, settings: Object.freeze({ maxLength: 100 }) }),
  ]),
})
const hero: FieldBlockDefinition = Object.freeze({ key: 'hero', title: 'Hero', fieldGroupKeys: Object.freeze(['block_fields']) })
const card: FieldBlockDefinition = Object.freeze({ key: 'card', title: 'Card', fieldGroupKeys: Object.freeze(['block_fields']) })
const ref = Object.freeze({ entityType: 'content', entityId: 'entry-1' })
const noReusable: ReusableBlockResolver = Object.freeze({ resolve: async () => null })

let teardown: (() => Promise<void>) | undefined
afterEach(async () => { await teardown?.(); teardown = undefined })

function context(): FieldsContext {
  const principal: FieldsPrincipal = Object.freeze({ id: 'editor-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))
  const hash = await canonicalFieldGroupHash(group), now = new Date('2026-08-22T00:00:00.000Z').toISOString()
  await harness.db.execute(sql`INSERT INTO field_group_definitions (key,origin,version,active,current_revision,canonical_hash,definition,created_at,updated_at) VALUES (${group.key},'db',1,true,1,${hash},${JSON.stringify(group)},${now},${now})`)
  await harness.db.execute(sql`INSERT INTO field_definition_versions (group_key,revision,canonical_hash,definition,origin,created_at) VALUES (${group.key},1,${hash},${JSON.stringify(group)},'db',${now})`)
  return harness
}
function heroDocument(type = 'hero'): BlockDocument {
  return Object.freeze({ version: 1, roots: Object.freeze([Object.freeze({
    kind: 'inline' as const,
    id: 'node-1',
    type,
    blockDefinitionRevision: 1,
    fieldGroups: Object.freeze([Object.freeze({ groupKey: 'block_fields', definitionRevision: 1, values: Object.freeze({ heading: 'Hello' }) })]),
  })]) })
}
async function foreignIdentity(db: Awaited<ReturnType<typeof makePgHarness>>['db']): Promise<TransactionIdentity> {
  let identity!: TransactionIdentity
  await db.transaction(async (tx) => { identity = getTransactionIdentity(tx) })
  return identity
}

describe('native field blocks', () => {
  it('creates immutable definitions and validates exact pinned group/block revisions', async () => {
    const { db } = await fixture()
    const created = await db.transaction((tx) => createFieldBlock(tx, hero, context(), { operationId: 'block:create' }))
    const replay = await db.transaction((tx) => createFieldBlock(tx, hero, context(), { operationId: 'block:create' }))
    expect(replay).toEqual(created)
    const registry = await resolveFieldBlocks(db)
    const validated = await validateBlockDocument(heroDocument(), registry, noReusable, context(), {
      maxBytes: 100_000, maxNodes: 100, maxDepth: 8, maxChildren: 20, maxAttributeBytes: 10_000,
    })
    expect(validated.roots[0]).toMatchObject({ kind: 'inline', type: 'hero', blockDefinitionRevision: 1 })
    const badPinned: BlockDocument = Object.freeze({ version: 1, roots: Object.freeze([{ kind: 'inline' as const, id: 'node-1', type: 'hero', blockDefinitionRevision: 99, fieldGroups: Object.freeze([Object.freeze({ groupKey: 'block_fields', definitionRevision: 1, values: Object.freeze({ heading: 'Hello' }) })]) }]) })
    await expect(validateBlockDocument(badPinned, registry, noReusable, context(), {
      maxBytes: 100_000, maxNodes: 100, maxDepth: 8, maxChildren: 20, maxAttributeBytes: 10_000,
    })).rejects.toMatchObject({ code: 'integrity' })
  })

  it('requires the parent transaction identity and replays one committed document write', async () => {
    const { db } = await fixture()
    await db.transaction((tx) => createFieldBlock(tx, hero, context(), { operationId: 'block:create' }))
    const registry = await resolveFieldBlocks(db)
    const validated = await validateBlockDocument(heroDocument(), registry, noReusable, context(), { maxBytes: 100_000, maxNodes: 100, maxDepth: 8, maxChildren: 20, maxAttributeBytes: 10_000 })
    const foreign = await foreignIdentity(db)
    await expect(db.transaction((tx) => setBlockDocument(tx, ref, validated, { expectedVersion: EMPTY_BLOCK_DOCUMENT_VERSION, operationId: 'block:wrong-tx', expectedTransactionIdentity: foreign, parentRevisionId: 'content-revision-1' }, registry, context()))).rejects.toMatchObject({ code: 'transaction-capability' })
    const first = await db.transaction((tx) => setBlockDocument(tx, ref, validated, { expectedVersion: EMPTY_BLOCK_DOCUMENT_VERSION, operationId: 'block:set', expectedTransactionIdentity: getTransactionIdentity(tx), parentRevisionId: 'content-revision-1' }, registry, context()))
    const replay = await db.transaction((tx) => setBlockDocument(tx, ref, validated, { expectedVersion: EMPTY_BLOCK_DOCUMENT_VERSION, operationId: 'block:set', expectedTransactionIdentity: getTransactionIdentity(tx), parentRevisionId: 'content-revision-1' }, registry, context()))
    expect(replay).toEqual(first)
    expect((await getBlockDocument(db, ref, context()))?.document).toEqual(heroDocument())
  })

  it('rejects reusable cycles and parent-rule violations before a document is branded', async () => {
    const { db } = await fixture()
    await db.transaction((tx) => createFieldBlock(tx, hero, context(), { operationId: 'block:create-hero' }))
    await db.transaction((tx) => createFieldBlock(tx, { ...card, parent: ['hero'] }, context(), { operationId: 'block:create-card' }))
    const registry = await resolveFieldBlocks(db)
    await expect(validateBlockDocument(heroDocument('card'), registry, noReusable, context(), { maxBytes: 100_000, maxNodes: 100, maxDepth: 8, maxChildren: 20, maxAttributeBytes: 10_000 })).rejects.toMatchObject({ code: 'validation' })
    const loop: BlockDocument = Object.freeze({ version: 1, roots: Object.freeze([{ kind: 'reusable' as const, id: 'reuse-node', reusableId: 'loop' }]) })
    const resolver: ReusableBlockResolver = Object.freeze({ resolve: async () => ({ document: loop, version: 1 }) })
    await expect(validateBlockDocument(loop, registry, resolver, context(), { maxBytes: 100_000, maxNodes: 100, maxDepth: 8, maxChildren: 20, maxAttributeBytes: 10_000 })).rejects.toMatchObject({ code: 'validation' })
  })

  it('maps current documents while preserving immutable historical revision bytes', async () => {
    const { db } = await fixture()
    await db.transaction((tx) => createFieldBlock(tx, hero, context(), { operationId: 'block:create-hero' }))
    await db.transaction((tx) => createFieldBlock(tx, card, context(), { operationId: 'block:create-card' }))
    const registry = await resolveFieldBlocks(db)
    const validated = await validateBlockDocument(heroDocument(), registry, noReusable, context(), { maxBytes: 100_000, maxNodes: 100, maxDepth: 8, maxChildren: 20, maxAttributeBytes: 10_000 })
    await db.transaction((tx) => setBlockDocument(tx, ref, validated, { expectedVersion: EMPTY_BLOCK_DOCUMENT_VERSION, operationId: 'block:set', expectedTransactionIdentity: getTransactionIdentity(tx), parentRevisionId: 'content-revision-1' }, registry, context()))
    const snapshot = heroDocument()
    await db.execute(sql`INSERT INTO field_revisions (id,entity_type,entity_id,parent_revision_id,kind,definition_versions,values,block_document,created_at,created_by,source_version) VALUES (${crypto.randomUUID()},${ref.entityType},${ref.entityId},'content-revision-1','revision',${JSON.stringify({ 'block:hero': 1, 'group:block_fields': 1 })},${JSON.stringify({})},${JSON.stringify(snapshot)},${new Date().toISOString()},'editor-1','v1')`)

    const impact = await previewFieldBlockChange(db, context(), { key: 'hero', expectedVersion: 1, deactivate: true, strategy: { kind: 'mapDefinition', replacementKey: 'card' } })
    expect(impact.incompatibilities).toEqual([])
    await db.transaction((tx) => applyFieldBlockChange(tx, context(), { impactToken: impact.token, operationId: 'block:map', strategy: { kind: 'mapDefinition', replacementKey: 'card' } }, {}))

    expect((await getBlockDocument(db, ref, context()))?.document.roots[0]).toMatchObject({ kind: 'inline', type: 'card' })
    const revisions = await db.transaction((tx) => tx.execute<{ blockDocument: unknown }>(sql`SELECT block_document AS "blockDocument" FROM field_revisions WHERE entity_type=${ref.entityType} AND entity_id=${ref.entityId}`))
    const historical = typeof revisions[0]!.blockDocument === 'string' ? JSON.parse(revisions[0]!.blockDocument) : revisions[0]!.blockDocument
    expect(historical).toEqual(snapshot)
  })
})
