/**
 * @module tests/unit/brainy/update-reserved-metadata-remap
 * @description Regression tests for the reserved-field metadata-bag trap,
 * ported from the 7.x fix and extended to the full 8.0 contract.
 *
 * History: `add({metadata: {confidence}})` lifted reserved fields to their
 * canonical top-level location, but `update({metadata: {confidence}})`
 * silently dropped the same shape — the patch value survived the merge and
 * was then clobbered by the preserve-existing spread. A production
 * consumer's confidence-evolution writes no-oped for weeks before being
 * caught by reading values back.
 *
 * These tests pin the LEGACY REMAP behavior, which in 8.0 is opt-in via
 * `reservedFieldPolicy: 'remap'` (the default is `'throw'` — see the policy
 * matrix in tests/unit/brainy/reserved-field-policy.test.ts). The brain in
 * every test below is constructed with `reservedFieldPolicy: 'remap'` so these
 * deep correctness assertions about the remap path stay exercised.
 *
 * Remap contract under test (every write path, entities AND relationships):
 * - user-mutable reserved fields (`confidence`, `weight`, `subtype` — plus
 *   `service`/`createdBy` at add()/relate() time) remap from the metadata
 *   bag to their dedicated top-level param, with top-level winning when both
 *   are present;
 * - system-managed reserved fields (`createdAt`, `_rev`, `noun`/`verb`,
 *   `data`, …) are dropped from the bag;
 * - the same normalization applies to `transact()` operations and `with()`
 *   speculative views;
 * - reads NEVER echo a reserved field inside `metadata`.
 *
 * TypeScript callers can't write these shapes at all (compile-time guard on
 * the metadata param types — see tests/unit/types/reserved-metadata-keys.test-d.ts);
 * these tests simulate untyped (JavaScript) callers, hence the `as object`
 * widenings on the metadata literals.
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../../src/index.js'
import { NounType, VerbType } from '../../../src/types/graphTypes.js'
import { createTestConfig } from '../../helpers/test-factory.js'

describe('reserved-field metadata remap (8.0 legacy remap path)', () => {
  let brain: Brainy

  beforeEach(async () => {
    // The remap path is opt-in in 8.0 (default policy is 'throw').
    brain = new Brainy(createTestConfig({ reservedFieldPolicy: 'remap' }))
    await brain.init()
  })

  afterEach(async () => {
    await brain.close()
  })

  describe('update() — the ported 7.x regression', () => {
    it('remaps metadata.confidence to the top-level field (the production repro)', async () => {
      const id = await brain.add({
        type: NounType.Concept,
        subtype: 'general',
        data: 'x',
        metadata: { confidence: 0.8 } as object
      })

      // Top-level write works (always did)
      await brain.update({ id, confidence: 0.42 })
      let entity = await brain.get(id)
      expect(entity?.confidence).toBe(0.42)

      // Metadata-patch write — silently dropped pre-fix, remapped now
      await brain.update({ id, metadata: { confidence: 0.33 } as object })
      entity = await brain.get(id)
      expect(entity?.confidence).toBe(0.33)
      // The reserved key must not linger inside the metadata bag
      expect((entity?.metadata as Record<string, unknown>)?.confidence).toBeUndefined()
    })

    it('remaps metadata.weight and metadata.subtype the same way', async () => {
      const id = await brain.add({
        type: NounType.Concept,
        subtype: 'general',
        data: 'y',
        metadata: {}
      })

      await brain.update({ id, metadata: { weight: 0.7, subtype: 'specialized' } as object })
      const entity = await brain.get(id)
      expect(entity?.weight).toBe(0.7)
      expect(entity?.subtype).toBe('specialized')
      expect((entity?.metadata as Record<string, unknown>)?.weight).toBeUndefined()
      expect((entity?.metadata as Record<string, unknown>)?.subtype).toBeUndefined()
    })

    it('top-level param wins when both top-level and metadata-patch carry the field', async () => {
      const id = await brain.add({
        type: NounType.Concept,
        subtype: 'general',
        data: 'z',
        metadata: { confidence: 0.5 } as object
      })

      await brain.update({ id, confidence: 0.9, metadata: { confidence: 0.1 } as object })
      const entity = await brain.get(id)
      expect(entity?.confidence).toBe(0.9)
    })

    it('drops system-managed fields from patches without corrupting the entity', async () => {
      const id = await brain.add({
        type: NounType.Concept,
        subtype: 'general',
        data: 'w',
        metadata: { keep: 'me' }
      })
      const before = await brain.get(id)

      await brain.update({
        id,
        metadata: { createdAt: 1, _rev: 999, noun: 'organization', other: 'applied' } as object
      })
      const after = await brain.get(id)

      expect(after?.createdAt).toBe(before?.createdAt) // immutable
      expect(after?.type).toBe('concept') // noun patch ignored
      expect(after?._rev).toBe((before?._rev ?? 1) + 1) // _rev patch ignored; normal bump applied
      expect((after?.metadata as Record<string, unknown>)?.other).toBe('applied') // custom fields still merge
      expect((after?.metadata as Record<string, unknown>)?.keep).toBe('me')
      expect((after?.metadata as Record<string, unknown>)?._rev).toBeUndefined()
      expect((after?.metadata as Record<string, unknown>)?.createdAt).toBeUndefined()
      expect((after?.metadata as Record<string, unknown>)?.noun).toBeUndefined()
    })

    it('custom (non-reserved) metadata patches are unaffected by the remap', async () => {
      const id = await brain.add({
        type: NounType.Concept,
        subtype: 'general',
        data: 'v',
        metadata: { status: 'draft' }
      })

      await brain.update({ id, metadata: { status: 'reviewed', rating: 4.5 } })
      const entity = await brain.get(id)
      expect((entity?.metadata as Record<string, unknown>)?.status).toBe('reviewed')
      expect((entity?.metadata as Record<string, unknown>)?.rating).toBe(4.5)
    })
  })

  describe('add() — explicit lift, identical contract', () => {
    it('lifts confidence/weight/subtype out of the bag to top level', async () => {
      const id = await brain.add({
        type: NounType.Person,
        data: 'lift check',
        metadata: { confidence: 0.8, weight: 0.6, subtype: 'employee', dept: 'eng' } as object
      })

      const entity = await brain.get(id)
      expect(entity?.confidence).toBe(0.8)
      expect(entity?.weight).toBe(0.6)
      expect(entity?.subtype).toBe('employee')
      expect(entity?.metadata).toEqual({ dept: 'eng' })
    })

    it('lifts service (settable at add time) and lets the top-level param win', async () => {
      const lifted = await brain.add({
        type: NounType.Person,
        subtype: 'employee',
        data: 'service lift',
        metadata: { service: 'orders' } as object
      })
      expect((await brain.get(lifted))?.service).toBe('orders')

      const topLevelWins = await brain.add({
        type: NounType.Person,
        subtype: 'employee',
        data: 'service precedence',
        service: 'billing',
        metadata: { service: 'orders' } as object
      })
      const entity = await brain.get(topLevelWins)
      expect(entity?.service).toBe('billing')
      expect((entity?.metadata as Record<string, unknown>)?.service).toBeUndefined()
    })

    it('a remapped subtype satisfies subtype enforcement like a top-level one', async () => {
      brain.requireSubtype(NounType.Document)

      // Top-level missing, but the bag carries it — must not throw.
      const id = await brain.add({
        type: NounType.Document,
        data: 'enforcement via remap',
        metadata: { subtype: 'invoice' } as object
      })
      expect((await brain.get(id))?.subtype).toBe('invoice')

      // Neither place carries it — must throw.
      await expect(
        brain.add({ type: NounType.Document, data: 'no subtype anywhere' })
      ).rejects.toThrow(/subtype/)
    })
  })

  describe('transact() — same remap on add and update ops', () => {
    it('normalizes reserved fields in transact add + update ops', async () => {
      const db1 = await brain.transact([
        {
          op: 'add',
          type: NounType.Concept,
          subtype: 'general',
          data: 'tx',
          metadata: { confidence: 0.7, custom: 'a' } as object
        }
      ])
      const id = db1.receipt!.ids[0]

      let entity = await brain.get(id)
      expect(entity?.confidence).toBe(0.7)
      expect(entity?.metadata).toEqual({ custom: 'a' })

      await brain.transact([
        { op: 'update', id, metadata: { confidence: 0.25, custom: 'b' } as object }
      ])
      entity = await brain.get(id)
      expect(entity?.confidence).toBe(0.25)
      expect(entity?.metadata).toEqual({ custom: 'b' })
      expect((entity?.metadata as Record<string, unknown>)?.confidence).toBeUndefined()
    })

    it('historical asOf() reads surface reserved fields ONLY top-level', async () => {
      const db1 = await brain.transact([
        {
          op: 'add',
          type: NounType.Concept,
          subtype: 'general',
          data: 'historical',
          metadata: { confidence: 0.9, custom: 'past' } as object
        }
      ])
      const id = db1.receipt!.ids[0]

      // Move the world forward so generation db1 is historical.
      await brain.transact([{ op: 'update', id, confidence: 0.1, metadata: { custom: 'now' } }])

      const past = await brain.asOf(db1.generation)
      const historical = await past.get(id)
      expect(historical?.confidence).toBe(0.9)
      expect(historical?.metadata).toEqual({ custom: 'past' })
      await past.release()
    })

    it('with() speculative views apply the same normalization', async () => {
      const base = await brain.now()
      const speculative = await base.with([
        {
          op: 'add',
          id: 'spec-entity',
          type: NounType.Concept,
          subtype: 'general',
          data: 'spec',
          metadata: { confidence: 0.65, custom: 'spec' } as object
        }
      ])

      const entity = await speculative.get('spec-entity')
      expect(entity?.confidence).toBe(0.65)
      // 8.0 id normalization: a natural-key id is mapped to a stable UUID and
      // the caller's original string is preserved under _originalId — surfaced
      // here exactly as the durable transact()/add() paths do.
      expect(entity?.metadata).toEqual({ custom: 'spec', _originalId: 'spec-entity' })
      await speculative.release()
      await base.release()
    })
  })

  describe('read paths never echo reserved fields inside metadata', () => {
    it('find() (storage pagination path) returns custom-only metadata with reserved fields top-level', async () => {
      const id = await brain.add({
        type: NounType.Person,
        subtype: 'employee',
        data: 'pagination echo check',
        confidence: 0.8,
        weight: 0.6,
        metadata: { dept: 'eng' }
      })

      // No query/filter → served by the direct storage pagination path
      // (getNounsWithPagination), which historically echoed the full flat
      // record (noun/subtype/createdAt/… inside metadata).
      const results = await brain.find({ limit: 50 })
      const result = results.find((r) => r.id === id)
      expect(result).toBeDefined()
      expect(result?.entity.metadata).toEqual({ dept: 'eng' })
      expect(result?.entity.type).toBe(NounType.Person)
      expect(result?.entity.subtype).toBe('employee')
      expect(result?.entity.confidence).toBe(0.8)
      expect(result?.entity.weight).toBe(0.6)
      expect(typeof result?.entity.createdAt).toBe('number')
      expect(result?.entity._rev).toBe(1)
    })

    it('related() by target surfaces reserved fields top-level, custom-only metadata', async () => {
      const a = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'src' })
      const b = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'tgt' })
      const relId = await brain.relate({
        from: a,
        to: b,
        type: VerbType.ReportsTo,
        subtype: 'direct',
        confidence: 0.9,
        weight: 0.5,
        service: 'orders',
        metadata: { note: 'target path' }
      })

      const relations = await brain.related({ to: b })
      const rel = relations.find((r) => r.id === relId)
      expect(rel).toBeDefined()
      expect(rel?.metadata).toEqual({ note: 'target path' })
      expect(rel?.subtype).toBe('direct')
      expect(rel?.confidence).toBe(0.9)
      expect(rel?.weight).toBe(0.5)
      expect(rel?.service).toBe('orders')
      expect(typeof rel?.createdAt).toBe('number')
    })
  })

  describe('relationships — relate() / updateRelation() mirror', () => {
    let a: string
    let b: string

    beforeEach(async () => {
      a = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'A' })
      b = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'B' })
    })

    it('relate() persists the top-level confidence and service params', async () => {
      const relId = await brain.relate({
        from: a,
        to: b,
        type: VerbType.ReportsTo,
        subtype: 'direct',
        confidence: 0.77,
        service: 'orders'
      })

      const relations = await brain.related({ from: a })
      const rel = relations.find((r) => r.id === relId)
      expect(rel?.confidence).toBe(0.77)
      expect(rel?.service).toBe('orders')
    })

    it('relate() remaps reserved fields out of the metadata bag', async () => {
      const relId = await brain.relate({
        from: a,
        to: b,
        type: VerbType.RelatedTo,
        subtype: 'colleague',
        metadata: { confidence: 0.4, weight: 0.3, role: 'peer' } as object
      })

      const relations = await brain.related({ from: a })
      const rel = relations.find((r) => r.id === relId)
      expect(rel?.confidence).toBe(0.4)
      expect(rel?.weight).toBe(0.3)
      expect(rel?.metadata).toEqual({ role: 'peer' })
    })

    it('relation.metadata never echoes the verb type key', async () => {
      const relId = await brain.relate({
        from: a,
        to: b,
        type: VerbType.RelatedTo,
        subtype: 'colleague',
        metadata: { note: 'no echo' }
      })

      const relations = await brain.related({ from: a })
      const rel = relations.find((r) => r.id === relId)
      expect(rel?.type).toBe(VerbType.RelatedTo)
      expect((rel?.metadata as Record<string, unknown>)?.verb).toBeUndefined()
      expect(rel?.metadata).toEqual({ note: 'no echo' })
    })

    it('updateRelation() remaps the user-mutable trio and preserves service', async () => {
      const relId = await brain.relate({
        from: a,
        to: b,
        type: VerbType.ReportsTo,
        subtype: 'direct',
        service: 'orders',
        metadata: { keep: 'me' }
      })

      await brain.updateRelation({
        id: relId,
        metadata: { confidence: 0.55, subtype: 'dotted-line', extra: 'applied' } as object
      })

      const relations = await brain.related({ from: a })
      const rel = relations.find((r) => r.id === relId)
      expect(rel?.confidence).toBe(0.55)
      expect(rel?.subtype).toBe('dotted-line')
      expect(rel?.service).toBe('orders') // fixed at relate() time, never erased by updates
      expect(rel?.metadata).toEqual({ keep: 'me', extra: 'applied' })
    })
  })
})
