/**
 * Tests for framework security models.
 * Validates that each model's safe/unsafe pattern detection works correctly.
 */

import { reactModel } from '../../model/framework-models/react'
import { nextjsModel } from '../../model/framework-models/nextjs'
import { expressModel } from '../../model/framework-models/express'
import { djangoModel } from '../../model/framework-models/django'
import { sequelizeModel } from '../../model/framework-models/sequelize'
import { prismaModel } from '../../model/framework-models/prisma'
import { analyzeFrameworkPatterns } from '../../model/framework-models'
import type { ProjectContext } from '../../model/project-context'
import type { ScanFile } from '../../shared/types'

// ============================================================================
// React Model
// ============================================================================

describe('React Framework Model', () => {
  describe('safe patterns', () => {
    it('detects JSX expression auto-escaping in .tsx files', () => {
      const safe = reactModel.safePatterns.find(p => p.id === 'react_jsx_auto_escape')!
      expect(safe.detect('  return <div>{userName}</div>', 'src/App.tsx', '')).toBe(true)
    })

    it('does not detect JSX auto-escape in non-JSX files', () => {
      const safe = reactModel.safePatterns.find(p => p.id === 'react_jsx_auto_escape')!
      expect(safe.detect('  return <div>{userName}</div>', 'src/App.ts', '')).toBe(false)
    })

    it('does not mark dangerouslySetInnerHTML as safe', () => {
      const safe = reactModel.safePatterns.find(p => p.id === 'react_jsx_auto_escape')!
      expect(safe.detect('  <div dangerouslySetInnerHTML={{ __html: data }} />', 'src/App.tsx', '')).toBe(false)
    })

    it('detects React.createElement as safe', () => {
      const safe = reactModel.safePatterns.find(p => p.id === 'react_create_element_safe')!
      expect(safe.detect('  React.createElement("div", null, text)', 'src/App.tsx', '')).toBe(true)
    })

    it('does not match object literals in .tsx files (no JSX context)', () => {
      const safe = reactModel.safePatterns.find(p => p.id === 'react_jsx_auto_escape')!
      expect(safe.detect('  const obj = { foo: bar }', 'src/App.tsx', '')).toBe(false)
    })

    it('does not match destructuring in .tsx files (no JSX context)', () => {
      const safe = reactModel.safePatterns.find(p => p.id === 'react_jsx_auto_escape')!
      expect(safe.detect('  const { id } = params', 'src/App.tsx', '')).toBe(false)
    })

    it('does not match function bodies in .tsx files (no JSX context)', () => {
      const safe = reactModel.safePatterns.find(p => p.id === 'react_jsx_auto_escape')!
      expect(safe.detect('  function handleClick() { setCount(prev + 1) }', 'src/App.tsx', '')).toBe(false)
    })
  })

  describe('unsafe patterns', () => {
    it('detects dangerouslySetInnerHTML', () => {
      const unsafe = reactModel.unsafePatterns.find(p => p.id === 'react_dangerous_inner_html')!
      expect(unsafe.detect('  <div dangerouslySetInnerHTML={{ __html: userContent }} />', 'src/App.tsx', '')).toBe(true)
    })

    it('detects ref.innerHTML assignment', () => {
      const unsafe = reactModel.unsafePatterns.find(p => p.id === 'react_ref_inner_html')!
      expect(unsafe.detect('  divRef.current.innerHTML = html', 'src/App.tsx', '')).toBe(true)
    })
  })
})

// ============================================================================
// Next.js Model
// ============================================================================

describe('Next.js Framework Model', () => {
  describe('safe patterns', () => {
    it('detects Server Component files in app/ directory', () => {
      const safe = nextjsModel.safePatterns.find(p => p.id === 'nextjs_server_component_no_dom')!
      const serverContent = 'export default function Page() {\n  return <div>Hello</div>\n}'
      expect(safe.detect('export default function Page() {', 'src/app/page.tsx', serverContent)).toBe(true)
    })

    it('does not mark non-app directory files as Server Components', () => {
      const safe = nextjsModel.safePatterns.find(p => p.id === 'nextjs_server_component_no_dom')!
      expect(safe.detect('export default function Page() {', 'src/pages/index.tsx', '')).toBe(false)
    })

    it('does not mark non-tsx/jsx files as Server Components', () => {
      const safe = nextjsModel.safePatterns.find(p => p.id === 'nextjs_server_component_no_dom')!
      expect(safe.detect('export default function handler() {', 'src/app/api/route.ts', '')).toBe(false)
    })

    it('does not mark "use client" files as Server Components', () => {
      const safe = nextjsModel.safePatterns.find(p => p.id === 'nextjs_server_component_no_dom')!
      const clientContent = "'use client'\n\nexport default function ClientPage() {\n  return <div>Hello</div>\n}"
      expect(safe.detect('export default function ClientPage() {', 'src/app/page.tsx', clientContent)).toBe(false)
    })

    it('does not mark "use client" (double quotes) files as Server Components', () => {
      const safe = nextjsModel.safePatterns.find(p => p.id === 'nextjs_server_component_no_dom')!
      const clientContent = '"use client"\n\nexport default function ClientPage() {\n  return <div>Hello</div>\n}'
      expect(safe.detect('export default function ClientPage() {', 'src/app/page.tsx', clientContent)).toBe(false)
    })
  })

  describe('unsafe patterns', () => {
    it('detects searchParams as user input', () => {
      const unsafe = nextjsModel.unsafePatterns.find(p => p.id === 'nextjs_search_params_user_input')!
      expect(unsafe.detect('  const q = searchParams.get("q")', 'src/app/search/page.tsx', '')).toBe(true)
    })

    it('detects params destructuring as user input', () => {
      const unsafe = nextjsModel.unsafePatterns.find(p => p.id === 'nextjs_params_user_input')!
      expect(unsafe.detect('  const { params } = props', 'src/app/[id]/page.tsx', '')).toBe(true)
    })

    it('detects params.id access as user input', () => {
      const unsafe = nextjsModel.unsafePatterns.find(p => p.id === 'nextjs_params_user_input')!
      expect(unsafe.detect('  const id = params.id', 'src/app/[id]/page.tsx', '')).toBe(true)
    })

    it('detects FormData in Server Actions', () => {
      const unsafe = nextjsModel.unsafePatterns.find(p => p.id === 'nextjs_server_action_form_data')!
      const serverActionContent = "'use server'\n\nexport async function createUser(formData: FormData) {\n  const name = formData.get('name')\n}"
      expect(unsafe.detect('  const name = formData.get("name")', 'src/app/actions.ts', serverActionContent)).toBe(true)
    })

    it('does not detect FormData in non-server-action files', () => {
      const unsafe = nextjsModel.unsafePatterns.find(p => p.id === 'nextjs_server_action_form_data')!
      const clientContent = "import { useState } from 'react'\n\nfunction Form() {\n  const formData = new FormData()\n}"
      expect(unsafe.detect('  const formData = new FormData()', 'src/components/Form.tsx', clientContent)).toBe(false)
    })
  })
})

// ============================================================================
// Express Model
// ============================================================================

describe('Express Framework Model', () => {
  describe('safe patterns', () => {
    it('detects res.json() as safe from XSS', () => {
      const safe = expressModel.safePatterns.find(p => p.id === 'express_res_json_safe')!
      expect(safe.detect('  res.json({ user: data })', 'src/routes/api.ts', '')).toBe(true)
    })

    it('detects helmet middleware as safe', () => {
      const safe = expressModel.safePatterns.find(p => p.id === 'express_helmet_headers')!
      expect(safe.detect('app.use(helmet())', 'src/app.ts', '')).toBe(true)
    })
  })

  describe('unsafe patterns', () => {
    it('detects template literal in res.send()', () => {
      const unsafe = expressModel.unsafePatterns.find(p => p.id === 'express_res_send_template')!
      expect(unsafe.detect('  res.send(`<h1>${title}</h1>`)', 'src/routes/page.ts', '')).toBe(true)
    })

    it('detects string concatenation in res.send()', () => {
      const unsafe = expressModel.unsafePatterns.find(p => p.id === 'express_res_send_concat')!
      expect(unsafe.detect('  res.send("<h1>" + title + "</h1>")', 'src/routes/page.ts', '')).toBe(true)
    })
  })
})

// ============================================================================
// Django Model
// ============================================================================

describe('Django Framework Model', () => {
  describe('safe patterns', () => {
    it('detects Django ORM filter() as safe', () => {
      const safe = djangoModel.safePatterns.find(p => p.id === 'orm_django_parameterised')!
      expect(safe.detect('  users = User.objects.filter(name=query)', 'app/views.py', '')).toBe(true)
    })

    it('detects Django ORM get() as safe', () => {
      const safe = djangoModel.safePatterns.find(p => p.id === 'orm_django_parameterised')!
      expect(safe.detect('  user = User.objects.get(id=pk)', 'app/views.py', '')).toBe(true)
    })

    it('does not detect ORM in non-Python files', () => {
      const safe = djangoModel.safePatterns.find(p => p.id === 'orm_django_parameterised')!
      expect(safe.detect('  User.objects.filter(name=query)', 'app/views.ts', '')).toBe(false)
    })

    it('detects Django template auto-escaping', () => {
      const safe = djangoModel.safePatterns.find(p => p.id === 'django_template_auto_escape')!
      expect(safe.detect('  <p>{{ user.name }}</p>', 'templates/profile.html', '')).toBe(true)
    })

    it('does not mark |safe filter as auto-escaped', () => {
      const safe = djangoModel.safePatterns.find(p => p.id === 'django_template_auto_escape')!
      expect(safe.detect('  <p>{{ user.bio | safe }}</p>', 'templates/profile.html', '')).toBe(false)
    })
  })

  describe('unsafe patterns', () => {
    it('detects |safe template filter', () => {
      const unsafe = djangoModel.unsafePatterns.find(p => p.id === 'django_safe_filter')!
      expect(unsafe.detect('  {{ content | safe }}', 'templates/page.html', '')).toBe(true)
    })

    it('detects .extra() raw SQL', () => {
      const unsafe = djangoModel.unsafePatterns.find(p => p.id === 'django_extra_raw_sql')!
      expect(unsafe.detect('  qs = User.objects.extra(where=["name=%s"])', 'app/views.py', '')).toBe(true)
    })

    it('detects .raw() queries', () => {
      const unsafe = djangoModel.unsafePatterns.find(p => p.id === 'django_raw_query')!
      expect(unsafe.detect('  User.objects.raw("SELECT * FROM users WHERE id=" + uid)', 'app/views.py', '')).toBe(true)
    })

    it('detects cursor().execute()', () => {
      const unsafe = djangoModel.unsafePatterns.find(p => p.id === 'django_cursor_execute')!
      expect(unsafe.detect('  connection.cursor().execute(sql)', 'app/views.py', '')).toBe(true)
    })

    it('detects mark_safe()', () => {
      const unsafe = djangoModel.unsafePatterns.find(p => p.id === 'django_mark_safe')!
      expect(unsafe.detect('  return mark_safe(html)', 'app/templatetags/custom.py', '')).toBe(true)
    })
  })
})

// ============================================================================
// Sequelize Model
// ============================================================================

describe('Sequelize Framework Model', () => {
  describe('safe patterns', () => {
    it('detects findAll() as safe', () => {
      const safe = sequelizeModel.safePatterns.find(p => p.id === 'orm_sequelize_parameterised')!
      expect(safe.detect('  const users = await User.findAll({ where: { name } })', 'src/db.ts', '')).toBe(true)
    })

    it('detects findOne() as safe', () => {
      const safe = sequelizeModel.safePatterns.find(p => p.id === 'orm_sequelize_parameterised')!
      expect(safe.detect('  const user = await User.findOne({ where: { id } })', 'src/db.ts', '')).toBe(true)
    })

    it('detects create() as safe', () => {
      const safe = sequelizeModel.safePatterns.find(p => p.id === 'orm_sequelize_parameterised')!
      expect(safe.detect('  await User.create({ name, email })', 'src/db.ts', '')).toBe(true)
    })

    it('detects query with replacements as safe', () => {
      const safe = sequelizeModel.safePatterns.find(p => p.id === 'orm_sequelize_replacements')!
      expect(safe.detect("  sequelize.query('SELECT * FROM users WHERE id = ?', { replacements: [id] })", 'src/db.ts', '')).toBe(true)
    })
  })

  describe('unsafe patterns', () => {
    it('detects raw query with template literal', () => {
      const unsafe = sequelizeModel.unsafePatterns.find(p => p.id === 'sequelize_raw_template')!
      expect(unsafe.detect('  sequelize.query(`SELECT * FROM users WHERE id = ${id}`)', 'src/db.ts', '')).toBe(true)
    })

    it('detects raw query with string concatenation', () => {
      const unsafe = sequelizeModel.unsafePatterns.find(p => p.id === 'sequelize_raw_concat')!
      expect(unsafe.detect("  sequelize.query('SELECT * FROM users WHERE id = ' + id)", 'src/db.ts', '')).toBe(true)
    })

    it('detects Sequelize.literal()', () => {
      const unsafe = sequelizeModel.unsafePatterns.find(p => p.id === 'sequelize_literal_injection')!
      expect(unsafe.detect('  Sequelize.literal(userInput)', 'src/db.ts', '')).toBe(true)
    })

    it('does not flag raw query with replacements as unsafe', () => {
      const unsafe = sequelizeModel.unsafePatterns.find(p => p.id === 'sequelize_raw_template')!
      expect(unsafe.detect("  sequelize.query(`SELECT * FROM users`, { replacements: [] })", 'src/db.ts', '')).toBe(false)
    })
  })
})

// ============================================================================
// Prisma Model
// ============================================================================

describe('Prisma Framework Model', () => {
  describe('safe patterns', () => {
    it('detects prisma.findMany() as safe', () => {
      const safe = prismaModel.safePatterns.find(p => p.id === 'orm_prisma_parameterised')!
      expect(safe.detect('  const users = await prisma.user.findMany({ where: { active: true } })', 'src/db.ts', '')).toBe(true)
    })

    it('detects prisma.findFirst() as safe', () => {
      const safe = prismaModel.safePatterns.find(p => p.id === 'orm_prisma_parameterised')!
      expect(safe.detect('  const user = await prisma.user.findFirst({ where: { email } })', 'src/db.ts', '')).toBe(true)
    })

    it('detects prisma.findUnique() as safe', () => {
      const safe = prismaModel.safePatterns.find(p => p.id === 'orm_prisma_parameterised')!
      expect(safe.detect('  const user = await prisma.user.findUnique({ where: { id } })', 'src/db.ts', '')).toBe(true)
    })

    it('detects prisma.create() as safe', () => {
      const safe = prismaModel.safePatterns.find(p => p.id === 'orm_prisma_parameterised')!
      expect(safe.detect('  await prisma.user.create({ data: { name, email } })', 'src/db.ts', '')).toBe(true)
    })

    it('detects prisma.update() as safe', () => {
      const safe = prismaModel.safePatterns.find(p => p.id === 'orm_prisma_parameterised')!
      expect(safe.detect('  await prisma.user.update({ where: { id }, data: { name } })', 'src/db.ts', '')).toBe(true)
    })

    it('detects prisma.delete() as safe', () => {
      const safe = prismaModel.safePatterns.find(p => p.id === 'orm_prisma_parameterised')!
      expect(safe.detect('  await prisma.user.delete({ where: { id } })', 'src/db.ts', '')).toBe(true)
    })

    it('detects prisma.count() as safe', () => {
      const safe = prismaModel.safePatterns.find(p => p.id === 'orm_prisma_parameterised')!
      expect(safe.detect('  const count = await prisma.user.count({ where: { active: true } })', 'src/db.ts', '')).toBe(true)
    })

    it('detects $queryRaw with tagged template as safe', () => {
      const safe = prismaModel.safePatterns.find(p => p.id === 'orm_prisma_tagged_raw')!
      expect(safe.detect('  const result = await prisma.$queryRaw`SELECT * FROM users WHERE id = ${id}`', 'src/db.ts', '')).toBe(true)
    })
  })

  describe('unsafe patterns', () => {
    it('detects $queryRawUnsafe', () => {
      const unsafe = prismaModel.unsafePatterns.find(p => p.id === 'prisma_unsafe_raw_concat')!
      expect(unsafe.detect('  await prisma.$queryRawUnsafe(sql)', 'src/db.ts', '')).toBe(true)
    })

    it('detects $executeRawUnsafe', () => {
      const unsafe = prismaModel.unsafePatterns.find(p => p.id === 'prisma_unsafe_raw_concat')!
      expect(unsafe.detect('  await prisma.$executeRawUnsafe("DELETE FROM " + table)', 'src/db.ts', '')).toBe(true)
    })

    it('detects $queryRaw with string concatenation', () => {
      const unsafe = prismaModel.unsafePatterns.find(p => p.id === 'prisma_raw_string_concat')!
      expect(unsafe.detect('  await prisma.$queryRaw("SELECT * FROM users WHERE id = " + id)', 'src/db.ts', '')).toBe(true)
    })

    it('does not flag tagged template $queryRaw as unsafe concatenation', () => {
      const unsafe = prismaModel.unsafePatterns.find(p => p.id === 'prisma_raw_string_concat')!
      expect(unsafe.detect('  await prisma.$queryRaw`SELECT * FROM users WHERE id = ${id}`', 'src/db.ts', '')).toBe(false)
    })
  })
})

// ============================================================================
// Orchestrator
// ============================================================================

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

  function makeProjectContext(overrides: Partial<ProjectContext['frameworks']> = {}, orm?: string): ProjectContext {
    return {
      summary: '',
      auth: {
        hasGlobalMiddleware: false,
        authProvider: undefined,
        publicPaths: [],
        throwingAuthHelpers: [],
      },
      dataAccess: {
        orm: orm as any,
        hasRLS: false,
        validationLibrary: undefined,
        rawQueryPatterns: [],
      },
      secrets: {
        envVarHandling: 'none' as any,
        secretPatterns: [],
      },
      frameworks: {
        isMonorepo: false,
        usesTypeScript: true,
        usesServerComponents: false,
        usesServerActions: false,
        ...overrides,
      },
      structure: {
        hasApiDirectory: false,
        hasMiddleware: false,
      },
      oauth: { hasOAuthFlow: false, providers: [], hasStateParameter: false, summary: '' },
      trpc: { hasTRPC: false, routers: [], publicProcedures: [], summary: '' },
    } as any
  }

  it('returns empty map when no frameworks detected', () => {
    const files = [makeFile('src/app.ts', 'const x = 1')]
    const ctx = makeProjectContext()
    const result = analyzeFrameworkPatterns(files, ctx)
    expect(result.size).toBe(0)
  })

  it('applies React model when frontend is react', () => {
    const files = [makeFile('src/App.tsx', '<div>{userName}</div>')]
    const ctx = makeProjectContext({ frontend: 'react' })
    const result = analyzeFrameworkPatterns(files, ctx)
    expect(result.size).toBe(1)
    const analysis = result.get('src/App.tsx')!
    expect(analysis.safeLines.size).toBeGreaterThan(0)
  })

  it('applies Sequelize model when ORM is sequelize', () => {
    const files = [makeFile('src/db.ts', 'const users = await User.findAll({ where: { name } })')]
    const ctx = makeProjectContext({}, 'sequelize')
    const result = analyzeFrameworkPatterns(files, ctx)
    expect(result.size).toBe(1)
    const analysis = result.get('src/db.ts')!
    expect(analysis.safeLines.size).toBeGreaterThan(0)
  })

  it('applies Prisma model when ORM is prisma', () => {
    const files = [makeFile('src/db.ts', 'const users = await prisma.user.findMany({ where: { active: true } })')]
    const ctx = makeProjectContext({}, 'prisma')
    const result = analyzeFrameworkPatterns(files, ctx)
    expect(result.size).toBe(1)
    const analysis = result.get('src/db.ts')!
    expect(analysis.safeLines.size).toBeGreaterThan(0)
    expect(analysis.safeLines.get(1)![0].id).toBe('orm_prisma_parameterised')
  })

  it('applies multiple models (React + Next.js)', () => {
    const files = [
      makeFile('src/app/page.tsx', [
        '<div>{userName}</div>',
        'const q = searchParams.get("q")',
      ].join('\n')),
    ]
    const ctx = makeProjectContext({ primary: 'nextjs', frontend: 'react' })
    const result = analyzeFrameworkPatterns(files, ctx)
    expect(result.size).toBe(1)
    const analysis = result.get('src/app/page.tsx')!
    // Line 1: JSX auto-escape (React safe)
    expect(analysis.safeLines.has(1)).toBe(true)
    // Line 2: searchParams (Next.js unsafe)
    expect(analysis.unsafeLines.has(2)).toBe(true)
  })

  it('stores multiple patterns per line in arrays', () => {
    // A line in an app/ .tsx file with JSX auto-escape (React) AND Server Component (Next.js)
    const files = [
      makeFile('src/app/page.tsx', '<div>{userName}</div>'),
    ]
    const ctx = makeProjectContext({ primary: 'nextjs', frontend: 'react' })
    const result = analyzeFrameworkPatterns(files, ctx)
    const analysis = result.get('src/app/page.tsx')!
    const line1Safe = analysis.safeLines.get(1)!
    // Both React JSX auto-escape and Next.js Server Component safe patterns should match
    expect(line1Safe.length).toBeGreaterThanOrEqual(2)
    const ids = line1Safe.map(p => p.id)
    expect(ids).toContain('react_jsx_auto_escape')
    expect(ids).toContain('nextjs_server_component_no_dom')
  })
})
