import { describe, it, expect } from 'vitest'
import { ExecutionError, type ExecutionErrorCategory } from '../errors'

// Concrete implementation for testing abstract class
class TestExecutionError extends ExecutionError {
  readonly severity = 'warning' as const
  readonly category = 'system' as const

  constructor(message: string, context?: Record<string, unknown>) {
    super(message, context)
  }
}

class TestCriticalError extends ExecutionError {
  readonly severity = 'critical' as const
  readonly category = 'llm' as const

  constructor(message: string, context?: Record<string, unknown>) {
    super(message, context)
  }
}

class TestRetryableError extends ExecutionError {
  readonly severity = 'warning' as const
  readonly category = 'tool' as const

  constructor(message: string, context?: Record<string, unknown>) {
    super(message, context)
  }

  override isRetryable(): boolean {
    return true
  }
}

describe('ExecutionError', () => {
  describe('abstract class enforcement', () => {
    it('cannot be instantiated directly (TypeScript compile-time check)', () => {
      // TypeScript compile-time check - this test documents the requirement
      // The abstract class enforcement happens at compile time, not runtime
      // @ts-expect-error - Cannot create an instance of an abstract class
      const _checkCompileTimeError = new ExecutionError('test')
      // Test passes if TypeScript compilation shows error on the line above
      expect(true).toBe(true)
    })

    it('requires subclasses to implement severity property', () => {
      const error = new TestExecutionError('test error')
      expect(error.severity).toBe('warning')
    })

    it('requires subclasses to implement category property', () => {
      const error = new TestExecutionError('test error')
      expect(error.category).toBe('system')
    })
  })

  describe('constructor', () => {
    it('sets message property', () => {
      const error = new TestExecutionError('test error message')
      expect(error.message).toBe('test error message')
    })

    it('sets name to constructor name', () => {
      const error = new TestExecutionError('test error')
      expect(error.name).toBe('TestExecutionError')
    })

    it('sets context property when provided', () => {
      const context = { executionId: '123', step: 'validation' }
      const error = new TestExecutionError('test error', context)
      expect(error.context).toEqual(context)
    })

    it('leaves context undefined when not provided', () => {
      const error = new TestExecutionError('test error')
      expect(error.context).toBeUndefined()
    })

    it('captures stack trace', () => {
      const error = new TestExecutionError('test error')
      expect(error.stack).toBeDefined()
      expect(error.stack).toContain('TestExecutionError')
    })

    it('preserves stack trace origin', () => {
      function throwError() {
        throw new TestExecutionError('test error')
      }

      try {
        throwError()
      } catch (error) {
        expect(error).toBeInstanceOf(TestExecutionError)
        expect((error as Error).stack).toContain('throwError')
      }
    })
  })

  describe('name property', () => {
    it('returns constructor name', () => {
      const error = new TestExecutionError('test error')
      expect(error.name).toBe('TestExecutionError')
    })

    it('returns correct name for different error classes', () => {
      const criticalError = new TestCriticalError('critical error')
      expect(criticalError.name).toBe('TestCriticalError')
    })
  })

  describe('isRetryable', () => {
    it('returns false by default', () => {
      const error = new TestExecutionError('test error')
      expect(error.isRetryable()).toBe(false)
    })

    it('can be overridden to return true', () => {
      const error = new TestRetryableError('test error')
      expect(error.isRetryable()).toBe(true)
    })
  })

  describe('severity levels', () => {
    it('supports critical severity', () => {
      const error = new TestCriticalError('critical error')
      expect(error.severity).toBe('critical')
    })

    it('supports warning severity', () => {
      const error = new TestExecutionError('warning error')
      expect(error.severity).toBe('warning')
    })

    it('supports info severity', () => {
      class TestInfoError extends ExecutionError {
        readonly severity = 'info' as const
        readonly category = 'validation' as const
      }

      const error = new TestInfoError('info error')
      expect(error.severity).toBe('info')
    })
  })

  describe('error categories', () => {
    it('supports llm category', () => {
      const error = new TestCriticalError('llm error')
      expect(error.category).toBe('llm')
    })

    it('supports tool category', () => {
      const error = new TestRetryableError('tool error')
      expect(error.category).toBe('tool')
    })

    it('supports system category', () => {
      const error = new TestExecutionError('system error')
      expect(error.category).toBe('system')
    })

    it('supports workflow category', () => {
      class TestWorkflowError extends ExecutionError {
        readonly severity = 'warning' as const
        readonly category = 'workflow' as const
      }

      const error = new TestWorkflowError('workflow error')
      expect(error.category).toBe('workflow')
    })

    it('supports agent category', () => {
      class TestAgentError extends ExecutionError {
        readonly severity = 'warning' as const
        readonly category = 'agent' as const
      }

      const error = new TestAgentError('agent error')
      expect(error.category).toBe('agent')
    })

    it('supports validation category', () => {
      class TestValidationError extends ExecutionError {
        readonly severity = 'info' as const
        readonly category = 'validation' as const
      }

      const error = new TestValidationError('validation error')
      expect(error.category).toBe('validation')
    })
  })

  describe('instanceof checks', () => {
    it('is instance of ExecutionError', () => {
      const error = new TestExecutionError('test error')
      expect(error).toBeInstanceOf(ExecutionError)
    })

    it('is instance of Error', () => {
      const error = new TestExecutionError('test error')
      expect(error).toBeInstanceOf(Error)
    })

    it('preserves inheritance chain', () => {
      const error = new TestCriticalError('test error')
      expect(error).toBeInstanceOf(TestCriticalError)
      expect(error).toBeInstanceOf(ExecutionError)
      expect(error).toBeInstanceOf(Error)
    })
  })

  describe('context metadata', () => {
    it('preserves complex context objects', () => {
      const context = {
        executionId: '123',
        organizationId: 'org-456',
        workflowId: 'wf-789',
        stepIndex: 2,
        metadata: {
          retryCount: 3,
          lastAttempt: new Date().toISOString(),
        },
      }

      const error = new TestExecutionError('test error', context)
      expect(error.context).toEqual(context)
    })

    it('allows context to be modified after creation', () => {
      const error = new TestExecutionError('test error', { initial: 'value' })
      // Note: context is readonly at the property level, but the object itself is mutable
      // This is intentional to allow adding context during error propagation
      if (error.context) {
        error.context.additional = 'data'
      }
      expect(error.context).toEqual({ initial: 'value', additional: 'data' })
    })
  })

  describe('ExecutionErrorCategory type', () => {
    it('type checks valid categories', () => {
      const categories: ExecutionErrorCategory[] = ['llm', 'tool', 'workflow', 'agent', 'validation', 'system']
      expect(categories).toHaveLength(6)
    })
  })
})
