/**
 * OSV Check Unit Tests
 *
 * Tests for the OSV.dev security advisory integration.
 * Mocks fetch to avoid live API calls during tests.
 *
 * Run: npx jest src/layer3/__tests__/osv-check.test.ts
 */

import {
  checkPackageAdvisories,
  queryOSV,
  queryOSVBatch,
  mapOSVSeverity,
  isMaliciousPackage,
  getCacheKey,
  advisoryCache,
} from '../osv-check'

// Mock fetch globally
const mockFetch = jest.fn()
global.fetch = mockFetch

// Helper to create OSV vulnerability response
function createOSVVuln(options: {
  id: string
  summary?: string
  cvssScore?: number
  malicious?: boolean
  dbSeverity?: string
}) {
  return {
    id: options.id,
    summary: options.summary || 'Test vulnerability',
    severity: options.cvssScore
      ? [{ type: 'CVSS_V3', score: options.cvssScore.toString() }]
      : undefined,
    database_specific: {
      malicious: options.malicious,
      severity: options.dbSeverity,
    },
  }
}

describe('OSV Check', () => {
  beforeEach(() => {
    // Clear cache and mocks before each test
    advisoryCache.clear()
    mockFetch.mockReset()
  })

  describe('mapOSVSeverity()', () => {
    it('returns critical for malicious packages', () => {
      const vuln = createOSVVuln({ id: 'MAL-123', malicious: true })
      expect(mapOSVSeverity(vuln)).toBe('critical')
    })

    it('maps CVSS scores correctly', () => {
      expect(mapOSVSeverity(createOSVVuln({ id: 'CVE-1', cvssScore: 9.5 }))).toBe('critical')
      expect(mapOSVSeverity(createOSVVuln({ id: 'CVE-2', cvssScore: 9.0 }))).toBe('critical')
      expect(mapOSVSeverity(createOSVVuln({ id: 'CVE-3', cvssScore: 7.5 }))).toBe('high')
      expect(mapOSVSeverity(createOSVVuln({ id: 'CVE-4', cvssScore: 7.0 }))).toBe('high')
      expect(mapOSVSeverity(createOSVVuln({ id: 'CVE-5', cvssScore: 5.0 }))).toBe('medium')
      expect(mapOSVSeverity(createOSVVuln({ id: 'CVE-6', cvssScore: 4.0 }))).toBe('medium')
      expect(mapOSVSeverity(createOSVVuln({ id: 'CVE-7', cvssScore: 2.0 }))).toBe('low')
    })

    it('maps database_specific severity when no CVSS', () => {
      expect(mapOSVSeverity(createOSVVuln({ id: 'X-1', dbSeverity: 'critical' }))).toBe('critical')
      expect(mapOSVSeverity(createOSVVuln({ id: 'X-2', dbSeverity: 'high' }))).toBe('high')
      expect(mapOSVSeverity(createOSVVuln({ id: 'X-3', dbSeverity: 'moderate' }))).toBe('medium')
      expect(mapOSVSeverity(createOSVVuln({ id: 'X-4', dbSeverity: 'medium' }))).toBe('medium')
      expect(mapOSVSeverity(createOSVVuln({ id: 'X-5', dbSeverity: 'low' }))).toBe('low')
    })

    it('defaults to high for unknown severity', () => {
      const vuln = createOSVVuln({ id: 'UNKNOWN-1' })
      expect(mapOSVSeverity(vuln)).toBe('high')
    })
  })

  describe('isMaliciousPackage()', () => {
    it('detects MAL- prefixed IDs', () => {
      const vuln = createOSVVuln({ id: 'MAL-2023-1234' })
      expect(isMaliciousPackage(vuln)).toBe(true)
    })

    it('detects malicious flag in database_specific', () => {
      const vuln = createOSVVuln({ id: 'GHSA-xxxx', malicious: true })
      expect(isMaliciousPackage(vuln)).toBe(true)
    })

    it('detects "malicious" in summary', () => {
      const vuln = createOSVVuln({
        id: 'GHSA-xxxx',
        summary: 'This package contains malicious code',
      })
      expect(isMaliciousPackage(vuln)).toBe(true)
    })

    it('returns false for normal vulnerabilities', () => {
      const vuln = createOSVVuln({
        id: 'CVE-2024-1234',
        summary: 'SQL injection vulnerability',
      })
      expect(isMaliciousPackage(vuln)).toBe(false)
    })
  })

  describe('getCacheKey()', () => {
    it('creates cache keys with lowercase package names', () => {
      expect(getCacheKey('React', 'npm')).toBe('npm:react')
      expect(getCacheKey('Django', 'PyPI')).toBe('PyPI:django')
    })
  })

  describe('queryOSV()', () => {
    it('returns cached results on subsequent calls', async () => {
      // First call - API returns vulns
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ vulns: [createOSVVuln({ id: 'CVE-1' })] }),
      })

      const first = await queryOSV('test-package', 'npm')
      expect(first).toHaveLength(1)
      expect(mockFetch).toHaveBeenCalledTimes(1)

      // Second call - should use cache
      const second = await queryOSV('test-package', 'npm')
      expect(second).toHaveLength(1)
      expect(mockFetch).toHaveBeenCalledTimes(1) // Still 1, used cache
    })

    it('returns empty array when API returns non-ok response', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
      })

      const result = await queryOSV('error-package', 'npm')
      expect(result).toEqual([])
    })

    it('returns empty array on network error', async () => {
      mockFetch.mockRejectedValueOnce(new Error('Network error'))

      const result = await queryOSV('network-error-package', 'npm')
      expect(result).toEqual([])
    })
  })

  describe('queryOSVBatch()', () => {
    it('uses cache for previously queried packages', async () => {
      // Pre-populate cache
      advisoryCache.set('npm:cached-pkg', {
        timestamp: Date.now(),
        advisories: [createOSVVuln({ id: 'CACHED-1' })],
      })

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          results: [{ vulns: [createOSVVuln({ id: 'NEW-1' })] }],
        }),
      })

      const results = await queryOSVBatch([
        { name: 'cached-pkg', ecosystem: 'npm' },
        { name: 'new-pkg', ecosystem: 'npm' },
      ])

      // Should have results for both
      expect(results.get('npm:cached-pkg')).toHaveLength(1)
      expect(results.get('npm:new-pkg')).toHaveLength(1)

      // API should only be called once (for new-pkg batch)
      expect(mockFetch).toHaveBeenCalledTimes(1)
    })

    it('returns empty arrays on API error', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 503,
      })

      const results = await queryOSVBatch([
        { name: 'pkg1', ecosystem: 'npm' },
        { name: 'pkg2', ecosystem: 'npm' },
      ])

      expect(results.get('npm:pkg1')).toEqual([])
      expect(results.get('npm:pkg2')).toEqual([])
    })
  })

  describe('checkPackageAdvisories()', () => {
    it('returns vulnerabilities for packages with advisories', async () => {
      // Mock OSV batch response
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          results: [
            {
              vulns: [
                createOSVVuln({
                  id: 'MAL-2023-1234',
                  summary: 'Malicious package',
                  malicious: true,
                }),
              ],
            },
          ],
        }),
      })

      const packageJson = JSON.stringify({
        dependencies: {
          'malicious-pkg': '1.0.0',
        },
      })

      const findings = await checkPackageAdvisories(packageJson, 'package.json')

      expect(findings).toHaveLength(1)
      expect(findings[0].category).toBe('ai_package_malicious')
      expect(findings[0].severity).toBe('critical')
      expect(findings[0].title).toContain('Malicious package')
    })

    it('returns empty array for clean packages', async () => {
      // Mock OSV batch response - no vulns
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          results: [{ vulns: [] }],
        }),
      })

      const packageJson = JSON.stringify({
        dependencies: {
          'clean-pkg': '1.0.0',
        },
      })

      const findings = await checkPackageAdvisories(packageJson, 'package.json')
      expect(findings).toHaveLength(0)
    })

    it('handles requirements.txt files', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          results: [
            {
              vulns: [
                createOSVVuln({
                  id: 'CVE-2024-1234',
                  summary: 'Critical vulnerability',
                  cvssScore: 9.8,
                }),
              ],
            },
          ],
        }),
      })

      const requirements = `requests==2.28.0
flask>=2.0.0`

      const findings = await checkPackageAdvisories(requirements, 'requirements.txt')

      expect(findings).toHaveLength(1)
      expect(findings[0].category).toBe('suspicious_package')
      expect(findings[0].severity).toBe('critical')
    })

    it('returns empty for unsupported file types', async () => {
      const findings = await checkPackageAdvisories('some content', 'random.txt')
      expect(findings).toHaveLength(0)
    })

    it('categorizes vulnerable (non-malicious) packages correctly', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          results: [
            {
              vulns: [
                createOSVVuln({
                  id: 'CVE-2024-5678',
                  summary: 'XSS vulnerability in template rendering',
                  cvssScore: 7.5,
                }),
              ],
            },
          ],
        }),
      })

      const packageJson = JSON.stringify({
        dependencies: {
          'vuln-pkg': '1.0.0',
        },
      })

      const findings = await checkPackageAdvisories(packageJson, 'package.json')

      expect(findings).toHaveLength(1)
      expect(findings[0].category).toBe('suspicious_package')
      expect(findings[0].severity).toBe('high')
      expect(findings[0].title).toContain('Vulnerable package')
    })

    it('sets requiresAIValidation to false (authoritative data)', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          results: [
            {
              vulns: [createOSVVuln({ id: 'CVE-1', cvssScore: 5.0 })],
            },
          ],
        }),
      })

      const packageJson = JSON.stringify({
        dependencies: {
          'test-pkg': '1.0.0',
        },
      })

      const findings = await checkPackageAdvisories(packageJson, 'package.json')

      expect(findings).toHaveLength(1)
      expect(findings[0].requiresAIValidation).toBe(false)
    })

    it('gracefully handles API failures', async () => {
      mockFetch.mockRejectedValueOnce(new Error('Network error'))

      const packageJson = JSON.stringify({
        dependencies: {
          'some-pkg': '1.0.0',
        },
      })

      const findings = await checkPackageAdvisories(packageJson, 'package.json')
      expect(findings).toHaveLength(0) // Graceful degradation
    })

    it('aggregates multiple advisories for same package', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          results: [
            {
              vulns: [
                createOSVVuln({ id: 'CVE-1', cvssScore: 5.0, summary: 'Low risk' }),
                createOSVVuln({ id: 'CVE-2', cvssScore: 9.0, summary: 'High risk' }),
                createOSVVuln({ id: 'CVE-3', cvssScore: 7.0, summary: 'Medium risk' }),
              ],
            },
          ],
        }),
      })

      const packageJson = JSON.stringify({
        dependencies: {
          'multi-vuln-pkg': '1.0.0',
        },
      })

      const findings = await checkPackageAdvisories(packageJson, 'package.json')

      expect(findings).toHaveLength(1) // Single aggregated finding
      expect(findings[0].severity).toBe('critical') // Uses highest severity
      expect(findings[0].title).toContain('3 advisories')
      expect(findings[0].description).toContain('CVE-1')
      expect(findings[0].description).toContain('CVE-2')
      expect(findings[0].description).toContain('CVE-3')
    })
  })
})
