/**
 * @module tests/unit/brainy/reserved-field-policy
 * @description The 8.0 `reservedFieldPolicy` matrix — what happens when an
 * untyped (JavaScript) caller smuggles a Brainy-reserved field INSIDE the
 * `metadata` bag of a write call, past the compile-time guard.
 *
 * 8.0 is a clean break with no silent failures. The decided contract:
 * - `'throw'` (DEFAULT): a reserved key in the bag throws a clear Error naming
 *   the offending key(s) and the correct write path. No remap, no data loss.
 * - `'warn'`: legacy remap PLUS a one-shot (per method+field, per process)
 *   warning for EVERY reserved key found.
 * - `'remap'`: the pre-8.0 silent remap, no warning.
 *
 * The deep correctness of the remap itself (top-level precedence, system-managed
 * drops, transact()/with() mirrors, read-side splitting) lives in
 * tests/unit/brainy/update-reserved-metadata-remap.test.ts (which now runs under
 * `reservedFieldPolicy: 'remap'`). This file pins the POLICY SELECTION and the
 * throw/warn behaviors.
 *
 * Compile-time callers can't write these shapes at all (see
 * tests/unit/types/reserved-metadata-keys.test-d.ts); the `as object` widenings
 * below simulate untyped callers.
 */

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

describe('reservedFieldPolicy', () => {
  describe("default policy is 'throw'", () => {
    let brain: Brainy

    beforeEach(async () => {
      // No reservedFieldPolicy override → resolves to 'throw'.
      brain = new Brainy(createTestConfig())
      await brain.init()
    })

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

    it('add() throws naming the offending key and the correct write path', async () => {
      await expect(
        brain.add({
          type: NounType.Concept,
          subtype: 'general',
          data: 'x',
          metadata: { confidence: 0.8 } as object
        })
      ).rejects.toThrow(/metadata\.confidence is a reserved field/)

      // The error names the right param and the reserved list for discoverability.
      await expect(
        brain.add({
          type: NounType.Concept,
          subtype: 'general',
          data: 'x',
          metadata: { confidence: 0.8 } as object
        })
      ).rejects.toThrow(/'confidence' param.*RESERVED_ENTITY_FIELDS/s)
    })

    it('add() lists EVERY offending key when several are present', async () => {
      const err = await brain
        .add({
          type: NounType.Person,
          data: 'multi',
          metadata: { confidence: 0.5, weight: 0.6, subtype: 'employee' } as object
        })
        .catch((e) => e as Error)
      expect(err).toBeInstanceOf(Error)
      expect(err.message).toMatch(/confidence/)
      expect(err.message).toMatch(/weight/)
      expect(err.message).toMatch(/subtype/)
    })

    it('update() throws on a reserved key in the patch', async () => {
      const id = await brain.add({ type: NounType.Concept, subtype: 'general', data: 'y' })
      await expect(
        brain.update({ id, metadata: { confidence: 0.3 } as object })
      ).rejects.toThrow(/metadata\.confidence is a reserved field/)
    })

    it('relate() throws on a reserved key in the bag', async () => {
      const a = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'A' })
      const b = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'B' })
      await expect(
        brain.relate({
          from: a,
          to: b,
          type: VerbType.RelatedTo,
          subtype: 'colleague',
          metadata: { confidence: 0.4 } as object
        })
      ).rejects.toThrow(/metadata\.confidence is a reserved field.*RESERVED_RELATION_FIELDS/s)
    })

    it('updateRelation() throws on a reserved key in the patch', async () => {
      const a = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'A' })
      const b = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'B' })
      const relId = await brain.relate({
        from: a,
        to: b,
        type: VerbType.ReportsTo,
        subtype: 'direct'
      })
      await expect(
        brain.updateRelation({ id: relId, metadata: { weight: 0.2 } as object })
      ).rejects.toThrow(/metadata\.weight is a reserved field/)
    })

    it('transact() add op throws on a reserved key in the bag', async () => {
      await expect(
        brain.transact([
          {
            op: 'add',
            type: NounType.Concept,
            subtype: 'general',
            data: 'tx',
            metadata: { confidence: 0.7 } as object
          }
        ])
      ).rejects.toThrow(/metadata\.confidence is a reserved field/)
    })

    it('a custom (non-reserved) key in the bag does NOT throw', async () => {
      const id = await brain.add({
        type: NounType.Concept,
        subtype: 'general',
        data: 'ok',
        metadata: { status: 'draft', rating: 4 }
      })
      const entity = await brain.get(id)
      expect(entity?.metadata).toEqual({ status: 'draft', rating: 4 })
    })
  })

  describe("'remap' policy remaps silently (no warning)", () => {
    let brain: Brainy
    let warnSpy: ReturnType<typeof vi.spyOn>

    beforeEach(async () => {
      warnSpy = vi.spyOn(prodLog, 'warn').mockImplementation(() => {})
      brain = new Brainy(createTestConfig({ reservedFieldPolicy: 'remap' }))
      await brain.init()
    })

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

    it('lifts user-mutable reserved fields to top-level without warning', async () => {
      const id = await brain.add({
        type: NounType.Person,
        data: 'remap lift',
        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' })
      // 'remap' is silent about reserved fields (unrelated storage logs may fire,
      // so assert specifically that no reserved-field warning was emitted).
      const reservedWarned = warnSpy.mock.calls.some((c) =>
        String(c[0]).includes('reserved field')
      )
      expect(reservedWarned).toBe(false)
    })

    it('preserves _originalId on natural-key ids through the remap path', async () => {
      // A speculative view applies the same normalization and maps a natural-key
      // id to a stable UUID, preserving the caller's original string.
      const base = await brain.now()
      const speculative = await base.with([
        {
          op: 'add',
          id: 'remap-spec-entity',
          type: NounType.Concept,
          subtype: 'general',
          data: 'spec',
          metadata: { confidence: 0.65, custom: 'spec' } as object
        }
      ])
      const entity = await speculative.get('remap-spec-entity')
      expect(entity?.confidence).toBe(0.65)
      expect(entity?.metadata).toEqual({ custom: 'spec', _originalId: 'remap-spec-entity' })
      await speculative.release()
      await base.release()
    })
  })

  describe("'warn' policy remaps AND warns once per key", () => {
    let brain: Brainy
    let warnSpy: ReturnType<typeof vi.spyOn>

    beforeEach(async () => {
      warnSpy = vi.spyOn(prodLog, 'warn').mockImplementation(() => {})
      brain = new Brainy(createTestConfig({ reservedFieldPolicy: 'warn' }))
      await brain.init()
    })

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

    it('remaps the value (same as remap) and emits a warning naming the field', async () => {
      // Use a method+field combo unique to this test so the per-process one-shot
      // registry has not already consumed it.
      const id = await brain.add({
        type: NounType.Person,
        data: 'warn lift',
        // weight is user-mutable → remapped; this is the only 'warn'-policy
        // add({ weight }) in the suite, so the one-shot warning fires here.
        metadata: { weight: 0.42, dept: 'eng' } as object
      })
      const entity = await brain.get(id)
      // Value is honored (remap still happens under 'warn').
      expect(entity?.weight).toBe(0.42)
      expect(entity?.metadata).toEqual({ dept: 'eng' })
      // And a warning was emitted naming the reserved field.
      expect(warnSpy).toHaveBeenCalled()
      const warned = warnSpy.mock.calls.some((c) =>
        String(c[0]).includes("'weight'")
      )
      expect(warned).toBe(true)
    })

    it('warns for system-managed keys too (closes the historical gap)', async () => {
      // Pre-8.0 only system-managed fields warned; 'warn' warns for every key.
      // 'createdBy' (system-managed on update) is unique to this test.
      const id = await brain.add({ type: NounType.Concept, subtype: 'general', data: 'sys' })
      warnSpy.mockClear()
      await brain.update({ id, metadata: { createdBy: 'nope', keep: 'me' } as object })
      const entity = await brain.get(id)
      // System-managed key dropped; custom field merged.
      expect((entity?.metadata as Record<string, unknown>)?.createdBy).toBeUndefined()
      expect((entity?.metadata as Record<string, unknown>)?.keep).toBe('me')
      // A warning was emitted for the dropped system-managed key.
      const warned = warnSpy.mock.calls.some((c) =>
        String(c[0]).includes("'createdBy'")
      )
      expect(warned).toBe(true)
    })
  })
})
