/**
 * @module tests/unit/brainy/visibility
 * @description Tests for the 8.0 reserved `visibility` field — the three-tier
 * (`public` | `internal` | `system`) gate that controls whether an entity or
 * relationship surfaces on default user-facing reads.
 *
 * Contract under test:
 * - Absent === `'public'`: counted and returned everywhere.
 * - `'internal'`: hidden from default `find()` / `related()` / counts / `stats()`,
 *   but retrievable with `includeInternal: true`.
 * - `'system'`: Brainy plumbing (the VFS root); hidden everywhere by default,
 *   surfaced only with `includeSystem: true`. Not settable through the public
 *   `add()` / `relate()` params (the param type narrows to `'public' | 'internal'`).
 * - `visibility` is a reserved top-level field: surfaced top-level on reads, never
 *   inside `metadata`, and an untyped caller smuggling it through `metadata` is
 *   normalized (a `'public'`/`'internal'` value is lifted; `'system'` is dropped).
 *
 * THE key regression: a fresh brain reports `getNounCount() === 0` even though the
 * VFS root entity exists — because the root is `visibility: 'system'` and excluded.
 *
 * Runs under the deterministic embedder (the unit setup sets `BRAINY_UNIT_TEST`).
 */

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('visibility (8.0 reserved field)', () => {
  let brain: Brainy

  beforeEach(async () => {
    brain = new Brainy(createTestConfig())
    await brain.init()
  })

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

  describe('counts exclude system + internal', () => {
    it('a fresh brain reports getNounCount() === 0 (the VFS root is system → excluded)', async () => {
      // KEY REGRESSION: the VFS root entity exists after init() but is
      // visibility:'system', so it must not show up in the user-facing count.
      expect(await brain.getNounCount()).toBe(0)
    })

    it('add() with no visibility is counted (public default)', async () => {
      await brain.add({ type: NounType.Concept, data: 'public thing' })
      expect(await brain.getNounCount()).toBe(1)
    })

    it('add({ visibility: "internal" }) is NOT counted', async () => {
      await brain.add({ type: NounType.Concept, data: 'public one' })
      await brain.add({ type: NounType.Concept, data: 'app-internal', visibility: 'internal' })
      // Only the public entity counts.
      expect(await brain.getNounCount()).toBe(1)
    })

    it('flipping visibility via update() moves the entity in/out of the count', async () => {
      const id = await brain.add({ type: NounType.Concept, data: 'flip me' })
      expect(await brain.getNounCount()).toBe(1)

      await brain.update({ id, visibility: 'internal' })
      expect(await brain.getNounCount()).toBe(0)

      await brain.update({ id, visibility: 'public' })
      expect(await brain.getNounCount()).toBe(1)
    })

    it('stats().entityCount reports only public entities', async () => {
      await brain.add({ type: NounType.Concept, data: 'a' })
      await brain.add({ type: NounType.Concept, data: 'b', visibility: 'internal' })
      const stats = await brain.stats()
      expect(stats.entityCount).toBe(1)
    })
  })

  describe('find() default-excludes internal + system', () => {
    it('public entities are returned; internal are hidden by default', async () => {
      const pubId = await brain.add({ type: NounType.Concept, data: 'visible' })
      await brain.add({ type: NounType.Concept, data: 'hidden', visibility: 'internal' })

      const def = await brain.find({ type: NounType.Concept, limit: 100 })
      const ids = def.map((r) => r.id)
      expect(ids).toContain(pubId)
      expect(def.length).toBe(1)
    })

    it('find({ includeInternal: true }) also returns internal entities', async () => {
      const pubId = await brain.add({ type: NounType.Concept, data: 'visible' })
      const intId = await brain.add({ type: NounType.Concept, data: 'hidden', visibility: 'internal' })

      const withInternal = await brain.find({ type: NounType.Concept, includeInternal: true, limit: 100 })
      const ids = withInternal.map((r) => r.id)
      expect(ids).toContain(pubId)
      expect(ids).toContain(intId)
      expect(withInternal.length).toBe(2)
    })

    it('empty-query find() (no filter) also excludes internal by default', async () => {
      const pubId = await brain.add({ type: NounType.Concept, data: 'visible' })
      await brain.add({ type: NounType.Concept, data: 'hidden', visibility: 'internal' })

      const all = await brain.find({ limit: 100 })
      const ids = all.map((r) => r.id)
      expect(ids).toContain(pubId)
      // Only the one public user entity (VFS root is system → excluded too).
      expect(all.length).toBe(1)
    })

    it('limit stays exact when internal entities are interleaved (hard candidate filter)', async () => {
      // Two public + two internal; a default find(limit:2) must return exactly the
      // two public ones, not get short-changed by the hidden ones.
      await brain.add({ type: NounType.Concept, data: 'pub-1' })
      await brain.add({ type: NounType.Concept, data: 'int-1', visibility: 'internal' })
      await brain.add({ type: NounType.Concept, data: 'pub-2' })
      await brain.add({ type: NounType.Concept, data: 'int-2', visibility: 'internal' })

      const page = await brain.find({ type: NounType.Concept, limit: 2 })
      expect(page.length).toBe(2)
      for (const r of page) {
        expect(r.visibility === undefined || r.visibility === 'public').toBe(true)
      }
    })
  })

  describe('the VFS root (system) entity', () => {
    it('never appears in find(), even with includeInternal', async () => {
      const rootId = '00000000-0000-0000-0000-000000000000'
      // Sanity: the root really exists (get() is an explicit by-id read, not a default surface).
      expect(await brain.get(rootId)).not.toBeNull()

      const def = await brain.find({ limit: 1000 })
      expect(def.map((r) => r.id)).not.toContain(rootId)

      const withInternal = await brain.find({ includeInternal: true, limit: 1000 })
      expect(withInternal.map((r) => r.id)).not.toContain(rootId)
    })

    it('appears only with includeSystem: true', async () => {
      const rootId = '00000000-0000-0000-0000-000000000000'
      const withSystem = await brain.find({ includeSystem: true, limit: 1000 })
      expect(withSystem.map((r) => r.id)).toContain(rootId)
    })

    it('the root carries visibility "system" when surfaced via get()', async () => {
      const rootId = '00000000-0000-0000-0000-000000000000'
      const root = await brain.get(rootId)
      expect(root?.visibility).toBe('system')
    })
  })

  describe('verbs (relationships) — symmetric to nouns', () => {
    it('relate({ visibility: "internal" }) is excluded from getVerbCount() + related() by default, included with includeInternal', async () => {
      const a = await brain.add({ type: NounType.Person, data: 'a' })
      const b = await brain.add({ type: NounType.Person, data: 'b' })
      const c = await brain.add({ type: NounType.Person, data: 'c' })

      // One public edge, one internal edge from the same source.
      await brain.relate({ from: a, to: b, type: VerbType.RelatedTo })
      await brain.relate({ from: a, to: c, type: VerbType.RelatedTo, visibility: 'internal' })

      // Counts: only the public edge.
      expect(await brain.getVerbCount()).toBe(1)

      // related() default: only the public edge.
      const def = await brain.related({ from: a })
      expect(def.length).toBe(1)
      expect(def[0].to).toBe(b)

      // related({ includeInternal }): both edges.
      const withInternal = await brain.related({ from: a, includeInternal: true })
      expect(withInternal.length).toBe(2)
      expect(withInternal.map((r) => r.to).sort()).toEqual([b, c].sort())
    })
  })

  describe('visibility is a reserved top-level field', () => {
    it('is surfaced top-level on get(), never inside metadata', async () => {
      const id = await brain.add({
        type: NounType.Concept,
        data: 'x',
        visibility: 'internal',
        metadata: { kind: 'note' }
      })
      const entity = await brain.get(id)
      expect(entity?.visibility).toBe('internal')
      // metadata holds ONLY custom fields, never the reserved visibility key.
      expect(entity?.metadata).toEqual({ kind: 'note' })
      expect((entity?.metadata as Record<string, unknown>)?.visibility).toBeUndefined()
    })

    it('a public-default entity stores no visibility (absent === public)', async () => {
      const id = await brain.add({ type: NounType.Concept, data: 'plain' })
      const entity = await brain.get(id)
      // Absent — not the literal string 'public'. Kept lean on the common path.
      expect(entity?.visibility).toBeUndefined()
    })

    it('an untyped caller passing visibility inside metadata is normalized under reservedFieldPolicy:"remap" (lifted to top-level)', async () => {
      // Simulate a JavaScript caller smuggling the reserved key past the compile-time guard.
      // The legacy remap behavior is now opt-in (8.0 default is 'throw').
      const remapBrain = new Brainy(createTestConfig({ reservedFieldPolicy: 'remap' }))
      await remapBrain.init()
      try {
        const id = await remapBrain.add({
          type: NounType.Concept,
          data: 'y',
          metadata: { visibility: 'internal', tag: 't' } as object
        })
        const entity = await remapBrain.get(id)
        // Lifted to the top-level field…
        expect(entity?.visibility).toBe('internal')
        // …and stripped from the metadata bag.
        expect((entity?.metadata as Record<string, unknown>)?.visibility).toBeUndefined()
        expect((entity?.metadata as Record<string, unknown>)?.tag).toBe('t')
        // It is excluded from the default count, exactly like a top-level internal write.
        expect(await remapBrain.getNounCount()).toBe(0)
      } finally {
        await remapBrain.close()
      }
    })

    it('a "system" value smuggled through metadata is dropped under reservedFieldPolicy:"remap", not honored', async () => {
      // 'system' is Brainy-only; an untyped caller must not be able to set it.
      const remapBrain = new Brainy(createTestConfig({ reservedFieldPolicy: 'remap' }))
      await remapBrain.init()
      try {
        const id = await remapBrain.add({
          type: NounType.Concept,
          data: 'z',
          metadata: { visibility: 'system' } as object
        })
        const entity = await remapBrain.get(id)
        // The smuggled 'system' was dropped → entity stays public (counted, visible).
        expect(entity?.visibility).toBeUndefined()
        expect(await remapBrain.getNounCount()).toBe(1)
        const found = await remapBrain.find({ type: NounType.Concept, limit: 10 })
        expect(found.map((r) => r.id)).toContain(id)
      } finally {
        await remapBrain.close()
      }
    })

    it('an untyped caller passing visibility inside metadata throws under the default policy', async () => {
      // 8.0 default: no silent remap — a reserved key in the bag is a loud error.
      await expect(
        brain.add({
          type: NounType.Concept,
          data: 'throws',
          metadata: { visibility: 'internal', tag: 't' } as object
        })
      ).rejects.toThrow(/visibility.*reserved field/)
    })
  })
})
