/**
 * Category Filter Unit Tests
 *
 * Tests for the category-based filtering feature that allows CI to fail
 * only on specific vulnerability categories.
 *
 * Run: npm test -- --testPathPattern=category-filter
 */

import {
  normalizeCategory,
  expandCategoryPattern,
  matchesAnyCategory,
  shouldFailOnCategories,
  getMatchingCategories,
  parseCategoryList,
  validateCategories,
  CATEGORY_GROUPS,
  ALL_CATEGORIES,
} from '../shared/category-filter'
import type { Vulnerability, VulnerabilityCategory } from '../shared/types'

// Helper to create a minimal vulnerability for testing
function createVulnerability(
  category: VulnerabilityCategory,
  severity: 'critical' | 'high' | 'medium' | 'low' | 'info' = 'high'
): Vulnerability {
  return {
    id: `test-${category}-${Date.now()}`,
    filePath: 'test/file.ts',
    lineNumber: 1,
    lineContent: 'test line',
    severity,
    category,
    title: `Test ${category}`,
    description: `Test finding for ${category}`,
    confidence: 'high',
    layer: 1,
  }
}

describe('normalizeCategory', () => {
  it('should convert to lowercase', () => {
    expect(normalizeCategory('SQL_INJECTION')).toBe('sql_injection')
    expect(normalizeCategory('HIGH_ENTROPY_STRING')).toBe('high_entropy_string')
  })

  it('should convert hyphens to underscores', () => {
    expect(normalizeCategory('sql-injection')).toBe('sql_injection')
    expect(normalizeCategory('ai-prompt-injection')).toBe('ai_prompt_injection')
  })

  it('should trim whitespace', () => {
    expect(normalizeCategory('  sql_injection  ')).toBe('sql_injection')
    expect(normalizeCategory('\tcommand_injection\n')).toBe('command_injection')
  })

  it('should handle mixed cases and formats', () => {
    expect(normalizeCategory('SQL-Injection')).toBe('sql_injection')
    expect(normalizeCategory('  AI-PROMPT-INJECTION  ')).toBe('ai_prompt_injection')
  })
})

describe('expandCategoryPattern', () => {
  it('should expand ai-* to all AI categories', () => {
    const expanded = expandCategoryPattern('ai-*')
    expect(expanded).toContain('ai_pattern')
    expect(expanded).toContain('ai_prompt_injection')
    expect(expanded).toContain('ai_unsafe_execution')
    expect(expanded).toContain('ai_overpermissive_tool')
    expect(expanded.length).toBeGreaterThanOrEqual(24) // At least 24 AI categories
  })

  it('should expand ai_* (underscore variant) to all AI categories', () => {
    const expanded = expandCategoryPattern('ai_*')
    expect(expanded).toContain('ai_pattern')
    expect(expanded).toContain('ai_prompt_injection')
    expect(expanded.length).toBeGreaterThanOrEqual(24)
  })

  it('should expand secrets-* to secret-related categories', () => {
    const expanded = expandCategoryPattern('secrets-*')
    expect(expanded).toContain('hardcoded_secret')
    expect(expanded).toContain('high_entropy_string')
    expect(expanded).toContain('sensitive_variable')
    expect(expanded.length).toBe(3)
  })

  it('should expand owasp-* to OWASP-mapped categories', () => {
    const expanded = expandCategoryPattern('owasp-*')
    expect(expanded).toContain('sql_injection')
    expect(expanded).toContain('xss')
    expect(expanded).toContain('command_injection')
    expect(expanded).toContain('missing_auth')
    expect(expanded.length).toBe(9)
  })

  it('should return single category for non-wildcards', () => {
    const expanded = expandCategoryPattern('sql_injection')
    expect(expanded).toEqual(['sql_injection'])
  })

  it('should handle hyphenated single category names', () => {
    const expanded = expandCategoryPattern('sql-injection')
    expect(expanded).toEqual(['sql_injection'])
  })

  it('should return empty array for unknown wildcards', () => {
    const expanded = expandCategoryPattern('unknown-*')
    expect(expanded).toEqual([])
  })

  it('should return empty array for invalid category names', () => {
    const expanded = expandCategoryPattern('not_a_real_category')
    expect(expanded).toEqual([])
  })

  it('should NOT match partial wildcards (ai* without dash)', () => {
    // 'ai*' is not a valid wildcard pattern (must end with -* or _*)
    const expanded = expandCategoryPattern('ai*')
    expect(expanded).toEqual([])
  })
})

describe('matchesAnyCategory', () => {
  it('should match exact category name', () => {
    expect(matchesAnyCategory('sql_injection', ['sql_injection'])).toBe(true)
    expect(matchesAnyCategory('xss', ['xss', 'sql_injection'])).toBe(true)
  })

  it('should be case-insensitive', () => {
    expect(matchesAnyCategory('sql_injection', ['SQL_INJECTION'])).toBe(true)
    expect(matchesAnyCategory('SQL_INJECTION' as VulnerabilityCategory, ['sql_injection'])).toBe(true)
  })

  it('should handle underscores and hyphens interchangeably', () => {
    expect(matchesAnyCategory('sql_injection', ['sql-injection'])).toBe(true)
    expect(matchesAnyCategory('ai_prompt_injection', ['ai-prompt-injection'])).toBe(true)
  })

  it('should match ai-* to all ai_ prefixed categories', () => {
    expect(matchesAnyCategory('ai_pattern', ['ai-*'])).toBe(true)
    expect(matchesAnyCategory('ai_prompt_injection', ['ai-*'])).toBe(true)
    expect(matchesAnyCategory('ai_unsafe_execution', ['ai-*'])).toBe(true)
    expect(matchesAnyCategory('ai_package_hallucination', ['ai-*'])).toBe(true)
  })

  it('should match secrets-* to secret-related categories', () => {
    expect(matchesAnyCategory('hardcoded_secret', ['secrets-*'])).toBe(true)
    expect(matchesAnyCategory('high_entropy_string', ['secrets-*'])).toBe(true)
    expect(matchesAnyCategory('sensitive_variable', ['secrets-*'])).toBe(true)
  })

  it('should match owasp-* to OWASP-mapped categories', () => {
    expect(matchesAnyCategory('sql_injection', ['owasp-*'])).toBe(true)
    expect(matchesAnyCategory('xss', ['owasp-*'])).toBe(true)
    expect(matchesAnyCategory('command_injection', ['owasp-*'])).toBe(true)
  })

  it('should NOT match unrelated categories', () => {
    expect(matchesAnyCategory('sql_injection', ['ai-*'])).toBe(false)
    expect(matchesAnyCategory('ai_pattern', ['secrets-*'])).toBe(false)
    expect(matchesAnyCategory('hardcoded_secret', ['owasp-*'])).toBe(false)
  })

  it('should return false for empty category list', () => {
    expect(matchesAnyCategory('sql_injection', [])).toBe(false)
  })

  it('should handle whitespace in category names', () => {
    expect(matchesAnyCategory('sql_injection', ['  sql_injection  '])).toBe(true)
    expect(matchesAnyCategory('ai_pattern', ['  ai-*  '])).toBe(true)
  })
})

describe('shouldFailOnCategories', () => {
  it('should return true if any finding matches category list', () => {
    const vulns = [
      createVulnerability('sql_injection', 'high'),
      createVulnerability('xss', 'medium'),
    ]

    expect(shouldFailOnCategories(vulns, ['sql_injection'])).toBe(true)
    expect(shouldFailOnCategories(vulns, ['xss'])).toBe(true)
    expect(shouldFailOnCategories(vulns, ['owasp-*'])).toBe(true)
  })

  it('should return false if no findings match', () => {
    const vulns = [
      createVulnerability('sql_injection', 'high'),
      createVulnerability('xss', 'medium'),
    ]

    expect(shouldFailOnCategories(vulns, ['ai-*'])).toBe(false)
    expect(shouldFailOnCategories(vulns, ['secrets-*'])).toBe(false)
  })

  it('should work with severity threshold (both must match)', () => {
    const vulns = [
      createVulnerability('ai_prompt_injection', 'high'),
      createVulnerability('ai_unsafe_execution', 'medium'),
      createVulnerability('ai_pattern', 'low'),
    ]

    // High+ AI findings should fail
    expect(shouldFailOnCategories(vulns, ['ai-*'], 'high')).toBe(true)

    // Medium+ AI findings should fail
    expect(shouldFailOnCategories(vulns, ['ai-*'], 'medium')).toBe(true)

    // Critical AI findings should NOT fail (no critical findings)
    expect(shouldFailOnCategories(vulns, ['ai-*'], 'critical')).toBe(false)
  })

  it('should return true when severity=none and category matches', () => {
    const vulns = [createVulnerability('ai_pattern', 'info')]

    // No severity threshold = any AI finding fails
    expect(shouldFailOnCategories(vulns, ['ai-*'])).toBe(true)
  })

  it('should return false for empty vulnerabilities', () => {
    expect(shouldFailOnCategories([], ['ai-*'])).toBe(false)
  })

  it('should return false for empty category filter', () => {
    const vulns = [createVulnerability('sql_injection', 'critical')]
    expect(shouldFailOnCategories(vulns, [])).toBe(false)
  })

  it('should handle combined severity + category filtering correctly', () => {
    const vulns = [
      createVulnerability('sql_injection', 'high'),
      createVulnerability('ai_prompt_injection', 'medium'),
      createVulnerability('hardcoded_secret', 'low'),
    ]

    // High+ OWASP findings - sql_injection qualifies
    expect(shouldFailOnCategories(vulns, ['owasp-*'], 'high')).toBe(true)

    // High+ AI findings - ai_prompt_injection is medium, not high
    expect(shouldFailOnCategories(vulns, ['ai-*'], 'high')).toBe(false)

    // Medium+ AI findings - ai_prompt_injection qualifies
    expect(shouldFailOnCategories(vulns, ['ai-*'], 'medium')).toBe(true)

    // High+ secrets - hardcoded_secret is low
    expect(shouldFailOnCategories(vulns, ['secrets-*'], 'high')).toBe(false)
  })
})

describe('getMatchingCategories', () => {
  it('should return categories that matched the filter', () => {
    const vulns = [
      createVulnerability('sql_injection', 'high'),
      createVulnerability('xss', 'medium'),
      createVulnerability('ai_pattern', 'low'),
    ]

    const matched = getMatchingCategories(vulns, ['owasp-*'])
    expect(matched).toContain('sql_injection')
    expect(matched).toContain('xss')
    expect(matched).not.toContain('ai_pattern')
  })

  it('should respect severity threshold', () => {
    const vulns = [
      createVulnerability('sql_injection', 'high'),
      createVulnerability('xss', 'low'),
    ]

    const matched = getMatchingCategories(vulns, ['owasp-*'], 'high')
    expect(matched).toContain('sql_injection')
    expect(matched).not.toContain('xss')
  })

  it('should return empty array for no matches', () => {
    const vulns = [createVulnerability('sql_injection', 'high')]
    const matched = getMatchingCategories(vulns, ['ai-*'])
    expect(matched).toEqual([])
  })
})

describe('parseCategoryList', () => {
  it('should parse comma-separated categories', () => {
    const result = parseCategoryList('sql_injection,xss,command_injection')
    expect(result).toEqual(['sql_injection', 'xss', 'command_injection'])
  })

  it('should handle single category', () => {
    const result = parseCategoryList('sql_injection')
    expect(result).toEqual(['sql_injection'])
  })

  it('should trim whitespace from category names', () => {
    const result = parseCategoryList('  sql_injection , xss , ai-*  ')
    expect(result).toEqual(['sql_injection', 'xss', 'ai-*'])
  })

  it('should handle wildcard patterns', () => {
    const result = parseCategoryList('ai-*,secrets-*,owasp-*')
    expect(result).toEqual(['ai-*', 'secrets-*', 'owasp-*'])
  })

  it('should filter out empty strings', () => {
    const result = parseCategoryList('sql_injection,,xss,')
    expect(result).toEqual(['sql_injection', 'xss'])
  })

  it('should return empty array for empty input', () => {
    expect(parseCategoryList('')).toEqual([])
    expect(parseCategoryList('   ')).toEqual([])
  })

  it('should handle undefined/null gracefully', () => {
    expect(parseCategoryList(undefined as any)).toEqual([])
    expect(parseCategoryList(null as any)).toEqual([])
  })
})

describe('validateCategories', () => {
  it('should validate known category names', () => {
    const result = validateCategories(['sql_injection', 'xss', 'ai_pattern'])
    expect(result.valid).toEqual(['sql_injection', 'xss', 'ai_pattern'])
    expect(result.invalid).toEqual([])
  })

  it('should validate wildcard patterns', () => {
    const result = validateCategories(['ai-*', 'secrets-*', 'owasp-*'])
    expect(result.valid).toEqual(['ai-*', 'secrets-*', 'owasp-*'])
    expect(result.invalid).toEqual([])
  })

  it('should separate valid from invalid categories', () => {
    const result = validateCategories(['sql_injection', 'fake_category', 'ai-*', 'unknown-*'])
    expect(result.valid).toEqual(['sql_injection', 'ai-*'])
    expect(result.invalid).toEqual(['fake_category', 'unknown-*'])
  })

  it('should handle mixed valid and invalid', () => {
    const result = validateCategories([
      'ai_prompt_injection',
      'not_real',
      'xss',
      'definitely_fake',
    ])
    expect(result.valid).toContain('ai_prompt_injection')
    expect(result.valid).toContain('xss')
    expect(result.invalid).toContain('not_real')
    expect(result.invalid).toContain('definitely_fake')
  })

  it('should handle hyphenated category names', () => {
    const result = validateCategories(['sql-injection', 'command-injection'])
    expect(result.valid).toEqual(['sql-injection', 'command-injection'])
    expect(result.invalid).toEqual([])
  })

  it('should return empty arrays for empty input', () => {
    const result = validateCategories([])
    expect(result.valid).toEqual([])
    expect(result.invalid).toEqual([])
  })
})

describe('CATEGORY_GROUPS', () => {
  it('should have ai-* group with correct categories', () => {
    expect(CATEGORY_GROUPS['ai-*']).toBeDefined()
    expect(CATEGORY_GROUPS['ai-*'].length).toBeGreaterThanOrEqual(24)

    // Verify some key AI categories are included
    expect(CATEGORY_GROUPS['ai-*']).toContain('ai_prompt_injection')
    expect(CATEGORY_GROUPS['ai-*']).toContain('ai_unsafe_execution')
    expect(CATEGORY_GROUPS['ai-*']).toContain('ai_package_hallucination')
    expect(CATEGORY_GROUPS['ai-*']).toContain('ai_mcp_tool_poisoning')
  })

  it('should have secrets-* group with correct categories', () => {
    expect(CATEGORY_GROUPS['secrets-*']).toEqual([
      'hardcoded_secret',
      'high_entropy_string',
      'sensitive_variable',
    ])
  })

  it('should have owasp-* group with correct categories', () => {
    expect(CATEGORY_GROUPS['owasp-*']).toEqual([
      'sql_injection',
      'xss',
      'command_injection',
      'missing_auth',
      'security_bypass',
      'insecure_config',
      'cors_misconfiguration',
      'data_exposure',
      'weak_crypto',
    ])
  })
})

describe('ALL_CATEGORIES', () => {
  it('should contain all AI categories', () => {
    for (const category of CATEGORY_GROUPS['ai-*']) {
      expect(ALL_CATEGORIES).toContain(category)
    }
  })

  it('should contain all secret categories', () => {
    for (const category of CATEGORY_GROUPS['secrets-*']) {
      expect(ALL_CATEGORIES).toContain(category)
    }
  })

  it('should contain all OWASP categories', () => {
    for (const category of CATEGORY_GROUPS['owasp-*']) {
      expect(ALL_CATEGORIES).toContain(category)
    }
  })

  it('should have no duplicates', () => {
    const uniqueCategories = new Set(ALL_CATEGORIES)
    expect(uniqueCategories.size).toBe(ALL_CATEGORIES.length)
  })
})

describe('Integration: Category filtering in CI workflow', () => {
  it('should support gradual rollout - only fail on AI issues first', () => {
    const vulns = [
      createVulnerability('sql_injection', 'critical'),
      createVulnerability('xss', 'high'),
      createVulnerability('ai_prompt_injection', 'medium'),
    ]

    // First phase: Only fail on AI issues
    const failsOnAI = shouldFailOnCategories(vulns, ['ai-*'])
    expect(failsOnAI).toBe(true)

    // But the high+ AI findings specifically
    const failsOnHighAI = shouldFailOnCategories(vulns, ['ai-*'], 'high')
    expect(failsOnHighAI).toBe(false) // No high+ AI findings
  })

  it('should support blocking only prompt injection and hallucination', () => {
    const vulns = [
      createVulnerability('ai_pattern', 'medium'),
      createVulnerability('ai_prompt_injection', 'high'),
      createVulnerability('ai_package_hallucination', 'critical'),
    ]

    // Block only on specific AI categories
    const shouldFail = shouldFailOnCategories(
      vulns,
      ['ai_prompt_injection', 'ai_package_hallucination']
    )
    expect(shouldFail).toBe(true)

    // Without these categories, should not fail
    const shouldNotFail = shouldFailOnCategories(vulns, ['ai_pattern'], 'critical')
    expect(shouldNotFail).toBe(false)
  })

  it('should work with combined severity + categories for strict control', () => {
    const vulns = [
      createVulnerability('ai_prompt_injection', 'high'),
      createVulnerability('hardcoded_secret', 'critical'),
      createVulnerability('sql_injection', 'medium'),
    ]

    // Only block on critical secrets
    expect(shouldFailOnCategories(vulns, ['secrets-*'], 'critical')).toBe(true)

    // Only block on high+ AI
    expect(shouldFailOnCategories(vulns, ['ai-*'], 'high')).toBe(true)

    // Only block on critical AI (none exist)
    expect(shouldFailOnCategories(vulns, ['ai-*'], 'critical')).toBe(false)
  })
})
