/**
 * Tests for sink matching — matching tainted variables to dangerous sinks.
 */

import { matchSinks } from '../../model/sink-matcher'
import type { TaintedVariable, 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,
  }
}

function makeTaintedVar(name: string, overrides: Partial<TaintedVariable> = {}): TaintedVariable {
  return {
    name,
    taintedAt: 2,
    source: makeSource(),
    propagation: 'direct_assignment',
    sanitised: false,
    scopeDepth: 0,
    ...overrides,
  }
}

describe('matchSinks', () => {
  describe('SQL injection', () => {
    it('matches tainted variable in .query() call', () => {
      const code = `
const userInput = req.body
db.query("SELECT * FROM users WHERE id = " + userInput)
`
      const taintedVars = [makeTaintedVar('userInput')]
      const paths = matchSinks(code, 'src/db.ts', taintedVars, [])
      expect(paths).toHaveLength(1)
      expect(paths[0].sink.sinkType).toBe('sql_query')
      expect(paths[0].sanitised).toBe(false)
      expect(paths[0].confidence).toBe('high')
    })

    it('marks parameterised queries as sanitised', () => {
      const code = `
const userId = req.query
db.query('SELECT * FROM users WHERE id = ?', [userId])
`
      const taintedVars = [makeTaintedVar('userId')]
      const paths = matchSinks(code, 'src/db.ts', taintedVars, [])
      expect(paths).toHaveLength(1)
      expect(paths[0].sanitised).toBe(true)
    })
  })

  describe('XSS', () => {
    it('matches tainted variable in innerHTML assignment', () => {
      const code = `
const userInput = req.body
el.innerHTML = userInput
`
      const taintedVars = [makeTaintedVar('userInput')]
      const paths = matchSinks(code, 'src/render.ts', taintedVars, [])
      expect(paths).toHaveLength(1)
      expect(paths[0].sink.sinkType).toBe('inner_html')
    })

    it('matches dangerouslySetInnerHTML', () => {
      const code = `
const userInput = req.body
return <div dangerouslySetInnerHTML={{ __html: userInput }} />
`
      const taintedVars = [makeTaintedVar('userInput')]
      const paths = matchSinks(code, 'src/component.tsx', taintedVars, [])
      expect(paths).toHaveLength(1)
      expect(paths[0].sink.sinkType).toBe('inner_html')
    })
  })

  describe('Command injection', () => {
    it('matches tainted variable in exec() call', () => {
      const code = `
const cmd = req.body
exec(cmd)
`
      const taintedVars = [makeTaintedVar('cmd')]
      const paths = matchSinks(code, 'src/utils.ts', taintedVars, [])
      expect(paths).toHaveLength(1)
      expect(paths[0].sink.sinkType).toBe('exec')
    })

    it('matches Python subprocess.run()', () => {
      const code = `
cmd = request.args
subprocess.run(cmd, shell=True)
`
      const taintedVars = [makeTaintedVar('cmd')]
      const paths = matchSinks(code, 'app/utils.py', taintedVars, [])
      expect(paths).toHaveLength(1)
      expect(paths[0].sink.sinkType).toBe('command_exec')
    })
  })

  describe('SSRF', () => {
    it('matches tainted variable in fetch() call', () => {
      const code = `
const url = req.body
fetch(url)
`
      const taintedVars = [makeTaintedVar('url')]
      const paths = matchSinks(code, 'src/api.ts', taintedVars, [])
      expect(paths).toHaveLength(1)
      expect(paths[0].sink.sinkType).toBe('http_request')
    })

    it('matches tainted variable in axios call', () => {
      const code = `
const url = req.body
axios.get(url)
`
      const taintedVars = [makeTaintedVar('url')]
      const paths = matchSinks(code, 'src/api.ts', taintedVars, [])
      expect(paths).toHaveLength(1)
      expect(paths[0].sink.sinkType).toBe('http_request')
    })
  })

  describe('Code execution', () => {
    it('matches tainted variable in eval()', () => {
      const code = `
const code = req.body
eval(code)
`
      const taintedVars = [makeTaintedVar('code')]
      const paths = matchSinks(code, 'src/api.ts', taintedVars, [])
      expect(paths).toHaveLength(1)
      expect(paths[0].sink.sinkType).toBe('eval')
    })
  })

  describe('File system', () => {
    it('matches tainted variable in fs.readFile', () => {
      const code = `
const filePath = req.body
fs.readFile(filePath)
`
      const taintedVars = [makeTaintedVar('filePath')]
      const paths = matchSinks(code, 'src/fs.ts', taintedVars, [])
      expect(paths.some(p => p.sink.sinkType === 'path_access')).toBe(true)
    })
  })

  describe('Redirect', () => {
    it('matches tainted variable in res.redirect()', () => {
      const code = `
const target = req.body
res.redirect(target)
`
      const taintedVars = [makeTaintedVar('target')]
      const paths = matchSinks(code, 'src/routes.ts', taintedVars, [])
      expect(paths).toHaveLength(1)
      expect(paths[0].sink.sinkType).toBe('redirect')
    })
  })

  describe('Sanitised paths', () => {
    it('marks path as sanitised when sanitiser wraps sink', () => {
      const code = `
const userInput = req.body
const id = Number(userInput)
db.query("SELECT * FROM users WHERE id = " + id)
`
      // id is sanitised (Number)
      const taintedVars = [
        makeTaintedVar('userInput'),
        makeTaintedVar('id', { sanitised: true, sanitisedAt: 3, sanitisationMethod: 'Number' }),
      ]
      const paths = matchSinks(code, 'src/db.ts', taintedVars, [])
      // Only unsanitised vars should match sinks
      // 'id' is sanitised so it shouldn't be in activeTaintedVars
      // 'userInput' doesn't appear on the query line, only 'id' does
      expect(paths).toHaveLength(0)
    })
  })

  describe('Static data (no match)', () => {
    it('does not create path when no tainted variable reaches sink', () => {
      const code = `
const hardcodedId = "12345"
db.query("SELECT * FROM users WHERE id = " + hardcodedId)
`
      const taintedVars: TaintedVariable[] = []
      const paths = matchSinks(code, 'src/db.ts', taintedVars, [])
      expect(paths).toHaveLength(0)
    })
  })

  describe('Confidence computation', () => {
    it('assigns high confidence for short chain with high-confidence source', () => {
      const code = `
const userInput = req.body
db.query(userInput)
`
      const taintedVars = [makeTaintedVar('userInput')]
      const paths = matchSinks(code, 'src/db.ts', taintedVars, [])
      expect(paths[0].confidence).toBe('high')
    })

    it('assigns medium confidence for medium-confidence source', () => {
      const code = `
const userInput = searchParams
db.query(userInput)
`
      const taintedVars = [makeTaintedVar('userInput', {
        source: makeSource({ confidence: 'medium' }),
      })]
      const paths = matchSinks(code, 'src/db.ts', taintedVars, [])
      expect(paths[0].confidence).toBe('medium')
    })

    it('assigns low confidence for low-confidence source', () => {
      const code = `
const args = process.argv
db.query(args)
`
      const taintedVars = [makeTaintedVar('args', {
        source: makeSource({ confidence: 'low' }),
      })]
      const paths = matchSinks(code, 'src/db.ts', taintedVars, [])
      expect(paths[0].confidence).toBe('low')
    })
  })

  describe('Deduplication', () => {
    it('deduplicates paths with same source, sink type, and line', () => {
      const code = `
const userInput = req.body
db.query(userInput)
`
      // Two tainted vars pointing to the same source
      const taintedVars = [
        makeTaintedVar('userInput'),
      ]
      const paths = matchSinks(code, 'src/db.ts', taintedVars, [])
      // Should only produce one path
      expect(paths).toHaveLength(1)
    })
  })
})
