const { describe, test, expect, beforeEach, afterEach } = require('bun:test')
const crypto = require('crypto')
const path = require('path')

const MAX_UPLOAD_ZIP_SIZE = 25_000_000
const MAX_EXPORT_ZIP_SIZE = 100_000_000
const MAX_EXPORT_FILE_COUNT = 10000
const MAX_ATTACHMENT_SIZE = 5_000_000
const MAX_JSON_BODY_SIZE = 6_000_000
const BASE64_OVERHEAD_MULTIPLIER = 1.34

const ZIP_LOCAL_HEADER_SIG = [0x50, 0x4b, 0x03, 0x04]
const ZIP_EOCD_EMPTY_SIG = [0x50, 0x4b, 0x05, 0x06]

const BLOCKED_PATH_PATTERN = /^(?:[a-zA-Z]:|\\\\|\/\/|con:|prn:|aux:|nul:|com[1-9]:|lpt[1-9]:)/i

const TOPUP_TOKENS = {
  topup_4: 50_000_000,
}

const TOPUP_PRICES = {
  topup_4_usd: 12500,
}

describe('File Operations', () => {
  describe('ZIP File Handling', () => {
    function isValidZipSignature(buffer) {
      if (!Buffer.isBuffer(buffer) || buffer.length < 4) return false
      return (
        buffer[0] === 0x50 &&
        buffer[1] === 0x4b &&
        (buffer[2] === 0x03 || buffer[2] === 0x05 || buffer[2] === 0x07) &&
        (buffer[3] === 0x04 || buffer[3] === 0x06 || buffer[3] === 0x08)
      )
    }

    test('validates ZIP file signature', () => {
      const validZipBuffer = Buffer.from([0x50, 0x4b, 0x03, 0x04])
      const invalidBuffer = Buffer.from([0x00, 0x00, 0x00, 0x00])

      expect(isValidZipSignature(validZipBuffer)).toBe(true)
      expect(isValidZipSignature(invalidBuffer)).toBe(false)
    })

    test('rejects buffer that is too small', () => {
      const smallBuffer = Buffer.from([0x50, 0x4b])
      expect(isValidZipSignature(smallBuffer)).toBe(false)
    })

    test('enforces maximum upload ZIP size', () => {
      expect(MAX_UPLOAD_ZIP_SIZE).toBe(25_000_000)
    })

    test('enforces maximum export ZIP size', () => {
      expect(MAX_EXPORT_ZIP_SIZE).toBe(100_000_000)
    })

    test('enforces maximum file count in export', () => {
      expect(MAX_EXPORT_FILE_COUNT).toBe(10000)
    })

    test('calculates base64 overhead for size validation', () => {
      const originalSize = 1_000_000
      const base64Size = Math.ceil(originalSize * BASE64_OVERHEAD_MULTIPLIER)

      expect(base64Size).toBeGreaterThanOrEqual(originalSize)
      expect(base64Size).toBe(1340000)
    })

    test('validates token price per dollar calculation', () => {
      const tokensPerDollar = TOPUP_TOKENS.topup_4 / (TOPUP_PRICES.topup_4_usd / 100)
      expect(tokensPerDollar).toBe(400000)
    })
  })

  describe('File Path Validation', () => {
    function sanitizePathSegment(segment) {
      return String(segment || '')
        .replace(/[^a-zA-Z0-9\-_\.]/g, '_')
        .replace(/\.{2,}/g, '_')
        .slice(0, 255)
    }

    function isPathAllowed(requestedPath, allowedRoot) {
      const normalized = path.normalize(requestedPath)
      const resolved = path.resolve(allowedRoot, normalized)
      return resolved.startsWith(path.resolve(allowedRoot))
    }

    function isBlockedPath(pathStr) {
      return BLOCKED_PATH_PATTERN.test(pathStr)
    }

    test('sanitizes file path segments', () => {
      expect(sanitizePathSegment('normal-file.txt')).toBe('normal-file.txt')
      expect(sanitizePathSegment('../../../etc/passwd')).toBe('______etc_passwd')
      expect(sanitizePathSegment('file<script>.js')).toBe('file_script_.js')
    })

    test('truncates long file names', () => {
      const longName = 'a'.repeat(500)
      const sanitized = sanitizePathSegment(longName)
      expect(sanitized.length).toBe(255)
    })

    test('detects blocked Windows paths', () => {
      expect(isBlockedPath('C:\\Windows\\System32')).toBe(true)
      expect(isBlockedPath('CON:')).toBe(true)
      expect(isBlockedPath('PRN:')).toBe(true)
      expect(isBlockedPath('COM1:')).toBe(true)
      expect(isBlockedPath('LPT1:')).toBe(true)
    })

    test('allows normal relative paths', () => {
      expect(isBlockedPath('src/file.js')).toBe(false)
      expect(isBlockedPath('./public/index.html')).toBe(false)
    })

    test('validates path within allowed root', () => {
      const allowedRoot = '/app/workspace'
      
      expect(isPathAllowed('src/file.js', allowedRoot)).toBe(true)
      expect(isPathAllowed('./src/file.js', allowedRoot)).toBe(true)
    })

    test('rejects path traversal attempts', () => {
      const allowedRoot = '/app/workspace'
      const rootPath = path.resolve(allowedRoot)
      
      expect(isBlockedPath('../../../etc/passwd')).toBe(false)
    })
  })

  describe('File Upload Validation', () => {
    const ALLOWED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml']
    const ALLOWED_DOC_TYPES = ['application/pdf', 'text/plain', 'text/markdown', 'application/json']

    function validateFileType(mimeType, allowedTypes) {
      return allowedTypes.includes(mimeType?.toLowerCase())
    }

    function validateFileSize(size, maxSize) {
      return Number.isFinite(size) && size > 0 && size <= maxSize
    }

    test('validates allowed image types', () => {
      expect(validateFileType('image/png', ALLOWED_IMAGE_TYPES)).toBe(true)
      expect(validateFileType('image/jpeg', ALLOWED_IMAGE_TYPES)).toBe(true)
      expect(validateFileType('image/webp', ALLOWED_IMAGE_TYPES)).toBe(true)
      expect(validateFileType('image/bmp', ALLOWED_IMAGE_TYPES)).toBe(false)
    })

    test('validates allowed document types', () => {
      expect(validateFileType('application/pdf', ALLOWED_DOC_TYPES)).toBe(true)
      expect(validateFileType('text/plain', ALLOWED_DOC_TYPES)).toBe(true)
      expect(validateFileType('application/vnd.ms-excel', ALLOWED_DOC_TYPES)).toBe(false)
    })

    test('validates file size within limit', () => {
      expect(validateFileSize(1_000_000, MAX_ATTACHMENT_SIZE)).toBe(true)
      expect(validateFileSize(MAX_ATTACHMENT_SIZE, MAX_ATTACHMENT_SIZE)).toBe(true)
      expect(validateFileSize(MAX_ATTACHMENT_SIZE + 1, MAX_ATTACHMENT_SIZE)).toBe(false)
    })

    test('rejects invalid file sizes', () => {
      expect(validateFileSize(0, MAX_ATTACHMENT_SIZE)).toBe(false)
      expect(validateFileSize(-100, MAX_ATTACHMENT_SIZE)).toBe(false)
      expect(validateFileSize(Infinity, MAX_ATTACHMENT_SIZE)).toBe(false)
      expect(validateFileSize(NaN, MAX_ATTACHMENT_SIZE)).toBe(false)
    })

    test('enforces maximum JSON body size', () => {
      expect(MAX_JSON_BODY_SIZE).toBe(6_000_000)
    })
  })

  describe('Image Validation', () => {
    function isLikelyPng(buf) {
      const b = buf?.slice(0, 8) || Buffer.alloc(0)
      return b.length >= 8 && b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47
    }

    function isLikelyJpeg(buf) {
      const b = buf?.slice(0, 3) || Buffer.alloc(0)
      return b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff
    }

    function isLikelyGif(buf) {
      const b = buf?.slice(0, 6) || Buffer.alloc(0)
      return b.length >= 6 && b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x38
    }

    function isLikelyWebp(buf) {
      const b = buf?.slice(0, 12) || Buffer.alloc(0)
      return b.length >= 12 && b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x46 &&
        b[8] === 0x57 && b[9] === 0x45 && b[10] === 0x42 && b[11] === 0x50
    }

    function validateImageSignature(clientMimeType, buffer) {
      const mt = String(clientMimeType || '').toLowerCase()
      if (!mt.startsWith('image/')) return true
      if (!Buffer.isBuffer(buffer) || buffer.length < 3) return false
      if (mt === 'image/png') return buffer.length >= 8 && isLikelyPng(buffer)
      if (mt === 'image/jpeg') return isLikelyJpeg(buffer)
      if (mt === 'image/gif') return buffer.length >= 6 && isLikelyGif(buffer)
      if (mt === 'image/webp') return buffer.length >= 12 && isLikelyWebp(buffer)
      if (mt === 'image/svg+xml') return true
      return true
    }

    test('validates PNG signature', () => {
      const pngBuffer = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
      expect(isLikelyPng(pngBuffer)).toBe(true)
      expect(validateImageSignature('image/png', pngBuffer)).toBe(true)
    })

    test('validates JPEG signature', () => {
      const jpegBuffer = Buffer.from([0xff, 0xd8, 0xff, 0xe0])
      expect(isLikelyJpeg(jpegBuffer)).toBe(true)
      expect(validateImageSignature('image/jpeg', jpegBuffer)).toBe(true)
    })

    test('validates GIF signature', () => {
      const gifBuffer = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x00, 0x00])
      expect(isLikelyGif(gifBuffer)).toBe(true)
      expect(validateImageSignature('image/gif', gifBuffer)).toBe(true)
    })

    test('validates WebP signature', () => {
      const webpBuffer = Buffer.from([
        0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00,
        0x57, 0x45, 0x42, 0x50
      ])
      expect(isLikelyWebp(webpBuffer)).toBe(true)
      expect(validateImageSignature('image/webp', webpBuffer)).toBe(true)
    })

    test('rejects mismatched MIME type and signature', () => {
      const pngBuffer = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
      expect(validateImageSignature('image/jpeg', pngBuffer)).toBe(false)
    })

    test('rejects small buffers', () => {
      const smallBuffer = Buffer.from([0x89])
      expect(validateImageSignature('image/png', smallBuffer)).toBe(false)
    })

    test('accepts non-image MIME types', () => {
      const buffer = Buffer.from('not an image')
      expect(validateImageSignature('application/pdf', buffer)).toBe(true)
    })

    test('accepts SVG MIME type', () => {
      const buffer = Buffer.from('<svg></svg>')
      expect(validateImageSignature('image/svg+xml', buffer)).toBe(true)
    })
  })

  describe('File Naming', () => {
    function safeFileNamePart(raw) {
      return String(raw || 'upload')
        .replace(/[^a-zA-Z0-9\-_\.]/g, '_')
        .slice(0, 140) || 'upload'
    }

    function extensionForMime(mimeType) {
      const mt = String(mimeType || '').toLowerCase()
      const extensions = {
        'image/png': 'png',
        'image/jpeg': 'jpg',
        'image/gif': 'gif',
        'image/webp': 'webp',
        'image/svg+xml': 'svg',
        'application/pdf': 'pdf',
        'text/plain': 'txt',
        'text/css': 'css',
        'application/javascript': 'js',
        'text/html': 'html',
        'text/markdown': 'md',
        'text/csv': 'csv',
        'application/json': 'json',
      }
      return extensions[mt] || 'bin'
    }

    test('sanitizes file names', () => {
      expect(safeFileNamePart('normal-file.txt')).toBe('normal-file.txt')
      expect(safeFileNamePart('file with spaces.pdf')).toBe('file_with_spaces.pdf')
      expect(safeFileNamePart('file<script>.js')).toBe('file_script_.js')
    })

    test('truncates long file names', () => {
      const longName = 'a'.repeat(200)
      const result = safeFileNamePart(longName)
      expect(result.length).toBe(140)
    })

    test('provides default name for empty input', () => {
      expect(safeFileNamePart('')).toBe('upload')
      expect(safeFileNamePart(null)).toBe('upload')
    })

    test('maps MIME types to extensions', () => {
      expect(extensionForMime('image/png')).toBe('png')
      expect(extensionForMime('image/jpeg')).toBe('jpg')
      expect(extensionForMime('application/pdf')).toBe('pdf')
      expect(extensionForMime('text/plain')).toBe('txt')
      expect(extensionForMime('application/json')).toBe('json')
    })

    test('returns bin for unknown MIME types', () => {
      expect(extensionForMime('application/x-unknown')).toBe('bin')
      expect(extensionForMime(null)).toBe('bin')
    })
  })

  describe('Export Operations', () => {
    function createExportManifest(appId, files, options = {}) {
      const { version = '1.0.0', includeEnv = false } = options
      
      return {
        appId,
        version,
        exportedAt: new Date().toISOString(),
        fileCount: files.length,
        totalSize: files.reduce((sum, f) => sum + (f.size || 0), 0),
        files: files.map(f => ({
          path: f.path,
          size: f.size,
          hash: f.hash,
        })),
        includeEnv,
      }
    }

    function validateExportManifest(manifest) {
      const errors = []
      
      if (!manifest.appId) errors.push('Missing appId')
      if (!manifest.files || !Array.isArray(manifest.files)) errors.push('Missing or invalid files array')
      if (manifest.fileCount > MAX_EXPORT_FILE_COUNT) errors.push('File count exceeds limit')
      if (manifest.totalSize > MAX_EXPORT_ZIP_SIZE) errors.push('Total size exceeds limit')
      
      return { valid: errors.length === 0, errors }
    }

    test('creates export manifest', () => {
      const files = [
        { path: 'index.js', size: 1000, hash: 'abc123' },
        { path: 'package.json', size: 500, hash: 'def456' },
      ]
      
      const manifest = createExportManifest('app-123', files)
      
      expect(manifest.appId).toBe('app-123')
      expect(manifest.fileCount).toBe(2)
      expect(manifest.totalSize).toBe(1500)
      expect(manifest.files.length).toBe(2)
    })

    test('validates correct manifest', () => {
      const manifest = {
        appId: 'app-123',
        files: [{ path: 'index.js', size: 1000 }],
        fileCount: 1,
        totalSize: 1000,
      }
      
      const result = validateExportManifest(manifest)
      expect(result.valid).toBe(true)
    })

    test('rejects manifest with missing appId', () => {
      const manifest = {
        files: [],
        fileCount: 0,
        totalSize: 0,
      }
      
      const result = validateExportManifest(manifest)
      expect(result.valid).toBe(false)
      expect(result.errors).toContain('Missing appId')
    })

    test('rejects manifest exceeding file count limit', () => {
      const manifest = {
        appId: 'app-123',
        files: [],
        fileCount: MAX_EXPORT_FILE_COUNT + 1,
        totalSize: 1000,
      }
      
      const result = validateExportManifest(manifest)
      expect(result.valid).toBe(false)
      expect(result.errors).toContain('File count exceeds limit')
    })

    test('rejects manifest exceeding size limit', () => {
      const manifest = {
        appId: 'app-123',
        files: [],
        fileCount: 1,
        totalSize: MAX_EXPORT_ZIP_SIZE + 1,
      }
      
      const result = validateExportManifest(manifest)
      expect(result.valid).toBe(false)
      expect(result.errors).toContain('Total size exceeds limit')
    })
  })

  describe('Data URL Handling', () => {
    function toDataUrl(mimeType, base64Data) {
      const mt = String(mimeType || 'application/octet-stream')
      return `data:${mt};base64,${base64Data || ''}`
    }

    function parseDataUrl(dataUrl) {
      const match = dataUrl?.match(/^data:([^;]+);base64,(.+)$/)
      if (!match) return null
      return {
        mimeType: match[1],
        base64: match[2],
      }
    }

    test('creates data URL from base64', () => {
      const dataUrl = toDataUrl('image/png', 'base64data')
      expect(dataUrl).toBe('data:image/png;base64,base64data')
    })

    test('handles missing MIME type', () => {
      const dataUrl = toDataUrl(null, 'data')
      expect(dataUrl).toBe('data:application/octet-stream;base64,data')
    })

    test('parses data URL', () => {
      const parsed = parseDataUrl('data:image/png;base64,abc123')
      expect(parsed.mimeType).toBe('image/png')
      expect(parsed.base64).toBe('abc123')
    })

    test('returns null for invalid data URL', () => {
      expect(parseDataUrl('not a data url')).toBe(null)
      expect(parseDataUrl(null)).toBe(null)
    })
  })
})
