import { configManager } from '@main/services/ConfigManager'
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,
  validateGitBashPath
} from '../process'

// Mock configManager
vi.mock('@main/services/ConfigManager', () => ({
  ConfigKeys: {
    GitBashPath: 'gitBashPath'
  },
  configManager: {
    get: vi.fn(),
    set: vi.fn()
  }
}))

// 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(() => {
      vi.mocked(configManager.get).mockReset()
      vi.mocked(configManager.set).mockReset()
      delete process.env.CLAUDE_CODE_GIT_BASH_PATH
    })

    afterEach(() => {
      // Restore original environment variable
      if (originalEnvVar !== undefined) {
        process.env.CLAUDE_CODE_GIT_BASH_PATH = originalEnvVar
      } else {
        delete process.env.CLAUDE_CODE_GIT_BASH_PATH
      }
    })

    /**
     * Helper to mock fs.existsSync with a set of valid paths
     */
    const mockExistingPaths = (...validPaths: string[]) => {
      vi.mocked(fs.existsSync).mockImplementation((p) => validPaths.includes(p as string))
    }

    describe('with no existing config path', () => {
      it('should discover and persist Git Bash path when not configured', () => {
        const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
        const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'

        vi.mocked(configManager.get).mockReturnValue(undefined)
        process.env.ProgramFiles = 'C:\\Program Files'
        mockExistingPaths(gitPath, bashPath)

        const result = autoDiscoverGitBash()

        expect(result).toBe(bashPath)
        expect(configManager.set).toHaveBeenCalledWith('gitBashPath', bashPath)
      })

      it('should return null and not persist when Git Bash is not found', () => {
        vi.mocked(configManager.get).mockReturnValue(undefined)
        vi.mocked(fs.existsSync).mockReturnValue(false)
        vi.mocked(execFileSync).mockImplementation(() => {
          throw new Error('Not found')
        })

        const result = autoDiscoverGitBash()

        expect(result).toBeNull()
        expect(configManager.set).not.toHaveBeenCalled()
      })
    })

    describe('environment variable precedence', () => {
      it('should use env var over valid config path', () => {
        const envPath = 'C:\\EnvGit\\bin\\bash.exe'
        const configPath = 'C:\\ConfigGit\\bin\\bash.exe'

        process.env.CLAUDE_CODE_GIT_BASH_PATH = envPath
        vi.mocked(configManager.get).mockReturnValue(configPath)
        mockExistingPaths(envPath, configPath)

        const result = autoDiscoverGitBash()

        // Env var should take precedence
        expect(result).toBe(envPath)
        // Should not persist env var path (it's a runtime override)
        expect(configManager.set).not.toHaveBeenCalled()
      })

      it('should fall back to config path when env var is invalid', () => {
        const envPath = 'C:\\Invalid\\bash.exe'
        const configPath = 'C:\\ConfigGit\\bin\\bash.exe'

        process.env.CLAUDE_CODE_GIT_BASH_PATH = envPath
        vi.mocked(configManager.get).mockReturnValue(configPath)
        // Env path is invalid (doesn't exist), only config path exists
        mockExistingPaths(configPath)

        const result = autoDiscoverGitBash()

        // Should fall back to config path
        expect(result).toBe(configPath)
        expect(configManager.set).not.toHaveBeenCalled()
      })

      it('should fall back to auto-discovery when both env var and config are invalid', () => {
        const envPath = 'C:\\InvalidEnv\\bash.exe'
        const configPath = 'C:\\InvalidConfig\\bash.exe'
        const discoveredPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
        const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'

        process.env.CLAUDE_CODE_GIT_BASH_PATH = envPath
        process.env.ProgramFiles = 'C:\\Program Files'
        vi.mocked(configManager.get).mockReturnValue(configPath)
        // Both env and config paths are invalid, only standard Git exists
        mockExistingPaths(gitPath, discoveredPath)

        const result = autoDiscoverGitBash()

        expect(result).toBe(discoveredPath)
        expect(configManager.set).toHaveBeenCalledWith('gitBashPath', discoveredPath)
      })
    })

    describe('with valid existing config path', () => {
      it('should validate and return existing path without re-discovering', () => {
        const existingPath = 'C:\\CustomGit\\bin\\bash.exe'

        vi.mocked(configManager.get).mockReturnValue(existingPath)
        mockExistingPaths(existingPath)

        const result = autoDiscoverGitBash()

        expect(result).toBe(existingPath)
        // Should not call findGitBash or persist again
        expect(configManager.set).not.toHaveBeenCalled()
        // Should not call execFileSync (which findGitBash would use for discovery)
        expect(execFileSync).not.toHaveBeenCalled()
      })

      it('should not override existing valid config with auto-discovery', () => {
        const existingPath = 'C:\\CustomGit\\bin\\bash.exe'
        const discoveredPath = 'C:\\Program Files\\Git\\bin\\bash.exe'

        vi.mocked(configManager.get).mockReturnValue(existingPath)
        mockExistingPaths(existingPath, discoveredPath)

        const result = autoDiscoverGitBash()

        expect(result).toBe(existingPath)
        expect(configManager.set).not.toHaveBeenCalled()
      })
    })

    describe('with invalid existing config path', () => {
      it('should attempt auto-discovery when existing path does not exist', () => {
        const existingPath = 'C:\\NonExistent\\bin\\bash.exe'
        const discoveredPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
        const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'

        vi.mocked(configManager.get).mockReturnValue(existingPath)
        process.env.ProgramFiles = 'C:\\Program Files'
        // Invalid path doesn't exist, but Git is installed at standard location
        mockExistingPaths(gitPath, discoveredPath)

        const result = autoDiscoverGitBash()

        // Should discover and return the new path
        expect(result).toBe(discoveredPath)
        // Should persist the discovered path (overwrites invalid)
        expect(configManager.set).toHaveBeenCalledWith('gitBashPath', discoveredPath)
      })

      it('should attempt auto-discovery when existing path is not bash.exe', () => {
        const existingPath = 'C:\\CustomGit\\bin\\git.exe'
        const discoveredPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
        const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'

        vi.mocked(configManager.get).mockReturnValue(existingPath)
        process.env.ProgramFiles = 'C:\\Program Files'
        // Invalid path exists but is not bash.exe (validation will fail)
        // Git is installed at standard location
        mockExistingPaths(existingPath, gitPath, discoveredPath)

        const result = autoDiscoverGitBash()

        // Should discover and return the new path
        expect(result).toBe(discoveredPath)
        // Should persist the discovered path (overwrites invalid)
        expect(configManager.set).toHaveBeenCalledWith('gitBashPath', discoveredPath)
      })

      it('should return null when existing path is invalid and discovery fails', () => {
        const existingPath = 'C:\\NonExistent\\bin\\bash.exe'

        vi.mocked(configManager.get).mockReturnValue(existingPath)
        vi.mocked(fs.existsSync).mockReturnValue(false)
        vi.mocked(execFileSync).mockImplementation(() => {
          throw new Error('Not found')
        })

        const result = autoDiscoverGitBash()

        // Both validation and discovery failed
        expect(result).toBeNull()
        // Should not persist when discovery fails
        expect(configManager.set).not.toHaveBeenCalled()
      })
    })

    describe('config persistence verification', () => {
      it('should persist discovered path with correct config key', () => {
        const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
        const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'

        vi.mocked(configManager.get).mockReturnValue(undefined)
        process.env.ProgramFiles = 'C:\\Program Files'
        mockExistingPaths(gitPath, bashPath)

        autoDiscoverGitBash()

        // On Windows, configManager.set is called twice (for path and source)
        // On other platforms, it's called once
        const expectedCalls = process.platform === 'win32' ? 2 : 1
        expect(configManager.set).toHaveBeenCalledTimes(expectedCalls)
        expect(configManager.set).toHaveBeenCalledWith('gitBashPath', bashPath)
      })

      it('should persist on each discovery when config remains undefined', () => {
        const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
        const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'

        vi.mocked(configManager.get).mockReturnValue(undefined)
        process.env.ProgramFiles = 'C:\\Program Files'
        mockExistingPaths(gitPath, bashPath)

        autoDiscoverGitBash()
        autoDiscoverGitBash()

        // On Windows, configManager.set is called twice per discovery (for path and source)
        // On other platforms, it's called once per discovery
        const expectedCallsPerCall = process.platform === 'win32' ? 2 : 1
        expect(configManager.set).toHaveBeenCalledTimes(expectedCallsPerCall * 2)
      })
    })

    describe('real-world scenarios', () => {
      it('should discover and persist standard Git for Windows installation', () => {
        const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
        const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'

        vi.mocked(configManager.get).mockReturnValue(undefined)
        process.env.ProgramFiles = 'C:\\Program Files'
        mockExistingPaths(gitPath, bashPath)

        const result = autoDiscoverGitBash()

        expect(result).toBe(bashPath)
        expect(configManager.set).toHaveBeenCalledWith('gitBashPath', bashPath)
      })

      it('should discover portable Git via where.exe and persist', () => {
        const gitPath = 'D:\\PortableApps\\Git\\bin\\git.exe'
        const bashPath = 'D:\\PortableApps\\Git\\bin\\bash.exe'

        vi.mocked(configManager.get).mockReturnValue(undefined)

        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 path exists
          if (pathStr === bashPath) return true
          return false
        })

        vi.mocked(execFileSync).mockReturnValue(gitPath)

        const result = autoDiscoverGitBash()

        expect(result).toBe(bashPath)
        expect(configManager.set).toHaveBeenCalledWith('gitBashPath', bashPath)
      })

      it('should respect user-configured path over auto-discovery', () => {
        const userConfiguredPath = 'D:\\MyGit\\bin\\bash.exe'
        const systemPath = 'C:\\Program Files\\Git\\bin\\bash.exe'

        vi.mocked(configManager.get).mockReturnValue(userConfiguredPath)
        mockExistingPaths(userConfiguredPath, systemPath)

        const result = autoDiscoverGitBash()

        expect(result).toBe(userConfiguredPath)
        expect(configManager.set).not.toHaveBeenCalled()
        // Verify findGitBash was not called for discovery
        expect(execFileSync).not.toHaveBeenCalled()
      })
    })
  })
})

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()
    })
  })
})
