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 { fieldsRequestHash, FieldsLifecycleError } from './definition-lifecycle.js'
import {
  captureFieldsRevision,
  fieldsRuntimeMigrationSql,
  getFieldsRevision,
  listFieldsRevisions,
  restoreFieldsRevision,
} from './revisions.js'
import type { FieldsAuthorization, FieldsContext, FieldsPrincipal } from './types.js'

const group: FieldGroup = Object.freeze({
  key: 'profile',
  title: 'Profile',
  active: true,
  location: Object.freeze([]),
  fields: Object.freeze([
    Object.freeze({ key: 'title', name: 'title', label: 'Title', type: 'text', settings: Object.freeze({}) }),
    Object.freeze({ key: 'secret', name: 'secret', label: 'Secret', type: 'password', settings: Object.freeze({ hasherKey: 'test' }) }),
  ]),
})
const principal: FieldsPrincipal = Object.freeze({ id: 'editor-1', capabilities: new Set(['fields.read', 'fields.write']) })
const authorization: FieldsAuthorization = Object.freeze({
  assert: () => undefined,
  assertDefinitionMigration: () => ({ policyVersion: 'policy-1' }),
})
const context: FieldsContext = Object.freeze({ principal, authorization })
const ref = Object.freeze({ entityType: 'content', entityId: 'entry-1' })

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))
  const hash = await canonicalFieldGroupHash(group)
  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', ${new Date('2026-08-22T00:00:00.000Z').toISOString()})
  `)
  await writeCurrent(harness.db, 'A', true)
  return harness
}

async function writeCurrent(db: Awaited<ReturnType<typeof makePgHarness>>['db'], title: string, configured: boolean) {
  await db.execute(sql`DELETE FROM field_value_nodes WHERE entity_type = ${ref.entityType} AND entity_id = ${ref.entityId}`)
  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
      (${crypto.randomUUID()},${ref.entityType},${ref.entityId},'profile',1,'title','/f:title',NULL,NULL,NULL,'leaf',0,false,${title},NULL,NULL,NULL,NULL,NULL,NOW(),NOW()),
      (${crypto.randomUUID()},${ref.entityType},${ref.entityId},'profile',1,'secret','/f:secret',NULL,NULL,NULL,'leaf',1,false,NULL,NULL,NULL,NULL,NULL,${JSON.stringify({ configured })},NOW(),NOW())
  `)
}

async function currentVersion(title: string, configured: boolean): Promise<string> {
  return fieldsRequestHash({
    definitionVersions: { profile: 1 },
    values: { profile: { secret: { configured }, title } },
    blockDocument: null,
  })
}

async function transactionIdentity(db: Awaited<ReturnType<typeof makePgHarness>>['db']): Promise<TransactionIdentity> {
  let identity!: TransactionIdentity
  await db.transaction(async (tx) => { identity = getTransactionIdentity(tx) })
  return identity
}

describe('fields revisions', () => {
  it('captures immutable snapshots, redacts password material, and replays the same operation exactly', async () => {
    const { db } = await fixture()
    const expectedVersion = await currentVersion('A', true)
    const first = await db.transaction(async (tx) => captureFieldsRevision(tx, ref, {
      expectedVersion,
      operationId: 'revision:1',
      expectedTransactionIdentity: getTransactionIdentity(tx),
      parentRevisionId: 'content-revision-1',
    }, context))
    const replay = await db.transaction(async (tx) => captureFieldsRevision(tx, ref, {
      expectedVersion,
      operationId: 'revision:1',
      expectedTransactionIdentity: getTransactionIdentity(tx),
      parentRevisionId: 'content-revision-1',
    }, context))

    expect(replay.id).toBe(first.id)
    expect(first.values).toEqual({ profile: { secret: { configured: true }, title: 'A' } })
    expect(JSON.stringify(first)).not.toContain('password')
    expect(await getFieldsRevision(db, ref, first.id, context)).toEqual(first)
  })

  it('rejects an independently minted parent transaction identity before mutation', async () => {
    const { db } = await fixture()
    const foreign = await transactionIdentity(db)
    await expect(db.transaction(async (tx) => captureFieldsRevision(tx, ref, {
      expectedVersion: await currentVersion('A', true),
      operationId: 'revision:wrong-tx',
      expectedTransactionIdentity: foreign,
      parentRevisionId: 'content-revision-1',
    }, context))).rejects.toEqual(expect.objectContaining<Partial<FieldsLifecycleError>>({ code: 'transaction-capability' }))
    const rows = await db.transaction((tx) => tx.execute<{ count: number }>(sql`SELECT COUNT(*)::int AS count FROM field_revisions`))
    expect(rows[0]?.count).toBe(0)
  })

  it('restores values under a new immutable revision without restoring password credentials and paginates stably', async () => {
    const { db } = await fixture()
    const source = await db.transaction(async (tx) => captureFieldsRevision(tx, ref, {
      expectedVersion: await currentVersion('A', true),
      operationId: 'revision:source',
      expectedTransactionIdentity: getTransactionIdentity(tx),
      parentRevisionId: 'content-revision-1',
    }, context))

    await writeCurrent(db, 'B', false)
    const restored = await db.transaction(async (tx) => restoreFieldsRevision(tx, ref, source.id, {
      expectedVersion: await currentVersion('B', false),
      operationId: 'revision:restore',
      expectedTransactionIdentity: getTransactionIdentity(tx),
      parentRevisionId: 'content-revision-2',
    }, context))

    expect(restored.revision.id).not.toBe(source.id)
    expect(restored.revision.values).toEqual({ profile: { secret: { configured: false }, title: 'A' } })
    expect((await getFieldsRevision(db, ref, source.id, context))?.values).toEqual({ profile: { secret: { configured: true }, title: 'A' } })

    const firstPage = await listFieldsRevisions(db, ref, { limit: 1 }, context)
    expect(firstPage.items).toHaveLength(1)
    expect(firstPage.nextCursor).not.toBeNull()
    const secondPage = await listFieldsRevisions(db, ref, { limit: 1, cursor: firstPage.nextCursor! }, context)
    expect(secondPage.items).toHaveLength(1)
    expect(secondPage.items[0]!.id).not.toBe(firstPage.items[0]!.id)
  })
})
