import { execFileSync, spawn } from 'child_process'
import { EventEmitter } from 'events'
import fs from 'fs'
import path from 'path'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import {
  autoDiscoverGitBash,
  findCommandInShellEnv,
  findExecutable,
  findGitBash,
  findViaMise,
  getBinaryName,
  getBinaryPath,
  getBinarySearchDirs,
  validateGitBashPath
} from '../process'

// Mock dependencies
vi.mock('child_process')
vi.mock('fs')
vi.mock('path')

// These tests only run on Windows since the functions have platform guards
describe.skipIf(process.platform !== 'win32')('process utilities', () => {
  beforeEach(() => {
    vi.clearAllMocks()

    // Mock path.join to concatenate paths with backslashes (Windows-style)
    vi.mocked(path.join).mockImplementation((...args) => args.join('\\'))

    // Mock path.resolve to handle path resolution with .. support
    vi.mocked(path.resolve).mockImplementation((...args) => {
      let result = args.join('\\')

      // Handle .. navigation
      while (result.includes('\\..')) {
        result = result.replace(/\\[^\\]+\\\.\./g, '')
      }

      // Ensure absolute path
      if (!result.match(/^[A-Z]:/)) {
        result = `C:\\cwd\\${result}`
      }

      return result
    })

    // Mock path.dirname
    vi.mocked(path.dirname).mockImplementation((p) => {
      const parts = p.split('\\')
      parts.pop()
      return parts.join('\\')
    })

    // Mock path.sep
    Object.defineProperty(path, 'sep', { value: '\\', writable: true })

    // Mock process.cwd()
    vi.spyOn(process, 'cwd').mockReturnValue('C:\\cwd')
  })

  describe('findExecutable', () => {
    describe('git common paths', () => {
      it('should find git at Program Files path', () => {
        const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
        process.env.ProgramFiles = 'C:\\Program Files'

        vi.mocked(fs.existsSync).mockImplementation((p) => p === gitPath)

        const result = findExecutable('git')

        expect(result).toBe(gitPath)
        expect(fs.existsSync).toHaveBeenCalledWith(gitPath)
      })

      it('should find git at Program Files (x86) path', () => {
        const gitPath = 'C:\\Program Files (x86)\\Git\\cmd\\git.exe'
        process.env['ProgramFiles(x86)'] = 'C:\\Program Files (x86)'

        vi.mocked(fs.existsSync).mockImplementation((p) => p === gitPath)

        const result = findExecutable('git')

        expect(result).toBe(gitPath)
        expect(fs.existsSync).toHaveBeenCalledWith(gitPath)
      })

      it('should use fallback paths when environment variables are not set', () => {
        delete process.env.ProgramFiles
        delete process.env['ProgramFiles(x86)']

        const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
        vi.mocked(fs.existsSync).mockImplementation((p) => p === gitPath)

        const result = findExecutable('git')

        expect(result).toBe(gitPath)
      })
    })

    describe('where.exe PATH lookup', () => {
      beforeEach(() => {
        Object.defineProperty(process, 'platform', { value: 'win32', writable: true })
        // Common paths don't exist
        vi.mocked(fs.existsSync).mockReturnValue(false)
      })

      it('should find executable via where.exe', () => {
        const gitPath = 'C:\\Git\\bin\\git.exe'

        vi.mocked(execFileSync).mockReturnValue(gitPath)

        const result = findExecutable('git')

        expect(result).toBe(gitPath)
        // Now searches without extension and filters by allowed extensions
        // Check that execFileSync was called with 'where.exe' and ['git']
        expect(execFileSync).toHaveBeenCalledWith('where.exe', ['git'], expect.any(Object))
      })

      it('should search without extension and filter results', () => {
        vi.mocked(execFileSync).mockImplementation(() => {
          throw new Error('Not found')
        })

        findExecutable('node')

        // Now searches without extension (filters by allowed extensions afterward)
        expect(execFileSync).toHaveBeenCalledWith('where.exe', ['node'], expect.any(Object))
      })

      it('should handle Windows line endings (CRLF)', () => {
        const gitPath1 = 'C:\\Git\\bin\\git.exe'
        const gitPath2 = 'C:\\Program Files\\Git\\cmd\\git.exe'

        vi.mocked(execFileSync).mockReturnValue(`${gitPath1}\r\n${gitPath2}\r\n`)

        const result = findExecutable('git')

        // Should return the first valid path
        expect(result).toBe(gitPath1)
      })

      it('should handle Unix line endings (LF)', () => {
        const gitPath1 = 'C:\\Git\\bin\\git.exe'
        const gitPath2 = 'C:\\Program Files\\Git\\cmd\\git.exe'

        vi.mocked(execFileSync).mockReturnValue(`${gitPath1}\n${gitPath2}\n`)

        const result = findExecutable('git')

        expect(result).toBe(gitPath1)
      })

      it('should handle mixed line endings', () => {
        const gitPath1 = 'C:\\Git\\bin\\git.exe'
        const gitPath2 = 'C:\\Program Files\\Git\\cmd\\git.exe'

        vi.mocked(execFileSync).mockReturnValue(`${gitPath1}\r\n${gitPath2}\n`)

        const result = findExecutable('git')

        expect(result).toBe(gitPath1)
      })

      it('should trim whitespace from paths', () => {
        const gitPath = 'C:\\Git\\bin\\git.exe'

        vi.mocked(execFileSync).mockReturnValue(`  ${gitPath}  \n`)

        const result = findExecutable('git')

        expect(result).toBe(gitPath)
      })

      it('should filter empty lines', () => {
        const gitPath = 'C:\\Git\\bin\\git.exe'

        vi.mocked(execFileSync).mockReturnValue(`\n\n${gitPath}\n\n`)

        const result = findExecutable('git')

        expect(result).toBe(gitPath)
      })
    })

    describe('security checks', () => {
      beforeEach(() => {
        Object.defineProperty(process, 'platform', { value: 'win32', writable: true })
        vi.mocked(fs.existsSync).mockReturnValue(false)
      })

      it('should skip executables in current directory', () => {
        const maliciousPath = 'C:\\cwd\\git.exe'
        const safePath = 'C:\\Git\\bin\\git.exe'

        vi.mocked(execFileSync).mockReturnValue(`${maliciousPath}\n${safePath}`)

        vi.mocked(path.resolve).mockImplementation((p) => {
          if (p.includes('cwd\\git.exe')) return 'c:\\cwd\\git.exe'
          return 'c:\\git\\bin\\git.exe'
        })

        vi.mocked(path.dirname).mockImplementation((p) => {
          if (p.includes('cwd\\git.exe')) return 'c:\\cwd'
          return 'c:\\git\\bin'
        })

        const result = findExecutable('git')

        // Should skip malicious path and return safe path
        expect(result).toBe(safePath)
      })

      it('should skip executables in current directory subdirectories', () => {
        const maliciousPath = 'C:\\cwd\\subdir\\git.exe'
        const safePath = 'C:\\Git\\bin\\git.exe'

        vi.mocked(execFileSync).mockReturnValue(`${maliciousPath}\n${safePath}`)

        vi.mocked(path.resolve).mockImplementation((p) => {
          if (p.includes('cwd\\subdir')) return 'c:\\cwd\\subdir\\git.exe'
          return 'c:\\git\\bin\\git.exe'
        })

        vi.mocked(path.dirname).mockImplementation((p) => {
          if (p.includes('cwd\\subdir')) return 'c:\\cwd\\subdir'
          return 'c:\\git\\bin'
        })

        const result = findExecutable('git')

        expect(result).toBe(safePath)
      })

      it('should return null when only malicious executables are found', () => {
        const maliciousPath = 'C:\\cwd\\git.exe'

        vi.mocked(execFileSync).mockReturnValue(maliciousPath)

        vi.mocked(path.resolve).mockReturnValue('c:\\cwd\\git.exe')
        vi.mocked(path.dirname).mockReturnValue('c:\\cwd')

        const result = findExecutable('git')

        expect(result).toBeNull()
      })
    })

    describe('error handling', () => {
      beforeEach(() => {
        Object.defineProperty(process, 'platform', { value: 'win32', writable: true })
        vi.mocked(fs.existsSync).mockReturnValue(false)
      })

      it('should return null when where.exe fails', () => {
        vi.mocked(execFileSync).mockImplementation(() => {
          throw new Error('Command failed')
        })

        const result = findExecutable('nonexistent')

        expect(result).toBeNull()
      })

      it('should return null when where.exe returns empty output', () => {
        vi.mocked(execFileSync).mockReturnValue('')

        const result = findExecutable('git')

        expect(result).toBeNull()
      })

      it('should return null when where.exe returns only whitespace', () => {
        vi.mocked(execFileSync).mockReturnValue('   \n\n  ')

        const result = findExecutable('git')

        expect(result).toBeNull()
      })
    })

    describe('non-git executables', () => {
      beforeEach(() => {
        Object.defineProperty(process, 'platform', { value: 'win32', writable: true })
      })

      it('should skip common paths check for non-git executables', () => {
        const nodePath = 'C:\\Program Files\\nodejs\\node.exe'

        vi.mocked(execFileSync).mockReturnValue(nodePath)

        const result = findExecutable('node')

        expect(result).toBe(nodePath)
        // Should not check common Git paths
        expect(fs.existsSync).not.toHaveBeenCalledWith(expect.stringContaining('Git\\cmd\\node.exe'))
      })
    })

    describe('options parameter', () => {
      beforeEach(() => {
        Object.defineProperty(process, 'platform', { value: 'win32', writable: true })
        vi.mocked(fs.existsSync).mockReturnValue(false)
      })

      it('should filter results by custom extensions', () => {
        // where.exe returns multiple files with different extensions
        vi.mocked(execFileSync).mockReturnValue('C:\\nodejs\\npm\nC:\\nodejs\\npm.cmd\nC:\\nodejs\\npm.ps1\n')

        const result = findExecutable('npm', { extensions: ['.cmd'] })

        expect(result).toBe('C:\\nodejs\\npm.cmd')
      })

      it('should accept multiple extensions', () => {
        vi.mocked(execFileSync).mockReturnValue('C:\\nodejs\\npm\nC:\\nodejs\\npm.cmd\nC:\\nodejs\\npm.exe\n')

        const result = findExecutable('npm', { extensions: ['.cmd', '.exe'] })

        // Should return first matching extension
        expect(result).toBe('C:\\nodejs\\npm.cmd')
      })

      it('should return null when no results match allowed extensions', () => {
        vi.mocked(execFileSync).mockReturnValue('C:\\nodejs\\npm\nC:\\nodejs\\npm.ps1\n')

        const result = findExecutable('npm', { extensions: ['.cmd', '.exe'] })

        expect(result).toBeNull()
      })

      it('should match both .exe and .cmd by default', () => {
        vi.mocked(execFileSync).mockReturnValue('C:\\nodejs\\node.cmd\nC:\\nodejs\\node.exe\n')

        const result = findExecutable('node')

        // Default extensions include both .exe and .cmd, returns first match
        expect(result).toBe('C:\\nodejs\\node.cmd')
      })

      it('should handle case-insensitive extension matching', () => {
        vi.mocked(execFileSync).mockReturnValue('C:\\nodejs\\npm.CMD\n')

        const result = findExecutable('npm', { extensions: ['.cmd'] })

        expect(result).toBe('C:\\nodejs\\npm.CMD')
      })
    })
  })

  describe('validateGitBashPath', () => {
    it('returns null when path is null', () => {
      const result = validateGitBashPath(null)

      expect(result).toBeNull()
    })

    it('returns null when path is undefined', () => {
      const result = validateGitBashPath(undefined)

      expect(result).toBeNull()
    })

    it('returns normalized path when valid bash.exe exists', () => {
      const customPath = 'C:\\PortableGit\\bin\\bash.exe'
      vi.mocked(fs.existsSync).mockImplementation((p) => p === 'C:\\PortableGit\\bin\\bash.exe')

      const result = validateGitBashPath(customPath)

      expect(result).toBe('C:\\PortableGit\\bin\\bash.exe')
    })

    it('returns null when file does not exist', () => {
      vi.mocked(fs.existsSync).mockReturnValue(false)

      const result = validateGitBashPath('C:\\missing\\bash.exe')

      expect(result).toBeNull()
    })

    it('returns null when path is not bash.exe', () => {
      const customPath = 'C:\\PortableGit\\bin\\git.exe'
      vi.mocked(fs.existsSync).mockReturnValue(true)

      const result = validateGitBashPath(customPath)

      expect(result).toBeNull()
    })
  })

  describe('findGitBash', () => {
    describe('customPath parameter', () => {
      beforeEach(() => {
        delete process.env.CLAUDE_CODE_GIT_BASH_PATH
      })

      it('uses customPath when valid', () => {
        const customPath = 'C:\\CustomGit\\bin\\bash.exe'
        vi.mocked(fs.existsSync).mockImplementation((p) => p === customPath)

        const result = findGitBash(customPath)

        expect(result).toBe(customPath)
        expect(execFileSync).not.toHaveBeenCalled()
      })

      it('falls back when customPath is invalid', () => {
        const customPath = 'C:\\Invalid\\bash.exe'
        const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
        const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'

        vi.mocked(fs.existsSync).mockImplementation((p) => {
          if (p === customPath) return false
          if (p === gitPath) return true
          if (p === bashPath) return true
          return false
        })

        vi.mocked(execFileSync).mockReturnValue(gitPath)

        const result = findGitBash(customPath)

        expect(result).toBe(bashPath)
      })

      it('prioritizes customPath over env override', () => {
        const customPath = 'C:\\CustomGit\\bin\\bash.exe'
        const envPath = 'C:\\EnvGit\\bin\\bash.exe'
        process.env.CLAUDE_CODE_GIT_BASH_PATH = envPath

        vi.mocked(fs.existsSync).mockImplementation((p) => p === customPath || p === envPath)

        const result = findGitBash(customPath)

        expect(result).toBe(customPath)
      })
    })

    describe('env override', () => {
      beforeEach(() => {
        delete process.env.CLAUDE_CODE_GIT_BASH_PATH
      })

      it('uses CLAUDE_CODE_GIT_BASH_PATH when valid', () => {
        const envPath = 'C:\\OverrideGit\\bin\\bash.exe'
        process.env.CLAUDE_CODE_GIT_BASH_PATH = envPath

        vi.mocked(fs.existsSync).mockImplementation((p) => p === envPath)

        const result = findGitBash()

        expect(result).toBe(envPath)
        expect(execFileSync).not.toHaveBeenCalled()
      })

      it('falls back when CLAUDE_CODE_GIT_BASH_PATH is invalid', () => {
        const envPath = 'C:\\Invalid\\bash.exe'
        const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
        const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'

        process.env.CLAUDE_CODE_GIT_BASH_PATH = envPath

        vi.mocked(fs.existsSync).mockImplementation((p) => {
          if (p === envPath) return false
          if (p === gitPath) return true
          if (p === bashPath) return true
          return false
        })

        vi.mocked(execFileSync).mockReturnValue(gitPath)

        const result = findGitBash()

        expect(result).toBe(bashPath)
      })
    })

    describe('git.exe path derivation', () => {
      it('should derive bash.exe from standard Git installation (Git/cmd/git.exe)', () => {
        const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
        const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'

        // findExecutable will find git at common path
        process.env.ProgramFiles = 'C:\\Program Files'
        vi.mocked(fs.existsSync).mockImplementation((p) => {
          return p === gitPath || p === bashPath
        })

        const result = findGitBash()

        expect(result).toBe(bashPath)
      })

      it('should derive bash.exe from portable Git installation (Git/bin/git.exe)', () => {
        const gitPath = 'C:\\PortableGit\\bin\\git.exe'
        const bashPath = 'C:\\PortableGit\\bin\\bash.exe'

        // Mock: common git paths don't exist, but where.exe finds portable git
        vi.mocked(fs.existsSync).mockImplementation((p) => {
          const pathStr = p?.toString() || ''
          // Common git paths don't exist
          if (pathStr.includes('Program Files\\Git\\cmd\\git.exe')) return false
          if (pathStr.includes('Program Files (x86)\\Git\\cmd\\git.exe')) return false
          // Portable bash.exe exists at Git/bin/bash.exe (second path in possibleBashPaths)
          if (pathStr === bashPath) return true
          return false
        })

        // where.exe returns portable git path
        vi.mocked(execFileSync).mockReturnValue(gitPath)

        const result = findGitBash()

        expect(result).toBe(bashPath)
      })

      it('should derive bash.exe from MSYS2 Git installation (Git/usr/bin/bash.exe)', () => {
        const gitPath = 'C:\\msys64\\usr\\bin\\git.exe'
        const bashPath = 'C:\\msys64\\usr\\bin\\bash.exe'

        vi.mocked(fs.existsSync).mockImplementation((p) => {
          const pathStr = p?.toString() || ''
          // Common git paths don't exist
          if (pathStr.includes('Program Files\\Git\\cmd\\git.exe')) return false
          if (pathStr.includes('Program Files (x86)\\Git\\cmd\\git.exe')) return false
          // MSYS2 bash.exe exists at usr/bin/bash.exe (third path in possibleBashPaths)
          if (pathStr === bashPath) return true
          return false
        })

        vi.mocked(execFileSync).mockReturnValue(gitPath)

        const result = findGitBash()

        expect(result).toBe(bashPath)
      })

      it('should try multiple bash.exe locations in order', () => {
        const gitPath = 'C:\\Git\\cmd\\git.exe'
        const bashPath = 'C:\\Git\\bin\\bash.exe'

        vi.mocked(fs.existsSync).mockImplementation((p) => {
          const pathStr = p?.toString() || ''
          // Common git paths don't exist
          if (pathStr.includes('Program Files\\Git\\cmd\\git.exe')) return false
          if (pathStr.includes('Program Files (x86)\\Git\\cmd\\git.exe')) return false
          // Standard path exists (first in possibleBashPaths)
          if (pathStr === bashPath) return true
          return false
        })

        vi.mocked(execFileSync).mockReturnValue(gitPath)

        const result = findGitBash()

        expect(result).toBe(bashPath)
      })

      it('should handle when git.exe is found but bash.exe is not at any derived location', () => {
        const gitPath = 'C:\\Git\\cmd\\git.exe'

        // git.exe exists via where.exe, but bash.exe doesn't exist at any derived location
        vi.mocked(fs.existsSync).mockImplementation(() => {
          // Only return false for all bash.exe checks
          return false
        })

        vi.mocked(execFileSync).mockReturnValue(gitPath)

        const result = findGitBash()

        // Should fall back to common paths check
        expect(result).toBeNull()
      })
    })

    describe('common paths fallback', () => {
      beforeEach(() => {
        // git.exe not found
        vi.mocked(execFileSync).mockImplementation(() => {
          throw new Error('Not found')
        })
      })

      it('should check Program Files path', () => {
        const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
        process.env.ProgramFiles = 'C:\\Program Files'

        vi.mocked(fs.existsSync).mockImplementation((p) => p === bashPath)

        const result = findGitBash()

        expect(result).toBe(bashPath)
      })

      it('should check Program Files (x86) path', () => {
        const bashPath = 'C:\\Program Files (x86)\\Git\\bin\\bash.exe'
        process.env['ProgramFiles(x86)'] = 'C:\\Program Files (x86)'

        vi.mocked(fs.existsSync).mockImplementation((p) => p === bashPath)

        const result = findGitBash()

        expect(result).toBe(bashPath)
      })

      it('should check LOCALAPPDATA path', () => {
        const bashPath = 'C:\\Users\\User\\AppData\\Local\\Programs\\Git\\bin\\bash.exe'
        process.env.LOCALAPPDATA = 'C:\\Users\\User\\AppData\\Local'

        vi.mocked(fs.existsSync).mockImplementation((p) => p === bashPath)

        const result = findGitBash()

        expect(result).toBe(bashPath)
      })

      it('should skip LOCALAPPDATA check when environment variable is not set', () => {
        delete process.env.LOCALAPPDATA

        vi.mocked(fs.existsSync).mockReturnValue(false)

        const result = findGitBash()

        expect(result).toBeNull()
        // Should not check invalid path with empty LOCALAPPDATA
        expect(fs.existsSync).not.toHaveBeenCalledWith(expect.stringContaining('undefined'))
      })

      it('should use fallback values when environment variables are not set', () => {
        delete process.env.ProgramFiles
        delete process.env['ProgramFiles(x86)']

        const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
        vi.mocked(fs.existsSync).mockImplementation((p) => p === bashPath)

        const result = findGitBash()

        expect(result).toBe(bashPath)
      })
    })

    describe('priority order', () => {
      it('should prioritize git.exe derivation over common paths', () => {
        const gitPath = 'C:\\CustomPath\\Git\\cmd\\git.exe'
        const derivedBashPath = 'C:\\CustomPath\\Git\\bin\\bash.exe'
        const commonBashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'

        // Both exist
        vi.mocked(fs.existsSync).mockImplementation((p) => {
          const pathStr = p?.toString() || ''
          // Common git paths don't exist (so findExecutable uses where.exe)
          if (pathStr.includes('Program Files\\Git\\cmd\\git.exe')) return false
          if (pathStr.includes('Program Files (x86)\\Git\\cmd\\git.exe')) return false
          // Both bash paths exist, but derived should be checked first
          if (pathStr === derivedBashPath) return true
          if (pathStr === commonBashPath) return true
          return false
        })

        vi.mocked(execFileSync).mockReturnValue(gitPath)

        const result = findGitBash()

        // Should return derived path, not common path
        expect(result).toBe(derivedBashPath)
      })
    })

    describe('error scenarios', () => {
      it('should return null when Git is not installed anywhere', () => {
        vi.mocked(fs.existsSync).mockReturnValue(false)
        vi.mocked(execFileSync).mockImplementation(() => {
          throw new Error('Not found')
        })

        const result = findGitBash()

        expect(result).toBeNull()
      })

      it('should return null when git.exe exists but bash.exe does not', () => {
        const gitPath = 'C:\\Git\\cmd\\git.exe'

        vi.mocked(fs.existsSync).mockImplementation((p) => {
          // git.exe exists, but no bash.exe anywhere
          return p === gitPath
        })

        vi.mocked(execFileSync).mockReturnValue(gitPath)

        const result = findGitBash()

        expect(result).toBeNull()
      })
    })

    describe('real-world scenarios', () => {
      it('should handle official Git for Windows installer', () => {
        const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
        const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'

        process.env.ProgramFiles = 'C:\\Program Files'
        vi.mocked(fs.existsSync).mockImplementation((p) => {
          return p === gitPath || p === bashPath
        })

        const result = findGitBash()

        expect(result).toBe(bashPath)
      })

      it('should handle portable Git installation in custom directory', () => {
        const gitPath = 'D:\\DevTools\\PortableGit\\bin\\git.exe'
        const bashPath = 'D:\\DevTools\\PortableGit\\bin\\bash.exe'

        vi.mocked(fs.existsSync).mockImplementation((p) => {
          const pathStr = p?.toString() || ''
          // Common paths don't exist
          if (pathStr.includes('Program Files\\Git\\cmd\\git.exe')) return false
          if (pathStr.includes('Program Files (x86)\\Git\\cmd\\git.exe')) return false
          // Portable Git paths exist (portable uses second path: Git/bin/bash.exe)
          if (pathStr === bashPath) return true
          return false
        })

        vi.mocked(execFileSync).mockReturnValue(gitPath)

        const result = findGitBash()

        expect(result).toBe(bashPath)
      })

      it('should handle Git installed via Scoop', () => {
        // Scoop typically installs to %USERPROFILE%\scoop\apps\git\current
        const gitPath = 'C:\\Users\\User\\scoop\\apps\\git\\current\\cmd\\git.exe'
        const bashPath = 'C:\\Users\\User\\scoop\\apps\\git\\current\\bin\\bash.exe'

        vi.mocked(fs.existsSync).mockImplementation((p) => {
          const pathStr = p?.toString() || ''
          // Common paths don't exist
          if (pathStr.includes('Program Files\\Git\\cmd\\git.exe')) return false
          if (pathStr.includes('Program Files (x86)\\Git\\cmd\\git.exe')) return false
          // Scoop bash path exists (standard structure: cmd -> bin)
          if (pathStr === bashPath) return true
          return false
        })

        vi.mocked(execFileSync).mockReturnValue(gitPath)

        const result = findGitBash()

        expect(result).toBe(bashPath)
      })
    })
  })

  describe('autoDiscoverGitBash', () => {
    const originalEnvVar = process.env.CLAUDE_CODE_GIT_BASH_PATH

    beforeEach(() => {
      delete process.env.CLAUDE_CODE_GIT_BASH_PATH
    })

    afterEach(() => {
      if (originalEnvVar !== undefined) {
        process.env.CLAUDE_CODE_GIT_BASH_PATH = originalEnvVar
      } else {
        delete process.env.CLAUDE_CODE_GIT_BASH_PATH
      }
    })

    const mockExistingPaths = (...validPaths: string[]) => {
      vi.mocked(fs.existsSync).mockImplementation((p) => validPaths.includes(p as string))
    }

    it('returns the CLAUDE_CODE_GIT_BASH_PATH override when it is valid', () => {
      const envPath = 'C:\\EnvGit\\bin\\bash.exe'
      process.env.CLAUDE_CODE_GIT_BASH_PATH = envPath
      mockExistingPaths(envPath)

      expect(autoDiscoverGitBash()).toBe(envPath)
    })

    it('falls back to auto-discovery when the env override is invalid', () => {
      const envPath = 'C:\\Invalid\\bash.exe'
      const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
      const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'

      process.env.CLAUDE_CODE_GIT_BASH_PATH = envPath
      process.env.ProgramFiles = 'C:\\Program Files'
      // env path does not exist; the standard Git install does
      mockExistingPaths(gitPath, bashPath)

      expect(autoDiscoverGitBash()).toBe(bashPath)
    })

    it('discovers Git Bash from a standard Git for Windows install', () => {
      const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
      const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'

      process.env.ProgramFiles = 'C:\\Program Files'
      mockExistingPaths(gitPath, bashPath)

      expect(autoDiscoverGitBash()).toBe(bashPath)
    })

    it('returns null when Git Bash cannot be found', () => {
      vi.mocked(fs.existsSync).mockReturnValue(false)
      vi.mocked(execFileSync).mockImplementation(() => {
        throw new Error('Not found')
      })

      expect(autoDiscoverGitBash()).toBeNull()
    })
  })
})

describe.skipIf(process.platform !== 'win32')('findViaMise', () => {
  const misePath = 'C:\\Users\\User\\AppData\\Local\\mise\\bin\\mise.exe'
  const env = { PATH: 'C:\\Windows\\system32' }

  beforeEach(() => {
    vi.clearAllMocks()

    // Mock path.isAbsolute to recognise Windows absolute paths
    vi.mocked(path.isAbsolute).mockImplementation((p) => /^[A-Z]:/i.test(p))
  })

  it('returns null when mise is not installed', () => {
    // where.exe mise fails
    vi.mocked(execFileSync).mockImplementation(() => {
      throw new Error('Not found')
    })

    const result = findViaMise('node', env)

    expect(result).toBeNull()
  })

  it('returns null when mise is installed but tool is not managed', () => {
    let callCount = 0
    vi.mocked(execFileSync).mockImplementation(() => {
      callCount++
      // First call: where.exe mise -> returns mise path
      if (callCount === 1) return `${misePath}\r\n`
      // Second call: mise which node -> tool not managed
      throw new Error('No runtime found for node')
    })

    const result = findViaMise('node', env)

    expect(result).toBeNull()
  })

  it('returns the resolved path when mise manages the tool', () => {
    const nodePath = 'C:\\Users\\User\\AppData\\Local\\mise\\installs\\node\\22.0.0\\node.exe'

    let callCount = 0
    vi.mocked(execFileSync).mockImplementation(() => {
      callCount++
      if (callCount === 1) return `${misePath}\r\n`
      return `${nodePath}\n`
    })
    vi.mocked(fs.existsSync).mockImplementation((p) => p === nodePath)

    const result = findViaMise('node', env)

    expect(result).toBe(nodePath)
  })

  it('returns null when mise which times out', () => {
    let callCount = 0
    vi.mocked(execFileSync).mockImplementation(() => {
      callCount++
      if (callCount === 1) return `${misePath}\r\n`
      const err = new Error('ETIMEDOUT') as NodeJS.ErrnoException
      err.code = 'ETIMEDOUT'
      throw err
    })

    const result = findViaMise('node', env)

    expect(result).toBeNull()
  })

  it('returns null when mise which returns a non-existent path', () => {
    const ghostPath = 'C:\\Users\\User\\AppData\\Local\\mise\\installs\\node\\22.0.0\\node.exe'

    let callCount = 0
    vi.mocked(execFileSync).mockImplementation(() => {
      callCount++
      if (callCount === 1) return `${misePath}\r\n`
      return `${ghostPath}\n`
    })
    // The resolved path does not exist on disk
    vi.mocked(fs.existsSync).mockReturnValue(false)

    const result = findViaMise('node', env)

    expect(result).toBeNull()
  })
})

/**
 * Helper to create a mock child process for spawn
 */
function createMockChildProcess() {
  const mockChild = new EventEmitter() as EventEmitter & {
    stdout: EventEmitter
    stderr: EventEmitter
    kill: ReturnType<typeof vi.fn>
  }
  mockChild.stdout = new EventEmitter()
  mockChild.stderr = new EventEmitter()
  mockChild.kill = vi.fn()
  return mockChild
}

describe('findCommandInShellEnv', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    // Reset path.isAbsolute to real implementation for these tests
    vi.mocked(path.isAbsolute).mockImplementation((p) => p.startsWith('/') || /^[A-Z]:/i.test(p))
  })

  describe('command name validation', () => {
    it('should reject empty command name', async () => {
      const result = await findCommandInShellEnv('', {})
      expect(result).toBeNull()
      expect(spawn).not.toHaveBeenCalled()
    })

    it('should reject command names with shell metacharacters', async () => {
      const maliciousCommands = [
        'npx; rm -rf /',
        'npx && malicious',
        'npx | cat /etc/passwd',
        'npx`whoami`',
        '$(whoami)',
        'npx\nmalicious'
      ]

      for (const cmd of maliciousCommands) {
        const result = await findCommandInShellEnv(cmd, {})
        expect(result).toBeNull()
        expect(spawn).not.toHaveBeenCalled()
      }
    })

    it('should reject command names starting with hyphen', async () => {
      const result = await findCommandInShellEnv('-npx', {})
      expect(result).toBeNull()
      expect(spawn).not.toHaveBeenCalled()
    })

    it('should reject path traversal attempts', async () => {
      const pathTraversalCommands = ['../npx', '../../malicious', 'foo/bar', 'foo\\bar']

      for (const cmd of pathTraversalCommands) {
        const result = await findCommandInShellEnv(cmd, {})
        expect(result).toBeNull()
        expect(spawn).not.toHaveBeenCalled()
      }
    })

    it('should reject command names exceeding max length', async () => {
      const longCommand = 'a'.repeat(129)
      const result = await findCommandInShellEnv(longCommand, {})
      expect(result).toBeNull()
      expect(spawn).not.toHaveBeenCalled()
    })

    it('should accept valid command names', async () => {
      const mockChild = createMockChildProcess()
      vi.mocked(spawn).mockReturnValue(mockChild as never)

      // Don't await - just start the call
      const resultPromise = findCommandInShellEnv('npx', { PATH: '/usr/bin' })

      // Simulate command not found
      mockChild.emit('close', 1)

      const result = await resultPromise
      expect(result).toBeNull()
      expect(spawn).toHaveBeenCalled()
    })

    it('should accept command names with underscores and hyphens', async () => {
      const mockChild = createMockChildProcess()
      vi.mocked(spawn).mockReturnValue(mockChild as never)

      const resultPromise = findCommandInShellEnv('my_command-name', { PATH: '/usr/bin' })
      mockChild.emit('close', 1)

      await resultPromise
      expect(spawn).toHaveBeenCalled()
    })

    it('should accept command names at max length (128 chars)', async () => {
      const mockChild = createMockChildProcess()
      vi.mocked(spawn).mockReturnValue(mockChild as never)

      const maxLengthCommand = 'a'.repeat(128)
      const resultPromise = findCommandInShellEnv(maxLengthCommand, { PATH: '/usr/bin' })
      mockChild.emit('close', 1)

      await resultPromise
      expect(spawn).toHaveBeenCalled()
    })
  })

  describe.skipIf(process.platform === 'win32')('Unix/macOS behavior', () => {
    it('should find command and return absolute path', async () => {
      const mockChild = createMockChildProcess()
      vi.mocked(spawn).mockReturnValue(mockChild as never)

      const resultPromise = findCommandInShellEnv('npx', { PATH: '/usr/bin' })

      // Simulate successful command -v output
      mockChild.stdout.emit('data', '/usr/local/bin/npx\n')
      mockChild.emit('close', 0)

      const result = await resultPromise
      expect(result).toBe('/usr/local/bin/npx')
      expect(spawn).toHaveBeenCalledWith('/bin/sh', ['-c', 'command -v "$1"', '--', 'npx'], expect.any(Object))
    })

    it('should return null for non-absolute paths (aliases/builtins)', async () => {
      const mockChild = createMockChildProcess()
      vi.mocked(spawn).mockReturnValue(mockChild as never)

      const resultPromise = findCommandInShellEnv('cd', { PATH: '/usr/bin' })

      // Simulate builtin output (just command name)
      mockChild.stdout.emit('data', 'cd\n')
      mockChild.emit('close', 0)

      const result = await resultPromise
      expect(result).toBeNull()
    })

    it('should return null when command not found', async () => {
      const mockChild = createMockChildProcess()
      vi.mocked(spawn).mockReturnValue(mockChild as never)

      const resultPromise = findCommandInShellEnv('nonexistent', { PATH: '/usr/bin' })

      // Simulate command not found (exit code 1)
      mockChild.emit('close', 1)

      const result = await resultPromise
      expect(result).toBeNull()
    })

    it('should handle spawn errors gracefully', async () => {
      const mockChild = createMockChildProcess()
      vi.mocked(spawn).mockReturnValue(mockChild as never)

      const resultPromise = findCommandInShellEnv('npx', { PATH: '/usr/bin' })

      // Simulate spawn error
      mockChild.emit('error', new Error('spawn failed'))

      const result = await resultPromise
      expect(result).toBeNull()
    })

    it('should handle timeout gracefully', async () => {
      vi.useFakeTimers()
      const mockChild = createMockChildProcess()
      vi.mocked(spawn).mockReturnValue(mockChild as never)

      const resultPromise = findCommandInShellEnv('npx', { PATH: '/usr/bin' })

      // Fast-forward past timeout (5000ms)
      vi.advanceTimersByTime(6000)

      const result = await resultPromise
      expect(result).toBeNull()
      expect(mockChild.kill).toHaveBeenCalledWith('SIGKILL')

      vi.useRealTimers()
    })
  })

  describe.skipIf(process.platform !== 'win32')('Windows behavior', () => {
    it('should find .exe files via where command', async () => {
      const mockChild = createMockChildProcess()
      vi.mocked(spawn).mockReturnValue(mockChild as never)

      const resultPromise = findCommandInShellEnv('npx', { PATH: 'C:\\nodejs' })

      // Simulate where output
      mockChild.stdout.emit('data', 'C:\\Program Files\\nodejs\\npx.exe\r\n')
      mockChild.emit('close', 0)

      const result = await resultPromise
      expect(result).toBe('C:\\Program Files\\nodejs\\npx.exe')
      expect(spawn).toHaveBeenCalledWith('where', ['npx'], expect.any(Object))
    })

    it('should reject .cmd files on Windows', async () => {
      const mockChild = createMockChildProcess()
      vi.mocked(spawn).mockReturnValue(mockChild as never)

      const resultPromise = findCommandInShellEnv('npx', { PATH: 'C:\\nodejs' })

      // Simulate where output with only .cmd file
      mockChild.stdout.emit('data', 'C:\\Program Files\\nodejs\\npx.cmd\r\n')
      mockChild.emit('close', 0)

      const result = await resultPromise
      expect(result).toBeNull()
    })

    it('should prefer .exe over .cmd when both exist', async () => {
      const mockChild = createMockChildProcess()
      vi.mocked(spawn).mockReturnValue(mockChild as never)

      const resultPromise = findCommandInShellEnv('npx', { PATH: 'C:\\nodejs' })

      // Simulate where output with both .cmd and .exe
      mockChild.stdout.emit('data', 'C:\\Program Files\\nodejs\\npx.cmd\r\nC:\\Program Files\\nodejs\\npx.exe\r\n')
      mockChild.emit('close', 0)

      const result = await resultPromise
      expect(result).toBe('C:\\Program Files\\nodejs\\npx.exe')
    })

    it('should handle spawn errors gracefully', async () => {
      const mockChild = createMockChildProcess()
      vi.mocked(spawn).mockReturnValue(mockChild as never)

      const resultPromise = findCommandInShellEnv('npx', { PATH: 'C:\\nodejs' })

      // Simulate spawn error
      mockChild.emit('error', new Error('spawn failed'))

      const result = await resultPromise
      expect(result).toBeNull()
    })
  })
})

describe('getBinaryPath', () => {
  // The global '@application' mock resolves 'cherry.bin' to '/mock/cherry.bin'
  // and 'feature.binary.data' to '/mock/feature.binary.data'. getBinaryPath
  // searches mise shims first, then the bundled cherry.bin fallback.
  const binDir = '/mock/cherry.bin'
  const shimsDir = '/mock/feature.binary.data/shims'

  beforeEach(() => {
    vi.clearAllMocks()
    vi.mocked(path.join).mockImplementation((...args) => args.join('/'))
  })

  it('returns the cherry bin directory when no name is given', async () => {
    const result = await getBinaryPath()
    expect(result).toBe(binDir)
  })

  it('returns the mise shim path when that binary exists, preferring it over cherry.bin', async () => {
    const binaryName = await getBinaryName('bun')
    vi.mocked(fs.existsSync).mockReturnValue(true)

    const result = await getBinaryPath('bun')

    // Shims are searched first, so a user-installed copy wins over the bundled one.
    expect(result).toBe(`${shimsDir}/${binaryName}`)
    expect(fs.existsSync).toHaveBeenCalledWith(`${shimsDir}/${binaryName}`)
  })

  it('falls back to the cherry.bin path when the binary exists only there', async () => {
    const binaryName = await getBinaryName('bun')
    vi.mocked(fs.existsSync).mockImplementation((p) => p === `${binDir}/${binaryName}`)

    const result = await getBinaryPath('bun')

    expect(result).toBe(`${binDir}/${binaryName}`)
  })

  it('falls back to the bare name (resolved via system PATH) when the binary is absent', async () => {
    const binaryName = await getBinaryName('bun')
    vi.mocked(fs.existsSync).mockReturnValue(false)

    const result = await getBinaryPath('bun')

    expect(result).toBe(binaryName)
  })
})

describe('getBinarySearchDirs', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    vi.mocked(path.join).mockImplementation((...args) => args.join('/'))
  })

  it('returns the mise shims dir before the bundled cherry.bin dir', () => {
    // Shims must precede cherry.bin so a user-installed copy shadows the bundled
    // one — the same ordering getBinaryPath() and shell-env.ts rely on.
    expect(getBinarySearchDirs()).toEqual(['/mock/feature.binary.data/shims', '/mock/cherry.bin'])
  })
})
