/**
 * @module tests/unit/brainy/fill-subtypes
 * @description Unit tests for `brain.fillSubtypes(rules)` — the 8.0 subtype
 * migration helper. Proves the full contract: literal and function rules,
 * entity + relationship fills through one rule map, never overwriting an
 * existing subtype, function rules declining entries (counted as skipped),
 * VFS-marker exclusion, the `{ scanned, filled, skipped, errors, byType }`
 * report, `_rev` bumping through the real update path, idempotent re-runs,
 * per-entry error collection, and fail-fast rule-map validation.
 */

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

describe('Brainy.fillSubtypes()', () => {
  let brain: Brainy

  beforeEach(async () => {
    // requireSubtype: false so the tests can create the pre-8.0 migration
    // debt (entities/relationships without subtype) that fillSubtypes exists
    // to clear.
    brain = new Brainy(createTestConfig())
    await brain.init()
  })

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

  describe('literal rules', () => {
    it('fills missing subtypes with a literal default', async () => {
      const a = await brain.add({ type: NounType.Person, data: 'Alice' })
      const b = await brain.add({ type: NounType.Person, data: 'Bob' })

      const report = await brain.fillSubtypes({
        [NounType.Person]: 'unspecified'
      })

      expect(report.filled).toBe(2)
      expect(report.byType).toEqual({ person: 2 })
      expect((await brain.get(a))!.subtype).toBe('unspecified')
      expect((await brain.get(b))!.subtype).toBe('unspecified')
    })

    it('never overwrites an existing subtype', async () => {
      const employee = await brain.add({
        type: NounType.Person,
        subtype: 'employee',
        data: 'Has a subtype already'
      })
      const blank = await brain.add({ type: NounType.Person, data: 'No subtype' })

      const report = await brain.fillSubtypes({
        [NounType.Person]: 'unspecified'
      })

      expect(report.filled).toBe(1)
      expect((await brain.get(employee))!.subtype).toBe('employee') // untouched
      expect((await brain.get(blank))!.subtype).toBe('unspecified')
    })

    it('leaves types without a rule untouched and counts them as skipped', async () => {
      await brain.add({ type: NounType.Person, data: 'Person without rule coverage' })
      const doc = await brain.add({ type: NounType.Document, data: 'Doc gets filled' })

      const report = await brain.fillSubtypes({
        [NounType.Document]: 'general'
      })

      expect(report.filled).toBe(1)
      expect(report.skipped).toBe(1) // the Person — still migration debt
      expect((await brain.get(doc))!.subtype).toBe('general')
      const audit = await brain.audit()
      expect(audit.total).toBe(report.skipped) // skipped IS the remaining debt
    })
  })

  describe('function rules', () => {
    it('derives the subtype from entity fields', async () => {
      const vendor = await brain.add({
        type: NounType.Person,
        data: 'Vendor person',
        metadata: { kind: 'vendor' }
      })
      const fallback = await brain.add({ type: NounType.Person, data: 'No kind field' })

      const report = await brain.fillSubtypes({
        [NounType.Person]: (e) => (e.metadata as { kind?: string })?.kind ?? 'unspecified'
      })

      expect(report.filled).toBe(2)
      expect((await brain.get(vendor))!.subtype).toBe('vendor')
      expect((await brain.get(fallback))!.subtype).toBe('unspecified')
    })

    it('treats undefined returns as declines (selective fill, counted as skipped)', async () => {
      const classified = await brain.add({
        type: NounType.Person,
        data: 'Classifiable',
        metadata: { department: 'engineering' }
      })
      const unclassified = await brain.add({ type: NounType.Person, data: 'Not classifiable' })

      const report = await brain.fillSubtypes({
        [NounType.Person]: (e) =>
          (e.metadata as { department?: string })?.department ? 'employee' : undefined
      })

      expect(report.filled).toBe(1)
      expect(report.skipped).toBe(1)
      expect((await brain.get(classified))!.subtype).toBe('employee')
      expect((await brain.get(unclassified))!.subtype).toBeUndefined()
    })

    it('treats empty-string returns as declines (an empty subtype would fail enforcement)', async () => {
      await brain.add({ type: NounType.Person, data: 'Rule returns empty string' })

      const report = await brain.fillSubtypes({
        [NounType.Person]: () => ''
      })

      expect(report.filled).toBe(0)
      expect(report.skipped).toBe(1)
    })

    it('collects per-entry rule errors and continues the pass', async () => {
      const poisoned = await brain.add({
        type: NounType.Person,
        data: 'Rule throws on this one',
        metadata: { poison: true }
      })
      const healthy = await brain.add({ type: NounType.Person, data: 'Rule works on this one' })

      const report = await brain.fillSubtypes({
        [NounType.Person]: (e) => {
          if ((e.metadata as { poison?: boolean })?.poison) {
            throw new Error('cannot classify poisoned entity')
          }
          return 'unspecified'
        }
      })

      expect(report.filled).toBe(1)
      expect(report.errors).toHaveLength(1)
      expect(report.errors[0].id).toBe(poisoned)
      expect(report.errors[0].error).toMatch(/cannot classify/)
      expect((await brain.get(healthy))!.subtype).toBe('unspecified')
    })
  })

  describe('relationship rules (VerbType keys)', () => {
    it('fills missing relationship subtypes through the same rule map', 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.RelatedTo })
      const labeled = await brain.relate({
        from: b,
        to: a,
        type: VerbType.RelatedTo,
        subtype: 'colleague'
      })

      const report = await brain.fillSubtypes({
        [VerbType.RelatedTo]: 'unspecified'
      })

      expect(report.filled).toBe(1)
      expect(report.byType).toEqual({ relatedTo: 1 })

      const relations = await brain.related({ from: a })
      expect(relations.find((r) => r.id === relId)!.subtype).toBe('unspecified')
      const reverse = await brain.related({ from: b })
      expect(reverse.find((r) => r.id === labeled)!.subtype).toBe('colleague') // untouched
    })

    it('passes the full Relation shape to relationship rule functions', 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 brain.relate({
        from: a,
        to: b,
        type: VerbType.ReportsTo,
        metadata: { dotted: true }
      })

      const report = await brain.fillSubtypes({
        [VerbType.ReportsTo]: (r) => {
          // The rule sees the public Relation shape — endpoints included.
          expect(r.from).toBe(a)
          expect(r.to).toBe(b)
          return (r.metadata as { dotted?: boolean })?.dotted ? 'dotted-line' : 'direct'
        }
      })

      expect(report.filled).toBe(1)
      const relations = await brain.related({ from: a })
      expect(relations[0].subtype).toBe('dotted-line')
    })

    it('handles entity and relationship rules in a single pass', async () => {
      const a = await brain.add({ type: NounType.Person, data: 'A' })
      const b = await brain.add({ type: NounType.Person, data: 'B' })
      await brain.relate({ from: a, to: b, type: VerbType.RelatedTo })

      const report = await brain.fillSubtypes({
        [NounType.Person]: 'unspecified',
        [VerbType.RelatedTo]: 'unspecified'
      })

      expect(report.filled).toBe(3)
      expect(report.byType).toEqual({ person: 2, relatedTo: 1 })
      const audit = await brain.audit()
      expect(audit.total).toBe(0)
    })
  })

  describe('report + write semantics', () => {
    it('bumps _rev through the real update path', async () => {
      const id = await brain.add({ type: NounType.Person, data: 'Rev check' })
      const before = await brain.get(id)

      await brain.fillSubtypes({ [NounType.Person]: 'unspecified' })

      const after = await brain.get(id)
      expect(after!._rev).toBe((before!._rev ?? 1) + 1)
    })

    it('is idempotent — a re-run scans but fills nothing', async () => {
      await brain.add({ type: NounType.Person, data: 'Fill once' })

      const first = await brain.fillSubtypes({ [NounType.Person]: 'unspecified' })
      expect(first.filled).toBe(1)

      const second = await brain.fillSubtypes({ [NounType.Person]: 'unspecified' })
      expect(second.filled).toBe(0)
      expect(second.skipped).toBe(0)
      expect(second.scanned).toBeGreaterThan(0)
      expect(second.errors).toEqual([])
    })

    it('excludes VFS-marked entries by default, fills them with includeVFS: true', async () => {
      // VFS markers bypass subtype enforcement, so marked entries are not
      // migration debt. Fabricate one explicitly (public consumers shouldn't
      // set this marker; the test exercises the exclusion contract).
      const vfsLike = await brain.add({
        type: NounType.Document,
        data: 'Infrastructure-marked entry',
        metadata: { isVFS: true }
      })

      const defaultRun = await brain.fillSubtypes({ [NounType.Document]: 'general' })
      expect(defaultRun.filled).toBe(0)
      expect((await brain.get(vfsLike))!.subtype).toBeUndefined()

      const inclusiveRun = await brain.fillSubtypes(
        { [NounType.Document]: 'general' },
        { includeVFS: true }
      )
      expect(inclusiveRun.filled).toBeGreaterThanOrEqual(1)
      expect((await brain.get(vfsLike))!.subtype).toBe('general')
    })

    it('reports progress after each batch', async () => {
      await brain.add({ type: NounType.Person, data: 'P1' })
      await brain.add({ type: NounType.Person, data: 'P2' })
      await brain.add({ type: NounType.Person, data: 'P3' })

      const snapshots: Array<{ scanned: number; filled: number; skipped: number }> = []
      await brain.fillSubtypes(
        { [NounType.Person]: 'unspecified' },
        {
          batchSize: 1,
          onProgress: (p) => snapshots.push({ ...p })
        }
      )

      expect(snapshots.length).toBeGreaterThanOrEqual(3)
      const last = snapshots[snapshots.length - 1]
      expect(last.filled).toBe(3)
    })

    it('clears the debt brain.audit() reports', async () => {
      await brain.add({ type: NounType.Person, data: 'Debt 1' })
      await brain.add({ type: NounType.Document, data: 'Debt 2' })

      const before = await brain.audit()
      expect(before.total).toBe(2)

      await brain.fillSubtypes({
        [NounType.Person]: 'unspecified',
        [NounType.Document]: 'general'
      })

      const after = await brain.audit()
      expect(after.total).toBe(0)
      expect(after.recommendation).toMatch(/strict-mode-ready/)
    })
  })

  describe('rule-map validation (fail fast, before touching data)', () => {
    it('rejects an empty rules map', async () => {
      await expect(brain.fillSubtypes({})).rejects.toThrow(/rules map is empty/)
    })

    it('rejects keys that are not a NounType or VerbType', async () => {
      await expect(
        brain.fillSubtypes({ 'not-a-type': 'whatever' } as never)
      ).rejects.toThrow(/'not-a-type' is not a valid NounType or VerbType/)
    })

    it('rejects empty-string literal rules', async () => {
      await expect(
        brain.fillSubtypes({ [NounType.Person]: '' })
      ).rejects.toThrow(/empty string/)
    })

    it('rejects rule values that are neither string nor function', async () => {
      await expect(
        brain.fillSubtypes({ [NounType.Person]: 42 } as never)
      ).rejects.toThrow(/must be a subtype string or a function/)
    })

    it('does not write anything when validation fails', async () => {
      const id = await brain.add({ type: NounType.Person, data: 'Untouched on failure' })

      await expect(
        brain.fillSubtypes({
          [NounType.Person]: 'unspecified',
          'bogus-type': 'x'
        } as never)
      ).rejects.toThrow(/bogus-type/)

      expect((await brain.get(id))!.subtype).toBeUndefined()
    })
  })
})
