import { describe, it, expect, vi } from 'vitest'
import { processActions } from '../processor'
import type { IterationContext } from '../../core/types'
import type { LLMIterationResponse } from '../../reasoning/types'
import type { AgentAction, ToolCallAction, CompleteAction, MessageAction } from '../types'
import type { ExecutionContext } from '../../../base/types'

describe('processActions - validateActionSequence', () => {
  const createMockIterationContext = (): IterationContext => {
    const mockMemoryManager: Record<string, unknown> = {
      addToHistory: vi.fn(),
      getHistory: vi.fn(() => []),
      estimateTokens: vi.fn(() => 0),
      clear: vi.fn()
    }
    const context = {
      executionContext: {
        executionId: 'exec-123',
        organizationId: 'org-123',
        userId: 'user-123',
        logger: {
          info: vi.fn(),
          error: vi.fn(),
          warn: vi.fn(),
          debug: vi.fn()
        }
      } as ExecutionContext,
      toolRegistry: new Map(),
      config: {
        type: 'agent' as const,
        resourceId: 'test-agent',
        name: 'Test Agent',
        description: 'Test',
        environment: 'dev',
        systemPrompt: 'Test'
      },
      contract: {
        inputSchema: {},
        outputSchema: {}
      },
      logger: {
        action: vi.fn(),
        info: vi.fn(),
        error: vi.fn(),
        warn: vi.fn(),
        debug: vi.fn()
      },
      modelConfig: {
        provider: 'mock' as const,
        model: 'mock',
        apiKey: 'test'
      },
      memoryManager: mockMemoryManager,
      iteration: 0
    } as IterationContext
    return context
  }

  const createMockResponse = (actions: AgentAction[]): LLMIterationResponse => ({
    reasoning: 'Test reasoning',
    nextActions: actions
  })

  describe('valid action sequences', () => {
    it('allows single complete action', async () => {
      const context = createMockIterationContext()
      const response = createMockResponse([{ type: 'complete' }])

      const result = await processActions(context, response)

      expect(result.shouldComplete).toBe(true)
    })

    it('allows single tool-call action', async () => {
      const context = createMockIterationContext()
      const toolAction: ToolCallAction = {
        type: 'tool-call',
        id: 'tool-1',
        name: 'testTool',
        input: {}
      }
      const response = createMockResponse([toolAction])

      // Tool not found is handled gracefully (adds error to memory, doesn't throw)
      const result = await processActions(context, response)
      expect(result.shouldComplete).toBe(false)
    })

    it('allows multiple tool-call actions', async () => {
      const context = createMockIterationContext()
      const actions: ToolCallAction[] = [
        { type: 'tool-call', id: 'tool-1', name: 'tool1', input: {} },
        { type: 'tool-call', id: 'tool-2', name: 'tool2', input: {} }
      ]
      const response = createMockResponse(actions)

      // Tools not found are handled gracefully (adds errors to memory, doesn't throw)
      const result = await processActions(context, response)
      expect(result.shouldComplete).toBe(false)
    })

    it('allows message action before complete', async () => {
      const context = createMockIterationContext()
      const actions: AgentAction[] = [{ type: 'message', text: 'Processing complete' }, { type: 'complete' }]
      const response = createMockResponse(actions)

      const result = await processActions(context, response)

      expect(result.shouldComplete).toBe(true)
    })

    it('allows multiple message actions before complete', async () => {
      const context = createMockIterationContext()
      const actions: AgentAction[] = [
        { type: 'message', text: 'Step 1 done' },
        { type: 'message', text: 'Step 2 done' },
        { type: 'complete' }
      ]
      const response = createMockResponse(actions)

      const result = await processActions(context, response)

      expect(result.shouldComplete).toBe(true)
    })
  })

  describe('invalid action sequences', () => {
    it('throws error for multiple complete actions', async () => {
      const context = createMockIterationContext()
      const actions: CompleteAction[] = [{ type: 'complete' }, { type: 'complete' }]
      const response = createMockResponse(actions)

      await expect(processActions(context, response)).rejects.toThrow(
        'Multiple complete actions not allowed in single iteration'
      )
    })

    it('allows tool-call mixed with complete (side-effect tools execute first)', async () => {
      const context = createMockIterationContext()

      const mockTool = {
        name: 'testTool',
        inputSchema: { parse: (x: unknown) => x },
        outputSchema: { parse: (x: unknown) => x },
        execute: vi.fn(async () => ({ result: 'done' }))
      }
      context.toolRegistry.set('testTool', mockTool as unknown as import('../../tools/types').Tool)

      const actions: AgentAction[] = [
        { type: 'tool-call', id: 'tool-1', name: 'testTool', input: {} },
        { type: 'complete' }
      ]
      const response = createMockResponse(actions)

      const result = await processActions(context, response)

      expect(mockTool.execute).toHaveBeenCalledTimes(1)
      expect(result.shouldComplete).toBe(true)
    })

    it('allows complete before tool-call (tools still execute first)', async () => {
      const context = createMockIterationContext()

      const mockTool = {
        name: 'testTool',
        inputSchema: { parse: (x: unknown) => x },
        outputSchema: { parse: (x: unknown) => x },
        execute: vi.fn(async () => ({ result: 'done' }))
      }
      context.toolRegistry.set('testTool', mockTool as unknown as import('../../tools/types').Tool)

      const actions: AgentAction[] = [
        { type: 'complete' },
        { type: 'tool-call', id: 'tool-1', name: 'testTool', input: {} }
      ]
      const response = createMockResponse(actions)

      const result = await processActions(context, response)

      expect(mockTool.execute).toHaveBeenCalledTimes(1)
      expect(result.shouldComplete).toBe(true)
    })
  })

  describe('action processing behavior', () => {
    it('handles message action with session context', async () => {
      const onMessageEvent = vi.fn()

      const context = createMockIterationContext()
      context.executionContext.onMessageEvent = onMessageEvent

      const messageAction: MessageAction = {
        type: 'message',
        text: 'Hello from agent'
      }
      const response = createMockResponse([messageAction, { type: 'complete' }])

      await processActions(context, response)

      expect(onMessageEvent).toHaveBeenCalledWith({
        type: 'assistant_message',
        text: 'Hello from agent'
      })
    })

    it('handles message action without session context', async () => {
      const context = createMockIterationContext()
      // No sessionContext

      const messageAction: MessageAction = {
        type: 'message',
        text: 'Hello from agent'
      }
      const response = createMockResponse([messageAction, { type: 'complete' }])

      // Should not throw - just silently skip streaming
      const result = await processActions(context, response)

      expect(result.shouldComplete).toBe(true)
    })

    it('processes multiple messages in order', async () => {
      const onMessageEvent = vi.fn()

      const context = createMockIterationContext()
      context.executionContext.onMessageEvent = onMessageEvent

      const actions: AgentAction[] = [
        { type: 'message', text: 'First message' },
        { type: 'message', text: 'Second message' },
        { type: 'message', text: 'Third message' },
        { type: 'complete' }
      ]
      const response = createMockResponse(actions)

      await processActions(context, response)

      expect(onMessageEvent).toHaveBeenCalledTimes(3)
      expect(onMessageEvent).toHaveBeenNthCalledWith(1, {
        type: 'assistant_message',
        text: 'First message'
      })
      expect(onMessageEvent).toHaveBeenNthCalledWith(2, {
        type: 'assistant_message',
        text: 'Second message'
      })
      expect(onMessageEvent).toHaveBeenNthCalledWith(3, {
        type: 'assistant_message',
        text: 'Third message'
      })
    })

    it('returns shouldComplete: false when no complete action', async () => {
      const context = createMockIterationContext()
      const response = createMockResponse([])

      const result = await processActions(context, response)

      expect(result.shouldComplete).toBe(false)
    })

    it('returns shouldComplete: true when complete action present', async () => {
      const context = createMockIterationContext()
      const response = createMockResponse([{ type: 'complete' }])

      const result = await processActions(context, response)

      expect(result.shouldComplete).toBe(true)
    })
  })

  describe('edge cases', () => {
    it('handles empty action array', async () => {
      const context = createMockIterationContext()
      const response = createMockResponse([])

      const result = await processActions(context, response)

      expect(result.shouldComplete).toBe(false)
    })

    it('validates actions before executing any', async () => {
      const context = createMockIterationContext()
      const executeTool = vi.fn()
      const mockTool = { name: 'testTool', execute: executeTool } as Partial<import('../../tools/types').Tool>
      context.toolRegistry.set('testTool', mockTool as import('../../tools/types').Tool)

      // Invalid: multiple completes
      const actions: AgentAction[] = [
        { type: 'tool-call', id: 'tool-1', name: 'testTool', input: {} },
        { type: 'complete' },
        { type: 'complete' }
      ]
      const response = createMockResponse(actions)

      await expect(processActions(context, response)).rejects.toThrow('Multiple complete actions not allowed')

      // Tool should NOT have been executed (validation happens first)
      expect(executeTool).not.toHaveBeenCalled()
    })
  })

  describe('validation rules compliance', () => {
    it('enforces Rule 1: no duplicate completions', async () => {
      const context = createMockIterationContext()
      const actions = [{ type: 'complete' }, { type: 'complete' }, { type: 'complete' }] as AgentAction[]
      const response = createMockResponse(actions)

      await expect(processActions(context, response)).rejects.toThrow(
        'Multiple complete actions not allowed in single iteration'
      )
    })

    it('enforces Rule 2: complete cannot mix with navigate-knowledge', async () => {
      const context = createMockIterationContext()
      const actions: AgentAction[] = [
        { type: 'complete' },
        { type: 'navigate-knowledge', id: 'nav-1', nodeId: 'node-1' }
      ]
      const response = createMockResponse(actions)

      await expect(processActions(context, response)).rejects.toThrow(
        'Complete action cannot mix with navigate-knowledge actions'
      )
    })

    it('allows Rule 2 exception: complete can mix with tool-call', async () => {
      const context = createMockIterationContext()
      const actions: AgentAction[] = [
        { type: 'complete' },
        { type: 'tool-call', id: 'tool-1', name: 'tool', input: {} }
      ]
      const response = createMockResponse(actions)

      // Should not throw — tool calls are side-effect compatible with completion
      const result = await processActions(context, response)
      expect(result.shouldComplete).toBe(true)
    })

    it('allows message with complete (exception to Rule 2)', async () => {
      const context = createMockIterationContext()
      const actions: AgentAction[] = [{ type: 'message', text: 'Done!' }, { type: 'complete' }]
      const response = createMockResponse(actions)

      const result = await processActions(context, response)

      expect(result.shouldComplete).toBe(true)
    })
  })

  describe('parallel tool execution', () => {
    it('executes multiple tool calls concurrently', async () => {
      const context = createMockIterationContext()
      const executionOrder: string[] = []

      // Create mock tools with configurable delays
      const mockTool1 = {
        name: 'tool1',
        inputSchema: { parse: (x: unknown) => x },
        outputSchema: { parse: (x: unknown) => x },
        execute: vi.fn(async () => {
          executionOrder.push('tool1-start')
          await new Promise((resolve) => setTimeout(resolve, 50))
          executionOrder.push('tool1-end')
          return { result: 'tool1' }
        })
      }
      const mockTool2 = {
        name: 'tool2',
        inputSchema: { parse: (x: unknown) => x },
        outputSchema: { parse: (x: unknown) => x },
        execute: vi.fn(async () => {
          executionOrder.push('tool2-start')
          await new Promise((resolve) => setTimeout(resolve, 50))
          executionOrder.push('tool2-end')
          return { result: 'tool2' }
        })
      }

      context.toolRegistry.set('tool1', mockTool1 as unknown as import('../../tools/types').Tool)
      context.toolRegistry.set('tool2', mockTool2 as unknown as import('../../tools/types').Tool)

      const actions: ToolCallAction[] = [
        { type: 'tool-call', id: 'call-1', name: 'tool1', input: {} },
        { type: 'tool-call', id: 'call-2', name: 'tool2', input: {} }
      ]
      const response = createMockResponse(actions)

      await processActions(context, response)

      // Both tools should have been executed
      expect(mockTool1.execute).toHaveBeenCalledTimes(1)
      expect(mockTool2.execute).toHaveBeenCalledTimes(1)

      // Verify parallel execution: both starts should happen before any end
      // (In sequential execution, we'd see: tool1-start, tool1-end, tool2-start, tool2-end)
      const tool1StartIndex = executionOrder.indexOf('tool1-start')
      const tool2StartIndex = executionOrder.indexOf('tool2-start')
      const tool1EndIndex = executionOrder.indexOf('tool1-end')
      const tool2EndIndex = executionOrder.indexOf('tool2-end')

      // Both should start before either ends (parallel behavior)
      expect(tool1StartIndex).toBeLessThan(tool1EndIndex)
      expect(tool2StartIndex).toBeLessThan(tool2EndIndex)
      // If running in parallel, both starts should happen before ends complete
      expect(Math.max(tool1StartIndex, tool2StartIndex)).toBeLessThan(Math.max(tool1EndIndex, tool2EndIndex))
    })

    it('handles mixed actions: tool calls in parallel, others sequential', async () => {
      const context = createMockIterationContext()
      const onMessageEvent = vi.fn()
      context.executionContext.onMessageEvent = onMessageEvent

      // Create mock tools
      const mockTool1 = {
        name: 'tool1',
        inputSchema: { parse: (x: unknown) => x },
        outputSchema: { parse: (x: unknown) => x },
        execute: vi.fn(async () => ({ result: 'tool1' }))
      }
      const mockTool2 = {
        name: 'tool2',
        inputSchema: { parse: (x: unknown) => x },
        outputSchema: { parse: (x: unknown) => x },
        execute: vi.fn(async () => ({ result: 'tool2' }))
      }

      context.toolRegistry.set('tool1', mockTool1 as unknown as import('../../tools/types').Tool)
      context.toolRegistry.set('tool2', mockTool2 as unknown as import('../../tools/types').Tool)

      // Mix tool calls with messages
      const actions: AgentAction[] = [
        { type: 'message', text: 'Starting work...' },
        { type: 'tool-call', id: 'call-1', name: 'tool1', input: {} },
        { type: 'tool-call', id: 'call-2', name: 'tool2', input: {} }
      ]
      const response = createMockResponse(actions)

      await processActions(context, response)

      // Both tools should be executed
      expect(mockTool1.execute).toHaveBeenCalledTimes(1)
      expect(mockTool2.execute).toHaveBeenCalledTimes(1)

      // Message should also be sent
      expect(onMessageEvent).toHaveBeenCalledWith({
        type: 'assistant_message',
        text: 'Starting work...'
      })
    })

    it('handles partial failure: successful tools complete even when one fails', async () => {
      const context = createMockIterationContext()

      // Create one successful and one failing tool
      const mockSuccessTool = {
        name: 'successTool',
        inputSchema: { parse: (x: unknown) => x },
        outputSchema: { parse: (x: unknown) => x },
        execute: vi.fn(async () => ({ result: 'success' }))
      }
      const mockFailTool = {
        name: 'failTool',
        inputSchema: { parse: (x: unknown) => x },
        outputSchema: { parse: (x: unknown) => x },
        execute: vi.fn(async () => {
          throw new Error('Tool failed intentionally')
        })
      }

      context.toolRegistry.set('successTool', mockSuccessTool as unknown as import('../../tools/types').Tool)
      context.toolRegistry.set('failTool', mockFailTool as unknown as import('../../tools/types').Tool)

      const actions: ToolCallAction[] = [
        { type: 'tool-call', id: 'call-1', name: 'successTool', input: {} },
        { type: 'tool-call', id: 'call-2', name: 'failTool', input: {} }
      ]
      const response = createMockResponse(actions)

      // Should not throw - Promise.allSettled handles partial failures
      const result = await processActions(context, response)

      // Both tools should have been attempted
      expect(mockSuccessTool.execute).toHaveBeenCalledTimes(1)
      expect(mockFailTool.execute).toHaveBeenCalledTimes(1)

      // Should complete without throwing
      expect(result.shouldComplete).toBe(false)
    })

    it('maintains timing advantage for parallel execution', async () => {
      const context = createMockIterationContext()
      const TOOL_DELAY = 100 // ms per tool (high enough to distinguish parallel vs sequential under load)

      // Create multiple tools with configurable delays
      const createDelayedTool = (name: string) => ({
        name,
        inputSchema: { parse: (x: unknown) => x },
        outputSchema: { parse: (x: unknown) => x },
        execute: vi.fn(async () => {
          await new Promise((resolve) => setTimeout(resolve, TOOL_DELAY))
          return { result: name }
        })
      })

      const tools = ['tool1', 'tool2', 'tool3'].map(createDelayedTool)
      tools.forEach((tool) => {
        context.toolRegistry.set(tool.name, tool as unknown as import('../../tools/types').Tool)
      })

      const actions: ToolCallAction[] = tools.map((tool, i) => ({
        type: 'tool-call',
        id: `call-${i}`,
        name: tool.name,
        input: {}
      }))
      const response = createMockResponse(actions)

      const startTime = Date.now()
      await processActions(context, response)
      const endTime = Date.now()
      const totalTime = endTime - startTime

      // In parallel: ~TOOL_DELAY ms (all run simultaneously)
      // In sequential: ~TOOL_DELAY * 3 = 90ms (one after another)
      // Allow some overhead, but should be significantly less than sequential time
      const sequentialTime = TOOL_DELAY * tools.length
      expect(totalTime).toBeLessThan(sequentialTime * 0.8) // At least 20% faster than sequential
    })
  })
})
