/**
 * Integration tests for the full taint analysis pipeline.
 * Tests end-to-end from source discovery through sink matching,
 * and cross-file analysis through buildProjectModel.
 */

import { discoverSources } from '../../model/source-discovery'
import { detectSanitisers } from '../../model/sanitiser-detection'
import { propagateTaint } from '../../model/taint-tracker'
import { matchSinks } from '../../model/sink-matcher'
import type { FileTaintAnalysis } from '../../model/taint-types'
import { buildProjectModel } from '../../model/index'
import { buildAdjustmentContext } from '../../score/index'
import type { ScanFile, Vulnerability } from '../../shared/types'

function analyzeFileTaint(content: string, filePath: string): FileTaintAnalysis {
  const sources = discoverSources(content, filePath)
  if (sources.length === 0) {
    return { filePath, sources: [], taintedVariables: [], taintPaths: [], sanitisers: [] }
  }
  const sanitisers = detectSanitisers(content, filePath)
  const taintedVariables = propagateTaint(content, filePath, sources, sanitisers)
  const taintPaths = matchSinks(content, filePath, taintedVariables, sanitisers)
  return { filePath, sources, taintedVariables, taintPaths, sanitisers }
}

describe('Taint Analysis Integration', () => {
  describe('SQL injection (Juice Shop style)', () => {
    it('detects unsanitised req.query flowing into sequelize.query', () => {
      const code = `
app.get('/api/products', (req, res) => {
  const searchTerm = req.query.q
  const query = "SELECT * FROM Products WHERE name LIKE '%" + searchTerm + "%'"
  sequelize.query(query)
})
`
      const result = analyzeFileTaint(code, 'src/routes/products.ts')
      expect(result.sources.length).toBeGreaterThan(0)
      expect(result.taintPaths.length).toBeGreaterThan(0)
      expect(result.taintPaths.some(p =>
        p.sink.sinkType === 'sql_query' && !p.sanitised
      )).toBe(true)
    })
  })

  describe('XSS via innerHTML', () => {
    it('detects unsanitised req.body flowing into innerHTML', () => {
      const code = `
app.post('/render', (req, res) => {
  const content = req.body.html
  const output = "<div>" + content + "</div>"
  el.innerHTML = output
})
`
      const result = analyzeFileTaint(code, 'src/routes/render.ts')
      expect(result.taintPaths.some(p =>
        p.sink.sinkType === 'inner_html' && !p.sanitised
      )).toBe(true)
    })
  })

  describe('Sanitised SQL (parameterised query)', () => {
    it('marks taint path as sanitised for parameterised queries', () => {
      const code = `
app.get('/api/users', (req, res) => {
  const userId = req.query.id
  db.query('SELECT * FROM users WHERE id = ?', [userId])
})
`
      const result = analyzeFileTaint(code, 'src/routes/users.ts')
      if (result.taintPaths.length > 0) {
        expect(result.taintPaths.every(p =>
          p.sink.sinkType === 'sql_query' ? p.sanitised : true
        )).toBe(true)
      }
    })
  })

  describe('Sanitised XSS (DOMPurify)', () => {
    it('marks taint path as sanitised when DOMPurify is used', () => {
      const code = `
import DOMPurify from 'dompurify'

app.post('/render', (req, res) => {
  const content = req.body.html
  const safe = DOMPurify.sanitize(content)
  el.innerHTML = safe
})
`
      const result = analyzeFileTaint(code, 'src/routes/render.ts')
      // DOMPurify.sanitize marks the variable as sanitised
      // The sink matcher should not pick it up since safe is sanitised
      const unsanitisedPaths = result.taintPaths.filter(p =>
        p.sink.sinkType === 'inner_html' && !p.sanitised
      )
      expect(unsanitisedPaths).toHaveLength(0)
    })
  })

  describe('Command injection', () => {
    it('detects unsanitised user input flowing into exec()', () => {
      const code = `
app.post('/exec', (req, res) => {
  const cmd = req.body.command
  exec(cmd)
})
`
      const result = analyzeFileTaint(code, 'src/routes/exec.ts')
      expect(result.taintPaths.some(p =>
        p.sink.sinkType === 'exec' && !p.sanitised
      )).toBe(true)
    })
  })

  describe('SSRF', () => {
    it('detects unsanitised user input flowing into fetch()', () => {
      const code = `
app.post('/proxy', (req, res) => {
  const url = req.body.url
  fetch(url)
})
`
      const result = analyzeFileTaint(code, 'src/routes/proxy.ts')
      expect(result.taintPaths.some(p =>
        p.sink.sinkType === 'http_request' && !p.sanitised
      )).toBe(true)
    })
  })

  describe('Type coercion sanitisation', () => {
    it('marks Number() coercion as sanitised', () => {
      const code = `
app.get('/api/users', (req, res) => {
  const userId = Number(req.query.id)
  db.query("SELECT * FROM users WHERE id = " + userId)
})
`
      const result = analyzeFileTaint(code, 'src/routes/users.ts')
      // userId should be sanitised by Number()
      // so it shouldn't produce an unsanitised taint path
      const unsanitisedSql = result.taintPaths.filter(p =>
        p.sink.sinkType === 'sql_query' && !p.sanitised
      )
      expect(unsanitisedSql).toHaveLength(0)
    })
  })

  describe('Short-circuit for non-input files', () => {
    it('returns empty analysis for config files', () => {
      const code = `
export default {
  port: 3000,
  host: 'localhost',
  database: {
    url: process.env.DATABASE_URL,
  },
}
`
      const result = analyzeFileTaint(code, 'src/config.ts')
      expect(result.sources).toHaveLength(0)
      expect(result.taintPaths).toHaveLength(0)
    })
  })

  describe('Multi-step propagation', () => {
    it('tracks taint through multiple assignment steps', () => {
      const code = `
app.post('/api', (req, res) => {
  const body = req.body
  const data = body
  const query = "SELECT * FROM users WHERE name = '" + data + "'"
  db.query(query)
})
`
      const result = analyzeFileTaint(code, 'src/routes/api.ts')
      expect(result.taintPaths.some(p =>
        p.sink.sinkType === 'sql_query' && !p.sanitised
      )).toBe(true)
    })
  })

  describe('Django pattern', () => {
    it('detects unsanitised request.GET in Python', () => {
      const code = `
def search(request):
    query = request.GET
    cursor.execute("SELECT * FROM items WHERE name = '" + query + "'")
`
      const result = analyzeFileTaint(code, 'app/views.py')
      expect(result.sources.length).toBeGreaterThan(0)
      // Python cursor.execute should match sql_query sink
      expect(result.taintPaths.some(p => p.sink.sinkType === 'sql_query')).toBe(true)
    })
  })

  describe('Framework-aware analysis integration', () => {
    function makeFile(path: string, content: string): ScanFile {
      return { path, content, language: 'typescript' }
    }

    it('populates frameworkAnalyses for React + Sequelize project', () => {
      const files = [
        makeFile('src/app/page.tsx', [
          'import { getUsers } from "../lib/db"',
          '',
          'export default function Page() {',
          '  return <div>{users.map(u => u.name)}</div>',
          '}',
        ].join('\n')),
        makeFile('src/lib/db.ts', [
          'import { Sequelize } from "sequelize"',
          'import { User } from "../models"',
          '',
          'export function getUsers() {',
          '  return User.findAll({ where: { active: true } })',
          '}',
        ].join('\n')),
        // Include a package.json to trigger framework detection
        makeFile('package.json', JSON.stringify({
          dependencies: { react: '18.0.0', next: '14.0.0', sequelize: '6.0.0' },
        })),
      ]

      const model = buildProjectModel(files)

      // Verify frameworkAnalyses is populated
      expect(model.contextEngine.frameworkAnalyses).toBeDefined()
      expect(model.contextEngine.frameworkAnalyses!.size).toBeGreaterThan(0)

      // Verify JSX auto-escape detected in page.tsx
      const pageAnalysis = model.contextEngine.frameworkAnalyses!.get('src/app/page.tsx')
      expect(pageAnalysis).toBeDefined()
      expect(pageAnalysis!.safeLines.size).toBeGreaterThan(0)

      // Verify Sequelize ORM detected in db.ts
      const dbAnalysis = model.contextEngine.frameworkAnalyses!.get('src/lib/db.ts')
      expect(dbAnalysis).toBeDefined()
      expect(dbAnalysis!.safeLines.size).toBeGreaterThan(0)

      // Verify scoring picks up framework context for a mock finding
      const mockFinding: Vulnerability = {
        id: 'test-fw-1',
        filePath: 'src/lib/db.ts',
        lineNumber: 5,
        lineContent: '  return User.findAll({ where: { active: true } })',
        category: 'sql_injection',
        severity: 'medium',
        message: 'Possible SQL injection',
        layer: 2,
        source: 'dangerous_functions',
      }

      const adjContext = buildAdjustmentContext(mockFinding, model.contextEngine)
      expect(dbAnalysis!.safeLines.has(5)).toBe(true)
      expect(adjContext.framework).toBeDefined()
      expect(adjContext.framework!.safePatternMatch).not.toBeNull()
      expect(adjContext.framework!.safePatternMatch!.id).toContain('orm_')
    })
  })

  describe('Cross-file pipeline integration', () => {
    function makeFile(path: string, content: string): ScanFile {
      return { path, content, language: 'typescript' }
    }

    it('detects cross-file taint through buildProjectModel and scoring', () => {
      const files = [
        makeFile('src/routes/users.ts', [
          `import { getUser } from '../lib/db'`,
          ``,
          `export async function GET(req) {`,
          `  const id = req.params.id`,
          `  const user = await getUser(id)`,
          `  return Response.json(user)`,
          `}`,
        ].join('\n')),
        makeFile('src/lib/db.ts', [
          `export function getUser(id: string) {`,
          `  return db.query('SELECT * FROM users WHERE id = ' + id)`,
          `}`,
        ].join('\n')),
      ]

      const model = buildProjectModel(files)

      // Verify cross-file taint paths are populated
      expect(model.contextEngine.crossFileTaintPaths).toBeDefined()
      expect(model.contextEngine.crossFileTaintPaths!.length).toBeGreaterThan(0)

      const cfPath = model.contextEngine.crossFileTaintPaths![0]
      expect(cfPath.sink.sinkType).toBe('sql_query')
      expect(cfPath.sourceFile).toBe('src/routes/users.ts')
      expect(cfPath.sinkFile).toBe('src/lib/db.ts')

      // Verify scoring annotation picks up cross-file taint at the call site.
      // A Layer 2 detector might flag the call line in users.ts. The scoring system
      // should match the cross-file path via sourceFile + callLine.
      const mockFinding: Vulnerability = {
        id: 'test-1',
        filePath: cfPath.sourceFile,
        lineNumber: cfPath.importLink.callLine,
        lineContent: 'const user = await getUser(id)',
        category: 'sql_injection',
        severity: 'high',
        message: 'SQL injection via cross-file taint',
        layer: 2,
        source: 'dangerous_functions',
      }

      const adjContext = buildAdjustmentContext(mockFinding, model.contextEngine)
      expect(adjContext.taint).toBeDefined()
      expect(adjContext.taint!.confirmed).toBe(true)
      expect(adjContext.taint!.sinkType).toBe('sql_query')

      // Verify function profiles are populated
      expect(model.contextEngine.functionProfiles).toBeDefined()
      expect(model.contextEngine.functionProfiles!.size).toBeGreaterThan(0)
    })
  })
})
