/**
 * Migration System Integration Tests
 *
 * Verifies:
 * - MigrationRunner with in-memory storage
 * - brain.migrate() public API (dry-run and apply)
 * - Pre-migration snapshots via `backupTo` (persist-before-migrate + restore)
 * - No-op when MIGRATIONS array is empty
 * - Warning log when autoMigrate is false
 * - Multi-tenant independent migration state
 * - Verb and noun transforms
 */

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { Brainy } from '../../src/brainy.js'
import { MigrationRunner, MIGRATIONS } from '../../src/migration/index.js'
import type { Migration } from '../../src/migration/index.js'
import { NounType, VerbType } from '../../src/types/graphTypes.js'

// Helper to temporarily inject migrations into the MIGRATIONS array
function withMigrations(migrations: Migration[], fn: () => Promise<void>): Promise<void> {
  const original = MIGRATIONS.splice(0, MIGRATIONS.length)
  MIGRATIONS.push(...migrations)
  return fn().finally(() => {
    MIGRATIONS.splice(0, MIGRATIONS.length)
    MIGRATIONS.push(...original)
  })
}

describe('Migration System', () => {
  let brain: Brainy

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

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

  // ─── No-op tests ───────────────────────────────────────────────

  describe('No-op with empty MIGRATIONS', () => {
    it('should have zero pending migrations by default', async () => {
      // MIGRATIONS is empty by default — init() should add ~0ms overhead
      const preview = await brain.migrate({ dryRun: true })
      expect(preview).toHaveProperty('pendingMigrations')
      const p = preview as any
      expect(p.pendingMigrations).toHaveLength(0)
      expect(p.affectedEntities).toBe(0)
    })

    it('should return no-op result when applying with no migrations', async () => {
      const result = await brain.migrate()
      expect(result).toHaveProperty('migrationsApplied')
      const r = result as any
      expect(r.migrationsApplied).toHaveLength(0)
      expect(r.entitiesModified).toBe(0)
    })
  })

  // ─── Dry-run preview ──────────────────────────────────────────

  describe('Dry-run preview', () => {
    it('should preview affected entities without writing', async () => {
      // Add some entities with a unique marker field
      const id1 = await brain.add({ type: NounType.Concept, data: { name: 'Alpha' }, metadata: { status: 'active' } })
      const id2 = await brain.add({ type: NounType.Concept, data: { name: 'Beta' }, metadata: { status: 'inactive' } })
      const id3 = await brain.add({ type: NounType.Concept, data: { name: 'Gamma' }, metadata: { status: 'pending' } })

      const testMigration: Migration = {
        id: 'test-1.0.0-add-version',
        version: '1.0.0',
        description: 'Add version field to entities with status',
        applies: 'nouns',
        transform: (m) => {
          // Only transform entities that have our specific 'status' field
          if ('status' in m && !('version' in m)) {
            return { ...m, version: 1 }
          }
          return null
        }
      }

      await withMigrations([testMigration], async () => {
        const preview = await brain.migrate({ dryRun: true })
        const p = preview as any
        expect(p.pendingMigrations).toHaveLength(1)
        expect(p.pendingMigrations[0].id).toBe('test-1.0.0-add-version')
        // All 3 entities have 'status' metadata
        expect(p.affectedEntities).toBeGreaterThanOrEqual(3)
        expect(p.sampleChanges.length).toBeGreaterThan(0)
        expect(p.sampleChanges[0].after.version).toBe(1)

        // Verify no data was modified (dry-run)
        const entity = await brain.get(id1)
        expect(entity?.metadata?.version).toBeUndefined()
      })
    })

    it('should show correct sample before/after', async () => {
      await brain.add({ type: NounType.Concept, data: { name: 'Test' }, metadata: { state: 'draft' } })

      const renameMigration: Migration = {
        id: 'test-rename',
        version: '1.0.0',
        description: 'Rename state to status',
        applies: 'nouns',
        transform: (m) => {
          if ('state' in m) {
            const { state, ...rest } = m
            return { ...rest, status: state }
          }
          return null
        }
      }

      await withMigrations([renameMigration], async () => {
        const preview = await brain.migrate({ dryRun: true })
        const p = preview as any
        expect(p.sampleChanges.length).toBeGreaterThanOrEqual(1)

        // Find the sample for our entity (it has the 'state' field)
        const sample = p.sampleChanges.find((s: any) => s.before.state === 'draft')
        expect(sample).toBeDefined()
        expect(sample.after.status).toBe('draft')
        expect(sample.after.state).toBeUndefined()
      })
    })
  })

  // ─── Apply migrations ─────────────────────────────────────────

  describe('Apply migrations', () => {
    it('should transform noun metadata and return result', async () => {
      const id = await brain.add({
        type: NounType.Concept,
        data: { name: 'Alice' },
        metadata: { priority: 'low' }
      })

      const addFieldMigration: Migration = {
        id: 'test-add-field',
        version: '1.0.0',
        description: 'Add migrated flag to entities with priority',
        applies: 'nouns',
        transform: (m) => {
          if ('priority' in m && !('migrated' in m)) {
            return { ...m, migrated: true }
          }
          return null
        }
      }

      await withMigrations([addFieldMigration], async () => {
        const result = await brain.migrate()
        const r = result as any
        expect(r.migrationsApplied).toContain('test-add-field')
        expect(r.entitiesModified).toBeGreaterThanOrEqual(1)

        // Verify data was actually transformed
        const entity = await brain.get(id)
        expect(entity?.metadata?.migrated).toBe(true)
        expect(entity?.metadata?.priority).toBe('low')
      })
    })

    it('should skip entities that return null from transform', async () => {
      await brain.add({ type: NounType.Concept, data: { name: 'Has status' }, metadata: { status: 'active' } })
      await brain.add({ type: NounType.Concept, data: { name: 'No status' }, metadata: { priority: 'high' } })

      const conditionalMigration: Migration = {
        id: 'test-conditional',
        version: '1.0.0',
        description: 'Uppercase status field only when present',
        applies: 'nouns',
        transform: (m) => {
          if (typeof m.status === 'string') {
            return { ...m, status: (m.status as string).toUpperCase() }
          }
          return null
        }
      }

      await withMigrations([conditionalMigration], async () => {
        const result = await brain.migrate()
        const r = result as any
        // Only 1 entity has 'status', the other has 'priority'
        expect(r.entitiesModified).toBeGreaterThanOrEqual(1)
        expect(r.entitiesProcessed).toBeGreaterThanOrEqual(2)
      })
    })

    it('should run multiple migrations in order', async () => {
      await brain.add({ type: NounType.Concept, data: { name: 'Test' }, metadata: { count: 1 } })

      const migration1: Migration = {
        id: 'test-double',
        version: '1.0.0',
        description: 'Double count',
        applies: 'nouns',
        transform: (m) => typeof m.count === 'number' ? { ...m, count: (m.count as number) * 2 } : null
      }

      const migration2: Migration = {
        id: 'test-add-ten',
        version: '1.1.0',
        description: 'Add 10 to count',
        applies: 'nouns',
        transform: (m) => typeof m.count === 'number' ? { ...m, count: (m.count as number) + 10 } : null
      }

      await withMigrations([migration1, migration2], async () => {
        const result = await brain.migrate()
        const r = result as any
        expect(r.migrationsApplied).toEqual(['test-double', 'test-add-ten'])
      })
    })

    it('should not re-run completed migrations', async () => {
      await brain.add({ type: NounType.Concept, data: { name: 'Test' }, metadata: { v: 1 } })

      const migration: Migration = {
        id: 'test-increment',
        version: '1.0.0',
        description: 'Increment v',
        applies: 'nouns',
        transform: (m) => typeof m.v === 'number' ? { ...m, v: (m.v as number) + 1 } : null
      }

      await withMigrations([migration], async () => {
        // Run once
        const result1 = await brain.migrate()
        expect((result1 as any).migrationsApplied).toHaveLength(1)

        // Run again — should be no-op
        const result2 = await brain.migrate()
        expect((result2 as any).migrationsApplied).toHaveLength(0)
        expect((result2 as any).entitiesModified).toBe(0)
      })
    })
  })

  // ─── Pre-migration snapshot tests ─────────────────────────────

  describe('Pre-migration snapshots (backupTo)', () => {
    let backupDir: string

    beforeEach(() => {
      backupDir = path.join(os.tmpdir(), `brainy-migration-backup-${Date.now()}-${Math.random().toString(36).slice(2)}`)
    })

    afterEach(() => {
      fs.rmSync(backupDir, { recursive: true, force: true })
    })

    it('should persist a snapshot before migrating and report its path', async () => {
      await brain.add({ type: NounType.Concept, data: { name: 'Test' }, metadata: { x: 1 } })

      const migration: Migration = {
        id: 'test-backup',
        version: '2.0.0',
        description: 'Add y field to entities with x',
        applies: 'nouns',
        transform: (m) => 'x' in m && !('y' in m) ? { ...m, y: 2 } : null
      }

      await withMigrations([migration], async () => {
        const result = await brain.migrate({ backupTo: backupDir })
        const r = result as any
        expect(r.backupPath).toBe(backupDir)
        expect(r.migrationsApplied).toContain('test-backup')

        // The snapshot is a self-contained store directory
        expect(fs.existsSync(backupDir)).toBe(true)
        expect(fs.readdirSync(backupDir).length).toBeGreaterThan(0)
      })
    })

    it('should restore the pre-migration state wholesale from the snapshot', async () => {
      const id = await brain.add({ type: NounType.Concept, data: { name: 'Restore Test' }, metadata: { original: true } })

      const migration: Migration = {
        id: 'test-restore',
        version: '3.0.0',
        description: 'Replace original with migrated',
        applies: 'nouns',
        transform: (m) => {
          if (m.original === true) {
            return { ...m, original: false, migrated: true }
          }
          return null
        }
      }

      await withMigrations([migration], async () => {
        const result = await brain.migrate({ backupTo: backupDir })
        const r = result as any
        expect(r.backupPath).toBe(backupDir)

        // Verify migration was applied
        const migrated = await brain.get(id)
        expect(migrated?.metadata?.migrated).toBe(true)
        expect(migrated?.metadata?.original).toBe(false)

        // Restore the snapshot — the store returns to its pre-migration state
        await brain.restore(backupDir, { confirm: true })
        const restored = await brain.get(id)
        expect(restored?.metadata?.original).toBe(true)
        expect(restored?.metadata?.migrated).toBeUndefined()
      })
    })

    it('should report backupPath null when no backupTo is supplied', async () => {
      await brain.add({ type: NounType.Concept, data: { name: 'NoBackup' }, metadata: { q: 1 } })

      const migration: Migration = {
        id: 'test-no-backup',
        version: '4.0.0',
        description: 'Add field',
        applies: 'nouns',
        transform: (m) => 'q' in m && !('r' in m) ? { ...m, r: 2 } : null
      }

      await withMigrations([migration], async () => {
        const result = await brain.migrate()
        const r = result as any
        expect(r.backupPath).toBeNull()
        expect(r.migrationsApplied).toContain('test-no-backup')
      })
    })
  })

  // ─── Warning log tests ────────────────────────────────────────

  describe('autoMigrate: false', () => {
    it('should log warning when pending migrations exist', async () => {
      const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})

      const migration: Migration = {
        id: 'test-warning',
        version: '1.0.0',
        description: 'Test warning',
        applies: 'nouns',
        transform: () => null
      }

      await withMigrations([migration], async () => {
        // autoMigrate defaults to TRUE in 8.0, which would run the migration
        // inline during init() (small dataset) instead of logging. Opt out so
        // the deferred-notice path is exercised. silent stays false (default)
        // so the notice is actually emitted to console.log.
        const warnBrain = new Brainy({ requireSubtype: false, autoMigrate: false, storage: { type: 'memory' } })
        await warnBrain.init()

        const migrationLogs = consoleSpy.mock.calls
          .filter(call => typeof call[0] === 'string' && call[0].includes('pending migration'))
        expect(migrationLogs.length).toBeGreaterThan(0)

        await warnBrain.close()
      })

      consoleSpy.mockRestore()
    })
  })

  // ─── autoMigrate: true inline ─────────────────────────────────

  describe('autoMigrate: true', () => {
    it('should auto-migrate small datasets during init', async () => {
      // Create a brain with data, close it
      const setupBrain = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
      await setupBrain.init()
      await setupBrain.add({ type: NounType.Concept, data: { name: 'AutoTest' }, metadata: { legacy: true } })

      // We can't easily share memory storage between instances,
      // so test the migrate() path directly instead
      const migration: Migration = {
        id: 'test-auto',
        version: '1.0.0',
        description: 'Auto migrate test',
        applies: 'nouns',
        transform: (m) => 'legacy' in m ? { ...m, legacy: false, upgraded: true } : null
      }

      await withMigrations([migration], async () => {
        const result = await setupBrain.migrate()
        const r = result as any
        expect(r.migrationsApplied).toContain('test-auto')
        expect(r.entitiesModified).toBeGreaterThanOrEqual(1)
      })

      await setupBrain.close()
    })
  })

  // ─── Progress callback ────────────────────────────────────────

  describe('Progress callback', () => {
    it('should call onProgress during migration', async () => {
      await brain.add({ type: NounType.Concept, data: { name: 'A' }, metadata: { x: 1 } })
      await brain.add({ type: NounType.Concept, data: { name: 'B' }, metadata: { x: 2 } })

      const migration: Migration = {
        id: 'test-progress',
        version: '1.0.0',
        description: 'Add y to entities with x',
        applies: 'nouns',
        transform: (m) => 'x' in m ? { ...m, y: true } : null
      }

      const progressCalls: any[] = []

      await withMigrations([migration], async () => {
        await brain.migrate({
          onProgress: (p) => progressCalls.push(p)
        })
      })

      expect(progressCalls.length).toBeGreaterThan(0)
      expect(progressCalls[0].migrationId).toBe('test-progress')
      expect(progressCalls[0].processed).toBeGreaterThan(0)
    })
  })

  // ─── Multi-tenant independence ─────────────────────────────────

  describe('Multi-tenant independence', () => {
    it('should have independent migration state per instance', async () => {
      const brain1 = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
      const brain2 = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
      await brain1.init()
      await brain2.init()

      await brain1.add({ type: NounType.Concept, data: { name: 'User1 data' }, metadata: { v: 1 } })
      await brain2.add({ type: NounType.Concept, data: { name: 'User2 data' }, metadata: { v: 1 } })

      const migration: Migration = {
        id: 'test-tenant',
        version: '1.0.0',
        description: 'Increment v on entities that have it',
        applies: 'nouns',
        transform: (m) => typeof m.v === 'number' ? { ...m, v: (m.v as number) + 1 } : null
      }

      await withMigrations([migration], async () => {
        // Migrate only brain1
        const r1 = await brain1.migrate()
        expect((r1 as any).entitiesModified).toBeGreaterThanOrEqual(1)

        // brain2 should still have pending migrations
        const preview = await brain2.migrate({ dryRun: true })
        expect((preview as any).pendingMigrations).toHaveLength(1)
        expect((preview as any).affectedEntities).toBeGreaterThanOrEqual(1)
      })

      await brain1.close()
      await brain2.close()
    })
  })

  // ─── Verb migration ───────────────────────────────────────────

  describe('Verb migration', () => {
    it('should transform verb metadata when applies is verbs', async () => {
      const id1 = await brain.add({ type: NounType.Concept, data: { name: 'A' } })
      const id2 = await brain.add({ type: NounType.Concept, data: { name: 'B' } })
      await brain.relate({ from: id1, to: id2, type: VerbType.RelatedTo, metadata: { strength: 'weak' } })

      const migration: Migration = {
        id: 'test-verb-migration',
        version: '1.0.0',
        description: 'Rename strength to intensity',
        applies: 'verbs',
        transform: (m) => {
          if ('strength' in m) {
            const { strength, ...rest } = m
            return { ...rest, intensity: strength }
          }
          return null
        }
      }

      await withMigrations([migration], async () => {
        const result = await brain.migrate()
        expect((result as any).migrationsApplied).toContain('test-verb-migration')
        // At least the one verb with 'strength' should be modified
        expect((result as any).entitiesModified).toBeGreaterThanOrEqual(1)
      })
    })
  })

  // ─── Both nouns and verbs ─────────────────────────────────────

  describe('applies: both', () => {
    it('should transform both nouns and verbs when applies is both', async () => {
      const id1 = await brain.add({ type: NounType.Concept, data: { name: 'A' }, metadata: { tag: 'old' } })
      const id2 = await brain.add({ type: NounType.Concept, data: { name: 'B' }, metadata: { tag: 'old' } })
      await brain.relate({ from: id1, to: id2, type: VerbType.RelatedTo, metadata: { tag: 'old' } })

      const migration: Migration = {
        id: 'test-both',
        version: '1.0.0',
        description: 'Update tag from old to new',
        applies: 'both',
        transform: (m) => m.tag === 'old' ? { ...m, tag: 'new' } : null
      }

      await withMigrations([migration], async () => {
        const result = await brain.migrate()
        const r = result as any
        // At least 2 nouns + 1 verb should be modified
        expect(r.entitiesModified).toBeGreaterThanOrEqual(3)
      })
    })
  })

  // ─── MigrationRunner unit-level tests ─────────────────────────

  describe('MigrationRunner', () => {
    it('should report no pending migrations when array is empty', async () => {
      const runner = new MigrationRunner((brain as any).storage)
      expect(await runner.hasPendingMigrations()).toBe(false)
    })

    it('should report pending migrations when array has entries', async () => {
      const migration: Migration = {
        id: 'runner-test',
        version: '1.0.0',
        description: 'Test',
        applies: 'nouns',
        transform: () => null
      }

      await withMigrations([migration], async () => {
        const runner = new MigrationRunner((brain as any).storage)
        expect(await runner.hasPendingMigrations()).toBe(true)
        expect(await runner.pendingCount()).toBe(1)
        expect(runner.nextMigrationVersion()).toBe('1.0.0')
      })
    })

    it('should estimate time correctly', async () => {
      const migration: Migration = {
        id: 'estimate-test',
        version: '1.0.0',
        description: 'Test',
        applies: 'nouns',
        transform: () => null
      }

      await withMigrations([migration], async () => {
        const runner = new MigrationRunner((brain as any).storage)
        const preview = await runner.preview()
        // With 0 entities, estimatedTime should be '0ms' or '<1s'
        expect(preview.estimatedTime).toBeDefined()
      })
    })
  })

  // ─── Error handling ──────────────────────────────────────────────

  describe('Error handling', () => {
    it('should track transform errors without crashing the whole migration', async () => {
      // Add 3 entities — one will trigger a throw
      await brain.add({ type: NounType.Concept, data: { name: 'Good1' }, metadata: { value: 1 } })
      await brain.add({ type: NounType.Concept, data: { name: 'Bad' }, metadata: { value: 'crash-me' } })
      await brain.add({ type: NounType.Concept, data: { name: 'Good2' }, metadata: { value: 3 } })

      const migration: Migration = {
        id: 'test-error-handling',
        version: '1.0.0',
        description: 'Transform that throws on non-number values',
        applies: 'nouns',
        transform: (m) => {
          if ('value' in m) {
            if (typeof m.value !== 'number') {
              throw new Error('value must be a number')
            }
            return { ...m, value: (m.value as number) * 10 }
          }
          return null
        }
      }

      await withMigrations([migration], async () => {
        const result = await brain.migrate()
        const r = result as any

        // Migration should still complete for the good entities
        expect(r.migrationsApplied).toContain('test-error-handling')
        expect(r.entitiesModified).toBeGreaterThanOrEqual(2)

        // Errors should be tracked
        expect(r.errors.length).toBeGreaterThanOrEqual(1)
        expect(r.errors[0].migrationId).toBe('test-error-handling')
        expect(r.errors[0].error).toContain('value must be a number')
        expect(r.errors[0].entityId).toBeDefined()
      })
    })

    it('should stop early when maxErrors is exceeded', async () => {
      // Add many entities that will all fail
      for (let i = 0; i < 5; i++) {
        await brain.add({ type: NounType.Concept, data: { name: `Fail${i}` }, metadata: { boom: true } })
      }

      const migration: Migration = {
        id: 'test-max-errors',
        version: '1.0.0',
        description: 'Always throws',
        applies: 'nouns',
        transform: (m) => {
          if ('boom' in m) {
            throw new Error('deliberate failure')
          }
          return null
        }
      }

      await withMigrations([migration], async () => {
        const result = await brain.migrate({ maxErrors: 2 })
        const r = result as any

        // Should have stopped at 2 errors
        expect(r.errors.length).toBe(2)
        // Not all entities should have been processed
        expect(r.entitiesProcessed).toBeLessThanOrEqual(5)
      })
    })
  })

  // ─── Validation ─────────────────────────────────────────────────

  describe('Validation (MigrationRunner.validateMigrations)', () => {
    it('should accept empty migrations array', () => {
      expect(() => MigrationRunner.validateMigrations([])).not.toThrow()
    })

    it('should accept valid migrations', () => {
      const valid: Migration[] = [
        { id: 'v1', version: '1.0.0', description: 'Test', applies: 'nouns', transform: () => null },
        { id: 'v2', version: '1.1.0', description: 'Test 2', applies: 'verbs', transform: () => null }
      ]
      expect(() => MigrationRunner.validateMigrations(valid)).not.toThrow()
    })

    it('should reject duplicate migration IDs', () => {
      const dupes: Migration[] = [
        { id: 'same-id', version: '1.0.0', description: 'First', applies: 'nouns', transform: () => null },
        { id: 'same-id', version: '1.1.0', description: 'Second', applies: 'nouns', transform: () => null }
      ]
      expect(() => MigrationRunner.validateMigrations(dupes)).toThrow(/Duplicate migration id.*same-id/)
    })

    it('should reject invalid applies value', () => {
      const bad = [
        { id: 'bad-applies', version: '1.0.0', description: 'Bad', applies: 'widgets' as any, transform: () => null }
      ]
      expect(() => MigrationRunner.validateMigrations(bad)).toThrow(/invalid applies value.*widgets/)
    })

    it('should reject non-function transform', () => {
      const bad = [
        { id: 'bad-transform', version: '1.0.0', description: 'Bad', applies: 'nouns' as const, transform: 'not a function' as any }
      ]
      expect(() => MigrationRunner.validateMigrations(bad)).toThrow(/non-function transform/)
    })

    it('should reject missing required fields', () => {
      const noId = [
        { id: '', version: '1.0.0', description: 'Bad', applies: 'nouns' as const, transform: () => null }
      ]
      expect(() => MigrationRunner.validateMigrations(noId)).toThrow(/missing or invalid id/)

      const noVersion = [
        { id: 'ok', version: '', description: 'Bad', applies: 'nouns' as const, transform: () => null }
      ]
      expect(() => MigrationRunner.validateMigrations(noVersion)).toThrow(/missing or invalid version/)

      const noDescription = [
        { id: 'ok', version: '1.0.0', description: '', applies: 'nouns' as const, transform: () => null }
      ]
      expect(() => MigrationRunner.validateMigrations(noDescription)).toThrow(/missing or invalid description/)
    })
  })
})
