/**
 * @module tests/integration/rev-and-ifabsent
 * @description End-to-end tests for the 7.31.0 optimistic-concurrency surface:
 *   - `_rev` is initialized to 1 on add() and surfaced on get()/find()/search()
 *   - update() auto-bumps `_rev`
 *   - update({ ifRev }) accepts a matching rev and rejects a stale one with
 *     RevisionConflictError carrying { id, expected, actual }
 *   - add({ ifAbsent: true }) is a no-op when the id is already present
 *   - addMany({ ifAbsent: true }) applies the flag to every item
 */

import { describe, it, expect, beforeEach } from 'vitest'
import { Brainy } from '../../src/brainy.js'
import { RevisionConflictError } from '../../src/transaction/RevisionConflictError.js'
import { NounType } from '../../src/types/graphTypes.js'

describe('7.31.0 — _rev CAS + ifAbsent', () => {
  let brain: Brainy

  beforeEach(async () => {
    brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' as const } })
    await brain.init()
  })

  describe('_rev initialization + surface', () => {
    it('initializes _rev to 1 on add()', async () => {
      const id = await brain.add({ data: 'hello', type: NounType.Document })
      const entity = await brain.get(id)
      expect(entity?._rev).toBe(1)
    })

    it('surfaces _rev on metadata-only get() fast path', async () => {
      const id = await brain.add({ data: 'fast-path', type: NounType.Document })
      const entity = await brain.get(id)  // metadata-only by default
      expect(entity?._rev).toBe(1)
    })

    it('surfaces _rev on includeVectors get() full path', async () => {
      const id = await brain.add({ data: 'full-path', type: NounType.Document })
      const entity = await brain.get(id, { includeVectors: true })
      expect(entity?._rev).toBe(1)
    })

    it('surfaces _rev on find() results', async () => {
      const id = await brain.add({ data: 'findable', type: NounType.Document })
      const results = await brain.find({ type: NounType.Document })
      const hit = results.find((r) => r.id === id)
      expect(hit?._rev).toBe(1)
    })
  })

  describe('_rev auto-bump on update()', () => {
    it('bumps _rev to 2 after first update()', async () => {
      const id = await brain.add({ data: 'v1', type: NounType.Document })
      await brain.update({ id, data: 'v2' })
      const entity = await brain.get(id)
      expect(entity?._rev).toBe(2)
    })

    it('bumps _rev monotonically across multiple updates', async () => {
      const id = await brain.add({ data: 'v1', type: NounType.Document })
      for (let i = 2; i <= 5; i++) {
        await brain.update({ id, data: `v${i}` })
        const entity = await brain.get(id)
        expect(entity?._rev).toBe(i)
      }
    })

    it('bumps _rev on metadata-only update', async () => {
      const id = await brain.add({
        data: 'doc',
        type: NounType.Document,
        metadata: { status: 'draft' }
      })
      await brain.update({ id, metadata: { status: 'published' } })
      const entity = await brain.get(id)
      expect(entity?._rev).toBe(2)
      expect(entity?.metadata?.status).toBe('published')
    })
  })

  describe('update({ ifRev }) optimistic concurrency', () => {
    it('passes when ifRev matches the persisted _rev', async () => {
      const id = await brain.add({ data: 'v1', type: NounType.Document })
      await expect(brain.update({ id, data: 'v2', ifRev: 1 })).resolves.not.toThrow()
      const after = await brain.get(id)
      expect(after?._rev).toBe(2)
    })

    it('throws RevisionConflictError when ifRev is stale', async () => {
      const id = await brain.add({ data: 'v1', type: NounType.Document })
      await brain.update({ id, data: 'v2' })  // bumps to 2

      let caught: unknown = null
      try {
        await brain.update({ id, data: 'v3-from-stale-reader', ifRev: 1 })
      } catch (err) {
        caught = err
      }
      expect(caught).toBeInstanceOf(RevisionConflictError)
      const e = caught as RevisionConflictError
      expect(e.id).toBe(id)
      expect(e.expected).toBe(1)
      expect(e.actual).toBe(2)
    })

    it('emits a message that points at the recovery recipe', async () => {
      const id = await brain.add({ data: 'v1', type: NounType.Document })
      await brain.update({ id, data: 'v2' })
      try {
        await brain.update({ id, ifRev: 1, data: 'v3' })
        throw new Error('should have thrown')
      } catch (err) {
        const msg = (err as Error).message
        expect(msg).toMatch(/persisted _rev is 2/)
        expect(msg).toMatch(/brain\.get/)
      }
    })

    it('omits the check entirely when ifRev is not provided', async () => {
      const id = await brain.add({ data: 'v1', type: NounType.Document })
      await brain.update({ id, data: 'v2' })
      await brain.update({ id, data: 'v3' })  // no ifRev — must succeed
      const after = await brain.get(id)
      expect(after?._rev).toBe(3)
    })

    it('treats pre-7.31.0 metadata with no _rev as rev 1', async () => {
      // Simulate a legacy entity by writing directly through the storage adapter
      // with no _rev field, then verifying ifRev: 1 succeeds.
      const id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'
      const storage = (brain as any).storage
      await storage.saveNounMetadata(id, {
        data: 'pre-7.31',
        noun: NounType.Document,
        createdAt: Date.now(),
        updatedAt: Date.now()
        // intentionally no _rev
      })
      // Also save the vector so get() doesn't return null on the full path.
      const vector = await (brain as any).embed('pre-7.31')
      await storage.saveNoun({
        id,
        vector,
        connections: new Map(),
        level: 0
      })
      const legacy = await brain.get(id)
      expect(legacy?._rev).toBe(1)
      await expect(brain.update({ id, data: 'updated', ifRev: 1 })).resolves.not.toThrow()
      const after = await brain.get(id)
      expect(after?._rev).toBe(2)
    })
  })

  describe('add({ ifAbsent: true })', () => {
    it('writes when no entity with the id exists', async () => {
      const id = await brain.add({
        id: '11111111-1111-4111-8111-111111111111',
        data: 'first-write',
        type: NounType.Document,
        ifAbsent: true
      })
      expect(id).toBe('11111111-1111-4111-8111-111111111111')
      const entity = await brain.get(id)
      expect(entity?.data).toBe('first-write')
      expect(entity?._rev).toBe(1)
    })

    it('returns the existing id without writing when the id is taken', async () => {
      await brain.add({
        id: '22222222-2222-4222-8222-222222222222',
        data: 'original',
        type: NounType.Document
      })
      const id = await brain.add({
        id: '22222222-2222-4222-8222-222222222222',
        data: 'attempted-overwrite',
        type: NounType.Document,
        ifAbsent: true
      })
      expect(id).toBe('22222222-2222-4222-8222-222222222222')
      const entity = await brain.get(id)
      expect(entity?.data).toBe('original')  // not overwritten
      expect(entity?._rev).toBe(1)  // not bumped (no write happened)
    })

    it('is a no-op (writes anyway) when no id is supplied', async () => {
      // ifAbsent without id can't mean anything — a fresh UUID can't collide.
      // The contract says "ignored when id is omitted." Verify it writes.
      const id = await brain.add({
        data: 'no-id-uuid-generated',
        type: NounType.Document,
        ifAbsent: true
      })
      expect(id).toMatch(/^[0-9a-f]{8}-/)  // UUID v4
      const entity = await brain.get(id)
      expect(entity?.data).toBe('no-id-uuid-generated')
    })
  })

  describe('addMany({ ifAbsent: true })', () => {
    it('applies ifAbsent to every item', async () => {
      // Seed one entity that should NOT be overwritten by the batch.
      await brain.add({
        id: '33333333-3333-4333-8333-333333333333',
        data: 'original',
        type: NounType.Document
      })

      const result = await brain.addMany({
        ifAbsent: true,
        items: [
          { id: '33333333-3333-4333-8333-333333333333', data: 'should-not-overwrite', type: NounType.Document },
          { id: '44444444-4444-4444-8444-444444444444', data: 'new', type: NounType.Document },
          { id: '55555555-5555-4555-8555-555555555555', data: 'new', type: NounType.Document }
        ]
      })

      expect(result.successful).toContain('33333333-3333-4333-8333-333333333333')
      expect(result.successful).toContain('44444444-4444-4444-8444-444444444444')
      expect(result.successful).toContain('55555555-5555-4555-8555-555555555555')

      const seeded = await brain.get('33333333-3333-4333-8333-333333333333')
      expect(seeded?.data).toBe('original')
      const fresh1 = await brain.get('44444444-4444-4444-8444-444444444444')
      expect(fresh1?.data).toBe('new')
    })

    it('per-item ifAbsent overrides the batch flag', async () => {
      await brain.add({
        id: '77777777-7777-4777-8777-777777777777',
        data: 'keep',
        type: NounType.Document
      })

      // Batch flag is false (default), but one item opts in.
      const result = await brain.addMany({
        items: [
          { id: '77777777-7777-4777-8777-777777777777', data: 'overwrite-attempt', type: NounType.Document, ifAbsent: true },
          { id: '66666666-6666-4666-8666-666666666666', data: 'new', type: NounType.Document }
        ]
      })

      expect(result.successful).toContain('77777777-7777-4777-8777-777777777777')
      const protectedEntity = await brain.get('77777777-7777-4777-8777-777777777777')
      expect(protectedEntity?.data).toBe('keep')
    })
  })

  describe('SDK scheduler use case — read-then-CAS lock pattern', () => {
    it('two concurrent CAS updates: one wins, one throws RevisionConflictError', async () => {
      const lockId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb'
      await brain.add({
        id: lockId,
        data: { owner: null, expiresAt: 0 },
        type: NounType.Document,
        metadata: { kind: 'job-state' },
        ifAbsent: true
      })

      // Two readers see the same _rev
      const a = await brain.get(lockId)
      const b = await brain.get(lockId)
      expect(a?._rev).toBe(b?._rev)

      // A wins the race
      await brain.update({
        id: lockId,
        data: { owner: 'worker-A', expiresAt: Date.now() + 300_000 },
        ifRev: a!._rev
      })

      // B's CAS now fails because the rev has moved
      let caught: unknown = null
      try {
        await brain.update({
          id: lockId,
          data: { owner: 'worker-B', expiresAt: Date.now() + 300_000 },
          ifRev: b!._rev
        })
      } catch (err) {
        caught = err
      }
      expect(caught).toBeInstanceOf(RevisionConflictError)
      // Final state: A owns the lock
      const final = await brain.get(lockId)
      expect((final?.data as any)?.owner).toBe('worker-A')
    })
  })
})
