/**
 * Comprehensive Public API Test Suite
 *
 * Validates all public API methods exposed by Brainy,
 * ensuring complete coverage of documented functionality.
 */

import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest'
import { Brainy } from '../../src/brainy'
import { NounType, VerbType } from '../../src/types/graphTypes'
import type { Entity, Result } from '../../src/types/brainy.types'
import * as fs from 'fs/promises'
import * as path from 'path'
import { tmpdir } from 'os'
import { randomUUID } from 'crypto'

describe('Brainy Public API - Complete Coverage', () => {
  let brain: Brainy
  let testDir: string

  beforeAll(async () => {
    testDir = path.join(tmpdir(), `brainy-test-${Date.now()}`)
    await fs.mkdir(testDir, { recursive: true })
  })

  afterAll(async () => {
    try {
      await fs.rm(testDir, { recursive: true, force: true })
    } catch (error) {
      console.warn('Failed to cleanup test directory:', error)
    }
  })

  describe('Core CRUD Operations', () => {
    beforeEach(async () => {
      brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
      await brain.init()
    })

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

    describe('brain.add()', () => {
      it('should handle all noun types', async () => {
        const nounTypes = Object.values(NounType)

        for (const nounType of nounTypes) {
          const id = await brain.add({
            data: `Test ${nounType}`,
            type: nounType,
            metadata: { nounType }
          })

          expect(id).toBeDefined()
          expect(typeof id).toBe('string')

          const entity = await brain.get(id)
          expect(entity).toBeDefined()
          expect(entity?.type).toBe(nounType)
        }
      }, 120000)

      it('should validate required fields', async () => {
        await expect(brain.add({
          type: NounType.Document
        } as any)).rejects.toThrow()

        await expect(brain.add({
          data: 'test'
        } as any)).rejects.toThrow()
      })

      it('should handle very large data', async () => {
        const largeText = 'x'.repeat(1_000_000)
        const id = await brain.add({
          data: largeText,
          type: NounType.Document
        })

        const entity = await brain.get(id)
        expect(entity?.data).toBe(largeText)
      })
    })

    describe('brain.addMany()', () => {
      it('should handle batch operations', async () => {
        const items = Array.from({ length: 10 }, (_, i) => ({
          data: `Batch item ${i}`,
          type: NounType.Document,
          metadata: { index: i }
        }))

        const result = await brain.addMany({ items })
        expect(result.successful).toHaveLength(10)
      }, 120000)

      it('should handle partial failures', async () => {
        const items = [
          { data: 'Valid 1', type: NounType.Document },
          { data: null as any, type: NounType.Document },
          { data: 'Valid 2', type: NounType.Document }
        ]

        try {
          const result = await brain.addMany({ items })
          expect(result.successful.length).toBeGreaterThan(0)
        } catch (error) {
          expect(error).toBeDefined()
        }
      })
    })

    describe('brain.update()', () => {
      it('should handle concurrent updates', async () => {
        const id = await brain.add({
          data: 'Initial',
          type: NounType.Document,
          metadata: { version: 1 }
        })

        const updates = Array.from({ length: 10 }, (_, i) =>
          brain.update({
            id,
            metadata: { version: i + 2, updatedBy: `thread-${i}` }
          })
        )

        await Promise.all(updates)

        const final = await brain.get(id)
        expect(final).toBeDefined()
        expect(final?.metadata?.version).toBeGreaterThan(1)
      })

      it('should reject update of a non-existent entity', async () => {
        const fakeId = randomUUID()
        await expect(
          brain.update({ id: fakeId, metadata: { test: true } })
        ).rejects.toThrow(/not found/)
      })

      it('should preserve unmodified fields', async () => {
        const id = await brain.add({
          data: 'Test',
          type: NounType.Document,
          metadata: {
            field1: 'value1',
            field2: 'value2',
            nested: { a: 1, b: 2 }
          }
        })

        await brain.update({
          id,
          metadata: { field1: 'updated' }
        })

        const updated = await brain.get(id)
        expect(updated?.metadata?.field1).toBe('updated')
        expect(updated?.metadata?.field2).toBe('value2')
        expect(updated?.metadata?.nested).toEqual({ a: 1, b: 2 })
      })
    })

    describe('brain.updateMany()', () => {
      it('should batch update multiple entities', async () => {
        const result = await brain.addMany({
          items: [
            { data: 'Item 1', type: NounType.Document },
            { data: 'Item 2', type: NounType.Document },
            { data: 'Item 3', type: NounType.Document }
          ]
        })

        const ids = result.successful
        const updates = ids.map(id => ({
          id,
          metadata: { updated: true }
        }))

        const updateResult = await brain.updateMany({ items: updates })
        expect(updateResult).toBeDefined()

        for (const id of ids) {
          const entity = await brain.get(id)
          expect(entity?.metadata?.updated).toBe(true)
        }
      })
    })

    describe('brain.remove()', () => {
      it('should handle cascade deletion of relationships', async () => {
        const id1 = await brain.add({ data: 'Entity 1', type: NounType.Person })
        const id2 = await brain.add({ data: 'Entity 2', type: NounType.Organization })

        await brain.relate({ from: id1, to: id2, type: VerbType.WorksWith })

        await brain.remove(id1)

        const relations = await brain.related(id2)
        expect(relations.filter(r => r.from === id1 || r.to === id1)).toHaveLength(0)
      })

      it('should treat deletion of a non-existent entity as a no-op', async () => {
        const fakeId = randomUUID()
        // delete() is idempotent: unknown IDs resolve without throwing
        await expect(brain.remove(fakeId)).resolves.toBeUndefined()
      })
    })

    describe('brain.removeMany()', () => {
      it('should efficiently delete batches', async () => {
        const result = await brain.addMany({
          items: Array.from({ length: 5 }, (_, i) => ({
            data: `Item ${i}`,
            type: NounType.Document
          }))
        })

        const ids = result.successful
        await brain.removeMany({ ids })

        for (const id of ids) {
          const entity = await brain.get(id)
          expect(entity).toBeNull()
        }
      })
    })
  })

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

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

    describe('brain.relate()', () => {
      it('should create relationships with multiple verb types', async () => {
        const id1 = await brain.add({ data: 'Source', type: NounType.Person })
        const id2 = await brain.add({ data: 'Target', type: NounType.Organization })

        // Test a representative subset of verb types (not all 127)
        const testVerbs = [
          VerbType.RelatedTo, VerbType.Creates, VerbType.References,
          VerbType.WorksWith, VerbType.DependsOn, VerbType.Contains,
          VerbType.Requires, VerbType.FriendOf
        ]

        for (const verbType of testVerbs) {
          const relationId = await brain.relate({
            from: id1,
            to: id2,
            type: verbType,
            metadata: { verbType }
          })

          expect(relationId).toBeDefined()
          expect(typeof relationId).toBe('string')
        }
      })

      it('should handle bidirectional relationships', async () => {
        const person1 = await brain.add({ data: 'Alice', type: NounType.Person })
        const person2 = await brain.add({ data: 'Bob', type: NounType.Person })

        await brain.relate({
          from: person1,
          to: person2,
          type: VerbType.FriendOf,
          bidirectional: true
        })

        const relations1 = await brain.related(person1)
        const relations2 = await brain.related(person2)

        expect(relations1.some(r => r.to === person2)).toBe(true)
        expect(relations2.some(r => r.to === person1)).toBe(true)
      })

      it('should allow duplicate relationships', async () => {
        const id1 = await brain.add({ data: 'A', type: NounType.Document })
        const id2 = await brain.add({ data: 'B', type: NounType.Document })

        await brain.relate({ from: id1, to: id2, type: VerbType.References })
        await brain.relate({ from: id1, to: id2, type: VerbType.References })

        const relations = await brain.related(id1)
        const referenceRelations = relations.filter(
          r => r.type === VerbType.References && r.to === id2
        )

        expect(referenceRelations.length).toBeGreaterThanOrEqual(1)
      })

      it('should handle relationship metadata and weights', async () => {
        const id1 = await brain.add({ data: 'Source', type: NounType.Document })
        const id2 = await brain.add({ data: 'Target', type: NounType.Document })

        const relationId = await brain.relate({
          from: id1,
          to: id2,
          type: VerbType.References,
          weight: 0.8,
          metadata: {
            context: 'academic',
            verified: true
          }
        })

        const relations = await brain.related(id1)
        const relation = relations.find(r => r.id === relationId)

        expect(relation).toBeDefined()
        expect(relation?.metadata?.context).toBe('academic')
      })
    })

    describe('brain.relateMany()', () => {
      it('should create multiple relationships efficiently', async () => {
        const result = await brain.addMany({
          items: Array.from({ length: 5 }, (_, i) => ({
            data: `Entity ${i}`,
            type: NounType.Document
          }))
        })

        const entities = result.successful

        const items = []
        for (let i = 0; i < entities.length - 1; i++) {
          items.push({
            from: entities[i],
            to: entities[i + 1],
            type: VerbType.Precedes
          })
        }

        const relationIds = await brain.relateMany({ items })
        expect(relationIds).toHaveLength(items.length)
      })
    })

    describe('brain.related()', () => {
      it('should retrieve outgoing relationships', async () => {
        const center = await brain.add({ data: 'Center', type: NounType.Person })
        const related1 = await brain.add({ data: 'Related1', type: NounType.Organization })
        const related2 = await brain.add({ data: 'Related2', type: NounType.Document })

        await brain.relate({ from: center, to: related1, type: VerbType.WorksWith })
        await brain.relate({ from: center, to: related2, type: VerbType.Creates })

        const outgoing = await brain.related({ from: center })

        expect(outgoing).toHaveLength(2)
        expect(outgoing.some(r => r.type === VerbType.WorksWith)).toBe(true)
        expect(outgoing.some(r => r.type === VerbType.Creates)).toBe(true)
      })

      it('should retrieve incoming relationships', async () => {
        const center = await brain.add({ data: 'Center', type: NounType.Person })
        const source = await brain.add({ data: 'Source', type: NounType.Task })

        await brain.relate({ from: source, to: center, type: VerbType.DependsOn })

        const incoming = await brain.related({ to: center })

        expect(incoming).toHaveLength(1)
        expect(incoming[0].type).toBe(VerbType.DependsOn)
      })

      it('should filter by relationship direction', async () => {
        const center = await brain.add({ data: 'Center', type: NounType.Document })
        const source = await brain.add({ data: 'Source', type: NounType.Person })
        const target = await brain.add({ data: 'Target', type: NounType.Task })

        await brain.relate({ from: source, to: center, type: VerbType.Creates })
        await brain.relate({ from: center, to: target, type: VerbType.Requires })

        const outgoing = await brain.related({ from: center })
        const incoming = await brain.related({ to: center })

        expect(outgoing).toHaveLength(1)
        expect(outgoing[0].to).toBe(target)

        expect(incoming).toHaveLength(1)
        expect(incoming[0].from).toBe(source)
      })

      it('should filter by verb type', async () => {
        const doc = await brain.add({ data: 'Document', type: NounType.Document })
        const ref1 = await brain.add({ data: 'Reference1', type: NounType.Document })
        const ref2 = await brain.add({ data: 'Reference2', type: NounType.Document })
        const author = await brain.add({ data: 'Author', type: NounType.Person })

        await brain.relate({ from: doc, to: ref1, type: VerbType.References })
        await brain.relate({ from: doc, to: ref2, type: VerbType.References })
        await brain.relate({ from: author, to: doc, type: VerbType.Creates })

        const references = await brain.related({
          from: doc,
          type: VerbType.References
        })

        expect(references).toHaveLength(2)
        expect(references.every(r => r.type === VerbType.References)).toBe(true)
      })
    })
  })

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

      await brain.addMany({
        items: [
          { data: 'The quick brown fox', type: NounType.Document, metadata: { category: 'animals' } },
          { data: 'jumps over the lazy dog', type: NounType.Document, metadata: { category: 'animals' } },
          { data: 'Machine learning algorithms', type: NounType.Document, metadata: { category: 'tech' } },
          { data: 'Deep neural networks', type: NounType.Document, metadata: { category: 'tech' } },
          { data: 'Natural language processing', type: NounType.Document, metadata: { category: 'tech' } }
        ]
      })
    }, 120000)

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

    describe('brain.find()', () => {
      it('should support metadata search', async () => {
        const results = await brain.find({
          where: { category: 'tech' },
          limit: 5
        })

        expect(results).toBeDefined()
        expect(Array.isArray(results)).toBe(true)
        expect(results.length).toBeGreaterThan(0)
      })

      it('should handle metadata filters', async () => {
        const results = await brain.find({
          where: { category: 'tech' },
          limit: 10
        })

        expect(results.length).toBeGreaterThan(0)
        expect(results.every(r => r.entity.metadata?.category === 'tech')).toBe(true)
      })

      it('should support graph-connected searches', async () => {
        const doc1 = await brain.add({ data: 'Primary document', type: NounType.Document })
        const doc2 = await brain.add({ data: 'Related document', type: NounType.Document })
        await brain.relate({ from: doc1, to: doc2, type: VerbType.References })

        const results = await brain.find({
          query: 'document',
          connected: { from: doc1 },
          limit: 5
        })

        expect(results.some(r => r.entity.id === doc2)).toBe(true)
      })

      it('should support pagination', async () => {
        const page1 = await brain.find({
          where: { category: 'tech' },
          limit: 2,
          offset: 0
        })

        const page2 = await brain.find({
          where: { category: 'tech' },
          limit: 2,
          offset: 2
        })

        if (page1.length > 0 && page2.length > 0) {
          expect(page1[0]?.entity.id).not.toBe(page2[0]?.entity.id)
        }
      })
    })

    describe('brain.similar()', () => {
      it('should find similar entities', async () => {
        const reference = await brain.add({
          data: 'Artificial intelligence and machine learning',
          type: NounType.Document
        })

        const similar = await brain.similar({
          to: reference,
          limit: 5
        })

        expect(similar).toBeDefined()
        expect(similar.length).toBeGreaterThan(0)
      })

      it('should return results sorted by score', async () => {
        const reference = await brain.add({
          data: 'Deep learning and neural networks research',
          type: NounType.Document
        })

        const similar = await brain.similar({
          to: reference,
          limit: 10
        })

        // Results should be sorted by score (descending)
        if (similar.length > 1) {
          for (let i = 1; i < similar.length; i++) {
            expect(similar[i - 1].score).toBeGreaterThanOrEqual(similar[i].score - 0.001)
          }
        }
      })
    })
  })

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

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

    describe('brain.getStats()', () => {
      it('should return statistics', async () => {
        await brain.addMany({
          items: [
            { data: 'Item 1', type: NounType.Document },
            { data: 'Item 2', type: NounType.Person },
            { data: 'Item 3', type: NounType.Task }
          ]
        })

        const stats = await brain.getStats()

        expect(stats).toBeDefined()
        // Stats returns { entities: { total, byType }, relationships, density }
        expect(stats.entities).toBeDefined()
        expect(stats.entities.total).toBeGreaterThanOrEqual(3)
        expect(stats.entities.byType).toBeDefined()
      })
    })
  })

  describe('FileSystem Storage', () => {
    it('should handle basic CRUD with filesystem', async () => {
      const fsTestDir = path.join(tmpdir(), `brainy-fs-crud-${Date.now()}`)
      await fs.mkdir(fsTestDir, { recursive: true })

      const fsBrain = new Brainy({ requireSubtype: false,
        storage: {
          type: 'filesystem',
          path: fsTestDir
        }
      })

      await fsBrain.init()

      const id = await fsBrain.add({
        data: 'Filesystem test',
        type: NounType.Document
      })

      const retrieved = await fsBrain.get(id)
      expect(retrieved?.data).toBe('Filesystem test')

      await fsBrain.update({ id, metadata: { updated: true } })

      const updated = await fsBrain.get(id)
      expect(updated?.metadata?.updated).toBe(true)

      await fsBrain.remove(id)
      const deleted = await fsBrain.get(id)
      expect(deleted).toBeNull()

      await fsBrain.close()
      await fs.rm(fsTestDir, { recursive: true, force: true }).catch(() => {})
    })

    it('should handle concurrent filesystem operations', async () => {
      const fsTestDir = path.join(tmpdir(), `brainy-fs-concurrent-${Date.now()}`)
      await fs.mkdir(fsTestDir, { recursive: true })

      const fsBrain = new Brainy({ requireSubtype: false,
        storage: {
          type: 'filesystem',
          path: fsTestDir
        }
      })

      await fsBrain.init()

      const operations = Array.from({ length: 5 }, async (_, i) => {
        const id = await fsBrain.add({
          data: `Concurrent ${i}`,
          type: NounType.Document
        })
        return id
      })

      const ids = await Promise.all(operations)
      expect(ids).toHaveLength(5)
      expect(new Set(ids).size).toBe(5)

      await fsBrain.close()
      await fs.rm(fsTestDir, { recursive: true, force: true }).catch(() => {})
    })
  })

  describe('Error Recovery and Resilience', () => {
    beforeEach(async () => {
      brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
      await brain.init()
    })

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

    it('should handle and recover from transient errors', async () => {
      const originalAdd = brain.add.bind(brain)
      let failCount = 0
      brain.add = vi.fn(async (...args) => {
        if (failCount++ < 2) {
          throw new Error('Storage temporarily unavailable')
        }
        return originalAdd(...args)
      })

      let succeeded = false
      for (let i = 0; i < 3; i++) {
        try {
          await brain.add({
            data: 'Test',
            type: NounType.Document
          })
          succeeded = true
          break
        } catch (error) {
          // Expected for first attempts
        }
      }

      expect(succeeded).toBe(true)
    })

    it('should handle malformed input gracefully', async () => {
      const malformedInputs = [
        null,
        undefined,
        {},
        { data: null },
        { type: 'invalid' },
      ]

      for (const input of malformedInputs) {
        try {
          await brain.add(input as any)
        } catch (error) {
          expect(error).toBeDefined()
          expect((error as Error).message).toBeDefined()
        }
      }
    })
  })

  describe('Performance', () => {
    it('should handle moderate-scale operations', async () => {
      const perfBrain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
      await perfBrain.init()

      // Add 50 items in batches
      const items = Array.from({ length: 50 }, (_, i) => ({
        data: `Performance item ${i}`,
        type: NounType.Document,
        metadata: { batch: Math.floor(i / 10), index: i }
      }))

      const result = await perfBrain.addMany({ items })
      expect(result.successful.length).toBe(50)

      // Search should work
      const results = await perfBrain.find({
        query: 'Performance item',
        limit: 20
      })
      expect(results.length).toBeGreaterThan(0)

      await perfBrain.close()
    }, 180000)
  })

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

      await brain.addMany({
        items: [
          { data: 'Doc 1', type: NounType.Document, metadata: { category: 'A' } },
          { data: 'Doc 2', type: NounType.Document, metadata: { category: 'B' } },
          { data: 'Person 1', type: NounType.Person, metadata: { category: 'A' } }
        ]
      })
    }, 120000)

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

    it('should clear data and allow re-use', async () => {
      const beforeClear = await brain.find({ where: { category: 'A' } })
      expect(beforeClear.length).toBeGreaterThan(0)

      await brain.clear()

      // After clear, add should still work
      const id = await brain.add({ data: 'After clear', type: NounType.Document })
      const entity = await brain.get(id)
      expect(entity).toBeDefined()
      expect(entity?.data).toBe('After clear')
    }, 120000)
  })
})
