/**
 * Tests for the modular prompt assembly system.
 *
 * Verifies:
 * - Category-to-module mapping correctness
 * - Prompt assembly includes only relevant modules
 * - Deterministic ordering for cache hits
 * - Legacy compatibility
 * - Category completeness (no unmapped categories slip through)
 */

import type { VulnerabilityCategory } from '../../shared/types'
import {
  assembleValidationPrompt,
  getFullValidationPrompt,
  CATEGORY_TO_MODULE,
  INTENTIONALLY_UNMAPPED,
  COMMON_PROMPT,
  AUTH_ACCESS_MODULE,
  XSS_PROMPT_MODULE,
  SECRETS_CRYPTO_MODULE,
  AI_PATTERNS_MODULE,
  OWASP_CLASSIC_MODULE,
} from '../prompts/modules'
import { HIGH_CONTEXT_VALIDATION_PROMPT } from '../prompts/validation'

// ============================================================================
// Key phrases for each module (used to verify inclusion/exclusion)
// ============================================================================

const COMMON_KEY_PHRASE = 'Validate security findings'
const COMMON_RESPONSE_FORMAT = 'Response Format'
const AUTH_KEY_PHRASE = 'CRITICAL CONTRADICTION HANDLING'
const XSS_KEY_PHRASE = 'XSS vs Prompt Injection'
const SECRETS_KEY_PHRASE = 'Math.random() for Security'
const AI_KEY_PHRASE = 'RAG Data Exfiltration'
const OWASP_KEY_PHRASE = 'Server-Side Request Forgery'

describe('assembleValidationPrompt', () => {
  test('empty categories returns COMMON only', () => {
    const result = assembleValidationPrompt([])
    expect(result).toContain(COMMON_KEY_PHRASE)
    expect(result).toContain(COMMON_RESPONSE_FORMAT)
    // Should NOT contain module-specific content
    expect(result).not.toContain(AUTH_KEY_PHRASE)
    expect(result).not.toContain(XSS_KEY_PHRASE)
    expect(result).not.toContain(SECRETS_KEY_PHRASE)
    expect(result).not.toContain(AI_KEY_PHRASE)
    expect(result).not.toContain(OWASP_KEY_PHRASE)
  })

  test('single module: missing_auth includes auth_access only', () => {
    const result = assembleValidationPrompt(['missing_auth'])
    expect(result).toContain(COMMON_KEY_PHRASE)
    expect(result).toContain(AUTH_KEY_PHRASE)
    // Should NOT contain other modules
    expect(result).not.toContain(SECRETS_KEY_PHRASE)
    expect(result).not.toContain(AI_KEY_PHRASE)
    expect(result).not.toContain(OWASP_KEY_PHRASE)
  })

  test('multiple modules: auth + secrets + owasp', () => {
    const result = assembleValidationPrompt([
      'missing_auth',
      'hardcoded_secret',
      'ssrf',
    ])
    expect(result).toContain(COMMON_KEY_PHRASE)
    expect(result).toContain(AUTH_KEY_PHRASE)
    expect(result).toContain(SECRETS_KEY_PHRASE)
    expect(result).toContain(OWASP_KEY_PHRASE)
    // Should NOT contain unrelated modules
    expect(result).not.toContain(AI_KEY_PHRASE)
  })

  test('unknown/unmapped categories get COMMON only, no crash', () => {
    const result = assembleValidationPrompt(['dangerous_function'])
    expect(result).toContain(COMMON_KEY_PHRASE)
    expect(result).toContain(COMMON_RESPONSE_FORMAT)
    // No module-specific content
    expect(result).not.toContain(AUTH_KEY_PHRASE)
    expect(result).not.toContain(XSS_KEY_PHRASE)
    expect(result).not.toContain(SECRETS_KEY_PHRASE)
    expect(result).not.toContain(AI_KEY_PHRASE)
    expect(result).not.toContain(OWASP_KEY_PHRASE)
  })

  test('deterministic ordering: different input order produces same output', () => {
    const result1 = assembleValidationPrompt(['ssrf', 'missing_auth', 'hardcoded_secret'])
    const result2 = assembleValidationPrompt(['missing_auth', 'hardcoded_secret', 'ssrf'])
    const result3 = assembleValidationPrompt(['hardcoded_secret', 'ssrf', 'missing_auth'])
    expect(result1).toBe(result2)
    expect(result2).toBe(result3)
  })

  test('ai_prompt_injection triggers both xss_prompt AND ai_patterns modules', () => {
    const result = assembleValidationPrompt(['ai_prompt_injection'])
    expect(result).toContain(XSS_KEY_PHRASE)
    expect(result).toContain(AI_KEY_PHRASE)
  })

  test('duplicate categories do not cause duplicate modules', () => {
    const result1 = assembleValidationPrompt(['missing_auth'])
    const result2 = assembleValidationPrompt(['missing_auth', 'missing_auth', 'missing_auth'])
    expect(result1).toBe(result2)
  })
})

describe('getFullValidationPrompt', () => {
  test('contains key phrases from EVERY module', () => {
    const full = getFullValidationPrompt()
    expect(full).toContain(COMMON_KEY_PHRASE)
    expect(full).toContain(COMMON_RESPONSE_FORMAT)
    expect(full).toContain(AUTH_KEY_PHRASE)
    expect(full).toContain(XSS_KEY_PHRASE)
    expect(full).toContain(SECRETS_KEY_PHRASE)
    expect(full).toContain(AI_KEY_PHRASE)
    expect(full).toContain(OWASP_KEY_PHRASE)
  })
})

describe('legacy compatibility', () => {
  test('HIGH_CONTEXT_VALIDATION_PROMPT equals getFullValidationPrompt()', () => {
    expect(HIGH_CONTEXT_VALIDATION_PROMPT).toBe(getFullValidationPrompt())
  })
})

describe('category completeness guard', () => {
  /**
   * Every VulnerabilityCategory must be either:
   * 1. Mapped to a module in CATEGORY_TO_MODULE, or
   * 2. Listed in INTENTIONALLY_UNMAPPED
   *
   * If a new category is added to types.ts without updating the mapping,
   * this test will fail.
   */
  test('every VulnerabilityCategory is accounted for', () => {
    // Get all categories from the type system at runtime
    // We can't enumerate a TS union at runtime, so we maintain this list manually.
    // This list MUST match the VulnerabilityCategory union in types.ts exactly.
    const ALL_CATEGORIES: VulnerabilityCategory[] = [
      'hardcoded_secret',
      'high_entropy_string',
      'sensitive_variable',
      'security_bypass',
      'dangerous_function',
      'sql_injection',
      'xss',
      'command_injection',
      'insecure_config',
      'missing_auth',
      'suspicious_package',
      'cors_misconfiguration',
      'root_container',
      'dangerous_file',
      'ai_pattern',
      'sensitive_url',
      'weak_crypto',
      'data_exposure',
      'ai_prompt_injection',
      'ai_unsafe_execution',
      'ai_overpermissive_tool',
      'ai_rag_exfiltration',
      'ai_endpoint_unprotected',
      'ai_schema_mismatch',
      'ai_package_hallucination',
      'ai_rag_corpus_poisoning',
      'ai_rag_pii_leakage',
      'ai_mcp_tool_poisoning',
      'ai_mcp_credential_issue',
      'ai_mcp_confused_deputy',
      'ai_mcp_description_injection',
      'ai_mcp_server_shadowing',
      'ai_mcp_config_secrets',
      'ai_mcp_config_permissions',
      'ai_rag_query_injection',
      'ai_rag_embedding_poisoning',
      'ai_rag_chunk_injection',
      'ai_package_typosquat',
      'ai_package_malicious',
      'ai_unsafe_model_load',
      'ai_unverified_model',
      'ai_unsafe_finetuning',
      'ai_excessive_agency',
      'ai_skill_injection',
      'missing_security_headers',
      'ssrf',
      'log_injection',
      'xxe',
    ]

    for (const cat of ALL_CATEGORIES) {
      const isMapped = CATEGORY_TO_MODULE.has(cat)
      const isUnmapped = INTENTIONALLY_UNMAPPED.has(cat)
      expect({
        category: cat,
        mapped: isMapped,
        intentionallyUnmapped: isUnmapped,
        accountedFor: isMapped || isUnmapped,
      }).toEqual(expect.objectContaining({ accountedFor: true }))
    }
  })

  test('no category is both mapped AND intentionally unmapped', () => {
    for (const cat of INTENTIONALLY_UNMAPPED) {
      expect(CATEGORY_TO_MODULE.has(cat)).toBe(false)
    }
  })
})

describe('module content integrity', () => {
  test('COMMON_PROMPT contains taint analysis guidance', () => {
    expect(COMMON_PROMPT).toContain('Taint Analysis Context')
    expect(COMMON_PROMPT).toContain('User input reaches this sink')
    expect(COMMON_PROMPT).toContain('No user-input data flow reaches this line')
    expect(COMMON_PROMPT).toContain('Sanitised: Yes')
    expect(COMMON_PROMPT).toContain('Confidence levels')
  })

  test('COMMON_PROMPT contains condensed heuristic reminders', () => {
    expect(COMMON_PROMPT).toContain('JSON.parse')
    expect(COMMON_PROMPT).toContain('error.message')
    expect(COMMON_PROMPT).toContain('innerHTML')
  })

  test('each module is non-empty', () => {
    expect(AUTH_ACCESS_MODULE.length).toBeGreaterThan(50)
    expect(XSS_PROMPT_MODULE.length).toBeGreaterThan(50)
    expect(SECRETS_CRYPTO_MODULE.length).toBeGreaterThan(50)
    expect(AI_PATTERNS_MODULE.length).toBeGreaterThan(50)
    expect(OWASP_CLASSIC_MODULE.length).toBeGreaterThan(50)
  })
})
