/**
 * Tests for Cross-File Taint Analysis — analyzeCrossFileTaint
 */

import { analyzeCrossFileTaint } from '../../model/cross-file-taint'
import { buildModuleGraph } from '../../model/module-graph'
import { buildFunctionProfiles } from '../../model/function-classifier'
import { discoverSources } from '../../model/source-discovery'
import { propagateTaint } from '../../model/taint-tracker'
import { matchSinks } from '../../model/sink-matcher'
import { detectSanitisers } from '../../model/sanitiser-detection'
import type { FileTaintAnalysis } from '../../model/taint-types'
import type { ScanFile } from '../../shared/types'

function makeFile(path: string, content: string): ScanFile {
  return { path, content, language: 'typescript' }
}

function buildTaintAnalyses(files: ScanFile[]): Map<string, FileTaintAnalysis> {
  const analyses = new Map<string, FileTaintAnalysis>()
  for (const file of files) {
    const sources = discoverSources(file.content, file.path)
    if (sources.length === 0) continue
    const sanitisers = detectSanitisers(file.content, file.path)
    const taintedVariables = propagateTaint(file.content, file.path, sources, sanitisers)
    const taintPaths = matchSinks(file.content, file.path, taintedVariables, sanitisers)
    analyses.set(file.path, { filePath: file.path, sources, taintedVariables, taintPaths, sanitisers })
  }
  return analyses
}

describe('analyzeCrossFileTaint', () => {
  describe('Scenario 1: Source in A, sink in imported function B', () => {
    it('detects cross-file taint from route to DB query helper', () => {
      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 graph = buildModuleGraph(files)
      const profiles = buildFunctionProfiles(files, graph)
      const analyses = buildTaintAnalyses(files)
      const paths = analyzeCrossFileTaint(files, graph, profiles, analyses)

      expect(paths.length).toBeGreaterThan(0)
      const path = paths[0]
      expect(path.sourceFile).toBe('src/routes/users.ts')
      expect(path.sinkFile).toBe('src/lib/db.ts')
      expect(path.sink.sinkType).toBe('sql_query')
      expect(path.importLink.symbolName).toBe('getUser')
      expect(path.confidence).toBe('medium')
    })

    it('detects cross-file taint with command execution', () => {
      const files = [
        makeFile('src/api/run.ts', [
          `import { execute } from '../lib/runner'`,
          ``,
          `export async function POST(req) {`,
          `  const { command } = req.body`,
          `  const result = await execute(command)`,
          `  return Response.json({ result })`,
          `}`,
        ].join('\n')),
        makeFile('src/lib/runner.ts', [
          `export function execute(cmd: string) {`,
          `  return exec(cmd)`,
          `}`,
        ].join('\n')),
      ]

      const graph = buildModuleGraph(files)
      const profiles = buildFunctionProfiles(files, graph)
      const analyses = buildTaintAnalyses(files)
      const paths = analyzeCrossFileTaint(files, graph, profiles, analyses)

      expect(paths.length).toBeGreaterThan(0)
      expect(paths[0].sink.sinkType).toBe('exec')
    })

    it('marks sanitised paths when imported function has sanitisation', () => {
      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) {`,
          `  const safeId = parseInt(id)`,
          `  return db.query('SELECT * FROM users WHERE id = ' + safeId)`,
          `}`,
        ].join('\n')),
      ]

      const graph = buildModuleGraph(files)
      const profiles = buildFunctionProfiles(files, graph)
      const analyses = buildTaintAnalyses(files)
      const paths = analyzeCrossFileTaint(files, graph, profiles, analyses)

      expect(paths.length).toBeGreaterThan(0)
      expect(paths[0].sanitised).toBe(true)
    })
  })

  describe('Scenario 2: Imported function returns user data', () => {
    it('detects when imported function return value flows to local sink', () => {
      const files = [
        makeFile('src/api/search.ts', [
          `import { getUserInput } from '../lib/input'`,
          ``,
          `export async function POST(req) {`,
          `  const data = req.body`,
          `  const input = await getUserInput(req)`,
          `  db.query('SELECT * FROM items WHERE name = ' + input)`,
          `}`,
        ].join('\n')),
        makeFile('src/lib/input.ts', [
          `export function getUserInput(req) {`,
          `  return req.body`,
          `}`,
        ].join('\n')),
      ]

      const graph = buildModuleGraph(files)
      const profiles = buildFunctionProfiles(files, graph)
      const analyses = buildTaintAnalyses(files)
      const paths = analyzeCrossFileTaint(files, graph, profiles, analyses)

      // Should detect at least the scenario 2 path
      const scenario2 = paths.filter(p => p.sinkFile === 'src/api/search.ts')
      expect(scenario2.length).toBeGreaterThan(0)
    })
  })

  describe('Safety', () => {
    it('skips files involved in circular imports', () => {
      const files = [
        makeFile('src/a.ts', [
          `import { b } from './b'`,
          `export function a(req) {`,
          `  const id = req.params.id`,
          `  return b(id)`,
          `}`,
        ].join('\n')),
        makeFile('src/b.ts', [
          `import { a } from './a'`,
          `export function b(input: string) {`,
          `  return db.query('SELECT * WHERE id = ' + input)`,
          `}`,
        ].join('\n')),
      ]

      const graph = buildModuleGraph(files)
      const profiles = buildFunctionProfiles(files, graph)
      const analyses = buildTaintAnalyses(files)
      const paths = analyzeCrossFileTaint(files, graph, profiles, analyses)

      // Should not crash and should produce no results for circular imports
      expect(paths).toBeDefined()
    })

    it('returns empty array when no taint sources exist', () => {
      const files = [
        makeFile('src/app.ts', `import { getUser } from './db'\nconst user = getUser('1')`),
        makeFile('src/db.ts', `export function getUser(id) { return db.query('SELECT * FROM users WHERE id = ' + id) }`),
      ]

      const graph = buildModuleGraph(files)
      const profiles = buildFunctionProfiles(files, graph)
      const analyses = buildTaintAnalyses(files)
      const paths = analyzeCrossFileTaint(files, graph, profiles, analyses)

      expect(paths).toEqual([])
    })

    it('handles empty file list', () => {
      const graph = buildModuleGraph([])
      const profiles = buildFunctionProfiles([], graph)
      const analyses = new Map()
      const paths = analyzeCrossFileTaint([], graph, profiles, analyses)

      expect(paths).toEqual([])
    })
  })

  describe('False positive prevention', () => {
    it('does not flag when tainted arg is passed to a non-dangerous param', () => {
      // File A passes tainted 'id' as second arg, but only first param 'query' is dangerous
      const files = [
        makeFile('src/routes/users.ts', [
          `import { searchUser } from '../lib/db'`,
          ``,
          `export async function GET(req) {`,
          `  const id = req.params.id`,
          `  const user = await searchUser('SELECT * FROM users', id)`,
          `  return Response.json(user)`,
          `}`,
        ].join('\n')),
        makeFile('src/lib/db.ts', [
          `export function searchUser(query: string, safeId: string) {`,
          `  return db.query(query)`,
          `}`,
        ].join('\n')),
      ]

      const graph = buildModuleGraph(files)
      const profiles = buildFunctionProfiles(files, graph)
      const analyses = buildTaintAnalyses(files)
      const paths = analyzeCrossFileTaint(files, graph, profiles, analyses)

      // 'id' is passed as second arg (safeId), which is NOT dangerous.
      // Only 'query' (first param) appears on the sink line.
      // With the tightened fallback, this should not produce a cross-file taint path
      // for the tainted variable 'id'.
      const idPaths = paths.filter(p => p.source.variable === 'id')
      expect(idPaths).toHaveLength(0)
    })

    it('does not flag functions with no sinks', () => {
      const files = [
        makeFile('src/routes/users.ts', [
          `import { formatUser } from '../lib/format'`,
          ``,
          `export async function GET(req) {`,
          `  const id = req.params.id`,
          `  const formatted = formatUser(id)`,
          `  return Response.json(formatted)`,
          `}`,
        ].join('\n')),
        makeFile('src/lib/format.ts', [
          `export function formatUser(id: string) {`,
          `  return { id: id, displayName: 'User ' + id }`,
          `}`,
        ].join('\n')),
      ]

      const graph = buildModuleGraph(files)
      const profiles = buildFunctionProfiles(files, graph)
      const analyses = buildTaintAnalyses(files)
      const paths = analyzeCrossFileTaint(files, graph, profiles, analyses)

      // formatUser has no sinks, so no cross-file taint path should exist
      expect(paths).toHaveLength(0)
    })

    it('does not resolve imports from node_modules', () => {
      const files = [
        makeFile('src/routes/users.ts', [
          `import { sanitize } from 'dompurify'`,
          ``,
          `export async function GET(req) {`,
          `  const input = req.body.html`,
          `  const clean = sanitize(input)`,
          `  return Response.json({ clean })`,
          `}`,
        ].join('\n')),
      ]

      const graph = buildModuleGraph(files)
      const profiles = buildFunctionProfiles(files, graph)
      const analyses = buildTaintAnalyses(files)
      const paths = analyzeCrossFileTaint(files, graph, profiles, analyses)

      // 'dompurify' is a node_modules import, should not resolve to any profiled file
      expect(paths).toHaveLength(0)
    })
  })
})
