/**
 * Navigate Knowledge Executor Unit Tests
 * Tests executeNavigateKnowledge action executor
 */

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { executeNavigateKnowledge } from '../../actions/navigate-knowledge-executor'
import type { IterationContext } from '../../core/types'
import type { NavigateKnowledgeAction } from '../../actions/types'
import type { KnowledgeMap, KnowledgeMapState } from '../types'
import type { ExecutionContext, Tool } from '../../../base/types'
import { z } from 'zod'

describe('executeNavigateKnowledge', () => {
  let mockContext: ExecutionContext
  let mockIterationContext: IterationContext

  beforeEach(() => {
    mockContext = {
      executionId: 'exec-123',
      organizationId: 'org-123',
      resourceId: 'agent-123',
      resourceType: 'agent',
      logger: {
        info: vi.fn(),
        error: vi.fn(),
        warn: vi.fn(),
        debug: vi.fn()
      },
      onMessageEvent: vi.fn()
    }

    mockIterationContext = {
      config: {
        resourceId: 'test-agent',
        name: 'Test Agent',
        description: 'Test',
        systemPrompt: 'Test prompt',
        type: 'agent',
        status: 'dev',
        version: '1.0.0'
      },
      contract: {
        inputSchema: z.object({}),
        outputSchema: z.object({})
      },
      toolRegistry: new Map(),
      memoryManager: {
        get: vi.fn(),
        set: vi.fn(),
        delete: vi.fn(),
        addToHistory: vi.fn(),
        toContext: vi.fn(),
        toSnapshot: vi.fn(),
        getSnapshot: vi.fn(),
        enforceHardLimits: vi.fn(),
        getStatus: vi.fn()
      } as unknown,
      executionContext: mockContext,
      iteration: 1,
      logger: {
        action: vi.fn(),
        toolCall: vi.fn(),
        lifecycle: vi.fn()
      } as unknown,
      modelConfig: {
        provider: 'openai',
        model: 'gpt-4',
        apiKey: 'test-key'
      },
      knowledgeMap: undefined // Will be set per test
    }

    vi.clearAllMocks()
  })

  describe('basic functionality', () => {
    it('loads knowledge node and registers tools', async () => {
      const mockTool: Tool = {
        name: 'test_tool',
        description: 'Test tool',
        inputSchema: z.object({ input: z.string() }),
        outputSchema: z.object({ output: z.string() }),
        execute: vi.fn()
      }

      const knowledgeMap: KnowledgeMap = {
        organizationId: 'org-test',
        nodes: {
          'test-node': {
            id: 'test-node',
            description: 'Test node',
            load: vi.fn().mockResolvedValue({
              prompt: 'Test knowledge prompt',
              tools: [mockTool]
            })
          }
        }
      }

      mockIterationContext.knowledgeMap = knowledgeMap

      const action: NavigateKnowledgeAction = {
        type: 'navigate-knowledge',
        id: 'nav-1',
        nodeId: 'test-node',
        sessionScope: false
      }

      await executeNavigateKnowledge(mockIterationContext, action)

      // Assert: Tool registered in toolRegistry
      expect(mockIterationContext.toolRegistry.has('test_tool')).toBe(true)
      expect(mockIterationContext.toolRegistry.get('test_tool')).toEqual(mockTool)

      // Assert: Node loaded with correct context
      expect(knowledgeMap.nodes['test-node'].load).toHaveBeenCalledWith(mockContext)

      // Assert: Loaded flag and prompt cached
      expect(knowledgeMap.nodes['test-node'].loaded).toBe(true)
      expect(knowledgeMap.nodes['test-node'].prompt).toBe('Test knowledge prompt')
    })
  })

  describe('hierarchical navigation', () => {
    it('flattens child nodes into main knowledge map', async () => {
      const childNode = {
        id: 'child-node',
        description: 'Child node',
        load: vi.fn().mockResolvedValue({
          prompt: 'Child prompt',
          tools: []
        })
      }

      const knowledgeMap: KnowledgeMap = {
        organizationId: 'org-test',
        nodes: {
          'parent-node': {
            id: 'parent-node',
            description: 'Parent',
            load: vi.fn().mockResolvedValue({
              prompt: 'Parent prompt',
              tools: [],
              nodes: {
                'child-node': childNode
              }
            })
          }
        }
      }

      mockIterationContext.knowledgeMap = knowledgeMap

      const action: NavigateKnowledgeAction = {
        type: 'navigate-knowledge',
        id: 'nav-1',
        nodeId: 'parent-node',
        sessionScope: false
      }

      await executeNavigateKnowledge(mockIterationContext, action)

      // Assert: Child node added to main map
      expect(knowledgeMap.nodes['child-node']).toEqual(childNode)
    })

    it('discovers multiple child nodes', async () => {
      const knowledgeMap: KnowledgeMap = {
        organizationId: 'org-test',
        nodes: {
          crm: {
            id: 'crm',
            description: 'CRM',
            load: vi.fn().mockResolvedValue({
              prompt: 'CRM base',
              tools: [],
              nodes: {
                'crm-customers': {
                  id: 'crm-customers',
                  description: 'Customer tools',
                  load: vi.fn()
                },
                'crm-deals': {
                  id: 'crm-deals',
                  description: 'Deal tools',
                  load: vi.fn()
                }
              }
            })
          }
        }
      }

      mockIterationContext.knowledgeMap = knowledgeMap

      const action: NavigateKnowledgeAction = {
        type: 'navigate-knowledge',
        id: 'nav-1',
        nodeId: 'crm',
        sessionScope: false
      }

      await executeNavigateKnowledge(mockIterationContext, action)

      // Assert: Both children added to map
      expect(knowledgeMap.nodes['crm-customers']).toBeDefined()
      expect(knowledgeMap.nodes['crm-deals']).toBeDefined()
    })
  })

  describe('state tracking', () => {
    it('creates knowledge-map-state when none exists', async () => {
      const knowledgeMap: KnowledgeMap = {
        organizationId: 'org-test',
        nodes: {
          'test-node': {
            id: 'test-node',
            description: 'Test',
            load: vi.fn().mockResolvedValue({
              prompt: 'Test prompt'
            })
          }
        }
      }

      mockIterationContext.knowledgeMap = knowledgeMap
      vi.mocked(mockIterationContext.memoryManager.get).mockReturnValue(undefined)

      const action: NavigateKnowledgeAction = {
        type: 'navigate-knowledge',
        id: 'nav-1',
        nodeId: 'test-node',
        sessionScope: false
      }

      await executeNavigateKnowledge(mockIterationContext, action)

      // Assert: State created and saved
      expect(mockIterationContext.memoryManager.set).toHaveBeenCalledWith(
        'knowledge-map-state',
        expect.stringContaining('test-node')
      )

      // Verify state structure
      const setCall = vi.mocked(mockIterationContext.memoryManager.set).mock.calls[0]
      const savedState = JSON.parse(setCall[1]) as KnowledgeMapState
      expect(savedState.loadedNodes).toEqual(['test-node'])
      expect(savedState.version).toBe(1)
    })

    it('appends to existing knowledge-map-state', async () => {
      const existingState: KnowledgeMapState = {
        loadedNodes: ['node-1'],
        version: 1
      }

      const knowledgeMap: KnowledgeMap = {
        organizationId: 'org-test',
        nodes: {
          'node-2': {
            id: 'node-2',
            description: 'Second node',
            load: vi.fn().mockResolvedValue({
              prompt: 'Node 2 prompt'
            })
          }
        }
      }

      mockIterationContext.knowledgeMap = knowledgeMap
      vi.mocked(mockIterationContext.memoryManager.get).mockReturnValue(
        JSON.stringify(existingState)
      )

      const action: NavigateKnowledgeAction = {
        type: 'navigate-knowledge',
        id: 'nav-1',
        nodeId: 'node-2',
        sessionScope: false
      }

      await executeNavigateKnowledge(mockIterationContext, action)

      // Assert: New node added to existing state
      const setCall = vi.mocked(mockIterationContext.memoryManager.set).mock.calls[0]
      const updatedState = JSON.parse(setCall[1]) as KnowledgeMapState
      expect(updatedState.loadedNodes).toEqual(['node-1', 'node-2'])
    })

    it('does not duplicate node ID in state', async () => {
      const existingState: KnowledgeMapState = {
        loadedNodes: ['test-node'],
        version: 1
      }

      const knowledgeMap: KnowledgeMap = {
        organizationId: 'org-test',
        nodes: {
          'test-node': {
            id: 'test-node',
            description: 'Test',
            load: vi.fn().mockResolvedValue({
              prompt: 'Test prompt'
            })
          }
        }
      }

      mockIterationContext.knowledgeMap = knowledgeMap
      vi.mocked(mockIterationContext.memoryManager.get).mockReturnValue(
        JSON.stringify(existingState)
      )

      const action: NavigateKnowledgeAction = {
        type: 'navigate-knowledge',
        id: 'nav-1',
        nodeId: 'test-node',
        sessionScope: false
      }

      await executeNavigateKnowledge(mockIterationContext, action)

      // Assert: State not updated (node already in list)
      expect(mockIterationContext.memoryManager.set).not.toHaveBeenCalledWith(
        'knowledge-map-state',
        expect.anything()
      )
    })
  })

  describe('tool registration', () => {
    it('registers new tools', async () => {
      const tool1: Tool = {
        name: 'tool_1',
        description: 'Tool 1',
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        execute: vi.fn()
      }

      const tool2: Tool = {
        name: 'tool_2',
        description: 'Tool 2',
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        execute: vi.fn()
      }

      const knowledgeMap: KnowledgeMap = {
        organizationId: 'org-test',
        nodes: {
          'multi-tool-node': {
            id: 'multi-tool-node',
            description: 'Multiple tools',
            load: vi.fn().mockResolvedValue({
              prompt: 'Test',
              tools: [tool1, tool2]
            })
          }
        }
      }

      mockIterationContext.knowledgeMap = knowledgeMap

      const action: NavigateKnowledgeAction = {
        type: 'navigate-knowledge',
        id: 'nav-1',
        nodeId: 'multi-tool-node',
        sessionScope: false
      }

      await executeNavigateKnowledge(mockIterationContext, action)

      // Assert: Both tools registered
      expect(mockIterationContext.toolRegistry.size).toBe(2)
      expect(mockIterationContext.toolRegistry.get('tool_1')).toEqual(tool1)
      expect(mockIterationContext.toolRegistry.get('tool_2')).toEqual(tool2)
    })

    it('skips already-registered tools', async () => {
      const existingTool: Tool = {
        name: 'existing_tool',
        description: 'Exists',
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        execute: vi.fn()
      }

      const newTool: Tool = {
        name: 'new_tool',
        description: 'New',
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        execute: vi.fn()
      }

      // Pre-register existing tool
      mockIterationContext.toolRegistry.set('existing_tool', existingTool)

      const knowledgeMap: KnowledgeMap = {
        organizationId: 'org-test',
        nodes: {
          'test-node': {
            id: 'test-node',
            description: 'Test',
            load: vi.fn().mockResolvedValue({
              prompt: 'Test',
              tools: [existingTool, newTool]
            })
          }
        }
      }

      mockIterationContext.knowledgeMap = knowledgeMap

      const action: NavigateKnowledgeAction = {
        type: 'navigate-knowledge',
        id: 'nav-1',
        nodeId: 'test-node',
        sessionScope: false
      }

      await executeNavigateKnowledge(mockIterationContext, action)

      // Assert: Only new tool added (duplicate skipped)
      expect(mockIterationContext.toolRegistry.size).toBe(2)
    })

    it('handles nodes with no tools', async () => {
      const knowledgeMap: KnowledgeMap = {
        organizationId: 'org-test',
        nodes: {
          'prompt-only': {
            id: 'prompt-only',
            description: 'Prompt only',
            load: vi.fn().mockResolvedValue({
              prompt: 'Just context, no tools'
            })
          }
        }
      }

      mockIterationContext.knowledgeMap = knowledgeMap

      const action: NavigateKnowledgeAction = {
        type: 'navigate-knowledge',
        id: 'nav-1',
        nodeId: 'prompt-only',
        sessionScope: false
      }

      await executeNavigateKnowledge(mockIterationContext, action)

    })
  })

  describe('error handling', () => {
    it('handles knowledge map not available', async () => {
      mockIterationContext.knowledgeMap = undefined

      const action: NavigateKnowledgeAction = {
        type: 'navigate-knowledge',
        id: 'nav-1',
        nodeId: 'test-node',
        sessionScope: false
      }

      await executeNavigateKnowledge(mockIterationContext, action)

      // Assert: Error added to history
      expect(mockIterationContext.memoryManager.addToHistory).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'error',
          content: expect.stringContaining('Knowledge map not available')
        })
      )
    })

    it('handles node not found', async () => {
      const knowledgeMap: KnowledgeMap = {
        organizationId: 'org-test',
        nodes: {
          'existing-node': {
            id: 'existing-node',
            description: 'Exists',
            load: vi.fn()
          }
        }
      }

      mockIterationContext.knowledgeMap = knowledgeMap

      const action: NavigateKnowledgeAction = {
        type: 'navigate-knowledge',
        id: 'nav-1',
        nodeId: 'missing-node',
        sessionScope: false
      }

      await executeNavigateKnowledge(mockIterationContext, action)

      // Assert: Error added to history
      expect(mockIterationContext.memoryManager.addToHistory).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'error',
          content: expect.stringContaining('missing-node')
        })
      )
    })

    it('handles node load failure', async () => {
      const knowledgeMap: KnowledgeMap = {
        organizationId: 'org-test',
        nodes: {
          'failing-node': {
            id: 'failing-node',
            description: 'Fails',
            load: vi.fn().mockRejectedValue(new Error('Load failed'))
          }
        }
      }

      mockIterationContext.knowledgeMap = knowledgeMap

      const action: NavigateKnowledgeAction = {
        type: 'navigate-knowledge',
        id: 'nav-1',
        nodeId: 'failing-node',
        sessionScope: false
      }

      await executeNavigateKnowledge(mockIterationContext, action)

      // Assert: Error added to history
      expect(mockIterationContext.memoryManager.addToHistory).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'error',
          content: expect.stringContaining('Load failed')
        })
      )
    })
  })

  describe('multi-tenancy', () => {
    it('passes organizationId to node.load', async () => {
      const knowledgeMap: KnowledgeMap = {
        organizationId: 'org-test',
        nodes: {
          'tenant-aware': {
            id: 'tenant-aware',
            description: 'Tenant aware node',
            load: vi.fn().mockResolvedValue({
              prompt: 'Tenant prompt'
            })
          }
        }
      }

      mockIterationContext.knowledgeMap = knowledgeMap
      mockIterationContext.executionContext.organizationId = 'specific-org-456'

      const action: NavigateKnowledgeAction = {
        type: 'navigate-knowledge',
        id: 'nav-1',
        nodeId: 'tenant-aware',
        sessionScope: false
      }

      await executeNavigateKnowledge(mockIterationContext, action)

      // Assert: Context with organizationId passed
      expect(knowledgeMap.nodes['tenant-aware'].load).toHaveBeenCalledWith(
        expect.objectContaining({
          organizationId: 'specific-org-456'
        })
      )
    })
  })
})
