/**
 * Dropbox Adapter Unit Tests
 *
 * Tests the DropboxAdapter class:
 * - Credential validation
 * - Method routing (uploadFile, createFolder)
 * - Error handling and mapping
 * - Dropbox-API-Arg header formatting
 * - 409 conflict handling for folder creation
 *
 * Mock Boundaries:
 * - global.fetch -> mock HTTP responses
 * - refreshOAuthTokenIfExpired -> mock token refresh
 * - getToolServices -> mock credentialsService
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { DropboxAdapter } from '../dropbox-adapter'
import type { ExecutionContext } from '../../../../../../base/types'

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

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

// Mock token refresh - returns token unchanged by default
vi.mock('../../../../../../integrations/oauth/server/refresh', () => ({
  refreshOAuthTokenIfExpired: vi.fn((token) => Promise.resolve(token))
}))

// Mock getToolServices for credential persistence
vi.mock('../../../../../registry', () => ({
  getToolServices: vi.fn(() => ({
    credentialsService: {
      updateCredential: vi.fn(() => Promise.resolve())
    }
  }))
}))

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

const validCredentials = {
  accessToken: 'sl.test_token_123',
  refreshToken: 'refresh_token_456',
  expiresAt: new Date(Date.now() + 3600000).toISOString(),
  provider: 'dropbox'
}

const minimalCredentials = {
  accessToken: 'sl.minimal_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()
  }
}

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

describe('DropboxAdapter', () => {
  let adapter: DropboxAdapter

  beforeEach(() => {
    adapter = new DropboxAdapter()
    vi.clearAllMocks()
  })

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

  // ==========================================================================
  // Credential Validation
  // ==========================================================================

  describe('validateCredentials', () => {
    it('accepts credentials with valid accessToken', () => {
      expect(adapter.validateCredentials(validCredentials)).toBe(true)
    })

    it('accepts minimal credentials with only accessToken', () => {
      expect(adapter.validateCredentials(minimalCredentials)).toBe(true)
    })

    it('rejects credentials without accessToken', () => {
      expect(adapter.validateCredentials({})).toBe(false)
    })

    it('rejects credentials with empty accessToken', () => {
      expect(adapter.validateCredentials({ accessToken: '' })).toBe(false)
    })

    it('rejects credentials with non-string accessToken', () => {
      expect(adapter.validateCredentials({ accessToken: 12345 })).toBe(false)
      expect(adapter.validateCredentials({ accessToken: null })).toBe(false)
      expect(adapter.validateCredentials({ accessToken: undefined })).toBe(false)
    })
  })

  // ==========================================================================
  // Method Routing
  // ==========================================================================

  describe('call - method routing', () => {
    it('throws credentials_invalid for invalid credentials', async () => {
      await expect(
        adapter.call('uploadFile', { path: '/test.pdf', contents: Buffer.from('test') }, {}, mockContext)
      ).rejects.toThrow('Invalid Dropbox credentials')
    })

    it('throws method_not_found for unknown method', async () => {
      await expect(
        adapter.call('unknownMethod', {}, validCredentials, mockContext)
      ).rejects.toThrow('Unknown Dropbox method: unknownMethod')
    })

    it('routes uploadFile to upload handler', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () =>
          Promise.resolve({
            id: 'id:abc123',
            name: 'test.pdf',
            path_lower: '/test.pdf',
            path_display: '/test.pdf',
            rev: '0123456789',
            size: 100
          })
      })

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

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

    it('routes createFolder to folder handler', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () =>
          Promise.resolve({
            metadata: {
              id: 'id:folder123',
              name: 'TestFolder',
              path_lower: '/testfolder',
              path_display: '/TestFolder'
            }
          })
      })

      const result = await adapter.call('createFolder', { path: '/TestFolder' }, validCredentials, mockContext)

      expect(mockFetch).toHaveBeenCalledWith(
        'https://api.dropboxapi.com/2/files/create_folder_v2',
        expect.any(Object)
      )
      expect(result).toHaveProperty('metadata')
    })
  })

  // ==========================================================================
  // Upload File
  // ==========================================================================

  describe('call - uploadFile', () => {
    it('sends request with correct Dropbox-API-Arg header', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () =>
          Promise.resolve({
            id: 'id:abc123',
            name: 'document.pdf',
            path_lower: '/clients/acme/contracts/document.pdf',
            path_display: '/Clients/Acme/Contracts/document.pdf',
            rev: '015a1234567890',
            size: 12345
          })
      })

      await adapter.call(
        'uploadFile',
        {
          path: '/Clients/Acme/Contracts/document.pdf',
          contents: Buffer.from('PDF content'),
          mode: 'overwrite'
        },
        validCredentials,
        mockContext
      )

      expect(mockFetch).toHaveBeenCalledTimes(1)

      const [url, options] = mockFetch.mock.calls[0]
      expect(url).toBe('https://content.dropboxapi.com/2/files/upload')
      expect(options.method).toBe('POST')
      expect(options.headers['Authorization']).toBe('Bearer sl.test_token_123')
      expect(options.headers['Content-Type']).toBe('application/octet-stream')

      // Verify Dropbox-API-Arg header
      const dropboxArg = JSON.parse(options.headers['Dropbox-API-Arg'])
      expect(dropboxArg.path).toBe('/Clients/Acme/Contracts/document.pdf')
      expect(dropboxArg.mode).toBe('overwrite')
      expect(dropboxArg.autorename).toBe(false)
    })

    it('uses default mode "overwrite" when not specified', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () =>
          Promise.resolve({
            id: 'id:abc123',
            name: 'file.txt',
            path_lower: '/file.txt',
            path_display: '/file.txt',
            rev: '123',
            size: 10
          })
      })

      await adapter.call(
        'uploadFile',
        { path: '/file.txt', contents: Buffer.from('test') },
        validCredentials,
        mockContext
      )

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

    it('passes binary content in request body', async () => {
      const binaryContent = Buffer.from([0x00, 0x01, 0x02, 0xff])

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () =>
          Promise.resolve({
            id: 'id:bin123',
            name: 'binary.bin',
            path_lower: '/binary.bin',
            path_display: '/binary.bin',
            rev: '456',
            size: 4
          })
      })

      await adapter.call(
        'uploadFile',
        { path: '/binary.bin', contents: binaryContent },
        validCredentials,
        mockContext
      )

      expect(mockFetch.mock.calls[0][1].body).toBe(binaryContent)
    })
  })

  // ==========================================================================
  // Create Folder
  // ==========================================================================

  describe('call - createFolder', () => {
    it('sends request with correct JSON body', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () =>
          Promise.resolve({
            metadata: {
              id: 'id:folder789',
              name: 'Contracts',
              path_lower: '/clients/acme/contracts',
              path_display: '/Clients/Acme/Contracts'
            }
          })
      })

      await adapter.call('createFolder', { path: '/Clients/Acme/Contracts' }, validCredentials, mockContext)

      const [url, options] = mockFetch.mock.calls[0]
      expect(url).toBe('https://api.dropboxapi.com/2/files/create_folder_v2')
      expect(options.method).toBe('POST')
      expect(options.headers['Authorization']).toBe('Bearer sl.test_token_123')
      expect(options.headers['Content-Type']).toBe('application/json')

      const body = JSON.parse(options.body)
      expect(body.path).toBe('/Clients/Acme/Contracts')
      expect(body.autorename).toBe(false)
    })

    it('handles 409 path/conflict as success (folder exists)', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 409,
        json: () => Promise.resolve({ error_summary: 'path/conflict/folder/' })
      })

      const result = await adapter.call(
        'createFolder',
        { path: '/ExistingFolder' },
        validCredentials,
        mockContext
      )

      // Should return null metadata (folder already exists, not an error)
      expect(result).toEqual({ metadata: null })
    })

    it('throws api_error for other 409 errors', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 409,
        json: () => Promise.resolve({ error_summary: 'path/not_found/...' })
      })

      await expect(
        adapter.call('createFolder', { path: '/Invalid/Path' }, validCredentials, mockContext)
      ).rejects.toThrow('path/not_found')
    })
  })

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

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

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

    it('maps 429 to rate_limited error', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 429,
        json: () => Promise.resolve({ error_summary: 'too_many_requests/...' })
      })

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

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

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

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

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

  // ==========================================================================
  // Adapter Properties
  // ==========================================================================

  describe('adapter properties', () => {
    it('has correct name property', () => {
      expect(adapter.name).toBe('dropbox')
    })
  })
})
