/**
 * Knowledge Map Utils Unit Tests
 * Tests reloadKnowledgeMapTools utility for cross-turn persistence
 */

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { reloadKnowledgeMapTools } from '../utils'
import type { KnowledgeMap, KnowledgeMapState } from '../types'
import type { AgentMemory, ExecutionContext, Tool } from '../../../base/types'
import { z } from 'zod'

describe('reloadKnowledgeMapTools', () => {
  const mockContext: ExecutionContext = {
    executionId: 'exec-test',
    organizationId: 'org-test',
    resourceId: 'agent-test',
    resourceType: 'agent',
    logger: {
      info: vi.fn(),
      error: vi.fn(),
      warn: vi.fn(),
      debug: vi.fn()
    }
  }

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

  it('returns empty array when no knowledge-map-state exists', async () => {
    const knowledgeMap: KnowledgeMap = {
      organizationId: 'org-test',
      nodes: {}
    }

    const memory: AgentMemory = {
      sessionMemory: {},
      history: []
    }

    const tools = await reloadKnowledgeMapTools(knowledgeMap, memory, mockContext)

    expect(tools).toEqual([])
  })

  it('reloads tools from single loaded node', 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 prompt',
            tools: [mockTool]
          })
        }
      }
    }

    const state: KnowledgeMapState = {
      loadedNodes: ['test-node'],
      version: 1
    }

    const memory: AgentMemory = {
      sessionMemory: {
        'knowledge-map-state': {
          content: JSON.stringify(state),
          timestamp: Date.now()
        }
      },
      history: []
    }

    const tools = await reloadKnowledgeMapTools(knowledgeMap, memory, mockContext)

    // Assert: Tool was reloaded
    expect(tools).toHaveLength(1)
    expect(tools[0]).toEqual(mockTool)

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

  it('reloads tools from multiple loaded nodes', 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: {
        'node-1': {
          id: 'node-1',
          description: 'Node 1',
          load: vi.fn().mockResolvedValue({
            prompt: 'Prompt 1',
            tools: [tool1]
          })
        },
        'node-2': {
          id: 'node-2',
          description: 'Node 2',
          load: vi.fn().mockResolvedValue({
            prompt: 'Prompt 2',
            tools: [tool2]
          })
        }
      }
    }

    const state: KnowledgeMapState = {
      loadedNodes: ['node-1', 'node-2'],
      version: 1
    }

    const memory: AgentMemory = {
      sessionMemory: {
        'knowledge-map-state': {
          content: JSON.stringify(state),
          timestamp: Date.now()
        }
      },
      history: []
    }

    const tools = await reloadKnowledgeMapTools(knowledgeMap, memory, mockContext)

    // Assert: Both tools reloaded
    expect(tools).toHaveLength(2)
    expect(tools).toContainEqual(tool1)
    expect(tools).toContainEqual(tool2)

    // Assert: Both nodes loaded
    expect(knowledgeMap.nodes['node-1'].load).toHaveBeenCalled()
    expect(knowledgeMap.nodes['node-2'].load).toHaveBeenCalled()
  })

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

    const state: KnowledgeMapState = {
      loadedNodes: ['brand-guidelines'],
      version: 1
    }

    const memory: AgentMemory = {
      sessionMemory: {
        'knowledge-map-state': {
          content: JSON.stringify(state),
          timestamp: Date.now()
        }
      },
      history: []
    }

    const tools = await reloadKnowledgeMapTools(knowledgeMap, memory, mockContext)

    // Assert: No tools returned (node has no tools)
    expect(tools).toEqual([])

    // Assert: Node was still loaded
    expect(knowledgeMap.nodes['brand-guidelines'].load).toHaveBeenCalled()
  })

  it('skips node that no longer exists in knowledge map', async () => {
    const knowledgeMap: KnowledgeMap = {
      organizationId: 'org-test',
      nodes: {
        'existing-node': {
          id: 'existing-node',
          description: 'Still exists',
          load: vi.fn().mockResolvedValue({
            prompt: 'Prompt',
            tools: []
          })
        }
      }
    }

    const state: KnowledgeMapState = {
      loadedNodes: ['existing-node', 'deleted-node'],
      version: 1
    }

    const memory: AgentMemory = {
      sessionMemory: {
        'knowledge-map-state': {
          content: JSON.stringify(state),
          timestamp: Date.now()
        }
      },
      history: []
    }

    const tools = await reloadKnowledgeMapTools(knowledgeMap, memory, mockContext)

    // Assert: No error thrown
    expect(tools).toEqual([])

    // Assert: Warning logged for missing node
    expect(mockContext.logger.warn).toHaveBeenCalledWith(
      expect.stringContaining('deleted-node')
    )

    // Assert: Existing node still loaded
    expect(knowledgeMap.nodes['existing-node'].load).toHaveBeenCalled()
  })

  it('handles node load failure gracefully', async () => {
    const workingTool: Tool = {
      name: 'working_tool',
      description: 'Working',
      inputSchema: z.object({}),
      outputSchema: z.object({}),
      execute: vi.fn()
    }

    const knowledgeMap: KnowledgeMap = {
      organizationId: 'org-test',
      nodes: {
        'failing-node': {
          id: 'failing-node',
          description: 'Will fail',
          load: vi.fn().mockRejectedValue(new Error('Load failed'))
        },
        'working-node': {
          id: 'working-node',
          description: 'Works fine',
          load: vi.fn().mockResolvedValue({
            prompt: 'Prompt',
            tools: [workingTool]
          })
        }
      }
    }

    const state: KnowledgeMapState = {
      loadedNodes: ['failing-node', 'working-node'],
      version: 1
    }

    const memory: AgentMemory = {
      sessionMemory: {
        'knowledge-map-state': {
          content: JSON.stringify(state),
          timestamp: Date.now()
        }
      },
      history: []
    }

    const tools = await reloadKnowledgeMapTools(knowledgeMap, memory, mockContext)

    // Assert: Working tool still returned
    expect(tools).toHaveLength(1)
    expect(tools[0]).toEqual(workingTool)

    // Assert: Error logged for failed node
    expect(mockContext.logger.error).toHaveBeenCalledWith(
      expect.stringContaining('failing-node')
    )
  })

  it('handles invalid JSON state gracefully', async () => {
    const knowledgeMap: KnowledgeMap = {
      organizationId: 'org-test',
      nodes: {}
    }

    const memory: AgentMemory = {
      sessionMemory: {
        'knowledge-map-state': {
          content: 'invalid json{{{',
          timestamp: Date.now()
        }
      },
      history: []
    }

    const tools = await reloadKnowledgeMapTools(knowledgeMap, memory, mockContext)

    // Assert: Returns empty array (no crash)
    expect(tools).toEqual([])

    // Assert: Error logged
    expect(mockContext.logger.error).toHaveBeenCalledWith(
      expect.stringContaining('Failed to parse knowledge-map-state')
    )
  })

  it('handles nodes with multiple tools', async () => {
    const tools: Tool[] = [
      {
        name: 'notion_search',
        description: 'Search',
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        execute: vi.fn()
      },
      {
        name: 'notion_read',
        description: 'Read',
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        execute: vi.fn()
      },
      {
        name: 'notion_create',
        description: 'Create',
        inputSchema: z.object({}),
        outputSchema: z.object({}),
        execute: vi.fn()
      }
    ]

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

    const state: KnowledgeMapState = {
      loadedNodes: ['notion'],
      version: 1
    }

    const memory: AgentMemory = {
      sessionMemory: {
        'knowledge-map-state': {
          content: JSON.stringify(state),
          timestamp: Date.now()
        }
      },
      history: []
    }

    const reloadedTools = await reloadKnowledgeMapTools(knowledgeMap, memory, mockContext)

    // Assert: All tools reloaded
    expect(reloadedTools).toHaveLength(3)
    expect(reloadedTools).toEqual(tools)
  })

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

    const state: KnowledgeMapState = {
      loadedNodes: ['tenant-aware-node'],
      version: 1
    }

    const memory: AgentMemory = {
      sessionMemory: {
        'knowledge-map-state': {
          content: JSON.stringify(state),
          timestamp: Date.now()
        }
      },
      history: []
    }

    const contextWithOrgId: ExecutionContext = {
      ...mockContext,
      organizationId: 'specific-org-123'
    }

    await reloadKnowledgeMapTools(knowledgeMap, memory, contextWithOrgId)

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

  it('flattens tools from multiple nodes into single array', async () => {
    const tool1: Tool = {
      name: 'crm_read',
      description: 'Read CRM',
      inputSchema: z.object({}),
      outputSchema: z.object({}),
      execute: vi.fn()
    }

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

    const tool3: Tool = {
      name: 'email_send',
      description: 'Send Email',
      inputSchema: z.object({}),
      outputSchema: z.object({}),
      execute: vi.fn()
    }

    const knowledgeMap: KnowledgeMap = {
      organizationId: 'org-test',
      nodes: {
        crm: {
          id: 'crm',
          description: 'CRM',
          load: vi.fn().mockResolvedValue({
            prompt: 'CRM',
            tools: [tool1, tool2]
          })
        },
        email: {
          id: 'email',
          description: 'Email',
          load: vi.fn().mockResolvedValue({
            prompt: 'Email',
            tools: [tool3]
          })
        }
      }
    }

    const state: KnowledgeMapState = {
      loadedNodes: ['crm', 'email'],
      version: 1
    }

    const memory: AgentMemory = {
      sessionMemory: {
        'knowledge-map-state': {
          content: JSON.stringify(state),
          timestamp: Date.now()
        }
      },
      history: []
    }

    const tools = await reloadKnowledgeMapTools(knowledgeMap, memory, mockContext)

    // Assert: All tools from both nodes flattened
    expect(tools).toHaveLength(3)
    expect(tools).toContain(tool1)
    expect(tools).toContain(tool2)
    expect(tools).toContain(tool3)
  })

  it('re-discovers child nodes during reload (rebuilds map hierarchy)', async () => {
    const childNode = {
      id: 'crm-customers',
      description: 'Customer tools',
      load: vi.fn().mockResolvedValue({
        prompt: 'Customer prompt',
        tools: []
      })
    }

    const parentTool: Tool = {
      name: 'crm_list',
      description: 'List CRM',
      inputSchema: z.object({}),
      outputSchema: z.object({}),
      execute: vi.fn()
    }

    const knowledgeMap: KnowledgeMap = {
      organizationId: 'org-test',
      nodes: {
        crm: {
          id: 'crm',
          description: 'CRM base',
          load: vi.fn().mockResolvedValue({
            prompt: 'CRM base prompt',
            tools: [parentTool],
            nodes: {
              'crm-customers': childNode  // Child discovered when parent loads
            }
          })
        }
      }
    }

    // State only has parent (child was discovered, not explicitly navigated)
    const state: KnowledgeMapState = {
      loadedNodes: ['crm'],
      version: 1
    }

    const memory: AgentMemory = {
      sessionMemory: {
        'knowledge-map-state': {
          content: JSON.stringify(state),
          timestamp: Date.now()
        }
      },
      history: []
    }

    const tools = await reloadKnowledgeMapTools(knowledgeMap, memory, mockContext)

    // Assert: Parent tools collected
    expect(tools).toHaveLength(1)
    expect(tools[0]).toEqual(parentTool)

    // Assert: Parent node marked as loaded with prompt cached
    expect(knowledgeMap.nodes['crm'].loaded).toBe(true)
    expect(knowledgeMap.nodes['crm'].prompt).toBe('CRM base prompt')

    // Assert: Child node re-discovered and added to runtime map
    expect(knowledgeMap.nodes['crm-customers']).toBeDefined()
    expect(knowledgeMap.nodes['crm-customers'].id).toBe('crm-customers')
  })

  it('continues loading remaining nodes when one fails', async () => {
    const workingTool: Tool = {
      name: 'working',
      description: 'Works',
      inputSchema: z.object({}),
      outputSchema: z.object({}),
      execute: vi.fn()
    }

    const knowledgeMap: KnowledgeMap = {
      organizationId: 'org-test',
      nodes: {
        'node-1': {
          id: 'node-1',
          description: 'First node',
          load: vi.fn().mockRejectedValue(new Error('Network error'))
        },
        'node-2': {
          id: 'node-2',
          description: 'Second node',
          load: vi.fn().mockResolvedValue({
            prompt: 'Prompt',
            tools: [workingTool]
          })
        }
      }
    }

    const state: KnowledgeMapState = {
      loadedNodes: ['node-1', 'node-2'],
      version: 1
    }

    const memory: AgentMemory = {
      sessionMemory: {
        'knowledge-map-state': {
          content: JSON.stringify(state),
          timestamp: Date.now()
        }
      },
      history: []
    }

    const tools = await reloadKnowledgeMapTools(knowledgeMap, memory, mockContext)

    // Assert: Working node's tool still returned
    expect(tools).toHaveLength(1)
    expect(tools[0]).toEqual(workingTool)

    // Assert: Error logged but didn't throw
    expect(mockContext.logger.error).toHaveBeenCalledWith(
      expect.stringContaining('node-1')
    )
  })
})
