import { describe, it, expect, vi } from 'vitest'
import { AppError, logError, getErrorMessage, tryAsync } from '../errorHandler'

describe('errorHandler', () => {
  describe('AppError', () => {
    it('should create an error with code and message', () => {
      const error = new AppError('Test error', 'TEST_CODE')
      expect(error.message).toBe('Test error')
      expect(error.code).toBe('TEST_CODE')
      expect(error.name).toBe('AppError')
      expect(error).toBeInstanceOf(Error)
    })

    it('should store cause', () => {
      const cause = new Error('root cause')
      const error = new AppError('Test error', 'TEST_CODE', cause)
      expect(error.cause).toBe(cause)
    })
  })

  describe('logError', () => {
    it('should log error with context', () => {
      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
      const error = new Error('test error')

      logError('TestContext', error)

      expect(consoleSpy).toHaveBeenCalledWith(
        '[TestContext] test error',
        error
      )
      consoleSpy.mockRestore()
    })

    it('should handle non-Error values', () => {
      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})

      logError('TestContext', 'string error')

      expect(consoleSpy).toHaveBeenCalledWith(
        '[TestContext] string error',
        'string error'
      )
      consoleSpy.mockRestore()
    })
  })

  describe('getErrorMessage', () => {
    it('should return message for AppError', () => {
      const error = new AppError('Custom error', 'CODE')
      expect(getErrorMessage(error, 'Fallback')).toBe('Custom error')
    })

    it('should return fallback + message for standard Error', () => {
      const error = new Error('details')
      expect(getErrorMessage(error, 'Fallback')).toBe('Fallback: details')
    })

    it('should return fallback for non-Error values', () => {
      expect(getErrorMessage('unknown', 'Fallback')).toBe('Fallback')
    })

    it('should return fallback for null', () => {
      expect(getErrorMessage(null, 'Fallback')).toBe('Fallback')
    })
  })

  describe('tryAsync', () => {
    it('should return result on success', async () => {
      const [result, error] = await tryAsync(async () => 42, 'Test')
      expect(result).toBe(42)
      expect(error).toBeNull()
    })

    it('should return AppError on failure', async () => {
      const [result, error] = await tryAsync(
        async () => { throw new Error('fail') },
        'TestContext'
      )
      expect(result).toBeNull()
      expect(error).toBeInstanceOf(AppError)
      expect(error?.message).toBe('TestContext: fail')
      expect(error?.code).toBe('TestContext')
    })

    it('should handle non-Error thrown values', async () => {
      const [result, error] = await tryAsync(
        async () => { throw 'string error' },
        'TestContext'
      )
      expect(result).toBeNull()
      expect(error).toBeInstanceOf(AppError)
      expect(error?.message).toBe('TestContext: string error')
    })

    it('should preserve AppError cause', async () => {
      const originalError = new Error('original')
      const [, error] = await tryAsync(
        async () => { throw originalError },
        'Ctx'
      )
      expect(error?.cause).toBe(originalError)
    })
  })
})
