import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { initializeSynapse, getSynapseService, uploadToSynapse, resetSynapseService } from '../../synapse-service.js'
import { createConfig } from '../../config.js'
import { createLogger } from '../../logger.js'
import type { Config } from '../../config.js'

// Mock the Synapse SDK
vi.mock('@filoz/synapse-sdk', async () => await import('../mocks/synapse-sdk.js'))

describe('synapse-service', () => {
  let config: Config
  let logger: ReturnType<typeof createLogger>

  beforeEach(() => {
    // Create test config with Synapse enabled
    config = {
      ...createConfig(),
      privateKey: '0x0000000000000000000000000000000000000000000000000000000000000001', // Fake test key
      rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1'
    }
    logger = createLogger(config)

    // Reset the service instances
    resetSynapseService()
    vi.clearAllMocks()
  })

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

  describe('initializeSynapse', () => {
    it('should throw error when private key is not configured', async () => {
      config.privateKey = undefined

      await expect(initializeSynapse(config, logger))
        .rejects.toThrow('PRIVATE_KEY environment variable is required')
    })

    it('should initialize Synapse when private key is configured', async () => {
      const result = await initializeSynapse(config, logger)

      expect(result).not.toBeNull()
      expect(result?.synapse).toBeDefined()
      expect(result?.storage).toBeDefined()
    })

    it('should log initialization events', async () => {
      const infoSpy = vi.spyOn(logger, 'info')

      await initializeSynapse(config, logger)

      // Check that initialization logs were called
      expect(infoSpy).toHaveBeenCalledWith(
        expect.objectContaining({
          hasPrivateKey: true,
          rpcUrl: config.rpcUrl
        }),
        'Initializing Synapse'
      )

      expect(infoSpy).toHaveBeenCalledWith(
        expect.objectContaining({ event: 'synapse.init' }),
        'Initializing Synapse SDK'
      )
    })

    it('should call provider selection callback', async () => {
      const callbacks: any[] = []
      const originalCreate = (await import('@filoz/synapse-sdk')).Synapse.create

      // Capture callbacks
      vi.mocked(originalCreate).mockImplementationOnce(async (options) => {
        const synapse = await originalCreate(options)
        const originalCreateStorage = synapse.createStorage.bind(synapse)

        synapse.createStorage = async (opts: any) => {
          if (opts?.callbacks?.onProviderSelected != null) {
            callbacks.push(opts.callbacks.onProviderSelected)
          }
          return await originalCreateStorage(opts)
        }

        return synapse
      })

      await initializeSynapse(config, logger)

      expect(callbacks.length).toBeGreaterThan(0)
    })
  })

  describe('getSynapseService', () => {
    it('should return null when not initialized', () => {
      // Ensure service is reset
      resetSynapseService()

      const result = getSynapseService()
      expect(result).toBeNull()
    })

    it('should return service after initialization', async () => {
      await initializeSynapse(config, logger)

      const result = getSynapseService()
      expect(result).not.toBeNull()
      expect(result?.synapse).toBeDefined()
      expect(result?.storage).toBeDefined()
    })
  })

  describe('uploadToSynapse', () => {
    beforeEach(async () => {
      await initializeSynapse(config, logger)
    })

    it('should throw error when service is not initialized', async () => {
      // Reset service to ensure it's not initialized
      resetSynapseService()

      const data = new Uint8Array([1, 2, 3])
      await expect(uploadToSynapse(data, 'pin-123', logger))
        .rejects.toThrow('Synapse service not initialized')
    })

    it('should upload data successfully', async () => {
      const data = new Uint8Array([1, 2, 3])
      const pinId = 'pin-123'

      const result = await uploadToSynapse(data, pinId, logger)

      expect(result).toHaveProperty('commp')
      expect(result).toHaveProperty('rootId')
      expect(result).toHaveProperty('proofSetId')
      expect(result.commp).toMatch(/^baga6ea4seaq/)
      expect(result.proofSetId).toBe('proof-set-123')
    })

    it('should log upload events', async () => {
      const infoSpy = vi.spyOn(logger, 'info')
      const data = new Uint8Array([1, 2, 3])
      const pinId = 'pin-456'

      await uploadToSynapse(data, pinId, logger)

      expect(infoSpy).toHaveBeenCalledWith(
        expect.objectContaining({
          event: 'synapse.upload.start',
          pinId,
          size: data.length
        }),
        'Starting Synapse upload'
      )

      expect(infoSpy).toHaveBeenCalledWith(
        expect.objectContaining({
          event: 'synapse.upload.complete',
          pinId
        }),
        'Synapse upload completed successfully'
      )
    })

    it('should call upload callbacks', async () => {
      let uploadCompleteCallbackCalled = false
      let rootAddedCallbackCalled = false

      // Mock the storage upload to capture callbacks
      const service = getSynapseService()
      if ((service?.storage) != null) {
        const originalUpload = service.storage.upload.bind(service.storage)
        service.storage.upload = async (data: any, callbacks: any) => {
          // Override callbacks to track calls
          const wrappedCallbacks = {
            ...callbacks,
            onUploadComplete: (commp: string) => {
              uploadCompleteCallbackCalled = true
              callbacks?.onUploadComplete?.(commp)
            },
            onRootAdded: () => {
              rootAddedCallbackCalled = true
              callbacks?.onRootAdded?.()
            }
          }
          return await originalUpload(data, wrappedCallbacks)
        }
      }

      const data = new Uint8Array([1, 2, 3])
      await uploadToSynapse(data, 'pin-789', logger)

      expect(uploadCompleteCallbackCalled).toBe(true)
      expect(rootAddedCallbackCalled).toBe(true)
    })
  })
})
