/**
 * Unit tests for memory limit calculation and container detection (v5.11.0)
 *
 * Tests verify:
 * - Container memory detection (cgroup v1/v2, env vars)
 * - Smart memory limit calculation
 * - Configuration overrides
 * - Memory stats API
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../../src/brainy.js'
import { ValidationConfig } from '../../../src/utils/paramValidation.js'
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'

describe('Memory Limits - Container Detection & Smart Calculation', () => {
  let testDir: string
  let originalEnv: Record<string, string | undefined>

  beforeEach(() => {
    testDir = mkdtempSync(join(tmpdir(), 'brainy-memory-test-'))

    // Save original environment variables
    originalEnv = {
      CLOUD_RUN_MEMORY: process.env.CLOUD_RUN_MEMORY,
      MEMORY_LIMIT: process.env.MEMORY_LIMIT
    }

    // Reset ValidationConfig singleton before each test
    ValidationConfig.reset()
  })

  afterEach(() => {
    rmSync(testDir, { recursive: true, force: true })

    // Restore original environment
    Object.keys(originalEnv).forEach(key => {
      if (originalEnv[key] === undefined) {
        delete process.env[key]
      } else {
        process.env[key] = originalEnv[key]
      }
    })

    // Reset ValidationConfig after each test
    ValidationConfig.reset()
  })

  describe('Container Memory Detection', () => {
    it('should detect Cloud Run memory limit from env var', () => {
      process.env.CLOUD_RUN_MEMORY = '4Gi'

      const config = ValidationConfig.getInstance()

      expect(config.detectedContainerLimit).toBe(4 * 1024 * 1024 * 1024)
      expect(config.limitBasis).toBe('containerMemory')

      // 4GB * 0.25 = 1GB query memory = 10k limit
      expect(config.maxLimit).toBeGreaterThan(5000)
    })

    it('should detect Cloud Run memory limit in Mi units', () => {
      process.env.CLOUD_RUN_MEMORY = '512Mi'

      const config = ValidationConfig.getInstance()

      expect(config.detectedContainerLimit).toBe(512 * 1024 * 1024)
      expect(config.limitBasis).toBe('containerMemory')

      // 512MB * 0.25 = 128MB query memory
      expect(config.maxLimit).toBeGreaterThan(0)
    })

    it('should detect generic MEMORY_LIMIT env var', () => {
      process.env.MEMORY_LIMIT = String(2 * 1024 * 1024 * 1024) // 2GB

      const config = ValidationConfig.getInstance()

      expect(config.detectedContainerLimit).toBe(2 * 1024 * 1024 * 1024)
      expect(config.limitBasis).toBe('containerMemory')
    })

    it('should fall back to free memory if no container detected', () => {
      // No environment variables set

      const config = ValidationConfig.getInstance()

      expect(config.limitBasis).toBe('freeMemory')
      expect(config.maxLimit).toBeGreaterThan(0)
    })
  })

  describe('Smart Memory Limit Calculation', () => {
    it('should allocate 25% of container memory for queries', () => {
      process.env.CLOUD_RUN_MEMORY = '4Gi'

      const config = ValidationConfig.getInstance()

      // 4 GB × 0.25 = 1 GB for queries; at 25 KB / result (7.30.2 calibration)
      // → 1 GB / 25 KB = ~40_960 → floor to 40 × 1000 = 40_000.
      // Pre-7.30.2 used 100 KB / result and returned 10_000 here.
      expect(config.maxLimit).toBe(40000)
    })

    it('should respect absolute maximum of 100k', () => {
      // Simulate huge container
      process.env.MEMORY_LIMIT = String(100 * 1024 * 1024 * 1024) // 100GB

      const config = ValidationConfig.getInstance()

      // Should cap at 100,000 even with huge memory
      expect(config.maxLimit).toBe(100000)
    })

    it('should handle small containers gracefully', () => {
      process.env.CLOUD_RUN_MEMORY = '512Mi'

      const config = ValidationConfig.getInstance()

      // 512 MB × 0.25 = 128 MB for queries; at 25 KB / result that raw figure
      // is ~5_242 → floor to 5 × 1000 = 5_000, which the MIN_AUTO_QUERY_LIMIT
      // floor lifts to 10_000. A small container must never throttle queries to
      // a near-useless ceiling (BRAINY-QUERYCAP-MISREAD).
      expect(config.maxLimit).toBe(10000)
      expect(config.limitBasis).toBe('containerMemory')
    })
  })

  describe('Auto-cap floor (BRAINY-QUERYCAP-MISREAD regression)', () => {
    // A transiently low memory reading once collapsed the auto-detected query
    // cap to a near-useless ceiling on healthy hosts (the cap was driven off
    // os.freemem()/MemFree, which excludes reclaimable page cache). The fix:
    // drive detection off /proc/meminfo MemAvailable AND floor every
    // *auto-detected* tier at MIN_AUTO_QUERY_LIMIT (10_000). Consumer-supplied
    // limits bypass the floor — an explicit caller knows their box best.

    it('floors a tiny container to 10_000, never below', () => {
      // 50 MB × 0.25 = 12.5 MB for queries → raw cap rounds to 0; the floor
      // must lift it to 10_000 rather than let it throttle to nothing.
      process.env.MEMORY_LIMIT = String(50 * 1024 * 1024)

      const config = ValidationConfig.getInstance()

      expect(config.limitBasis).toBe('containerMemory')
      expect(config.maxLimit).toBe(10000)
    })

    it('floors the free-memory tier at 10_000', () => {
      // No container env → free-memory tier (now MemAvailable-based). Whatever
      // the host reports, the auto cap is guaranteed never to fall below 10_000.
      const config = ValidationConfig.getInstance()

      expect(config.limitBasis).toBe('freeMemory')
      expect(config.maxLimit).toBeGreaterThanOrEqual(10000)
    })

    it('does NOT floor an explicit maxQueryLimit below 10_000', () => {
      // A consumer asking for 500 means 500 — the floor is for auto-detection.
      const config = ValidationConfig.getInstance({ maxQueryLimit: 500 })

      expect(config.limitBasis).toBe('override')
      expect(config.maxLimit).toBe(500)
    })

    it('does NOT floor an explicit reservedQueryMemory below 10_000', () => {
      // 100 MB / 25 KB = ~4_000 → kept as-is (no floor on the explicit tier).
      const config = ValidationConfig.getInstance({
        reservedQueryMemory: 100 * 1024 * 1024
      })

      expect(config.limitBasis).toBe('reservedMemory')
      expect(config.maxLimit).toBe(4000)
    })
  })

  describe('Configuration Overrides', () => {
    it('should respect maxQueryLimit override', () => {
      const config = ValidationConfig.getInstance({ maxQueryLimit: 50000 })

      expect(config.maxLimit).toBe(50000)
      expect(config.limitBasis).toBe('override')
    })

    it('should respect reservedQueryMemory override', () => {
      // Reserve 1 GB for queries; at 25 KB / result (7.30.2) → 1 GB / 25 KB
      // = ~40_960 → floor to 40 × 1000 = 40_000. Pre-7.30.2 used 100 KB /
      // result and this returned 10_000.
      const config = ValidationConfig.getInstance({
        reservedQueryMemory: 1 * 1024 * 1024 * 1024
      })

      expect(config.maxLimit).toBe(40000)
      expect(config.limitBasis).toBe('reservedMemory')
    })

    it('should prioritize maxQueryLimit over reservedQueryMemory', () => {
      const config = ValidationConfig.getInstance({
        maxQueryLimit: 25000,
        reservedQueryMemory: 1 * 1024 * 1024 * 1024
      })

      expect(config.maxLimit).toBe(25000)
      expect(config.limitBasis).toBe('override')
    })

    it('should cap explicit overrides at 100k for safety', () => {
      const config = ValidationConfig.getInstance({
        maxQueryLimit: 200000 // Try to set above max
      })

      expect(config.maxLimit).toBe(100000) // Capped
      expect(config.limitBasis).toBe('override')
    })
  })

  describe('ValidationConfig Reconfiguration', () => {
    it('should reconfigure singleton with new options', () => {
      const config1 = ValidationConfig.getInstance()
      const originalLimit = config1.maxLimit

      // Reconfigure
      const config2 = ValidationConfig.reconfigure({ maxQueryLimit: 30000 })

      expect(config2.maxLimit).toBe(30000)
      expect(config2.limitBasis).toBe('override')

      // Verify singleton updated
      const config3 = ValidationConfig.getInstance()
      expect(config3.maxLimit).toBe(30000)
    })

    it('should reset singleton', () => {
      const config1 = ValidationConfig.getInstance({ maxQueryLimit: 10000 })
      expect(config1.maxLimit).toBe(10000)

      ValidationConfig.reset()

      const config2 = ValidationConfig.getInstance()
      // Should recalculate based on system memory
      expect(config2.maxLimit).not.toBe(10000)
    })
  })

  describe('Brain Integration', () => {
    it('should configure memory limits via Brain constructor', async () => {
      const brain = new Brainy({ requireSubtype: false,
        storage: { type: 'memory' },
        maxQueryLimit: 15000,
        silent: true
      })

      await brain.init()

      const stats = brain.getMemoryStats()

      expect(stats.limits.maxQueryLimit).toBe(15000)
      expect(stats.limits.basis).toBe('override')
      expect(stats.config.maxQueryLimit).toBe(15000)

      await brain.close()
    })

    it('should configure reserved memory via Brain constructor', async () => {
      const brain = new Brainy({ requireSubtype: false,
        storage: { type: 'memory' },
        reservedQueryMemory: 500 * 1024 * 1024, // 500 MB
        silent: true
      })

      await brain.init()

      const stats = brain.getMemoryStats()

      // 500 MB / 25 KB per result (7.30.2 calibration) = ~20_000.
      // Pre-7.30.2 used 100 KB / result and this returned 5000.
      expect(stats.limits.maxQueryLimit).toBe(20000)
      expect(stats.limits.basis).toBe('reservedMemory')
      expect(stats.config.reservedQueryMemory).toBe(500 * 1024 * 1024)

      await brain.close()
    })

    it('should auto-detect container limits when no config provided', async () => {
      process.env.CLOUD_RUN_MEMORY = '2Gi'

      const brain = new Brainy({ requireSubtype: false,
        storage: { type: 'memory' },
        silent: true
      })

      await brain.init()

      const stats = brain.getMemoryStats()

      expect(stats.memory.containerLimit).toBe(2 * 1024 * 1024 * 1024)
      expect(stats.limits.basis).toBe('containerMemory')
      // 2 GB × 0.25 = 512 MB query budget; at 25 KB per result (7.30.2) →
      // 512 MB / 25 KB = ~20_971 → floor to 20 × 1000 = 20_000. Pre-7.30.2
      // used 100 KB per result and this returned 5_000.
      expect(stats.limits.maxQueryLimit).toBe(20000)

      await brain.close()
    })
  })

  describe('getMemoryStats() API', () => {
    it('should return complete memory statistics', async () => {
      process.env.CLOUD_RUN_MEMORY = '4Gi'

      const brain = new Brainy({ requireSubtype: false,
        storage: { type: 'memory' },
        maxQueryLimit: 20000,
        silent: true
      })

      await brain.init()

      const stats = brain.getMemoryStats()

      // Memory stats
      expect(stats.memory).toHaveProperty('heapUsed')
      expect(stats.memory).toHaveProperty('heapTotal')
      expect(stats.memory).toHaveProperty('external')
      expect(stats.memory).toHaveProperty('rss')
      expect(stats.memory).toHaveProperty('free')
      expect(stats.memory).toHaveProperty('total')
      expect(stats.memory).toHaveProperty('containerLimit')

      expect(stats.memory.containerLimit).toBe(4 * 1024 * 1024 * 1024)

      // Limits
      expect(stats.limits.maxQueryLimit).toBe(20000)
      expect(stats.limits.basis).toBe('override')
      expect(stats.limits.maxQueryLength).toBeGreaterThan(0)
      expect(stats.limits.maxVectorDimensions).toBe(384)

      // Config
      expect(stats.config.maxQueryLimit).toBe(20000)

      // Recommendations
      expect(Array.isArray(stats.recommendations)).toBe(true)

      await brain.close()
    })

    it('should provide recommendations when appropriate', async () => {
      // Large container but using free memory basis
      process.env.CLOUD_RUN_MEMORY = '4Gi'

      // Don't set overrides, let it use containerMemory
      const brain = new Brainy({ requireSubtype: false,
        storage: { type: 'memory' },
        silent: true
      })

      await brain.init()

      const stats = brain.getMemoryStats()

      expect(stats.recommendations).toBeDefined()
      expect(stats.recommendations!.length).toBeGreaterThanOrEqual(0)

      await brain.close()
    })

    it('should handle browser environment gracefully', async () => {
      const brain = new Brainy({ requireSubtype: false,
        storage: { type: 'memory' },
        silent: true
      })

      await brain.init()

      const stats = brain.getMemoryStats()

      // Should not crash in any environment
      expect(stats).toBeDefined()
      expect(stats.limits).toBeDefined()

      await brain.close()
    })
  })

  describe('Production Scenarios', () => {
    it('should handle 4GB Cloud Run container optimally', async () => {
      process.env.CLOUD_RUN_MEMORY = '4Gi'

      const brain = new Brainy({ requireSubtype: false,
        storage: { type: 'memory' },
        silent: true
      })

      await brain.init()

      const stats = brain.getMemoryStats()

      // Allocates 1 GB (25% of 4 GB) for queries; at 25 KB per result
      // (7.30.2 calibration) → 1 GB / 25 KB = ~40_960 → floor to 40 × 1000 =
      // 40_000. Pre-7.30.2 used 100 KB per result and this returned 10_000.
      expect(stats.memory.containerLimit).toBe(4 * 1024 * 1024 * 1024)
      expect(stats.limits.maxQueryLimit).toBe(40000)
      expect(stats.limits.basis).toBe('containerMemory')

      await brain.close()
    })

    it('should allow manual override for power users', async () => {
      process.env.CLOUD_RUN_MEMORY = '4Gi'

      const brain = new Brainy({ requireSubtype: false,
        storage: { type: 'memory' },
        maxQueryLimit: 50000, // Power user wants higher limit
        silent: true
      })

      await brain.init()

      const stats = brain.getMemoryStats()

      expect(stats.limits.maxQueryLimit).toBe(50000)
      expect(stats.limits.basis).toBe('override')

      // Should note override in recommendations
      const overrideNote = stats.recommendations?.find(r => r.includes('override'))
      expect(overrideNote).toBeDefined()

      await brain.close()
    })

    it('should handle bare metal deployment (no container)', async () => {
      // No container env vars

      const brain = new Brainy({ requireSubtype: false,
        storage: { type: 'memory' },
        silent: true
      })

      await brain.init()

      const stats = brain.getMemoryStats()

      expect(stats.memory.containerLimit).toBeNull()
      expect(stats.limits.basis).toBe('freeMemory')
      expect(stats.limits.maxQueryLimit).toBeGreaterThan(0)

      await brain.close()
    })
  })
})
