import { sql } from 'drizzle-orm'
import { describe, expect, it } from 'vitest'
import { admin, lifecycleFixture, reader, writeDeps } from './lifecycle.test-helpers.js'
import {
  changeContentPassword,
  createContentAutosave,
  createContentEntry,
  getContentAutosave,
  listContentRevisions,
  readProtectedContent,
  restoreContentAutosave,
  transitionContent,
  updateContentEntry,
  verifyContentPassword,
  type ContentTransitionDependencies,
} from './lifecycle.js'
import { resolveContentStatuses } from './status.js'
import { resolveContentType } from './registry.js'

const passwordAdapter={
  async hash(password:string){return {credential:`hash:${password}`,version:'v1'}},
  async verify(password:string,credential:string){return credential===`hash:${password}`},
  async issueProof(entryId:string,version:string){return `proof:${entryId}:${version}`},
  async verifyProof(entryId:string,version:string,proof:string){return proof===`proof:${entryId}:${version}`},
}

describe('content lifecycle',()=>{
  it('creates, updates, revisions, autosaves, transitions, and protects content transactionally',async()=>{
    const fx=await lifecycleFixture(),events:unknown[]=[]
    try{
      const deps=writeDeps(events)
      const created=await fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'create:1',slug:'root',type:'book',title:'Root',body:'<script>x()</script><p>safe</p>',excerpt:'E',author:'author-1',menuOrder:2,templateKey:'default',commentStatus:'closed',pingStatus:'closed',sticky:true,format:'standard'},admin,deps))
      expect(created.body).toBe('<p>safe</p>')
      expect(created).toMatchObject({status:'draft',author:'author-1',menuOrder:2,templateKey:'default',excerpt:'E',sticky:true,format:'standard',passwordProtected:false})

      const updated=await fx.db.transaction((tx)=>updateContentEntry(tx,created.id,{operationId:'update:1',expectedUpdatedAt:created.updatedAt,patch:{title:'Updated',author:'author-2',excerpt:'E2'}},admin,deps))
      expect(updated).toMatchObject({title:'Updated',author:'author-2',excerpt:'E2'})
      const revisions=await fx.db.transaction((tx)=>listContentRevisions(tx,created.id,admin))
      expect(revisions.items).toHaveLength(1)
      expect(revisions.items[0]?.snapshot).toMatchObject({title:'Root',author:'author-1'})

      const autosave=await fx.db.transaction((tx)=>createContentAutosave(tx,created.id,{operationId:'autosave:1',parentRevisionId:revisions.items[0]!.id,snapshot:{title:'Autosaved'}},admin,deps))
      expect((await fx.db.transaction((tx)=>getContentAutosave(tx,created.id,autosave.id,admin))).snapshot).toEqual({title:'Autosaved'})
      const restored=await fx.db.transaction((tx)=>restoreContentAutosave(tx,created.id,autosave.id,{operationId:'autosave:restore',expectedUpdatedAt:updated.updatedAt},admin,deps))
      expect(restored.title).toBe('Autosaved')

      const [type,statuses]=await Promise.all([fx.db.transaction((tx)=>resolveContentType(tx,'book')),fx.db.transaction((tx)=>resolveContentStatuses(tx))])
      const transitionDeps:ContentTransitionDependencies={types:[type],statuses,rules:[{from:'draft',to:'published',capability:'publish'}],outbox:deps.outbox}
      const published=await fx.db.transaction((tx)=>transitionContent(tx,created.id,{to:'published',publishedAt:new Date('2026-08-22T00:00:00Z'),expectedUpdatedAt:restored.updatedAt,operationId:'publish:1'},admin,transitionDeps))
      expect(published.status).toBe('published')
      expect(published.publishedAt?.toISOString()).toBe('2026-08-22T00:00:00.000Z')

      const protectedEntry=await fx.db.transaction((tx)=>changeContentPassword(tx,created.id,{operation:'set',password:'secret-value',expectedUpdatedAt:published.updatedAt,operationId:'password:1'},admin,{passwords:passwordAdapter,outbox:deps.outbox}))
      expect(protectedEntry.passwordProtected).toBe(true)
      expect(await fx.db.transaction((tx)=>readProtectedContent(tx,created.id,reader,passwordAdapter))).toMatchObject({access:'passwordRequired'})
      const proof=await fx.db.transaction((tx)=>verifyContentPassword(tx,created.id,'secret-value',passwordAdapter))
      expect(proof).toBeTruthy()
      expect(await fx.db.transaction((tx)=>readProtectedContent(tx,created.id,reader,passwordAdapter,proof!))).toMatchObject({access:'granted',entry:{body:'<p>safe</p>'}})
      const journal=await fx.db.transaction((tx)=>tx.execute<{payload:string}>(sql`SELECT CAST(payload AS text) AS payload FROM content_lifecycle_journal WHERE operation_id = 'password:1'`))
      expect(journal[0]?.payload).not.toContain('secret-value')
      expect(events.length).toBeGreaterThan(0)
    }finally{await fx.stop()}
  },90_000)


  it('runs beforeCommit before revisions or credentials become durable',async()=>{
    const fx=await lifecycleFixture()
    try{
      const created=await fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'hook:create',slug:'hooked',type:'book',title:'Hooked',body:'x'},admin,writeDeps()))
      await expect(fx.db.transaction(async(tx)=>{
        const deps=writeDeps()
        deps.hooks={beforeCommit:async()=>{
          const revisions=await tx.execute<{count:number|string}>(sql`SELECT COUNT(*) AS count FROM content_revisions WHERE entry_id = ${created.id}`)
          expect(Number(revisions[0]?.count ?? 0)).toBe(0)
          throw new Error('reject-before-commit')
        }}
        return updateContentEntry(tx,created.id,{operationId:'hook:update',expectedUpdatedAt:created.updatedAt,patch:{title:'Should not persist'}},admin,deps)
      })).rejects.toThrow('reject-before-commit')
      const afterUpdate=await fx.db.transaction((tx)=>tx.execute<{title:string}>(sql`SELECT title FROM content_entries WHERE id = ${created.id}`))
      expect(afterUpdate[0]?.title).toBe('Hooked')
      const revisions=await fx.db.transaction((tx)=>tx.execute<{count:number|string}>(sql`SELECT COUNT(*) AS count FROM content_revisions WHERE entry_id = ${created.id}`))
      expect(Number(revisions[0]?.count ?? 0)).toBe(0)

      await expect(fx.db.transaction(async(tx)=>{
        const deps=writeDeps()
        return changeContentPassword(tx,created.id,{operation:'set',password:'never-stored',expectedUpdatedAt:created.updatedAt,operationId:'hook:password'},admin,{
          passwords:passwordAdapter,
          outbox:deps.outbox,
          hooks:{beforeCommit:async()=>{
            const credentials=await tx.execute<{count:number|string}>(sql`SELECT COUNT(*) AS count FROM content_password_credentials WHERE entry_id = ${created.id}`)
            expect(Number(credentials[0]?.count ?? 0)).toBe(0)
            throw new Error('reject-password')
          }},
        })
      })).rejects.toThrow('reject-password')
      const credentials=await fx.db.transaction((tx)=>tx.execute<{count:number|string}>(sql`SELECT COUNT(*) AS count FROM content_password_credentials WHERE entry_id = ${created.id}`))
      expect(Number(credentials[0]?.count ?? 0)).toBe(0)
    }finally{await fx.stop()}
  },90_000)

  it('invalidates old password proofs on replacement and rejects non-advancing credential versions',async()=>{
    const fx=await lifecycleFixture()
    try{
      const deps=writeDeps()
      const created=await fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'proof:create',slug:'proofed',type:'book',title:'Proofed',body:'secret body'},admin,deps))
      let version=0
      const rotating={
        async hash(password:string){version+=1;return {credential:`hash:${password}`,version:`v${version}`}},
        async verify(password:string,credential:string){return credential===`hash:${password}`},
        async issueProof(entryId:string,credentialVersion:string){return `proof:${entryId}:${credentialVersion}`},
        async verifyProof(entryId:string,credentialVersion:string,proof:string){return proof===`proof:${entryId}:${credentialVersion}`},
      }
      const first=await fx.db.transaction((tx)=>changeContentPassword(tx,created.id,{operation:'set',password:'one',expectedUpdatedAt:created.updatedAt,operationId:'proof:set:1'},admin,{passwords:rotating,outbox:deps.outbox}))
      const proof1=await fx.db.transaction((tx)=>verifyContentPassword(tx,created.id,'one',rotating))
      expect(proof1).toBeTruthy()
      const second=await fx.db.transaction((tx)=>changeContentPassword(tx,created.id,{operation:'set',password:'two',expectedUpdatedAt:first.updatedAt,operationId:'proof:set:2'},admin,{passwords:rotating,outbox:deps.outbox}))
      expect(second.passwordProtected).toBe(true)
      expect(await fx.db.transaction((tx)=>readProtectedContent(tx,created.id,reader,rotating,proof1!))).toMatchObject({access:'passwordRequired'})
      const proof2=await fx.db.transaction((tx)=>verifyContentPassword(tx,created.id,'two',rotating))
      expect(await fx.db.transaction((tx)=>readProtectedContent(tx,created.id,reader,rotating,proof2!))).toMatchObject({access:'granted'})

      const sameVersion={...rotating,hash:async(password:string)=>({credential:`hash:${password}`,version:'v2'})}
      await expect(fx.db.transaction((tx)=>changeContentPassword(tx,created.id,{operation:'set',password:'three',expectedUpdatedAt:second.updatedAt,operationId:'proof:set:3'},admin,{passwords:sameVersion,outbox:deps.outbox}))).rejects.toMatchObject({code:'integrity'})
    }finally{await fx.stop()}
  },90_000)


  it('rejects plain database handles and rolls back mutation/revision/outbox together',async()=>{
    const fx=await lifecycleFixture()
    try{
      const deps=writeDeps()
      await expect(createContentEntry(fx.db as never,{operationId:'plain-db',slug:'plain-db',type:'book',title:'Plain',body:'x'},admin,deps)).rejects.toMatchObject({code:'transaction-capability'})

      const created=await fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'atomic:create',slug:'atomic',type:'book',title:'Atomic',body:'x'},admin,deps))
      await fx.db.execute(sql`CREATE TABLE test_content_outbox (id text PRIMARY KEY)`)
      const failingOutbox={enqueue:async(tx: Parameters<typeof deps.outbox.enqueue>[0],event: Parameters<typeof deps.outbox.enqueue>[1])=>{
        await tx.execute(sql`INSERT INTO test_content_outbox (id) VALUES (${event.id})`)
        throw new Error('outbox-failed')
      }}
      await expect(fx.db.transaction((tx)=>updateContentEntry(tx,created.id,{operationId:'atomic:update',expectedUpdatedAt:created.updatedAt,patch:{title:'Changed'}},admin,{...deps,outbox:failingOutbox}))).rejects.toThrow('outbox-failed')
      const row=await fx.db.transaction((tx)=>tx.execute<{title:string}>(sql`SELECT title FROM content_entries WHERE id = ${created.id}`))
      expect(row[0]?.title).toBe('Atomic')
      const revisions=await fx.db.transaction((tx)=>tx.execute<{count:number|string}>(sql`SELECT COUNT(*) AS count FROM content_revisions WHERE entry_id = ${created.id}`))
      expect(Number(revisions[0]?.count ?? 0)).toBe(0)
      const receipts=await fx.db.transaction((tx)=>tx.execute<{count:number|string}>(sql`SELECT COUNT(*) AS count FROM content_lifecycle_journal WHERE operation_id = 'atomic:update'`))
      expect(Number(receipts[0]?.count ?? 0)).toBe(0)
      const outbox=await fx.db.execute(sql`SELECT COUNT(*)::int AS count FROM test_content_outbox`) as unknown as {rows:Array<{count:number}>}
      expect(outbox.rows[0]?.count).toBe(0)
    }finally{await fx.stop()}
  },90_000)

  it('rejects stale updates and operation-id reuse with different bytes',async()=>{
    const fx=await lifecycleFixture()
    try{
      const deps=writeDeps()
      const created=await fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'create:2',slug:'a',type:'book',title:'A',body:'x'},admin,deps))
      await fx.db.transaction((tx)=>updateContentEntry(tx,created.id,{operationId:'u:1',expectedUpdatedAt:created.updatedAt,patch:{title:'B'}},admin,deps))
      await expect(fx.db.transaction((tx)=>updateContentEntry(tx,created.id,{operationId:'u:2',expectedUpdatedAt:created.updatedAt,patch:{title:'C'}},admin,deps))).rejects.toMatchObject({code:'stale-version'})
      await expect(fx.db.transaction((tx)=>createContentEntry(tx,{operationId:'create:2',slug:'different',type:'book',title:'X',body:'x'},admin,deps))).rejects.toMatchObject({code:'operation-conflict'})
    }finally{await fx.stop()}
  },90_000)
})
