import { describe, it, expect, vi } from 'vitest'
import { z } from 'zod'
import {
  validateEntryPoint,
  validateTerminalSteps,
  validateStepReferences,
  detectCycle,
  validateTerminalOutput,
  determineNextStep
} from '../utils'
import type { WorkflowStep } from '../types'
import { StepType } from '../types'
import type { ExecutionContext } from '../../base/types'
import { WorkflowValidationError, WorkflowStepError } from '../errors'

describe('validateEntryPoint', () => {
  const createMockSteps = (): Record<string, WorkflowStep> => {
    return {
      'step1': {
        id: 'step1',
        name: 'First Step',
        description: 'First step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: null
      },
      'step2': {
        id: 'step2',
        name: 'Second Step',
        description: 'Second step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: null
      }
    }
  }

  it('passes validation when entry point exists', () => {
    const steps = createMockSteps()

    expect(() => validateEntryPoint(steps, 'step1')).not.toThrow()
  })

  it('throws WorkflowValidationError when entry point does not exist', () => {
    const steps = createMockSteps()

    expect(() => validateEntryPoint(steps, 'nonexistent')).toThrow(
      "Entry point step 'nonexistent' not found in workflow"
    )
    expect(() => validateEntryPoint(steps, 'nonexistent')).toThrow(WorkflowValidationError)
  })

  it('validates entry point with different step IDs', () => {
    const steps = createMockSteps()

    expect(() => validateEntryPoint(steps, 'step1')).not.toThrow()
    expect(() => validateEntryPoint(steps, 'step2')).not.toThrow()
  })

  it('throws WorkflowValidationError for empty step map', () => {
    const steps: Record<string, WorkflowStep> = {}

    expect(() => validateEntryPoint(steps, 'step1')).toThrow(WorkflowValidationError)
  })

  it('handles case-sensitive step IDs', () => {
    const steps = createMockSteps()

    expect(() => validateEntryPoint(steps, 'Step1')).toThrow(WorkflowValidationError)
  })
})

describe('validateTerminalSteps', () => {
  it('passes validation when workflow has terminal steps', () => {
    const steps: Record<string, WorkflowStep> = {
      'step1': {
        id: 'step1',
        name: 'First',
        description: 'First step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: { type: StepType.LINEAR, target: 'step2' }
      },
      'step2': {
        id: 'step2',
        name: 'Second',
        description: 'Second step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: null // Terminal step
      }
    }

    expect(() => validateTerminalSteps(steps, 'test-workflow')).not.toThrow()
  })

  it('throws WorkflowValidationError when no terminal steps exist', () => {
    const steps: Record<string, WorkflowStep> = {
      'step1': {
        id: 'step1',
        name: 'First',
        description: 'First step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: { type: StepType.LINEAR, target: 'step2' }
      },
      'step2': {
        id: 'step2',
        name: 'Second',
        description: 'Second step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: { type: StepType.LINEAR, target: 'step1' } // Cycle - no terminal
      }
    }

    expect(() => validateTerminalSteps(steps, 'test-workflow')).toThrow(
      "Workflow 'test-workflow' must have at least one terminal step (next: null)"
    )
    expect(() => validateTerminalSteps(steps, 'test-workflow')).toThrow(WorkflowValidationError)
  })

  it('handles workflow with multiple terminal steps', () => {
    const steps: Record<string, WorkflowStep> = {
      'step1': {
        id: 'step1',
        name: 'First',
        description: 'First step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: null // Terminal
      },
      'step2': {
        id: 'step2',
        name: 'Second',
        description: 'Second step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: null // Terminal
      }
    }

    expect(() => validateTerminalSteps(steps, 'test-workflow')).not.toThrow()
  })

  it('includes workflow ID in error context', () => {
    const steps: Record<string, WorkflowStep> = {
      'step1': {
        id: 'step1',
        name: 'First',
        description: 'First step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: { type: StepType.LINEAR, target: 'step1' }
      }
    }

    try {
      validateTerminalSteps(steps, 'my-workflow')
      expect.unreachable('Should have thrown')
    } catch (error) {
      expect(error).toBeInstanceOf(WorkflowValidationError)
      expect((error as WorkflowValidationError).context).toEqual({ workflowId: 'my-workflow' })
    }
  })
})

describe('validateStepReferences', () => {
  it('passes validation when all linear references are valid', () => {
    const steps: Record<string, WorkflowStep> = {
      'step1': {
        id: 'step1',
        name: 'First',
        description: 'First step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: { type: StepType.LINEAR, target: 'step2' }
      },
      'step2': {
        id: 'step2',
        name: 'Second',
        description: 'Second step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: null
      }
    }

    expect(() => validateStepReferences(steps)).not.toThrow()
  })

  it('throws WorkflowValidationError when linear reference is invalid', () => {
    const steps: Record<string, WorkflowStep> = {
      'step1': {
        id: 'step1',
        name: 'First',
        description: 'First step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: { type: StepType.LINEAR, target: 'nonexistent' }
      }
    }

    expect(() => validateStepReferences(steps)).toThrow(
      "Step 'step1' references non-existent target 'nonexistent'"
    )
    expect(() => validateStepReferences(steps)).toThrow(WorkflowValidationError)
  })

  it('passes validation when conditional references are valid', () => {
    const steps: Record<string, WorkflowStep> = {
      'step1': {
        id: 'step1',
        name: 'First',
        description: 'First step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({ success: z.boolean() }),
        next: {
          type: StepType.CONDITIONAL,
          routes: [{ condition: (data: { success: boolean }) => data.success, target: 'step2' }],
          default: 'step3'
        }
      },
      'step2': {
        id: 'step2',
        name: 'Second',
        description: 'Second step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: null
      },
      'step3': {
        id: 'step3',
        name: 'Third',
        description: 'Third step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: null
      }
    }

    expect(() => validateStepReferences(steps)).not.toThrow()
  })

  it('throws WorkflowValidationError when conditional route target is invalid', () => {
    const steps: Record<string, WorkflowStep> = {
      'step1': {
        id: 'step1',
        name: 'First',
        description: 'First step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: {
          type: StepType.CONDITIONAL,
          routes: [{ condition: (_data: unknown) => true, target: 'nonexistent' }],
          default: 'step2'
        }
      },
      'step2': {
        id: 'step2',
        name: 'Second',
        description: 'Second step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: null
      }
    }

    expect(() => validateStepReferences(steps)).toThrow(WorkflowValidationError)
  })

  it('throws WorkflowValidationError when conditional default target is invalid', () => {
    const steps: Record<string, WorkflowStep> = {
      'step1': {
        id: 'step1',
        name: 'First',
        description: 'First step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: {
          type: StepType.CONDITIONAL,
          routes: [{ condition: (_data: unknown) => true, target: 'step2' }],
          default: 'nonexistent'
        }
      },
      'step2': {
        id: 'step2',
        name: 'Second',
        description: 'Second step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: null
      }
    }

    expect(() => validateStepReferences(steps)).toThrow(WorkflowValidationError)
  })

  it('ignores terminal steps (next: null)', () => {
    const steps: Record<string, WorkflowStep> = {
      'step1': {
        id: 'step1',
        name: 'First',
        description: 'First step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: null
      }
    }

    expect(() => validateStepReferences(steps)).not.toThrow()
  })

  it('validates multiple conditional routes', () => {
    const steps: Record<string, WorkflowStep> = {
      'step1': {
        id: 'step1',
        name: 'First',
        description: 'First step',
        handler: async () => ({}),
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        next: {
          type: StepType.CONDITIONAL,
          routes: [
            { condition: (_data: unknown) => true, target: 'step2' },
            { condition: (_data: unknown) => false, target: 'step3' },
            { condition: (_data: unknown) => false, target: 'step4' }
          ],
          default: 'step5'
        }
      },
      'step2': { id: 'step2', name: 'Second', description: '', handler: async () => ({}), inputSchema: z.object({}), outputSchema: z.object({}), next: null },
      'step3': { id: 'step3', name: 'Third', description: '', handler: async () => ({}), inputSchema: z.object({}), outputSchema: z.object({}), next: null },
      'step4': { id: 'step4', name: 'Fourth', description: '', handler: async () => ({}), inputSchema: z.object({}), outputSchema: z.object({}), next: null },
      'step5': { id: 'step5', name: 'Fifth', description: '', handler: async () => ({}), inputSchema: z.object({}), outputSchema: z.object({}), next: null }
    }

    expect(() => validateStepReferences(steps)).not.toThrow()
  })
})

describe('detectCycle', () => {
  it('does not throw when no cycle exists', () => {
    const visited = new Set<string>(['step1', 'step2'])
    const executionPath = ['step1', 'step2']

    expect(() => detectCycle(visited, executionPath, 'step3')).not.toThrow()
  })

  it('throws WorkflowStepError when cycle is detected', () => {
    const visited = new Set<string>(['step1', 'step2'])
    const executionPath = ['step1', 'step2']

    expect(() => detectCycle(visited, executionPath, 'step1')).toThrow(
      'Cycle detected in workflow: step1 -> step2 -> step1'
    )
    expect(() => detectCycle(visited, executionPath, 'step1')).toThrow(WorkflowStepError)
  })

  it('detects cycle at start of execution path', () => {
    const visited = new Set<string>(['start'])
    const executionPath = ['start']

    expect(() => detectCycle(visited, executionPath, 'start')).toThrow(WorkflowStepError)
  })

  it('detects cycle in middle of execution path', () => {
    const visited = new Set<string>(['step1', 'step2', 'step3', 'step4'])
    const executionPath = ['step1', 'step2', 'step3', 'step4']

    expect(() => detectCycle(visited, executionPath, 'step2')).toThrow(WorkflowStepError)
  })

  it('handles long execution paths', () => {
    const visited = new Set<string>(['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', 's10'])
    const executionPath = ['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', 's10']

    expect(() => detectCycle(visited, executionPath, 's5')).toThrow(WorkflowStepError)
  })
})

describe('validateTerminalOutput', () => {
  it('passes validation when output matches schema', () => {
    const schema = z.object({ result: z.string() })
    const output = { result: 'success' }

    expect(() => validateTerminalOutput('final-step', output, schema)).not.toThrow()
  })

  it('throws WorkflowValidationError when output does not match schema', () => {
    const schema = z.object({ result: z.string() })
    const output = { result: 123 } // Should be string

    expect(() => validateTerminalOutput('final-step', output, schema)).toThrow(
      "Terminal step 'final-step' produced invalid output"
    )
    expect(() => validateTerminalOutput('final-step', output, schema)).toThrow(WorkflowValidationError)
  })

  it('validates complex nested schemas', () => {
    const schema = z.object({
      user: z.object({
        id: z.string(),
        name: z.string(),
        age: z.number()
      }),
      metadata: z.record(z.string(), z.unknown())
    })

    const validOutput = {
      user: { id: 'user-123', name: 'Alice', age: 30 },
      metadata: { key1: 'value1', key2: 123 }
    }

    expect(() => validateTerminalOutput('final-step', validOutput, schema)).not.toThrow()
  })

  it('throws WorkflowValidationError for missing required fields', () => {
    const schema = z.object({
      result: z.string(),
      status: z.string()
    })

    const output = { result: 'success' } // Missing 'status'

    expect(() => validateTerminalOutput('final-step', output, schema)).toThrow(WorkflowValidationError)
  })

  it('includes step ID in error context', () => {
    const schema = z.object({ value: z.number() })
    const output = { value: 'not a number' }

    try {
      validateTerminalOutput('my-terminal-step', output, schema)
      expect.unreachable('Should have thrown')
    } catch (error) {
      expect(error).toBeInstanceOf(WorkflowValidationError)
      expect((error as WorkflowValidationError).context).toHaveProperty('stepId', 'my-terminal-step')
    }
  })

  it('validates array schemas', () => {
    const schema = z.array(z.object({ id: z.string() }))
    const validOutput = [{ id: '1' }, { id: '2' }]

    expect(() => validateTerminalOutput('final-step', validOutput, schema)).not.toThrow()
  })

  it('validates optional fields correctly', () => {
    const schema = z.object({
      required: z.string(),
      optional: z.string().optional()
    })

    const output1 = { required: 'value' }
    const output2 = { required: 'value', optional: 'extra' }

    expect(() => validateTerminalOutput('final-step', output1, schema)).not.toThrow()
    expect(() => validateTerminalOutput('final-step', output2, schema)).not.toThrow()
  })
})

describe('determineNextStep', () => {
  const createMockContext = (): ExecutionContext => ({
    executionId: 'exec-123',
    organizationId: 'org-123',
    userId: 'user-123',
    logger: {
      info: vi.fn(),
      error: vi.fn(),
      warn: vi.fn(),
      debug: vi.fn()
    }
  })

  it('returns null for terminal step (next: null)', async () => {
    const step: WorkflowStep = {
      id: 'terminal',
      name: 'Terminal Step',
      description: 'Terminal step',
      handler: async () => ({}),
      inputSchema: z.object({}),
      outputSchema: z.object({}),
      next: null
    }

    const nextStepId = await determineNextStep(step, {}, createMockContext())

    expect(nextStepId).toBeNull()
  })

  it('returns target for linear step', async () => {
    const step: WorkflowStep = {
      id: 'step1',
      name: 'First Step',
      description: 'First step',
      handler: async () => ({}),
      inputSchema: z.object({}),
      outputSchema: z.object({}),
      next: { type: StepType.LINEAR, target: 'step2' }
    }

    const nextStepId = await determineNextStep(step, {}, createMockContext())

    expect(nextStepId).toBe('step2')
  })

  it('returns matched route for conditional step', async () => {
    const step: WorkflowStep = {
      id: 'step1',
      name: 'Conditional Step',
      description: 'Conditional step',
      handler: async () => ({}),
      inputSchema: z.object({}),
      outputSchema: z.object({ success: z.boolean() }),
      next: {
        type: StepType.CONDITIONAL,
        routes: [{ condition: (data: { success: boolean }) => data.success === true, target: 'success-step' }],
        default: 'failure-step'
      }
    }

    const data = { success: true }
    const nextStepId = await determineNextStep(step, data, createMockContext())

    expect(nextStepId).toBe('success-step')
  })

  it('returns default route when no condition matches', async () => {
    const step: WorkflowStep = {
      id: 'step1',
      name: 'Conditional Step',
      description: 'Conditional step',
      handler: async () => ({}),
      inputSchema: z.object({}),
      outputSchema: z.object({ success: z.boolean() }),
      next: {
        type: StepType.CONDITIONAL,
        routes: [{ condition: (data: { success: boolean }) => data.success === true, target: 'success-step' }],
        default: 'failure-step'
      }
    }

    const data = { success: false }
    const nextStepId = await determineNextStep(step, data, createMockContext())

    expect(nextStepId).toBe('failure-step')
  })

  it('evaluates multiple routes in order', async () => {
    const step: WorkflowStep = {
      id: 'step1',
      name: 'Multi-route Step',
      description: 'Multi-route step',
      handler: async () => ({}),
      inputSchema: z.object({}),
      outputSchema: z.object({ value: z.number() }),
      next: {
        type: StepType.CONDITIONAL,
        routes: [
          { condition: (data: { value: number }) => data.value > 100, target: 'high' },
          { condition: (data: { value: number }) => data.value > 50, target: 'medium' },
          { condition: (data: { value: number }) => data.value > 0, target: 'low' }
        ],
        default: 'zero'
      }
    }

    expect(await determineNextStep(step, { value: 150 }, createMockContext())).toBe('high')
    expect(await determineNextStep(step, { value: 75 }, createMockContext())).toBe('medium')
    expect(await determineNextStep(step, { value: 25 }, createMockContext())).toBe('low')
    expect(await determineNextStep(step, { value: 0 }, createMockContext())).toBe('zero')
  })

  it('uses default when condition throws error', async () => {
    const step: WorkflowStep = {
      id: 'step1',
      name: 'Error-prone Step',
      description: 'Error-prone step',
      handler: async () => ({}),
      inputSchema: z.object({}),
      outputSchema: z.object({}),
      next: {
        type: StepType.CONDITIONAL,
        routes: [
          {
            condition: (_data: unknown) => {
              throw new Error('Condition evaluation failed')
            },
            target: 'error-target'
          }
        ],
        default: 'safe-default'
      }
    }

    const context = createMockContext()
    const nextStepId = await determineNextStep(step, {}, context)

    expect(nextStepId).toBe('safe-default')
    expect(context.logger.warn).toHaveBeenCalled()
  })

  it('logs debug message for conditional routing', async () => {
    const step: WorkflowStep = {
      id: 'step1',
      name: 'Conditional Step',
      description: 'Conditional step',
      handler: async () => ({}),
      inputSchema: z.object({}),
      outputSchema: z.object({}),
      next: {
        type: StepType.CONDITIONAL,
        routes: [{ condition: () => true, target: 'target-step' }],
        default: 'default-step'
      }
    }

    const context = createMockContext()
    await determineNextStep(step, {}, context)

    expect(context.logger.debug).toHaveBeenCalled()
  })
})
