const { describe, test, expect, beforeEach, afterEach, mock } = require('bun:test')
const { spawn } = require('child_process')
const { randomUUID, randomBytes } = require('crypto')
const path = require('path')

const OPENROUTER_STATIC_FALLBACK_MODELS = [
  'anthropic/claude-3.5-sonnet',
  'openai/gpt-4o-mini',
  'mistralai/mistral-large-latest',
  'google/gemini-flash-1.5',
]

const DEFAULT_PROVIDER_SEEDS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', 'cerebras', 'ollama', 'opencode', 'cohere', 'kilo']

const KIB = 1024
const MIB = KIB * 1024
const GIB = MIB * 1024
const TIB = GIB * 1024

function parseMemoryValue(raw) {
  if (raw === undefined || raw === null) return 0
  const str = String(raw).trim()
  const match = str.match(/^([0-9.]+)\s*([kKmMgGtT]i?)?(?:[bB])?$/)
  if (!match) return Number(str) || 0
  const value = parseFloat(match[1])
  const unit = (match[2] || '').toLowerCase()
  const multipliers = { '': 1, k: KIB, ki: KIB, m: MIB, mi: MIB, g: GIB, gi: GIB, t: TIB, ti: TIB }
  if (!Number.isFinite(value) || value <= 0) return 0
  return Math.round(value * (multipliers[unit] || 1))
}

function resolvePositiveInt(raw, fallback) {
  const parsed = Number(raw)
  if (!Number.isFinite(parsed) || parsed <= 0) return fallback
  return Math.round(parsed)
}

function resolveIntervalMs(raw, fallback, min) {
  const parsed = Number(raw)
  if (!Number.isFinite(parsed) || parsed <= 0) return fallback
  return Math.max(parsed, min)
}

function formatDuration(ms) {
  if (ms < 1000) return Math.round(ms) + 'ms'
  if (ms < 60000) return (ms / 1000).toFixed(1) + 's'
  if (ms < 3600000) return (ms / 60000).toFixed(1) + 'm'
  if (ms < 86400000) return (ms / 3600000).toFixed(1) + 'h'
  return (ms / 86400000).toFixed(1) + 'd'
}

describe('OpenCode Integration', () => {
  describe('Provider Configuration', () => {
    test('has default provider seeds defined', () => {
      expect(DEFAULT_PROVIDER_SEEDS.length).toBeGreaterThan(0)
      expect(DEFAULT_PROVIDER_SEEDS).toContain('openrouter')
      expect(DEFAULT_PROVIDER_SEEDS).toContain('mistral')
      expect(DEFAULT_PROVIDER_SEEDS).toContain('google')
    })

    test('opencode is the default fallback provider', () => {
      const DEFAULT_PROVIDER_FALLBACK = 'opencode'
      expect(DEFAULT_PROVIDER_SEEDS).toContain(DEFAULT_PROVIDER_FALLBACK)
    })

    test('openrouter has fallback models', () => {
      expect(OPENROUTER_STATIC_FALLBACK_MODELS.length).toBeGreaterThan(0)
    })
  })

  describe('Model Discovery', () => {
    const modelDiscoveryConfig = {
      openrouter: {
        url: 'https://openrouter.ai/api/v1/models',
        requiresAuth: true,
      },
      mistral: {
        url: 'https://api.mistral.ai/v1/models',
        requiresAuth: true,
      },
      google: {
        url: 'https://generativelanguage.googleapis.com/v1beta2/models',
        requiresAuth: true,
      },
    }

    test('has discovery configuration for providers', () => {
      expect(modelDiscoveryConfig.openrouter.url).toBeDefined()
      expect(modelDiscoveryConfig.mistral.url).toBeDefined()
      expect(modelDiscoveryConfig.google.url).toBeDefined()
    })

    test('openrouter discovery requires authentication', () => {
      expect(modelDiscoveryConfig.openrouter.requiresAuth).toBe(true)
    })

    test('validates model ID format', () => {
      const validModelId = 'anthropic/claude-3.5-sonnet'
      const invalidModelId = 'invalid-model-format'
      
      const modelIdPattern = /^[a-z0-9-]+\/[a-z0-9.-]+$/i
      expect(modelIdPattern.test(validModelId)).toBe(true)
      expect(modelIdPattern.test(invalidModelId)).toBe(false)
    })

    test('extracts provider from model ID', () => {
      function extractProvider(modelId) {
        const parts = modelId?.split('/')
        return parts?.length === 2 ? parts[0] : null
      }

      expect(extractProvider('anthropic/claude-3.5-sonnet')).toBe('anthropic')
      expect(extractProvider('openai/gpt-4o')).toBe('openai')
      expect(extractProvider('invalid')).toBe(null)
    })
  })

  describe('Message Building', () => {
    function buildOpenRouterMessages(history, currentMessage) {
      const prior = history
        .filter((m) => m && m.id !== currentMessage?.id && m.status === 'done')
        .slice(-8)
      
      const mapped = prior.map((m) => {
        if (m.role === 'assistant') {
          return { role: 'assistant', content: String(m.reply || m.partialOutput || '') }
        }
        return { role: 'user', content: String(m.content || '') }
      }).filter((m) => (m.content || '').trim().length)

      const text = String(currentMessage?.content || '').trim()
      const userMsg = { role: 'user', content: text }
      
      return mapped.concat([userMsg])
    }

    test('builds message history for OpenRouter', () => {
      const history = [
        { id: '1', role: 'user', content: 'Hello', status: 'done' },
        { id: '2', role: 'assistant', reply: 'Hi there!', status: 'done' },
        { id: '3', role: 'user', content: 'How are you?', status: 'done' },
      ]
      const currentMessage = { id: '4', content: 'Good thanks' }

      const messages = buildOpenRouterMessages(history, currentMessage)

      expect(messages.length).toBe(4)
      expect(messages[0].role).toBe('user')
      expect(messages[0].content).toBe('Hello')
      expect(messages[1].role).toBe('assistant')
      expect(messages[3].content).toBe('Good thanks')
    })

    test('filters out non-done messages', () => {
      const history = [
        { id: '1', role: 'user', content: 'Hello', status: 'done' },
        { id: '2', role: 'assistant', reply: 'Hi', status: 'running' },
        { id: '3', role: 'user', content: 'Test', status: 'queued' },
      ]
      const currentMessage = { id: '4', content: 'Current' }

      const messages = buildOpenRouterMessages(history, currentMessage)

      expect(messages.length).toBe(2)
      expect(messages[0].content).toBe('Hello')
    })

    test('limits history to last 8 messages', () => {
      const history = Array.from({ length: 15 }, (_, i) => ({
        id: String(i),
        role: 'user',
        content: `Message ${i}`,
        status: 'done',
      }))
      const currentMessage = { id: 'current', content: 'Current' }

      const messages = buildOpenRouterMessages(history, currentMessage)

      expect(messages.length).toBe(9)
    })

    test('filters empty content', () => {
      const history = [
        { id: '1', role: 'user', content: '', status: 'done' },
        { id: '2', role: 'assistant', reply: '   ', status: 'done' },
        { id: '3', role: 'user', content: 'Valid', status: 'done' },
      ]
      const currentMessage = { id: '4', content: 'Current' }

      const messages = buildOpenRouterMessages(history, currentMessage)

      expect(messages.length).toBe(2)
    })
  })

  describe('Process Management', () => {
    const processConfig = {
      maxConcurrency: 5,
      timeout: 300000,
      retryAttempts: 3,
      retryDelay: 1000,
    }

    test('enforces maximum concurrency', () => {
      const runningProcesses = new Map()
      const maxConcurrency = processConfig.maxConcurrency

      function canStartNewProcess() {
        return runningProcesses.size < maxConcurrency
      }

      for (let i = 0; i < maxConcurrency; i++) {
        runningProcesses.set(`process-${i}`, { started: Date.now() })
      }

      expect(canStartNewProcess()).toBe(false)
      
      runningProcesses.delete('process-0')
      expect(canStartNewProcess()).toBe(true)
    })

    test('tracks process start time', () => {
      const processInfo = {
        id: randomUUID(),
        started: Date.now(),
        sessionId: 'session-1',
        messageId: 'message-1',
      }

      expect(processInfo.started).toBeDefined()
      expect(processInfo.started).toBeLessThanOrEqual(Date.now())
    })

    test('calculates process duration', () => {
      const startTime = Date.now() - 5000
      const duration = Date.now() - startTime

      expect(duration).toBeGreaterThanOrEqual(5000)
    })

    test('generates unique process IDs', () => {
      const ids = new Set()
      for (let i = 0; i < 100; i++) {
        ids.add(randomUUID())
      }
      expect(ids.size).toBe(100)
    })
  })

  describe('Error Handling', () => {
    const ERROR_DETAIL_LIMIT = 400

    function truncateError(message, limit = ERROR_DETAIL_LIMIT) {
      if (!message || typeof message !== 'string') return ''
      if (message.length <= limit) return message
      return message.slice(0, limit) + '...'
    }

    test('truncates long error messages', () => {
      const longError = 'x'.repeat(500)
      const truncated = truncateError(longError)

      expect(truncated.length).toBe(ERROR_DETAIL_LIMIT + 3)
      expect(truncated.endsWith('...')).toBe(true)
    })

    test('keeps short error messages intact', () => {
      const shortError = 'Error: something went wrong'
      const result = truncateError(shortError)

      expect(result).toBe(shortError)
    })

    test('handles null/undefined errors', () => {
      expect(truncateError(null)).toBe('')
      expect(truncateError(undefined)).toBe('')
    })

    test('classifies error types', () => {
      function classifyError(error) {
        const msg = String(error || '').toLowerCase()
        if (msg.includes('timeout')) return 'timeout'
        if (msg.includes('rate limit')) return 'rate_limit'
        if (msg.includes('auth')) return 'auth'
        if (msg.includes('invalid')) return 'validation'
        return 'unknown'
      }

      expect(classifyError('Request timeout')).toBe('timeout')
      expect(classifyError('Rate limit exceeded')).toBe('rate_limit')
      expect(classifyError('Authentication failed')).toBe('auth')
      expect(classifyError('Invalid input')).toBe('validation')
      expect(classifyError('Something else')).toBe('unknown')
    })
  })

  describe('Stream Processing', () => {
    function parseSSELine(line) {
      if (!line || typeof line !== 'string') return null
      if (line.startsWith('data: ')) {
        const data = line.slice(6).trim()
        if (data === '[DONE]') return { done: true }
        try {
          return { data: JSON.parse(data) }
        } catch {
          return { data }
        }
      }
      return null
    }

    test('parses SSE data lines', () => {
      const line = 'data: {"content": "Hello"}'
      const parsed = parseSSELine(line)

      expect(parsed.data.content).toBe('Hello')
    })

    test('handles SSE done signal', () => {
      const line = 'data: [DONE]'
      const parsed = parseSSELine(line)

      expect(parsed.done).toBe(true)
    })

    test('handles malformed JSON in SSE', () => {
      const line = 'data: not valid json'
      const parsed = parseSSELine(line)

      expect(parsed.data).toBe('not valid json')
    })

    test('ignores non-data lines', () => {
      expect(parseSSELine(': comment')).toBe(null)
      expect(parseSSELine('')).toBe(null)
      expect(parseSSELine('event: message')).toBe(null)
    })

    test('accumulates stream chunks', () => {
      const chunks = [
        { choices: [{ delta: { content: 'Hello' } }] },
        { choices: [{ delta: { content: ' world' } }] },
        { choices: [{ delta: { content: '!' } }] },
      ]

      const content = chunks
        .map(c => c.choices?.[0]?.delta?.content || '')
        .join('')

      expect(content).toBe('Hello world!')
    })
  })

  describe('Resource Limits', () => {
    test('parses memory values correctly', () => {
      expect(parseMemoryValue('512M')).toBe(512 * MIB)
      expect(parseMemoryValue('1G')).toBe(GIB)
      expect(parseMemoryValue('2GB')).toBe(2 * GIB)
      expect(parseMemoryValue('1024')).toBe(1024)
    })

    test('handles null/undefined memory values', () => {
      expect(parseMemoryValue(null)).toBe(0)
      expect(parseMemoryValue(undefined)).toBe(0)
    })

    test('resolves positive integers with fallback', () => {
      expect(resolvePositiveInt('100', 50)).toBe(100)
      expect(resolvePositiveInt('invalid', 50)).toBe(50)
      expect(resolvePositiveInt(-5, 50)).toBe(50)
    })

    test('enforces minimum interval', () => {
      expect(resolveIntervalMs('100', 1000, 500)).toBe(500)
      expect(resolveIntervalMs('2000', 1000, 500)).toBe(2000)
      expect(resolveIntervalMs('invalid', 1000, 500)).toBe(1000)
    })

    test('formats durations correctly', () => {
      expect(formatDuration(500)).toBe('500ms')
      expect(formatDuration(5000)).toBe('5.0s')
      expect(formatDuration(90000)).toBe('1.5m')
      expect(formatDuration(7200000)).toBe('2.0h')
      expect(formatDuration(86400000)).toBe('1.0d')
    })
  })

  describe('Provider Fallback', () => {
    function selectNextProvider(currentProvider, failedProviders = new Set()) {
      const available = DEFAULT_PROVIDER_SEEDS.filter(p => !failedProviders.has(p))
      const currentIndex = DEFAULT_PROVIDER_SEEDS.indexOf(currentProvider)
      
      for (let i = 1; i < DEFAULT_PROVIDER_SEEDS.length; i++) {
        const nextIndex = (currentIndex + i) % DEFAULT_PROVIDER_SEEDS.length
        const nextProvider = DEFAULT_PROVIDER_SEEDS[nextIndex]
        if (!failedProviders.has(nextProvider)) {
          return nextProvider
        }
      }
      return null
    }

    test('selects next available provider on failure', () => {
      const next = selectNextProvider('openrouter', new Set(['openrouter']))
      expect(next).toBeDefined()
      expect(next).not.toBe('openrouter')
    })

    test('returns null when all providers failed', () => {
      const allFailed = new Set(DEFAULT_PROVIDER_SEEDS)
      const next = selectNextProvider('openrouter', allFailed)
      expect(next).toBe(null)
    })

    test('selects next model from fallback list', () => {
      const usedModels = new Set(['anthropic/claude-3.5-sonnet'])
      const nextModel = OPENROUTER_STATIC_FALLBACK_MODELS.find(m => !usedModels.has(m))

      expect(nextModel).toBeDefined()
      expect(nextModel).not.toBe('anthropic/claude-3.5-sonnet')
    })

    test('returns undefined when all fallback models used', () => {
      const usedModels = new Set(OPENROUTER_STATIC_FALLBACK_MODELS)
      const nextModel = OPENROUTER_STATIC_FALLBACK_MODELS.find(m => !usedModels.has(m))

      expect(nextModel).toBeUndefined()
    })
  })

  describe('External Directory Restriction', () => {
    const ENABLE_EXTERNAL_DIR_RESTRICTION = true

    function validateExternalDirectory(sessionId, requestedPath) {
      if (!ENABLE_EXTERNAL_DIR_RESTRICTION) return { allowed: true }
      
      const normalizedPath = path.normalize(requestedPath || '')
      const blockedPatterns = [
        /^(?:[a-zA-Z]:|\\\\|\/\/)/,
        /\.\./,
        /^\/etc\//,
        /^\/var\//,
        /^\/home\//,
      ]

      for (const pattern of blockedPatterns) {
        if (pattern.test(normalizedPath)) {
          return { allowed: false, reason: 'Path not allowed' }
        }
      }

      return { allowed: true }
    }

    test('blocks absolute paths on Windows', () => {
      const result = validateExternalDirectory('session-1', 'C:\\Windows\\System32')
      expect(result.allowed).toBe(false)
    })

    test('blocks UNC paths', () => {
      const result = validateExternalDirectory('session-1', '\\\\server\\share')
      expect(result.allowed).toBe(false)
    })

    test('blocks parent directory traversal', () => {
      const result = validateExternalDirectory('session-1', '../../../etc/passwd')
      expect(result.allowed).toBe(false)
    })

    test('allows relative paths within workspace', () => {
      const result = validateExternalDirectory('session-1', './src/file.js')
      expect(result.allowed).toBe(true)
    })
  })
})

describe('OpenCode CLI Integration', () => {
  describe('CLI Path Resolution', () => {
    test('resolves opencode CLI path', () => {
      const REPO_ROOT = process.cwd()
      const OPENCODE_REPO_ROOT = path.join(REPO_ROOT, 'opencode')
      const OPENCODE_REPO_CLI = path.join(OPENCODE_REPO_ROOT, 'packages', 'opencode', 'bin', 'opencode')

      expect(OPENCODE_REPO_CLI).toContain('opencode')
      expect(OPENCODE_REPO_CLI).toContain('bin')
    })

    test('validates CLI existence pattern', () => {
      const cliPath = 'packages/opencode/bin/opencode'
      expect(cliPath.endsWith('opencode')).toBe(true)
    })
  })

  describe('Environment Configuration', () => {
    test('has required environment variable patterns', () => {
      const envPatterns = {
        OPENROUTER_API_KEY: /^sk-or-v1-/,
        MISTRAL_API_KEY: /^[a-zA-Z0-9]+$/,
        GOOGLE_API_KEY: /^AIza/,
      }

      expect(Object.keys(envPatterns).length).toBeGreaterThan(0)
    })

    test('generates default values for missing env vars', () => {
      const defaults = {
        PORT: 4000,
        HOST: '0.0.0.0',
        DEFAULT_PLAN: 'hobby',
        DEFAULT_PROVIDER_FALLBACK: 'opencode',
      }

      expect(defaults.PORT).toBe(4000)
      expect(defaults.DEFAULT_PLAN).toBe('hobby')
    })
  })
})
