/**
 * Path Exclusion Utility
 * Provides configurable exclusion of test files, seed files, examples, and fixtures
 * to reduce false positives in security scans.
 */

import type { Vulnerability, VulnerabilityCategory } from '../shared/types'

export interface ExclusionConfig {
  testPatterns: string[]
  seedPatterns: string[]
  examplePatterns: string[]
  fixturePatterns: string[]
}

const DEFAULT_EXCLUSIONS: ExclusionConfig = {
  testPatterns: [
    '**/*.spec.ts',
    '**/*.spec.js',
    '**/*.test.ts',
    '**/*.test.js',
    '**/test/**',
    '**/tests/**',
    '**/__tests__/**',
    '**/e2e/**',
    '**/app-tests/**',
    '**/*.spec.tsx',
    '**/*.spec.jsx',
    '**/*.test.tsx',
    '**/*.test.jsx',
    '**/cypress/**',
    '**/playwright/**',
  ],
  seedPatterns: [
    '**/seed/**',
    '**/seeds/**',
    '**/prisma/seed/**',
    '**/prisma/seed.ts',
    '**/prisma/seed.js',
    '**/db/seed/**',
    '**/database/seed/**',
  ],
  examplePatterns: [
    '**/examples/**',
    '**/demo/**',
    '**/sample/**',
    '**/samples/**',
    '**/playground/**',
    '**/oculum.json',           // Scanner output files
    '**/*.scan.json',          // Any scan result files
  ],
  fixturePatterns: [
    '**/fixtures/**',
    '**/mocks/**',
    '**/__mocks__/**',
    '**/stubs/**',
    '**/__fixtures__/**',
  ],
}

/**
 * Categories that should be SUPPRESSED in test/seed/example files
 * These are typically false positives in non-production code
 */
const SUPPRESSIBLE_CATEGORIES: VulnerabilityCategory[] = [
  'weak_crypto',           // Math.random in tests is fine
  'dangerous_function',    // regex, eval in tests often intentional
  'hardcoded_secret',      // test fixtures have fake secrets
  'high_entropy_string',   // test data often looks like secrets
  'sensitive_url',         // localhost in test configs
  'sensitive_variable',    // test variables named 'password' etc.
  'insecure_config',       // test configs are intentionally loose
  'ai_pattern',            // AI patterns in test code
]

/**
 * Categories that should ALWAYS be scanned, even in test files
 * These represent real vulnerabilities that matter in test code too
 */
const ALWAYS_SCAN_CATEGORIES: VulnerabilityCategory[] = [
  'sql_injection',
  'command_injection',
  'xss',
  'path_traversal' as VulnerabilityCategory, // Cast since not in original types
  'missing_auth',          // Keep but may downgrade
]

export type ExclusionReason = 'test_file' | 'seed_file' | 'example_file' | 'fixture_file' | null

/**
 * Convert glob pattern to regex for matching
 */
function globToRegex(pattern: string): RegExp {
  // Escape special regex characters except * and **
  let regexStr = pattern
    .replace(/[.+^${}()|[\]\\]/g, '\\$&')
    .replace(/\*\*/g, '{{GLOBSTAR}}')
    .replace(/\*/g, '[^/]*')
    .replace(/\{\{GLOBSTAR\}\}/g, '.*')
  
  return new RegExp(regexStr, 'i')
}

/**
 * Check if a file path matches any pattern in a list
 */
function matchesPatterns(filePath: string, patterns: string[]): boolean {
  // Normalize path separators
  const normalizedPath = filePath.replace(/\\/g, '/')
  
  return patterns.some(pattern => {
    const regex = globToRegex(pattern)
    return regex.test(normalizedPath)
  })
}

/**
 * Check if a file path should be excluded from scanning
 * Returns the exclusion reason or null if not excluded
 */
export function getExclusionReason(
  filePath: string,
  config: ExclusionConfig = DEFAULT_EXCLUSIONS
): ExclusionReason {
  const normalizedPath = filePath.replace(/\\/g, '/')
  
  // Check test patterns
  if (matchesPatterns(normalizedPath, config.testPatterns)) {
    return 'test_file'
  }
  
  // Check seed patterns
  if (matchesPatterns(normalizedPath, config.seedPatterns)) {
    return 'seed_file'
  }
  
  // Check example patterns
  if (matchesPatterns(normalizedPath, config.examplePatterns)) {
    return 'example_file'
  }
  
  // Check fixture patterns
  if (matchesPatterns(normalizedPath, config.fixturePatterns)) {
    return 'fixture_file'
  }
  
  return null
}

/**
 * Check if a file path should be excluded
 */
export function isExcludedPath(
  filePath: string,
  config?: Partial<ExclusionConfig>
): boolean {
  const mergedConfig: ExclusionConfig = {
    ...DEFAULT_EXCLUSIONS,
    ...config,
  }
  
  return getExclusionReason(filePath, mergedConfig) !== null
}

/**
 * Determine if a finding should be suppressed based on file path and category
 * 
 * @param finding - The vulnerability finding
 * @param exclusionReason - The reason the file is excluded (if any)
 * @returns true if the finding should be suppressed
 */
export function shouldSuppressFinding(
  finding: Vulnerability,
  exclusionReason: ExclusionReason
): boolean {
  // If file is not excluded, don't suppress
  if (!exclusionReason) {
    return false
  }
  
  // Always scan certain critical categories even in test files
  if (ALWAYS_SCAN_CATEGORIES.includes(finding.category)) {
    return false
  }
  
  // Suppress suppressible categories in excluded files
  if (SUPPRESSIBLE_CATEGORIES.includes(finding.category)) {
    return true
  }
  
  // Default: don't suppress
  return false
}

/**
 * Get a human-readable description of the exclusion reason
 */
export function getExclusionDescription(reason: ExclusionReason): string {
  switch (reason) {
    case 'test_file':
      return 'Finding in test file'
    case 'seed_file':
      return 'Finding in seed/fixture data file'
    case 'example_file':
      return 'Finding in example/demo file'
    case 'fixture_file':
      return 'Finding in mock/fixture file'
    default:
      return ''
  }
}

/**
 * Filter findings based on path exclusions
 * Returns findings that should be kept and a list of suppressed findings with reasons
 */
export function filterFindingsByPath(
  findings: Vulnerability[],
  config?: Partial<ExclusionConfig>
): {
  kept: Vulnerability[]
  suppressed: Array<{ finding: Vulnerability; reason: ExclusionReason; description: string }>
} {
  const mergedConfig: ExclusionConfig = {
    ...DEFAULT_EXCLUSIONS,
    ...config,
  }
  
  const kept: Vulnerability[] = []
  const suppressed: Array<{ finding: Vulnerability; reason: ExclusionReason; description: string }> = []
  
  for (const finding of findings) {
    const exclusionReason = getExclusionReason(finding.filePath, mergedConfig)
    
    if (shouldSuppressFinding(finding, exclusionReason)) {
      suppressed.push({
        finding,
        reason: exclusionReason,
        description: getExclusionDescription(exclusionReason),
      })
    } else {
      kept.push(finding)
    }
  }
  
  return { kept, suppressed }
}

/**
 * Get the default exclusion configuration
 */
export function getDefaultExclusionConfig(): ExclusionConfig {
  return { ...DEFAULT_EXCLUSIONS }
}

/**
 * Merge custom exclusion patterns with defaults
 */
export function mergeExclusionConfig(
  custom: Partial<ExclusionConfig>
): ExclusionConfig {
  return {
    testPatterns: [...DEFAULT_EXCLUSIONS.testPatterns, ...(custom.testPatterns || [])],
    seedPatterns: [...DEFAULT_EXCLUSIONS.seedPatterns, ...(custom.seedPatterns || [])],
    examplePatterns: [...DEFAULT_EXCLUSIONS.examplePatterns, ...(custom.examplePatterns || [])],
    fixturePatterns: [...DEFAULT_EXCLUSIONS.fixturePatterns, ...(custom.fixturePatterns || [])],
  }
}
