/**
 * Tests for framework adjustment rules in the confidence scoring system.
 */

import { FRAMEWORK_RULES } from '../../score/adjustments'
import type { AdjustmentContext } from '../../score/types'
import type { Vulnerability } from '../../shared/types'

function makeFinding(overrides: Partial<Vulnerability> = {}): Vulnerability {
  return {
    id: 'test-1',
    filePath: 'src/app.tsx',
    lineNumber: 10,
    lineContent: '<div>{userData}</div>',
    severity: 'medium',
    category: 'xss',
    title: 'Test finding',
    description: 'Test',
    confidence: 'medium',
    layer: 2,
    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: '<div>{userData}</div>',
    filePath: 'src/app.tsx',
    ...overrides,
  }
}

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

describe('Framework Adjustment Rules', () => {
  it('has exactly 3 rules', () => {
    expect(FRAMEWORK_RULES).toHaveLength(3)
  })

  describe('framework_auto_protects', () => {
    const rule = findRule('framework_auto_protects')!

    it('fires when safePatternMatch is present (non-ORM)', () => {
      const ctx = makeContext({
        framework: {
          safePatternMatch: { id: 'react_jsx_auto_escape', reason: 'JSX auto-escapes' },
          unsafePatternMatch: null,
          activeFrameworks: ['react'],
          orm: null,
        },
      })
      expect(rule.applies(makeFinding(), ctx)).toBe(true)
    })

    it('does not fire when no framework context', () => {
      const ctx = makeContext()
      expect(rule.applies(makeFinding(), ctx)).toBe(false)
    })

    it('does not fire when safePatternMatch is null', () => {
      const ctx = makeContext({
        framework: {
          safePatternMatch: null,
          unsafePatternMatch: null,
          activeFrameworks: ['react'],
          orm: null,
        },
      })
      expect(rule.applies(makeFinding(), ctx)).toBe(false)
    })

    it('does not fire when safePatternMatch starts with orm_', () => {
      const ctx = makeContext({
        framework: {
          safePatternMatch: { id: 'orm_sequelize_parameterised', reason: 'ORM parameterises' },
          unsafePatternMatch: null,
          activeFrameworks: ['express'],
          orm: 'sequelize',
        },
      })
      expect(rule.applies(makeFinding(), ctx)).toBe(false)
    })

    it('fires for non-ORM patterns like nextjs_server_component', () => {
      const ctx = makeContext({
        framework: {
          safePatternMatch: { id: 'nextjs_server_component_no_dom', reason: 'Server Component' },
          unsafePatternMatch: null,
          activeFrameworks: ['nextjs'],
          orm: null,
        },
      })
      expect(rule.applies(makeFinding(), ctx)).toBe(true)
    })

    it('has delta -0.30', () => {
      expect(rule.delta).toBe(-0.30)
    })

    it('has source "framework"', () => {
      expect(rule.source).toBe('framework')
    })
  })

  describe('framework_unsafe_pattern', () => {
    const rule = findRule('framework_unsafe_pattern')!

    it('fires when unsafePatternMatch is present', () => {
      const ctx = makeContext({
        framework: {
          safePatternMatch: null,
          unsafePatternMatch: { id: 'react_dangerous_inner_html', reason: 'bypasses escaping' },
          activeFrameworks: ['react'],
          orm: null,
        },
      })
      expect(rule.applies(makeFinding(), ctx)).toBe(true)
    })

    it('does not fire when unsafePatternMatch is null', () => {
      const ctx = makeContext({
        framework: {
          safePatternMatch: null,
          unsafePatternMatch: null,
          activeFrameworks: ['react'],
          orm: null,
        },
      })
      expect(rule.applies(makeFinding(), ctx)).toBe(false)
    })

    it('has delta +0.10', () => {
      expect(rule.delta).toBe(+0.10)
    })
  })

  describe('orm_safe_query', () => {
    const rule = findRule('orm_safe_query')!

    it('fires for sql_injection with ORM safe pattern', () => {
      const finding = makeFinding({ category: 'sql_injection' })
      const ctx = makeContext({
        framework: {
          safePatternMatch: { id: 'orm_sequelize_parameterised', reason: 'ORM parameterises' },
          unsafePatternMatch: null,
          activeFrameworks: ['express'],
          orm: 'sequelize',
        },
      })
      expect(rule.applies(finding, ctx)).toBe(true)
    })

    it('fires for dangerous_function with ORM safe pattern', () => {
      const finding = makeFinding({ category: 'dangerous_function' })
      const ctx = makeContext({
        framework: {
          safePatternMatch: { id: 'orm_django_parameterised', reason: 'Django ORM parameterises' },
          unsafePatternMatch: null,
          activeFrameworks: ['django'],
          orm: 'django',
        },
      })
      expect(rule.applies(finding, ctx)).toBe(true)
    })

    it('does not fire for non-SQL categories', () => {
      const finding = makeFinding({ category: 'xss' })
      const ctx = makeContext({
        framework: {
          safePatternMatch: { id: 'orm_sequelize_parameterised', reason: 'ORM parameterises' },
          unsafePatternMatch: null,
          activeFrameworks: ['express'],
          orm: 'sequelize',
        },
      })
      expect(rule.applies(finding, ctx)).toBe(false)
    })

    it('does not fire without ORM', () => {
      const finding = makeFinding({ category: 'sql_injection' })
      const ctx = makeContext({
        framework: {
          safePatternMatch: { id: 'orm_sequelize_parameterised', reason: 'ORM parameterises' },
          unsafePatternMatch: null,
          activeFrameworks: ['express'],
          orm: null,
        },
      })
      expect(rule.applies(finding, ctx)).toBe(false)
    })

    it('does not fire when safe pattern is not ORM-prefixed', () => {
      const finding = makeFinding({ category: 'sql_injection' })
      const ctx = makeContext({
        framework: {
          safePatternMatch: { id: 'react_jsx_auto_escape', reason: 'JSX escapes' },
          unsafePatternMatch: null,
          activeFrameworks: ['react'],
          orm: 'sequelize',
        },
      })
      expect(rule.applies(finding, ctx)).toBe(false)
    })

    it('has delta -0.25', () => {
      expect(rule.delta).toBe(-0.25)
    })
  })

  describe('rule interactions', () => {
    it('ORM patterns: only orm_safe_query fires (not framework_auto_protects)', () => {
      const finding = makeFinding({ category: 'sql_injection' })
      const ctx = makeContext({
        framework: {
          safePatternMatch: { id: 'orm_sequelize_parameterised', reason: 'ORM parameterises' },
          unsafePatternMatch: null,
          activeFrameworks: ['express'],
          orm: 'sequelize',
        },
      })
      const autoProtects = findRule('framework_auto_protects')!
      const ormSafe = findRule('orm_safe_query')!
      // framework_auto_protects should NOT fire for ORM patterns
      expect(autoProtects.applies(finding, ctx)).toBe(false)
      // orm_safe_query should fire
      expect(ormSafe.applies(finding, ctx)).toBe(true)
      // Only -0.25, not -0.55
      expect(ormSafe.delta).toBe(-0.25)
    })

    it('non-ORM patterns: framework_auto_protects fires normally', () => {
      const finding = makeFinding({ category: 'xss' })
      const ctx = makeContext({
        framework: {
          safePatternMatch: { id: 'react_jsx_auto_escape', reason: 'JSX escapes' },
          unsafePatternMatch: null,
          activeFrameworks: ['react'],
          orm: null,
        },
      })
      const autoProtects = findRule('framework_auto_protects')!
      const ormSafe = findRule('orm_safe_query')!
      // framework_auto_protects fires for non-ORM safe patterns
      expect(autoProtects.applies(finding, ctx)).toBe(true)
      // orm_safe_query does NOT fire (wrong category and not ORM-prefixed)
      expect(ormSafe.applies(finding, ctx)).toBe(false)
    })

    it('no framework rules fire without framework context', () => {
      const finding = makeFinding()
      const ctx = makeContext()
      for (const rule of FRAMEWORK_RULES) {
        expect(rule.applies(finding, ctx)).toBe(false)
      }
    })
  })
})
