/**
 * Unit tests for adjustment rules
 */

import {
  FILE_CONTEXT_RULES,
  AUTO_DISMISS_RULES,
  ALL_ADJUSTMENT_RULES,
  TAINT_RULES,
  ROUTE_RULES,
  FRAMEWORK_RULES,
} from '../../score/adjustments'
import type { AdjustmentContext } from '../../score/types'
import type { Vulnerability } from '../../shared/types'

// Helper to create a minimal finding
function makeFinding(overrides: Partial<Vulnerability> = {}): Vulnerability {
  return {
    id: 'test-1',
    filePath: 'src/app.ts',
    lineNumber: 10,
    lineContent: 'const x = 1',
    severity: 'medium',
    category: 'hardcoded_secret',
    title: 'Test finding',
    description: 'Test',
    confidence: 'medium',
    layer: 1,
    baseConfidence: 0.50,
    ...overrides,
  }
}

function makeContext(overrides: Partial<AdjustmentContext> = {}): AdjustmentContext {
  return {
    file: {
      isTestFile: false,
      isExampleFile: false,
      isDocFile: false,
      isScannerCode: false,
      isToolingDir: false,
      isServerOnly: false,
      isClientBundled: false,
    },
    project: {
      hasAuthMiddleware: false,
      hasTypeScript: true,
      orm: null,
      validationLib: null,
    },
    lineContent: 'const x = 1',
    filePath: 'src/app.ts',
    ...overrides,
  }
}

function findRule(name: string) {
  return ALL_ADJUSTMENT_RULES.find(r => r.name === name)
}

describe('File Context Rules', () => {
  it('test_file applies for test files', () => {
    const rule = findRule('test_file')!
    expect(rule).toBeDefined()
    expect(rule.delta).toBe(-0.35)

    const finding = makeFinding({ filePath: 'src/__tests__/auth.test.ts' })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)
  })

  it('test_file does not apply for production files', () => {
    const rule = findRule('test_file')!
    const finding = makeFinding({ filePath: 'src/auth.ts' })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(false)
  })

  it('example_file applies for example files', () => {
    const rule = findRule('example_file')!
    expect(rule.delta).toBe(-0.30)

    const finding = makeFinding({ filePath: '/examples/basic/app.ts' })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)
  })

  it('example_file does not apply for production files', () => {
    const rule = findRule('example_file')!
    const finding = makeFinding({ filePath: 'src/app.ts' })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(false)
  })

  it('doc_file applies for markdown files', () => {
    const rule = findRule('doc_file')!
    expect(rule.delta).toBe(-0.30)

    const finding = makeFinding({ filePath: 'docs/setup.md' })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)
  })

  it('scanner_code applies for scanner files', () => {
    const rule = findRule('scanner_code')!
    expect(rule.delta).toBe(-0.30)

    const finding = makeFinding({ filePath: 'packages/scanner/src/detect/secrets/patterns.ts' })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)
  })

  it('tooling_dir applies for scripts directory', () => {
    const rule = findRule('tooling_dir')!
    expect(rule.delta).toBe(-0.25)

    const finding = makeFinding({ filePath: '/project/scripts/deploy.sh' })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)
  })

  it('config_env_substitution applies for env var references in secrets', () => {
    const rule = findRule('config_env_substitution')!
    expect(rule.delta).toBe(-0.15)

    const finding = makeFinding({
      category: 'hardcoded_secret',
      lineContent: 'const key = process.env.API_KEY',
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)
  })

  it('config_env_substitution does not apply for non-secret categories', () => {
    const rule = findRule('config_env_substitution')!
    const finding = makeFinding({
      category: 'missing_auth',
      lineContent: 'const key = process.env.API_KEY',
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(false)
  })

  it('comment_context applies for comment lines', () => {
    const rule = findRule('comment_context')!
    expect(rule.delta).toBe(-0.20)

    const finding = makeFinding({ lineContent: '// TODO: fix this secret key' })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)
  })

  it('comment_context does not apply for ai_pattern category', () => {
    const rule = findRule('comment_context')!
    const finding = makeFinding({
      category: 'ai_pattern',
      lineContent: '// TODO: fix this',
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(false)
  })

  it('server_only_client_finding applies for XSS in server files', () => {
    const rule = findRule('server_only_client_finding')!
    expect(rule.delta).toBe(-0.10)

    const finding = makeFinding({
      category: 'xss',
      filePath: 'src/api/handler.ts',
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)
  })
})

describe('Auto-Dismiss Equivalent Rules', () => {
  it('css_class_pattern applies for Tailwind CSS entropy findings', () => {
    const rule = findRule('css_class_pattern')!
    expect(rule.delta).toBe(-0.40)

    const finding = makeFinding({
      category: 'high_entropy_string',
      lineContent: 'className="flex grid text-lg bg-blue-500 px-4 py-2 rounded shadow"',
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)
  })

  it('css_class_pattern does not apply for non-entropy findings', () => {
    const rule = findRule('css_class_pattern')!
    const finding = makeFinding({
      category: 'hardcoded_secret',
      lineContent: 'className="flex grid text-lg bg-blue-500 px-4 py-2 rounded shadow"',
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(false)
  })

  it('health_check_endpoint applies for health checks', () => {
    const rule = findRule('health_check_endpoint')!
    expect(rule.delta).toBe(-0.35)

    const finding = makeFinding({
      category: 'missing_auth',
      lineContent: 'app.get("/health", handler)',
      filePath: 'src/routes.ts',
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)
  })

  it('info_severity_low_priority applies for info + high base confidence', () => {
    const rule = findRule('info_severity_low_priority')!
    expect(rule.delta).toBe(-0.15)

    const finding = makeFinding({
      severity: 'info',
      baseConfidence: 0.70,
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)
  })

  it('info_severity_low_priority does not apply for low base confidence', () => {
    const rule = findRule('info_severity_low_priority')!
    const finding = makeFinding({
      severity: 'info',
      baseConfidence: 0.20,
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(false)
  })

  it('low_severity_ai_pattern applies for low/info ai_pattern', () => {
    const rule = findRule('low_severity_ai_pattern')!
    expect(rule.delta).toBe(-0.10)

    const finding = makeFinding({ category: 'ai_pattern', severity: 'low' })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)

    const finding2 = makeFinding({ category: 'ai_pattern', severity: 'info' })
    expect(rule.applies(finding2, ctx)).toBe(true)
  })

  it('generic_message applies for generic UI messages', () => {
    const rule = findRule('generic_message')!
    expect(rule.delta).toBe(-0.25)

    const finding = makeFinding({
      category: 'ai_pattern',
      lineContent: 'console.log("success")',
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)
  })

  it('type_definition_any applies for .d.ts files', () => {
    const rule = findRule('type_definition_any')!
    expect(rule.delta).toBe(-0.25)

    const finding = makeFinding({
      category: 'ai_pattern',
      title: 'TypeScript any usage',
      filePath: 'types/global.d.ts',
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)
  })

  it('timeout_magic_number applies for setTimeout patterns', () => {
    const rule = findRule('timeout_magic_number')!
    expect(rule.delta).toBe(-0.20)

    const finding = makeFinding({
      category: 'ai_pattern',
      lineContent: 'setTimeout(callback, 3000)',
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)
  })

  it('data_config_file_secret applies for config file secret findings', () => {
    const rule = findRule('data_config_file_secret')!
    expect(rule).toBeDefined()
    expect(rule.delta).toBe(-0.15)

    const finding = makeFinding({
      category: 'high_entropy_string',
      filePath: 'config/defaults.yml',
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(true)

    // Also applies for JSON config files
    const jsonFinding = makeFinding({
      category: 'hardcoded_secret',
      filePath: 'data/seed.json',
    })
    expect(rule.applies(jsonFinding, ctx)).toBe(true)
  })

  it('data_config_file_secret does not apply for .env files', () => {
    const rule = findRule('data_config_file_secret')!

    const finding = makeFinding({
      category: 'hardcoded_secret',
      filePath: '.env.production',
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(false)
  })

  it('data_config_file_secret does not apply for non-secret categories', () => {
    const rule = findRule('data_config_file_secret')!

    const finding = makeFinding({
      category: 'missing_auth',
      filePath: 'config/defaults.yml',
    })
    const ctx = makeContext()
    expect(rule.applies(finding, ctx)).toBe(false)
  })
})

describe('Context Engine Rules', () => {
  it('TAINT_RULES has 4 taint adjustment rules', () => {
    expect(TAINT_RULES).toHaveLength(4)
    const ruleNames = TAINT_RULES.map(r => r.name)
    expect(ruleNames).toContain('confirmed_taint_unsanitised')
    expect(ruleNames).toContain('confirmed_taint_sanitised')
    expect(ruleNames).toContain('no_taint_path')
    expect(ruleNames).toContain('low_confidence_taint')
  })

  it('ROUTE_RULES has 5 rules', () => {
    expect(ROUTE_RULES).toHaveLength(5)
    const ruleNames = ROUTE_RULES.map(r => r.name)
    expect(ruleNames).toContain('no_auth_on_route')
    expect(ruleNames).toContain('has_auth_on_route')
    expect(ruleNames).toContain('public_route')
    expect(ruleNames).toContain('mutation_route')
    expect(ruleNames).toContain('rate_limited_route')
  })

  it('FRAMEWORK_RULES has 3 rules', () => {
    expect(FRAMEWORK_RULES).toHaveLength(3)
    const ruleNames = FRAMEWORK_RULES.map(r => r.name)
    expect(ruleNames).toContain('framework_auto_protects')
    expect(ruleNames).toContain('framework_unsafe_pattern')
    expect(ruleNames).toContain('orm_safe_query')
  })
})

describe('ALL_ADJUSTMENT_RULES', () => {
  it('contains all file context, auto-dismiss, taint, route, and framework rules', () => {
    expect(ALL_ADJUSTMENT_RULES.length).toBe(
      FILE_CONTEXT_RULES.length + AUTO_DISMISS_RULES.length + TAINT_RULES.length + ROUTE_RULES.length + FRAMEWORK_RULES.length
    )
  })

  it('each rule has required fields', () => {
    for (const rule of ALL_ADJUSTMENT_RULES) {
      expect(rule.name).toBeDefined()
      expect(typeof rule.name).toBe('string')
      expect(rule.source).toBeDefined()
      expect(typeof rule.applies).toBe('function')
      expect(typeof rule.delta).toBe('number')
      expect(typeof rule.reason).toBe('string')
    }
  })

  it('all deltas are between -1.0 and 1.0', () => {
    for (const rule of ALL_ADJUSTMENT_RULES) {
      expect(rule.delta).toBeGreaterThanOrEqual(-1.0)
      expect(rule.delta).toBeLessThanOrEqual(1.0)
    }
  })

  it('all rule names are unique', () => {
    const names = ALL_ADJUSTMENT_RULES.map(r => r.name)
    const uniqueNames = new Set(names)
    expect(uniqueNames.size).toBe(names.length)
  })
})
