/**
 * Tests for taint propagation tracker.
 */

import { propagateTaint } from '../../model/taint-tracker'
import type { UserInputSource, SanitiserInfo } from '../../model/taint-types'

function makeSource(overrides: Partial<UserInputSource> = {}): UserInputSource {
  return {
    line: 1,
    expression: 'req.body',
    variable: 'userInput',
    sourceType: 'http_body',
    confidence: 'high',
    ...overrides,
  }
}

describe('propagateTaint', () => {
  describe('Direct assignment propagation', () => {
    it('propagates taint through direct assignment', () => {
      const code = `
const userInput = req.body
const data = userInput
`
      const source = makeSource({ line: 2 })
      const result = propagateTaint(code, 'src/api.ts', [source], [])
      expect(result.some(tv => tv.name === 'data')).toBe(true)
    })

    it('propagates through multiple assignments', () => {
      const code = `
const userInput = req.body
const data = userInput
const processed = data
`
      const source = makeSource({ line: 2 })
      const result = propagateTaint(code, 'src/api.ts', [source], [])
      expect(result.some(tv => tv.name === 'data')).toBe(true)
      expect(result.some(tv => tv.name === 'processed')).toBe(true)
    })
  })

  describe('Template literal propagation', () => {
    it('propagates through template literals', () => {
      const code = `
const userInput = req.body
const query = \`SELECT * FROM users WHERE id = \${userInput}\`
`
      const source = makeSource({ line: 2 })
      const result = propagateTaint(code, 'src/api.ts', [source], [])
      expect(result.some(tv => tv.name === 'query')).toBe(true)
    })
  })

  describe('String concatenation propagation', () => {
    it('propagates through string concatenation', () => {
      const code = `
const userInput = req.body
const msg = "Hello " + userInput
`
      const source = makeSource({ line: 2 })
      const result = propagateTaint(code, 'src/api.ts', [source], [])
      expect(result.some(tv => tv.name === 'msg')).toBe(true)
    })
  })

  describe('Destructuring propagation', () => {
    it('propagates through object destructuring', () => {
      const code = `
const userInput = req.body
const { name, email } = userInput
`
      const source = makeSource({ line: 2 })
      const result = propagateTaint(code, 'src/api.ts', [source], [])
      expect(result.some(tv => tv.name === 'name')).toBe(true)
      expect(result.some(tv => tv.name === 'email')).toBe(true)
    })
  })

  describe('Method chain propagation', () => {
    it('does not propagate method chains on the same variable to a new variable', () => {
      const code = `
const userInput = req.body
const trimmed = userInput.trim()
`
      const source = makeSource({ line: 2 })
      const result = propagateTaint(code, 'src/api.ts', [source], [])
      expect(result.some(tv => tv.name === 'trimmed')).toBe(true)
    })
  })

  describe('Sanitisation detection', () => {
    it('marks variable as sanitised when Number() is applied', () => {
      const code = `
const userInput = req.body
const safeId = Number(userInput)
`
      const source = makeSource({ line: 2 })
      const result = propagateTaint(code, 'src/api.ts', [source], [])
      const safeIdVar = result.find(tv => tv.name === 'safeId')
      expect(safeIdVar).toBeDefined()
      expect(safeIdVar!.sanitised).toBe(true)
      expect(safeIdVar!.sanitisationMethod).toBe('Number')
    })

    it('marks variable as sanitised when parseInt() is applied', () => {
      const code = `
const userInput = req.body
const id = parseInt(userInput, 10)
`
      const source = makeSource({ line: 2 })
      const result = propagateTaint(code, 'src/api.ts', [source], [])
      const idVar = result.find(tv => tv.name === 'id')
      expect(idVar?.sanitised).toBe(true)
    })

    it('marks variable as sanitised when file-level sanitiser is used', () => {
      const code = `
const userInput = req.body
const safe = DOMPurify.sanitize(userInput)
`
      const source = makeSource({ line: 2 })
      const fileSanitisers: SanitiserInfo[] = [
        { name: 'DOMPurify.sanitize', line: 3, sanitises: 'html', detection: 'known_library', library: 'dompurify' }
      ]
      const result = propagateTaint(code, 'src/api.ts', [source], fileSanitisers)
      const safeVar = result.find(tv => tv.name === 'safe')
      expect(safeVar?.sanitised).toBe(true)
    })
  })

  describe('No false propagation', () => {
    it('does not taint variables with unrelated names', () => {
      const code = `
const userInput = req.body
const config = { port: 3000 }
`
      const source = makeSource({ line: 2 })
      const result = propagateTaint(code, 'src/api.ts', [source], [])
      expect(result.some(tv => tv.name === 'config')).toBe(false)
    })

    it('uses word boundary matching (url does not match urlParser)', () => {
      const code = `
const url = req.query
const urlParser = new URL("https://example.com")
`
      const source = makeSource({ line: 2, variable: 'url', expression: 'req.query', sourceType: 'http_query' })
      const result = propagateTaint(code, 'src/api.ts', [source], [])
      // urlParser should not be tainted because "url" should not match "urlParser"
      expect(result.some(tv => tv.name === 'urlParser')).toBe(false)
    })
  })

  describe('Scope tracking', () => {
    it('includes variables from nested scopes', () => {
      const code = `
const userInput = req.body
if (true) {
  const innerData = userInput
}
`
      const source = makeSource({ line: 2 })
      const result = propagateTaint(code, 'src/api.ts', [source], [])
      // innerData is created inside a block scope
      expect(result.some(tv => tv.name === 'innerData')).toBe(true)
    })
  })

  describe('Ternary/nullish propagation', () => {
    it('propagates through ternary/nullish operator', () => {
      const code = `
const userInput = req.body
const data = userInput ?? "default"
`
      const source = makeSource({ line: 2 })
      const result = propagateTaint(code, 'src/api.ts', [source], [])
      expect(result.some(tv => tv.name === 'data')).toBe(true)
    })
  })
})
