/**
 * Dropbox Upload File Unit Tests
 *
 * Tests the uploadFile fetch implementation:
 * - Request formatting (headers, body)
 * - Dropbox-API-Arg header structure
 * - Response parsing
 * - Error handling and mapping
 *
 * Mock Boundaries:
 * - global.fetch -> mock HTTP responses
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { uploadFile } from '../index'
import type { ExecutionContext } from '../../../../../../../../base/types'

// ============================================================================
// Mock Setup
// ============================================================================

const mockFetch = vi.fn()
vi.stubGlobal('fetch', mockFetch)

// ============================================================================
// Test Data
// ============================================================================

const credentials = { accessToken: 'sl.test_access_token' }

const mockContext: ExecutionContext = {
  executionId: 'test-exec-123',
  organizationId: 'org-test-456',
  resourceId: 'test-resource',
  resourceType: 'workflow',
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn()
  }
}

const successResponse = {
  id: 'id:abc123xyz',
  name: 'document.pdf',
  path_lower: '/clients/acme/contracts/document.pdf',
  path_display: '/Clients/Acme/Contracts/document.pdf',
  rev: '015a1234567890',
  size: 123456,
  is_downloadable: true,
  content_hash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
}

// ============================================================================
// Tests
// ============================================================================

describe('uploadFile', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  afterEach(() => {
    vi.resetAllMocks()
  })

  // ==========================================================================
  // Request Formatting
  // ==========================================================================

  describe('request formatting', () => {
    it('sends POST to correct Dropbox content endpoint', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(successResponse)
      })

      await uploadFile(credentials, { path: '/test.pdf', contents: Buffer.from('test') }, mockContext)

      expect(mockFetch).toHaveBeenCalledWith(
        'https://content.dropboxapi.com/2/files/upload',
        expect.any(Object)
      )
    })

    it('uses application/octet-stream Content-Type for binary upload', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(successResponse)
      })

      await uploadFile(credentials, { path: '/test.pdf', contents: Buffer.from('binary data') }, mockContext)

      const options = mockFetch.mock.calls[0][1]
      expect(options.headers['Content-Type']).toBe('application/octet-stream')
    })

    it('includes Bearer token in Authorization header', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(successResponse)
      })

      await uploadFile(credentials, { path: '/test.pdf', contents: Buffer.from('test') }, mockContext)

      const options = mockFetch.mock.calls[0][1]
      expect(options.headers['Authorization']).toBe('Bearer sl.test_access_token')
    })

    it('passes Buffer directly in request body', async () => {
      const content = Buffer.from('file content here')

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(successResponse)
      })

      await uploadFile(credentials, { path: '/test.txt', contents: content }, mockContext)

      const options = mockFetch.mock.calls[0][1]
      expect(options.body).toBe(content)
    })
  })

  // ==========================================================================
  // Dropbox-API-Arg Header
  // ==========================================================================

  describe('Dropbox-API-Arg header', () => {
    it('includes path in header', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(successResponse)
      })

      await uploadFile(
        credentials,
        { path: '/Clients/Acme/Contracts/proposal.pdf', contents: Buffer.from('test') },
        mockContext
      )

      const options = mockFetch.mock.calls[0][1]
      const dropboxArg = JSON.parse(options.headers['Dropbox-API-Arg'])
      expect(dropboxArg.path).toBe('/Clients/Acme/Contracts/proposal.pdf')
    })

    it('uses overwrite mode by default', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(successResponse)
      })

      await uploadFile(credentials, { path: '/test.pdf', contents: Buffer.from('test') }, mockContext)

      const options = mockFetch.mock.calls[0][1]
      const dropboxArg = JSON.parse(options.headers['Dropbox-API-Arg'])
      expect(dropboxArg.mode).toBe('overwrite')
    })

    it('respects custom mode parameter', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(successResponse)
      })

      await uploadFile(
        credentials,
        { path: '/test.pdf', contents: Buffer.from('test'), mode: 'add' },
        mockContext
      )

      const options = mockFetch.mock.calls[0][1]
      const dropboxArg = JSON.parse(options.headers['Dropbox-API-Arg'])
      expect(dropboxArg.mode).toBe('add')
    })

    it('sets autorename to false by default', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(successResponse)
      })

      await uploadFile(credentials, { path: '/test.pdf', contents: Buffer.from('test') }, mockContext)

      const options = mockFetch.mock.calls[0][1]
      const dropboxArg = JSON.parse(options.headers['Dropbox-API-Arg'])
      expect(dropboxArg.autorename).toBe(false)
    })

    it('respects autorename parameter when true', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(successResponse)
      })

      await uploadFile(
        credentials,
        { path: '/test.pdf', contents: Buffer.from('test'), autorename: true },
        mockContext
      )

      const options = mockFetch.mock.calls[0][1]
      const dropboxArg = JSON.parse(options.headers['Dropbox-API-Arg'])
      expect(dropboxArg.autorename).toBe(true)
    })

    it('includes mute and strict_conflict flags', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(successResponse)
      })

      await uploadFile(credentials, { path: '/test.pdf', contents: Buffer.from('test') }, mockContext)

      const options = mockFetch.mock.calls[0][1]
      const dropboxArg = JSON.parse(options.headers['Dropbox-API-Arg'])
      expect(dropboxArg.mute).toBe(false)
      expect(dropboxArg.strict_conflict).toBe(false)
    })
  })

  // ==========================================================================
  // Response Handling
  // ==========================================================================

  describe('response handling', () => {
    it('returns file metadata on success', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(successResponse)
      })

      const result = await uploadFile(
        credentials,
        { path: '/Clients/Acme/document.pdf', contents: Buffer.from('test') },
        mockContext
      )

      expect(result).toEqual(successResponse)
      expect(result.id).toBe('id:abc123xyz')
      expect(result.path_display).toBe('/Clients/Acme/Contracts/document.pdf')
      expect(result.size).toBe(123456)
    })

    it('preserves all metadata fields from response', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(successResponse)
      })

      const result = await uploadFile(credentials, { path: '/test.pdf', contents: Buffer.from('test') }, mockContext)

      expect(result).toHaveProperty('id')
      expect(result).toHaveProperty('name')
      expect(result).toHaveProperty('path_lower')
      expect(result).toHaveProperty('path_display')
      expect(result).toHaveProperty('rev')
      expect(result).toHaveProperty('size')
      expect(result).toHaveProperty('is_downloadable')
      expect(result).toHaveProperty('content_hash')
    })
  })

  // ==========================================================================
  // Error Handling
  // ==========================================================================

  describe('error handling', () => {
    it('throws credentials_invalid for 401 Unauthorized', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 401,
        json: () => Promise.resolve({ error_summary: 'expired_access_token/...' })
      })

      await expect(
        uploadFile(credentials, { path: '/test.pdf', contents: Buffer.from('test') }, mockContext)
      ).rejects.toThrow('Dropbox access token expired or invalid')
    })

    it('throws rate_limited for 429 Too Many Requests', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 429,
        json: () => Promise.resolve({ error_summary: 'too_many_requests/...' })
      })

      await expect(
        uploadFile(credentials, { path: '/test.pdf', contents: Buffer.from('test') }, mockContext)
      ).rejects.toThrow('too_many_requests')
    })

    it('throws api_error for 400 Bad Request', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 400,
        json: () => Promise.resolve({ error_summary: 'path/malformed_path/...' })
      })

      await expect(
        uploadFile(credentials, { path: 'invalid-path', contents: Buffer.from('test') }, mockContext)
      ).rejects.toThrow('path/malformed_path')
    })

    it('throws api_error for 500 Server Error', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
        statusText: 'Internal Server Error',
        json: () => Promise.resolve({ error_summary: 'internal_error' })
      })

      await expect(
        uploadFile(credentials, { path: '/test.pdf', contents: Buffer.from('test') }, mockContext)
      ).rejects.toThrow('internal_error')
    })

    it('includes error_summary in error message', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 409,
        json: () => Promise.resolve({ error_summary: 'path/conflict/file/...' })
      })

      await expect(
        uploadFile(credentials, { path: '/test.pdf', contents: Buffer.from('test') }, mockContext)
      ).rejects.toThrow('path/conflict/file')
    })

    it('handles non-JSON error responses gracefully', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 503,
        statusText: 'Service Unavailable',
        json: () => Promise.reject(new Error('Not JSON'))
      })

      await expect(
        uploadFile(credentials, { path: '/test.pdf', contents: Buffer.from('test') }, mockContext)
      ).rejects.toThrow()
    })
  })

  // ==========================================================================
  // Edge Cases
  // ==========================================================================

  describe('edge cases', () => {
    it('handles empty file upload', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () =>
          Promise.resolve({
            ...successResponse,
            size: 0
          })
      })

      const result = await uploadFile(
        credentials,
        { path: '/empty.txt', contents: Buffer.from('') },
        mockContext
      )

      expect(result.size).toBe(0)
    })

    it('handles paths with special characters', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(successResponse)
      })

      await uploadFile(
        credentials,
        { path: '/Clients/Acme Corp/Contracts 2024/Proposal - Draft.pdf', contents: Buffer.from('test') },
        mockContext
      )

      const options = mockFetch.mock.calls[0][1]
      const dropboxArg = JSON.parse(options.headers['Dropbox-API-Arg'])
      expect(dropboxArg.path).toBe('/Clients/Acme Corp/Contracts 2024/Proposal - Draft.pdf')
    })

    it('handles large file content', async () => {
      const largeContent = Buffer.alloc(10 * 1024 * 1024, 'x') // 10MB

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () =>
          Promise.resolve({
            ...successResponse,
            size: largeContent.length
          })
      })

      const result = await uploadFile(credentials, { path: '/large.bin', contents: largeContent }, mockContext)

      expect(result.size).toBe(10 * 1024 * 1024)
      expect(mockFetch.mock.calls[0][1].body).toBe(largeContent)
    })

    it('works without context parameter', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(successResponse)
      })

      const result = await uploadFile(credentials, { path: '/test.pdf', contents: Buffer.from('test') })

      expect(result).toEqual(successResponse)
    })
  })
})
