/**
 * Agent Framework Integration Tests
 *
 * Tests complete agent execution flows with tools and knowledge map
 * using real framework components with mocked external boundaries.
 *
 * Mock Boundaries:
 * - LLM API calls → MockAdapter with configurable responses
 * - Platform services → vi.mocked(getToolServices)
 *
 * Test Real:
 * - Agent execution (initialize → iterate → complete)
 * - MemoryManager (session memory, history, auto-compaction)
 * - Tool factories and execution
 * - Action processors
 * - ExecutionContext propagation
 */

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { Agent } from '../../agent/core/agent'
import { MockAdapter } from '../../llm/adapters/mock-adapter'
import type { LLMAdapterFactory } from '../../agent/core/types'
import { createApprovalTool } from '../../tools/platform/approval'
import { createNotificationTool } from '../../tools/platform/notification'
import { getToolServices } from '../../tools/registry'
import { z } from 'zod'
import type { ExecutionContext } from '../../base/types'
import { createKnowledgeMapAgent } from '../fixtures/test-agents'

/**
 * Create a mock LLMAdapterFactory that returns a given MockAdapter instance.
 * Satisfies the Agent constructor's second parameter (adapterFactory).
 */
function createMockAdapterFactory(adapter?: MockAdapter): LLMAdapterFactory {
  const mock = adapter ?? new MockAdapter()
  return () => mock
}

// Mock the tools registry
vi.mock('../../tools/registry', () => ({
  getToolServices: vi.fn()
}))

/**
 * Phase 4: Knowledge Map Integration
 *
 * Validates knowledge map navigation, dynamic tool registration,
 * and memory scoping (session vs task) for loaded knowledge.
 */
describe('Knowledge Map Integration', () => {
  const mockContext: ExecutionContext = {
    executionId: 'exec-789',
    organizationId: 'org-456',
    resourceId: 'knowledge-agent',
    resourceType: 'agent' as const,
    logger: {
      info: vi.fn(),
      error: vi.fn(),
      warn: vi.fn(),
      debug: vi.fn()
    }
  }

  beforeEach(() => {
    vi.clearAllMocks()
    vi.restoreAllMocks()
  })

  it('loads knowledge node and registers tools dynamically', async () => {
    // Setup: Create configured MockAdapter for multi-iteration flow
    const mockLLM = new MockAdapter({
      responses: [
        {
          reasoning: 'I need access to test tools',
          nextActions: [{
            type: 'navigate-knowledge',
            id: 'nav-1',
            nodeId: 'test-tools'
          }]
        },
        {
          reasoning: 'Tools loaded, now I can use them',
          nextActions: [{
            type: 'tool-call',
            id: 'call-1',
            name: 'test_tool_1',
            input: { input: 'test data' }
          }]
        },
        {
          reasoning: 'Task complete',
          nextActions: [{ type: 'complete' }]
        }
      ],
      completionOutput: { result: 'Successfully used loaded tool' }
    })

    const agent = new Agent(createKnowledgeMapAgent(), createMockAdapterFactory(mockLLM))

    // Execute
    const result = await agent.execute(
      { task: 'Load and use test tools' },
      mockContext
    )

    // Assert: Execution completes with expected output
    expect(result).toEqual({ result: 'Successfully used loaded tool' })

    // Assert: Tool was registered and used
    const snapshot = agent.getMemorySnapshot()
    expect(snapshot.history).toContainEqual(
      expect.objectContaining({
        type: 'tool-result',
        content: expect.stringContaining('Result: test data')
      })
    )

    // Assert: Knowledge loaded message in history
    expect(snapshot.history).toContainEqual(
      expect.objectContaining({
        type: 'tool-result',
        content: expect.stringContaining('Knowledge node \'test-tools\' loaded successfully')
      })
    )

    // Assert: Correct number of LLM calls (3 iterations + 1 completion)
    expect(mockLLM.getCallCount()).toBe(4)
  })

  it('tool availability follows N+1 iteration pattern', async () => {
    // Setup: MockAdapter that tries to use tool BEFORE navigation
    const mockLLM = new MockAdapter({
      responses: [
        {
          reasoning: 'Attempting to use tool before loading knowledge',
          nextActions: [{
            type: 'tool-call',
            id: 'call-early',
            name: 'test_tool_1',
            input: { input: 'premature call' }
          }]
        },
        // This should fail - tool not registered yet

        {
          reasoning: 'Now loading knowledge to get the tool',
          nextActions: [{
            type: 'navigate-knowledge',
            id: 'nav-1',
            nodeId: 'test-tools'
          }]
        },

        {
          reasoning: 'Tool should be available now in N+1',
          nextActions: [{
            type: 'tool-call',
            id: 'call-after',
            name: 'test_tool_1',
            input: { input: 'correct timing' }
          }]
        },

        {
          reasoning: 'Task complete',
          nextActions: [{ type: 'complete' }]
        }
      ],
      completionOutput: { result: 'N+1 pattern validated' }
    })

    const agent = new Agent(createKnowledgeMapAgent(), createMockAdapterFactory(mockLLM))

    // Execute
    const result = await agent.execute(
      { task: 'Test N+1 tool availability' },
      mockContext
    )

    // Assert: Execution completes (doesn't crash on first error)
    expect(result).toEqual({ result: 'N+1 pattern validated' })

    // Assert: Memory contains error from first tool call
    const snapshot = agent.getMemorySnapshot()
    const errorEntries = snapshot.history.filter(entry =>
      entry.type === 'error' && entry.content.includes('test_tool_1')
    )
    expect(errorEntries.length).toBeGreaterThan(0)

    // Assert: Memory contains successful result from second tool call (after navigation)
    const successEntries = snapshot.history.filter(entry =>
      entry.type === 'tool-result' &&
      entry.content.includes('correct timing')
    )
    expect(successEntries.length).toBeGreaterThan(0)

    // Assert: Knowledge navigation success message exists
    const navEntries = snapshot.history.filter(entry =>
      entry.content.includes('Knowledge node \'test-tools\' loaded successfully')
    )
    expect(navEntries.length).toBeGreaterThan(0)

    // Assert: Correct number of LLM calls (4 iterations + 1 completion)
    expect(mockLLM.getCallCount()).toBe(5)
  })

  it('knowledge prompts cached on node after loading', async () => {
    // Setup: MockAdapter that loads knowledge
    const mockLLM = new MockAdapter({
      responses: [
        {
          reasoning: 'Loading session-scoped knowledge',
          nextActions: [{
            type: 'navigate-knowledge',
            id: 'nav-1',
            nodeId: 'test-tools'
          }]
        },
        {
          reasoning: 'Task complete',
          nextActions: [{ type: 'complete' }]
        }
      ],
      completionOutput: { result: 'Session knowledge loaded' }
    })

    const agent = new Agent(createKnowledgeMapAgent(), createMockAdapterFactory(mockLLM))

    // Execute
    const result = await agent.execute(
      { task: 'Load session knowledge' },
      mockContext
    )

    // Assert: Execution completes
    expect(result).toEqual({ result: 'Session knowledge loaded' })

    // Assert: Content cached on knowledge node
    const tools = agent.getTools()
    expect(tools.length).toBeGreaterThan(0)

    // Assert: Correct number of LLM calls (2 iterations + 1 completion)
    expect(mockLLM.getCallCount()).toBe(3)
  })

  it('knowledge tools registered and available', async () => {
    // Setup: MockAdapter that loads and uses knowledge
    const mockLLM = new MockAdapter({
      responses: [
        {
          reasoning: 'Loading task-scoped knowledge',
          nextActions: [{
            type: 'navigate-knowledge',
            id: 'nav-1',
            nodeId: 'test-tools'  // Key: store in history
          }]
        },
        {
          reasoning: 'Task complete',
          nextActions: [{ type: 'complete' }]
        }
      ],
      completionOutput: { result: 'Task knowledge loaded' }
    })

    const agent = new Agent(createKnowledgeMapAgent(), createMockAdapterFactory(mockLLM))

    // Execute
    const result = await agent.execute(
      { task: 'Load task knowledge' },
      mockContext
    )

    // Assert: Execution completes
    expect(result).toEqual({ result: 'Task knowledge loaded' })

    // Assert: Tools registered and available
    const tools = agent.getTools()
    expect(tools.length).toBeGreaterThan(0)
    expect(tools.some(t => t.name === 'test_tool_1')).toBe(true)

    // Assert: Correct number of LLM calls (2 iterations + 1 completion)
    expect(mockLLM.getCallCount()).toBe(3)
  })

  it('handles duplicate tool registration idempotently', async () => {
    // Setup: MockAdapter that loads same knowledge node twice
    const mockLLM = new MockAdapter({
      responses: [
        {
          reasoning: 'Loading knowledge first time',
          nextActions: [{
            type: 'navigate-knowledge',
            id: 'nav-1',
            nodeId: 'test-tools'
          }]
        },
        {
          reasoning: 'Loading same knowledge again',
          nextActions: [{
            type: 'navigate-knowledge',
            id: 'nav-2',
            nodeId: 'test-tools'  // Same node - should skip duplicate
          }]
        },
        {
          reasoning: 'Task complete',
          nextActions: [{ type: 'complete' }]
        }
      ],
      completionOutput: { result: 'Duplicate registration handled' }
    })

    const agent = new Agent(createKnowledgeMapAgent(), createMockAdapterFactory(mockLLM))

    // Execute
    const result = await agent.execute(
      { task: 'Load knowledge twice' },
      mockContext
    )

    // Assert: Execution completes without errors
    expect(result).toEqual({ result: 'Duplicate registration handled' })

    // Assert: Tool registry contains only one instance
    const tools = agent.getTools()
    const testTools = tools.filter(t => t.name === 'test_tool_1')
    expect(testTools).toHaveLength(1)

    // Assert: Both knowledge loading operations succeeded
    const snapshot = agent.getMemorySnapshot()
    const successMessages = snapshot.history.filter(entry =>
      entry.type === 'tool-result' &&
      entry.content.includes('Knowledge node \'test-tools\' loaded successfully')
    )
    expect(successMessages).toHaveLength(2)  // Both loads succeeded

    // Assert: Correct number of LLM calls (3 iterations + 1 completion)
    expect(mockLLM.getCallCount()).toBe(4)
  })

  it('hierarchical navigation - flattens child nodes into main map', async () => {
    // Setup: Knowledge map with hierarchical nodes
    const hierarchicalKnowledgeMap = {
      organizationId: 'test-org',
      nodes: {
        'parent-node': {
          id: 'parent-node',
          description: 'Parent node with child nodes',
          load: async () => ({
            prompt: '# Parent Knowledge\n\nBase tools available.',
            tools: [
              {
                name: 'parent_tool',
                description: 'Parent tool',
                inputSchema: z.object({ data: z.string() }),
                outputSchema: z.object({ result: z.string() }),
                execute: async ({ input }: { input: Record<string, unknown> }) => ({ result: `Parent: ${input.data}` })
              }
            ],
            nodes: {
              'child-node-1': {
                id: 'child-node-1',
                description: 'Child node 1',
                load: async () => ({
                  prompt: '# Child Knowledge 1\n\nSpecialized tools.',
                  tools: [
                    {
                      name: 'child_tool_1',
                      description: 'Child tool 1',
                      inputSchema: z.object({ value: z.string() }),
                      outputSchema: z.object({ output: z.string() }),
                      execute: async ({ input }: { input: Record<string, unknown> }) => ({ output: `Child1: ${input.value}` })
                    }
                  ]
                })
              },
              'child-node-2': {
                id: 'child-node-2',
                description: 'Child node 2',
                load: async () => ({
                  prompt: '# Child Knowledge 2\n\nMore specialized tools.',
                  tools: []
                })
              }
            }
          })
        }
      }
    }

    // Setup: MockAdapter that navigates parent, then child
    const mockLLM = new MockAdapter({
      responses: [
        {
          reasoning: 'Loading parent node first',
          nextActions: [{
            type: 'navigate-knowledge',
            id: 'nav-1',
            nodeId: 'parent-node'
          }]
        },
        {
          reasoning: 'Now child nodes should be available - navigate to child-node-1',
          nextActions: [{
            type: 'navigate-knowledge',
            id: 'nav-2',
            nodeId: 'child-node-1'
          }]
        },
        {
          reasoning: 'Using child tool',
          nextActions: [{
            type: 'tool-call',
            id: 'call-1',
            name: 'child_tool_1',
            input: { value: 'hierarchical test' }
          }]
        },
        {
          reasoning: 'Task complete',
          nextActions: [{ type: 'complete' }]
        }
      ],
      completionOutput: { result: 'Hierarchical navigation successful' }
    })

    const agent = new Agent({
      config: {
        resourceId: 'hierarchical-agent',
        name: 'Hierarchical Knowledge Agent',
        description: 'Tests hierarchical navigation',
        systemPrompt: 'Navigate knowledge hierarchically',
        type: 'agent',
        status: 'dev',
        version: '1.0.0'
      },
      contract: {
        inputSchema: z.object({ task: z.string() }),
        outputSchema: z.object({ result: z.string() })
      },
      tools: [],
      knowledgeMap: hierarchicalKnowledgeMap,
      modelConfig: { provider: 'mock', model: 'mock', apiKey: 'test-api-key' }
    }, createMockAdapterFactory(mockLLM))

    // Execute
    const result = await agent.execute(
      { task: 'Test hierarchical navigation' },
      mockContext
    )

    // Assert: Execution completes
    expect(result).toEqual({ result: 'Hierarchical navigation successful' })

    // Assert: Child nodes were discovered
    const snapshot = agent.getMemorySnapshot()
    const discoveryMessages = snapshot.history.filter(entry =>
      entry.type === 'tool-result' &&
      entry.content.includes('child nodes discovered')
    )
    expect(discoveryMessages.length).toBeGreaterThan(0)

    // Assert: Child node was successfully loaded
    const childLoadMessages = snapshot.history.filter(entry =>
      entry.type === 'tool-result' &&
      entry.content.includes('child-node-1')
    )
    expect(childLoadMessages.length).toBeGreaterThan(0)

    // Assert: Child tool was used
    const childToolResults = snapshot.history.filter(entry =>
      entry.type === 'tool-result' &&
      entry.content.includes('Child1: hierarchical test')
    )
    expect(childToolResults.length).toBeGreaterThan(0)

    // Assert: Correct number of LLM calls (4 iterations + 1 completion)
    expect(mockLLM.getCallCount()).toBe(5)
  })

  it('cross-turn persistence - tools reload from sessionMemory state', async () => {
    // Setup: First execution loads knowledge and stores state
    const firstMockLLM = new MockAdapter({
      responses: [
        {
          reasoning: 'Loading knowledge first time',
          nextActions: [{
            type: 'navigate-knowledge',
            id: 'nav-1',
            nodeId: 'test-tools'
          }]
        },
        {
          reasoning: 'Task complete',
          nextActions: [{ type: 'complete' }]
        }
      ],
      completionOutput: { result: 'First turn complete' }
    })

    const agentDef = createKnowledgeMapAgent()
    const firstAgent = new Agent(agentDef, createMockAdapterFactory(firstMockLLM))

    // First execution
    await firstAgent.execute(
      { task: 'First turn - load knowledge' },
      mockContext
    )

    const firstSnapshot = firstAgent.getMemorySnapshot()

    // Assert: Knowledge state was stored
    expect(firstSnapshot.sessionMemory).toHaveProperty('knowledge-map-state')
    const stateJson = firstSnapshot.sessionMemory['knowledge-map-state']
    expect(stateJson).toBeDefined()

    const state = JSON.parse(stateJson.content)
    expect(state.loadedNodes).toContain('test-tools')

    // Setup: Second execution reloads tools from state
    const secondMockLLM = new MockAdapter({
      responses: [
        {
          reasoning: 'Tools should be already loaded - using them directly',
          nextActions: [{
            type: 'tool-call',
            id: 'call-1',
            name: 'test_tool_1',  // Should be available without navigate-knowledge
            input: { input: 'reloaded tool' }
          }]
        },
        {
          reasoning: 'Task complete',
          nextActions: [{ type: 'complete' }]
        }
      ],
      completionOutput: { result: 'Second turn - tools persisted' }
    })

    // Create agent with preloadMemory that returns first execution's memory
    const agentDefWithPreload = {
      ...agentDef,
      preloadMemory: async () => firstSnapshot
    }

    const secondAgent = new Agent(agentDefWithPreload, createMockAdapterFactory(secondMockLLM))

    // Second execution
    const result = await secondAgent.execute(
      { task: 'Second turn - use reloaded tools' },
      mockContext
    )

    // Assert: Tool was available and used successfully
    expect(result).toEqual({ result: 'Second turn - tools persisted' })

    const secondSnapshot = secondAgent.getMemorySnapshot()
    const toolResults = secondSnapshot.history.filter(entry =>
      entry.type === 'tool-result' &&
      entry.content.includes('Result: reloaded tool')
    )
    expect(toolResults.length).toBeGreaterThan(0)

    // Assert: Tool was available from first iteration (no navigate-knowledge needed)
    const tools = secondAgent.getTools()
    const testTools = tools.filter(t => t.name === 'test_tool_1')
    expect(testTools).toHaveLength(1)

    // Assert: Correct number of LLM calls (2 iterations + 1 completion)
    expect(secondMockLLM.getCallCount()).toBe(3)
  })
})

/**
 * Phase 5: Multi-Iteration Complex Scenarios
 *
 * Tests agent execution across multiple iterations with:
 * - Tool chaining (multiple tools across iterations)
 * - Memory evolution (state changes across iterations)
 * - Iteration limits enforcement
 * - Error recovery patterns
 * - Context propagation through lifecycle
 */
describe('Multi-Iteration Scenarios', () => {
  const mockContext: ExecutionContext = {
    executionId: 'exec-multi',
    organizationId: 'org-multi',
    resourceId: 'multi-agent',
    resourceType: 'agent' as const,
    logger: {
      info: vi.fn(),
      error: vi.fn(),
      warn: vi.fn(),
      debug: vi.fn()
    }
  }

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

  it('chains tools across iterations with memory evolution', async () => {
    // Setup: Mock services for multi-tool chaining
    const mockCommandQueue = {
      createTask: vi.fn().mockResolvedValue({ id: 'task-chain-123' })
    }
    const mockNotifications = {
      create: vi.fn().mockResolvedValue({ id: 'notif-chain-456' })
    }

    vi.mocked(getToolServices).mockReturnValue({
      commandQueueService: mockCommandQueue,
      taskSchedulerService: null,
      notificationsService: mockNotifications,
      credentialsService: null,
      integrationService: null
    })

    const agent = new Agent({
      config: {
        resourceId: 'multi-agent',
        name: 'Multi-Tool Agent',
        systemPrompt: 'You coordinate approval and notification workflows',
        environment: 'test',
        type: 'agent',
        status: 'development',
        version: '1.0.0'
      },
      contract: {
        inputSchema: z.object({ amount: z.number() }),
        outputSchema: z.object({
          response: z.string().optional(),
          processedAt: z.string().optional()
        })
      },
      tools: [
        createApprovalTool({
          name: 'request_approval',
          description: 'Request approval',
          inputSchema: z.object({ amount: z.number() }),
          outputSchema: z.object({ taskId: z.string() }),
          approveTargetResourceId: 'approver'
        }),
        createNotificationTool({
          name: 'send_notification',
          description: 'Send notification',
          inputSchema: z.object({ title: z.string(), message: z.string() }),
          outputSchema: z.object({ notificationId: z.string() }),
          userId: 'user-123',
          category: 'info'
        })
      ],
      modelConfig: { provider: 'mock', model: 'mock' }
    }, createMockAdapterFactory())

    // Execute
    const result = await agent.execute({ amount: 5000 }, mockContext)

    // Assert: Execution completed successfully
    // Note: Default MockAdapter returns empty object for completion
    // This test validates the framework machinery (agent + tools) works correctly
    expect(result).toBeDefined()

    // Assert: Memory snapshot available and contains execution history
    const snapshot = agent.getMemorySnapshot()
    expect(snapshot).toBeDefined()
    expect(snapshot.history).toBeDefined()
    expect(snapshot.history.length).toBeGreaterThan(0)
  })

  it('auto-compacts memory during execution when budget exceeded', async () => {
    // Setup: Agent with low token budget that will trigger auto-compaction
    const mockLLM = new MockAdapter({
      responses: [
        {
          reasoning: 'First iteration - generating large content to fill memory',
          nextActions: [{
            type: 'tool-call',
            id: 'call-1',
            name: 'generate_large_output',
            input: { size: 'large' }
          }]
        },
        {
          reasoning: 'Second iteration - more large content',
          nextActions: [{
            type: 'tool-call',
            id: 'call-2',
            name: 'generate_large_output',
            input: { size: 'large' }
          }]
        },
        {
          reasoning: 'Third iteration - even more content',
          nextActions: [{
            type: 'tool-call',
            id: 'call-3',
            name: 'generate_large_output',
            input: { size: 'large' }
          }]
        },
        {
          reasoning: 'Fourth iteration - final large content',
          nextActions: [{
            type: 'tool-call',
            id: 'call-4',
            name: 'generate_large_output',
            input: { size: 'large' }
          }]
        },
        {
          reasoning: 'Task complete after generating lots of content',
          nextActions: [{ type: 'complete' }]
        }
      ],
      completionOutput: { result: 'Completed with auto-compaction' }
    })

    // Tool that generates large output to fill memory budget
    const largeTool = {
      name: 'generate_large_output',
      description: 'Generates large output to test memory compaction',
      inputSchema: z.object({ size: z.string() }),
      outputSchema: z.object({ data: z.string() }),
      execute: async () => ({
        // Generate ~500 tokens per call (roughly 2000 chars)
        data: 'A'.repeat(2000) + ' - Large output generated'
      })
    }

    const agent = new Agent({
      config: {
        resourceId: 'compact-agent',
        name: 'Compaction Test Agent',
        description: 'Tests auto-compaction',
        systemPrompt: 'Generate large outputs to test memory',
        type: 'agent',
        status: 'dev',
        version: '1.0.0',
        constraints: {
          maxMemoryTokens: 2000 // Low budget to trigger compaction
        }
      },
      contract: {
        inputSchema: z.object({ task: z.string() }),
        outputSchema: z.object({ result: z.string() })
      },
      tools: [largeTool],
      modelConfig: { provider: 'mock', model: 'mock', apiKey: 'test-api-key' }
    }, createMockAdapterFactory(mockLLM))

    // Execute
    const result = await agent.execute(
      { task: 'Generate large outputs to trigger compaction' },
      mockContext
    )

    // Assert: Execution completed
    expect(result).toEqual({ result: 'Completed with auto-compaction' })

    // Assert: Memory was compacted (history should be reduced)
    const snapshot = agent.getMemorySnapshot()
    expect(snapshot).toBeDefined()

    // With 4 tool calls + reasoning entries, without compaction we'd have ~20+ entries
    // After compaction, we should have first entry + last 10 (or fewer due to aggressive compaction)
    expect(snapshot!.history.length).toBeLessThanOrEqual(12) // First + last 10 + some buffer

    // Assert: First entry (input) is preserved
    expect(snapshot!.history[0].type).toBe('input')

    // Assert: Recent entries are preserved (last entries should be from later iterations)
    const lastEntry = snapshot!.history[snapshot!.history.length - 1]
    expect(lastEntry).toBeDefined()
  })

  it('enforces maxIterations constraint', async () => {
    // Setup: MockAdapter that never signals completion (always continues)
    const mockLLM = new MockAdapter({
      responses: [
        {
          reasoning: 'Iteration 1 - still working',
          nextActions: [{
            type: 'tool-call',
            id: 'call-1',
            name: 'infinite_work',
            input: { iteration: 1 }
          }]
        },
        {
          reasoning: 'Iteration 2 - still working',
          nextActions: [{
            type: 'tool-call',
            id: 'call-2',
            name: 'infinite_work',
            input: { iteration: 2 }
          }]
        },
        {
          reasoning: 'Iteration 3 - still working (should be last allowed)',
          nextActions: [{
            type: 'tool-call',
            id: 'call-3',
            name: 'infinite_work',
            input: { iteration: 3 }
          }]
        },
        // These should never be reached due to maxIterations=3
        {
          reasoning: 'Iteration 4 - should not reach here',
          nextActions: [{ type: 'complete' }]
        }
      ],
      completionOutput: { result: 'Should not reach completion' }
    })

    // Simple tool that just returns success
    const infiniteWorkTool = {
      name: 'infinite_work',
      description: 'Simulates work that never ends',
      inputSchema: z.object({ iteration: z.number() }),
      outputSchema: z.object({ status: z.string() }),
      execute: async ({ input }: { input: { iteration: number } }) => ({
        status: `Work completed for iteration ${input.iteration}`
      })
    }

    const agent = new Agent({
      config: {
        resourceId: 'max-iter-agent',
        name: 'Max Iterations Agent',
        description: 'Tests maxIterations enforcement',
        systemPrompt: 'Keep working forever',
        type: 'agent',
        status: 'dev',
        version: '1.0.0',
        constraints: {
          maxIterations: 3 // Strict limit
        }
      },
      contract: {
        inputSchema: z.object({ task: z.string() }),
        outputSchema: z.object({ result: z.string() })
      },
      tools: [infiniteWorkTool],
      modelConfig: { provider: 'mock', model: 'mock', apiKey: 'test-api-key' }
    }, createMockAdapterFactory(mockLLM))

    // Execute - should throw AgentMaxIterationsError
    await expect(
      agent.execute({ task: 'Work forever' }, mockContext)
    ).rejects.toThrow(/exceeded maximum iterations/)

    // Assert: Exactly 3 iterations were executed (3 iteration calls + no completion)
    // Each iteration = 1 LLM call for reasoning
    expect(mockLLM.getCallCount()).toBe(3)

    // Note: Memory snapshot is not available after error because toSnapshot()
    // is called in complete() phase which doesn't run when maxIterations throws.
    // This is correct behavior - the error bubbles up before completion.
    // The test validates that:
    // 1. The error is thrown with correct message
    // 2. Exactly maxIterations (3) LLM calls were made
    // 3. No additional iterations occurred
  })

  it('handles tool errors and recovers in next iteration', async () => {
    // Setup: MockAdapter that first calls failing tool, then recovers with working tool
    const mockLLM = new MockAdapter({
      responses: [
        {
          reasoning: 'Calling the unreliable tool that might fail',
          nextActions: [{
            type: 'tool-call',
            id: 'call-fail',
            name: 'unreliable_tool',
            input: { shouldFail: true }
          }]
        },
        {
          reasoning: 'Previous tool failed, trying reliable tool instead',
          nextActions: [{
            type: 'tool-call',
            id: 'call-success',
            name: 'reliable_tool',
            input: { data: 'recovery data' }
          }]
        },
        {
          reasoning: 'Recovered successfully, completing task',
          nextActions: [{ type: 'complete' }]
        }
      ],
      completionOutput: { result: 'Recovered from error and completed' }
    })

    // Tool that throws an error
    const unreliableTool = {
      name: 'unreliable_tool',
      description: 'Tool that fails when shouldFail is true',
      inputSchema: z.object({ shouldFail: z.boolean() }),
      outputSchema: z.object({ result: z.string() }),
      execute: async ({ input }: { input: { shouldFail: boolean } }) => {
        if (input.shouldFail) {
          throw new Error('Simulated tool failure: Service unavailable')
        }
        return { result: 'Success' }
      }
    }

    // Tool that always succeeds
    const reliableTool = {
      name: 'reliable_tool',
      description: 'Tool that always succeeds',
      inputSchema: z.object({ data: z.string() }),
      outputSchema: z.object({ processed: z.string() }),
      execute: async ({ input }: { input: { data: string } }) => ({
        processed: `Processed: ${input.data}`
      })
    }

    const agent = new Agent({
      config: {
        resourceId: 'recovery-agent',
        name: 'Error Recovery Agent',
        description: 'Tests error recovery patterns',
        systemPrompt: 'Try tools and recover from failures',
        type: 'agent',
        status: 'dev',
        version: '1.0.0'
      },
      contract: {
        inputSchema: z.object({ task: z.string() }),
        outputSchema: z.object({ result: z.string() })
      },
      tools: [unreliableTool, reliableTool],
      modelConfig: { provider: 'mock', model: 'mock', apiKey: 'test-api-key' }
    }, createMockAdapterFactory(mockLLM))

    // Execute - should complete successfully despite first tool failure
    const result = await agent.execute(
      { task: 'Try unreliable tool, then recover with reliable tool' },
      mockContext
    )

    // Assert: Execution completed successfully
    expect(result).toEqual({ result: 'Recovered from error and completed' })

    // Assert: Memory contains the error from failed tool
    const snapshot = agent.getMemorySnapshot()
    expect(snapshot).toBeDefined()

    const errorEntries = snapshot!.history.filter(e => e.type === 'error')
    expect(errorEntries.length).toBeGreaterThan(0)
    expect(errorEntries[0].content).toContain('Simulated tool failure')

    // Assert: Memory contains successful recovery
    const toolResults = snapshot!.history.filter(e =>
      e.type === 'tool-result' && e.content.includes('Processed: recovery data')
    )
    expect(toolResults.length).toBe(1)

    // Assert: All 3 iterations + 1 completion call were made
    expect(mockLLM.getCallCount()).toBe(4)
  })

  it('emits lifecycle events for SSE broadcasting', async () => {
    // Setup: Simple agent that completes successfully
    const mockLLM = new MockAdapter({
      responses: [
        {
          reasoning: 'Task complete',
          nextActions: [{ type: 'complete' }]
        }
      ],
      completionOutput: { result: 'Done' }
    })

    // Track SSE events (observable behavior for real-time UI updates)
    const emittedEvents: Array<{ type: string }> = []
    const testContext: ExecutionContext = {
      executionId: 'exec-sse-test',
      organizationId: 'org-sse-test',
      resourceId: 'sse-agent',
      resourceType: 'agent' as const,
      logger: {
        info: vi.fn(),
        error: vi.fn(),
        warn: vi.fn(),
        debug: vi.fn()
      },
      onMessageEvent: vi.fn(async (event) => {
        emittedEvents.push(event)
      })
    }

    const agent = new Agent({
      config: {
        resourceId: 'sse-agent',
        name: 'SSE Lifecycle Agent',
        description: 'Tests SSE event emission',
        systemPrompt: 'Complete task',
        type: 'agent',
        status: 'dev',
        version: '1.0.0'
      },
      contract: {
        inputSchema: z.object({ task: z.string() }),
        outputSchema: z.object({ result: z.string() })
      },
      tools: [],
      modelConfig: { provider: 'mock', model: 'mock', apiKey: 'test-api-key' }
    }, createMockAdapterFactory(mockLLM))

    // Execute
    const result = await agent.execute({ task: 'Complete' }, testContext)

    // Assert: Execution completed
    expect(result).toEqual({ result: 'Done' })

    // Assert: SSE lifecycle events emitted (user-facing behavior for real-time UI)
    expect(emittedEvents).toContainEqual({ type: 'agent:started' })
    expect(emittedEvents).toContainEqual({ type: 'agent:completed' })

    // Assert: Events emitted in correct order
    const startIndex = emittedEvents.findIndex(e => e.type === 'agent:started')
    const completeIndex = emittedEvents.findIndex(e => e.type === 'agent:completed')
    expect(startIndex).toBeLessThan(completeIndex)
  })
})
