import { describe, it, expect } from 'vitest'
import { z } from 'zod'
import { serializeDefinition } from '../serialization'
import type { AgentDefinition } from '../../agent/core/types'
import type { WorkflowDefinition, WorkflowStepDefinition } from '../../workflow/types'
import type { Tool } from '../../tools/types'

describe('serializeDefinition', () => {
  describe('Type Conversion', () => {
    it('converts Zod schemas to JSON Schema', () => {
      const definition: Partial<AgentDefinition> = {
        contract: {
          inputSchema: z.object({ name: z.string() }),
          outputSchema: z.object({ result: z.number() })
        }
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify Zod schemas are converted
      expect(result.contract.inputSchema).toHaveProperty('type', 'object')
      expect(result.contract.outputSchema).toHaveProperty('type', 'object')
      expect(result.contract.inputSchema).not.toHaveProperty('_def') // Zod internal removed
    })

    it('converts Record-based steps to arrays with id field (legacy Map test)', () => {
      const stepsRecord: Record<string, WorkflowStepDefinition> = {
        'step1': {
          id: 'step1',
          name: 'First Step',
          description: 'First step description',
          handler: async () => ({ next: null })
        } as WorkflowStepDefinition,
        'step2': {
          id: 'step2',
          name: 'Second Step',
          description: 'Second step description',
          handler: async () => ({ next: null })
        } as WorkflowStepDefinition
      }

      const definition: Partial<WorkflowDefinition> = {
        steps: stepsRecord
      }

      const result = serializeDefinition(definition as WorkflowDefinition)

      // Verify Record converted to array with id
      expect(Array.isArray(result.steps)).toBe(true)
      expect(result.steps).toHaveLength(2)
      expect(result.steps[0]).toHaveProperty('id', 'step1')
      expect(result.steps[1]).toHaveProperty('id', 'step2')
      expect(result.steps[0]).toHaveProperty('name', 'First Step')
      expect(result.steps[1]).toHaveProperty('name', 'Second Step')
    })

    it('converts Record-based steps to arrays with id field', () => {
      const stepsRecord: Record<string, unknown> = {
        step1: {
          id: 'step1',
          name: 'First Step',
          description: 'First step description',
          handler: async () => ({ next: null })
        },
        step2: {
          id: 'step2',
          name: 'Second Step',
          description: 'Second step description',
          handler: async () => ({ next: null })
        }
      }

      const definition: Partial<WorkflowDefinition> = {
        steps: stepsRecord
      }

      const result = serializeDefinition(definition as WorkflowDefinition)

      // Verify Record converted to array with id (same format as Map)
      expect(Array.isArray(result.steps)).toBe(true)
      expect(result.steps).toHaveLength(2)
      expect(result.steps[0]).toHaveProperty('id', 'step1')
      expect(result.steps[1]).toHaveProperty('id', 'step2')
      expect(result.steps[0]).toHaveProperty('name', 'First Step')
      expect(result.steps[1]).toHaveProperty('name', 'Second Step')
      // Verify handler functions are removed
      expect(result.steps[0].handler).toBeUndefined()
      expect(result.steps[1].handler).toBeUndefined()
    })

    it('converts Sets to arrays', () => {
      const tagsSet = new Set(['tag1', 'tag2', 'tag3'])

      const definition: Partial<AgentDefinition> = {
        config: {
          tags: tagsSet
        } as unknown as AgentDefinition['config']
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify Set converted to array
      expect(Array.isArray(result.config.tags)).toBe(true)
      expect(result.config.tags).toContain('tag1')
      expect(result.config.tags).toContain('tag2')
      expect(result.config.tags).toContain('tag3')
    })

    it('converts Dates to ISO strings', () => {
      const testDate = new Date('2025-01-21T12:00:00Z')

      const definition: Partial<AgentDefinition> = {
        config: {
          createdAt: testDate
        } as unknown as AgentDefinition['config']
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify Date converted to ISO string
      expect(typeof result.config.createdAt).toBe('string')
      expect(result.config.createdAt).toBe('2025-01-21T12:00:00.000Z')
    })

    it('removes functions completely', () => {
      const definition: Partial<AgentDefinition> = {
        preloadMemory: async () => ({ sessionMemory: {}, history: [] }),
        config: {
          resourceId: 'test-agent',
          name: 'Test',
          description: 'Test',
          version: '1.0.0',
          type: 'agent',
          status: 'dev',
          systemPrompt: 'Test prompt',
          onStart: () => console.log('started')
        } as unknown as AgentDefinition['config']
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify functions removed
      expect(result.preloadMemory).toBeUndefined()
      expect(result.config.onStart).toBeUndefined()
      expect(result.config.resourceId).toBe('test-agent')
    })

    it('handles null and undefined', () => {
      const definition: Partial<AgentDefinition> = {
        knowledgeMap: undefined,
        metricsConfig: null as unknown as AgentDefinition['metricsConfig'],
        config: {
          memoryPreferences: undefined
        } as unknown as AgentDefinition['config']
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify null/undefined preserved or omitted
      expect(result.knowledgeMap).toBeUndefined()
      expect(result.metricsConfig).toBeNull() // null is preserved
      expect(result.config.memoryPreferences).toBeUndefined()
    })

    it('preserves primitives', () => {
      const definition: Partial<AgentDefinition> = {
        config: {
          resourceId: 'test-agent',
          name: 'Test Agent',
          description: 'Test description',
          version: '1.2.3',
          type: 'agent',
          status: 'prod',
          systemPrompt: 'You are a test agent',
          constraints: {
            maxIterations: 10,
            timeout: 30000,
            maxSessionMemoryKeys: 5,
            maxMemoryTokens: 14000
          }
        } as unknown as AgentDefinition['config']
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify primitives preserved
      expect(result.config.resourceId).toBe('test-agent')
      expect(result.config.name).toBe('Test Agent')
      expect(result.config.constraints.maxIterations).toBe(10)
      expect(result.config.constraints.timeout).toBe(30000)
    })
  })

  describe('Tool Serialization', () => {
    it('includes tool name and description', () => {
      const tools: Tool[] = [
        {
          name: 'calculator',
          description: 'Performs mathematical calculations',
          inputSchema: z.object({ expression: z.string() }),
          outputSchema: z.object({ result: z.number() }),
          execute: async () => ({ result: 42 })
        }
      ]

      const definition: Partial<AgentDefinition> = {
        tools
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify tool metadata included
      expect(result.tools).toHaveLength(1)
      expect(result.tools[0].name).toBe('calculator')
      expect(result.tools[0].description).toBe('Performs mathematical calculations')
    })

    it('converts tool input/output schemas', () => {
      const tools: Tool[] = [
        {
          name: 'web_search',
          description: 'Search the web',
          inputSchema: z.object({
            query: z.string(),
            maxResults: z.number().optional()
          }),
          outputSchema: z.object({
            results: z.array(z.object({
              title: z.string(),
              url: z.string()
            }))
          }),
          execute: async () => ({ results: [] })
        }
      ]

      const definition: Partial<AgentDefinition> = {
        tools
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify schemas converted to JSON Schema
      expect(result.tools[0].inputSchema).toHaveProperty('type', 'object')
      expect(result.tools[0].outputSchema).toHaveProperty('type', 'object')
      expect(result.tools[0].inputSchema).not.toHaveProperty('_def')
      expect(result.tools[0].outputSchema).not.toHaveProperty('_def')
    })

    it('removes execute function', () => {
      const tools: Tool[] = [
        {
          name: 'test_tool',
          description: 'Test tool',
          inputSchema: z.object({}),
          outputSchema: z.object({}),
          execute: async () => ({ success: true })
        }
      ]

      const definition: Partial<AgentDefinition> = {
        tools
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify execute function removed
      expect(result.tools[0].execute).toBeUndefined()
    })

    it('works with tools array', () => {
      const tools: Tool[] = [
        {
          name: 'tool1',
          description: 'First tool',
          inputSchema: z.object({}),
          outputSchema: z.object({}),
          execute: async () => ({})
        },
        {
          name: 'tool2',
          description: 'Second tool',
          inputSchema: z.object({}),
          outputSchema: z.object({}),
          execute: async () => ({})
        }
      ]

      const definition: Partial<AgentDefinition> = {
        tools
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify all tools serialized
      expect(result.tools).toHaveLength(2)
      expect(result.tools[0].name).toBe('tool1')
      expect(result.tools[1].name).toBe('tool2')
    })

    it('respects includeToolSchemas option', () => {
      const tools: Tool[] = [
        {
          name: 'test_tool',
          description: 'Test',
          inputSchema: z.object({ input: z.string() }),
          outputSchema: z.object({ output: z.string() }),
          execute: async () => ({ output: 'test' })
        }
      ]

      const definition: Partial<AgentDefinition> = {
        tools
      }

      // With schemas (default)
      const withSchemas = serializeDefinition(definition as AgentDefinition, {
        includeToolSchemas: true
      })
      expect(withSchemas.tools[0].inputSchema).toBeDefined()
      expect(withSchemas.tools[0].outputSchema).toBeDefined()

      // Without schemas
      const withoutSchemas = serializeDefinition(definition as AgentDefinition, {
        includeToolSchemas: false
      })
      expect(withoutSchemas.tools[0].inputSchema).toBeUndefined()
      expect(withoutSchemas.tools[0].outputSchema).toBeUndefined()
      expect(withoutSchemas.tools[0].name).toBe('test_tool')
      expect(withoutSchemas.tools[0].description).toBe('Test')
    })
  })

  describe('Secret Redaction', () => {
    it('redacts apiKey field', () => {
      const definition: Partial<AgentDefinition> = {
        modelConfig: {
          provider: 'openai',
          modelName: 'gpt-4o',
          apiKey: 'sk-proj-1234567890abcdefghijklmnop'
        } as unknown as AgentDefinition['modelConfig']
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify apiKey redacted
      expect(result.modelConfig.apiKey).toBe('sk-proj...')
      expect(result.modelConfig.provider).toBe('openai')
    })

    it('redacts fields containing "secret"', () => {
      const definition: Partial<AgentDefinition> = {
        config: {
          clientSecret: 'my-secret-value-12345',
          webhookSecret: 'webhook-secret-67890'
        } as unknown as AgentDefinition['config']
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify secrets redacted
      expect(result.config.clientSecret).toBe('my-secr...')
      expect(result.config.webhookSecret).toBe('webhook...')
    })

    it('redacts fields containing "password"', () => {
      const definition: Partial<AgentDefinition> = {
        config: {
          databasePassword: 'super-secure-password-123',
          adminPassword: 'admin123'
        } as unknown as AgentDefinition['config']
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify passwords redacted
      expect(result.config.databasePassword).toBe('super-s...')
      expect(result.config.adminPassword).toBe('admin12...') // 8 chars -> prefix + ...
    })

    it('redacts fields containing "token"', () => {
      const definition: Partial<AgentDefinition> = {
        config: {
          accessToken: 'ghp_1234567890abcdefghijklmnop',
          refreshToken: 'refresh-token-xyz'
        } as unknown as AgentDefinition['config']
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify tokens redacted
      expect(result.config.accessToken).toBe('ghp_123...')
      expect(result.config.refreshToken).toBe('refresh...')
    })

    it('preserves non-secret fields', () => {
      const definition: Partial<AgentDefinition> = {
        config: {
          resourceId: 'test-agent',
          name: 'Test Agent',
          systemPrompt: 'You are helpful'
        } as unknown as AgentDefinition['config']
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify non-secrets preserved
      expect(result.config.resourceId).toBe('test-agent')
      expect(result.config.name).toBe('Test Agent')
      expect(result.config.systemPrompt).toBe('You are helpful')
    })

    it('respects redactSecrets option', () => {
      const definition: Partial<AgentDefinition> = {
        modelConfig: {
          apiKey: 'sk-proj-secret-key-123'
        } as unknown as AgentDefinition['modelConfig']
      }

      // With redaction (default)
      const redacted = serializeDefinition(definition as AgentDefinition, {
        redactSecrets: true
      })
      expect(redacted.modelConfig.apiKey).toBe('sk-proj...')

      // Without redaction
      const notRedacted = serializeDefinition(definition as AgentDefinition, {
        redactSecrets: false
      })
      expect(notRedacted.modelConfig.apiKey).toBe('sk-proj-secret-key-123')
    })
  })

  describe('Knowledge Map Serialization', () => {
    it('extracts node metadata', () => {
      const definition: Partial<AgentDefinition> = {
        knowledgeMap: {
          nodes: {
            'excel': {
              id: 'excel',
              description: 'Excel expertise',
              load: async () => ({ prompt: 'Excel knowledge' })
            },
            'notion': {
              id: 'notion',
              description: 'Notion integration',
              load: async () => ({ prompt: 'Notion knowledge' })
            }
          }
        }
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify knowledge map metadata
      expect(result.knowledgeMap.nodeCount).toBe(2)
      expect(result.knowledgeMap.nodes).toHaveLength(2)
      expect(result.knowledgeMap.nodes[0]).toHaveProperty('id')
      expect(result.knowledgeMap.nodes[0]).toHaveProperty('description')
      expect(result.knowledgeMap.nodes[0]).toHaveProperty('loaded')
    })

    it('removes load functions', () => {
      const definition: Partial<AgentDefinition> = {
        knowledgeMap: {
          nodes: {
            'test': {
              id: 'test',
              description: 'Test node',
              load: async () => ({ prompt: 'Test' })
            }
          }
        }
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify load function removed
      expect(result.knowledgeMap.nodes[0].load).toBeUndefined()
    })

    it('handles empty knowledge maps', () => {
      const definition: Partial<AgentDefinition> = {
        knowledgeMap: {
          nodes: {}
        }
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify empty map handled
      expect(result.knowledgeMap.nodeCount).toBe(0)
      expect(result.knowledgeMap.nodes).toHaveLength(0)
    })

    it('includes loaded state', () => {
      const definition: Partial<AgentDefinition> = {
        knowledgeMap: {
          nodes: {
            'loaded': {
              id: 'loaded',
              description: 'Loaded node',
              load: async () => ({ prompt: 'Test' }),
              loaded: true
            },
            'unloaded': {
              id: 'unloaded',
              description: 'Unloaded node',
              load: async () => ({ prompt: 'Test' }),
              loaded: false
            }
          }
        }
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify loaded state included
      const loadedNode = result.knowledgeMap.nodes.find((n: { id: string }) => n.id === 'loaded')
      const unloadedNode = result.knowledgeMap.nodes.find((n: { id: string }) => n.id === 'unloaded')
      expect(loadedNode.loaded).toBe(true)
      expect(unloadedNode.loaded).toBe(false)
    })

    it('respects includeKnowledgeMap option', () => {
      const definition: Partial<AgentDefinition> = {
        knowledgeMap: {
          nodes: {
            'test': {
              id: 'test',
              description: 'Test',
              load: async () => ({ prompt: 'Test' })
            }
          }
        }
      }

      // With knowledge map (default)
      const withKM = serializeDefinition(definition as AgentDefinition, {
        includeKnowledgeMap: true
      })
      expect(withKM.knowledgeMap).toBeDefined()
      expect(withKM.knowledgeMap.nodeCount).toBe(1)

      // Without knowledge map
      const withoutKM = serializeDefinition(definition as AgentDefinition, {
        includeKnowledgeMap: false
      })
      expect(withoutKM.knowledgeMap).toBeUndefined()
    })
  })

  describe('Circular References', () => {
    it('detects and handles circular references', () => {
      const circularObj: Record<string, unknown> = {
        name: 'test',
        config: {}
      }
      ;(circularObj.config as Record<string, unknown>).self = circularObj

      const result = serializeDefinition(circularObj as AgentDefinition)

      // Verify circular reference marked
      expect(result.config.self).toEqual({ __circular: true })
      expect(result.name).toBe('test')
    })
  })

  describe('Complete Resources', () => {
    it('serializes complete AgentDefinition', () => {
      const definition: AgentDefinition = {
        config: {
          resourceId: 'test-agent',
          name: 'Test Agent',
          description: 'A test agent',
          version: '1.0.0',
          type: 'agent',
          status: 'dev',
          systemPrompt: 'You are a test agent',
          constraints: {
            maxIterations: 5,
            timeout: 30000
          }
        },
        contract: {
          inputSchema: z.object({ query: z.string() }),
          outputSchema: z.object({ answer: z.string() })
        },
        tools: [
          {
            name: 'calculator',
            description: 'Calculate',
            inputSchema: z.object({ expr: z.string() }),
            outputSchema: z.object({ result: z.number() }),
            execute: async () => ({ result: 0 })
          }
        ],
        modelConfig: {
          provider: 'openai',
          modelName: 'gpt-4o',
          apiKey: 'sk-test-1234567890'
        } as unknown as AgentDefinition['modelConfig']
      }

      const result = serializeDefinition(definition)

      // Verify complete serialization
      expect(result.config.resourceId).toBe('test-agent')
      expect(result.contract.inputSchema).toHaveProperty('type', 'object')
      expect(result.tools[0].name).toBe('calculator')
      expect(result.tools[0].execute).toBeUndefined()
      expect(result.modelConfig.apiKey).toBe('sk-test...')
    })

    it('output is JSON-serializable', () => {
      const definition: Partial<AgentDefinition> = {
        config: {
          resourceId: 'test',
          name: 'Test',
          description: 'Test',
          version: '1.0.0',
          type: 'agent',
          status: 'dev',
          systemPrompt: 'Test'
        },
        contract: {
          inputSchema: z.object({}),
          outputSchema: z.object({})
        },
        tools: []
      }

      const result = serializeDefinition(definition as AgentDefinition)

      // Verify can stringify and parse
      expect(() => JSON.stringify(result)).not.toThrow()
      const json = JSON.stringify(result)
      expect(() => JSON.parse(json)).not.toThrow()
    })

    it('round-trip preserves data', () => {
      const definition: Partial<AgentDefinition> = {
        config: {
          resourceId: 'test-agent',
          name: 'Test Agent',
          description: 'Test',
          version: '1.0.0',
          type: 'agent',
          status: 'dev',
          systemPrompt: 'Test prompt'
        },
        contract: {
          inputSchema: z.object({ input: z.string() }),
          outputSchema: z.object({ output: z.number() })
        },
        tools: []
      }

      const serialized = serializeDefinition(definition as AgentDefinition)
      const json = JSON.stringify(serialized)
      const parsed = JSON.parse(json)

      // Verify data preserved
      expect(parsed.config.resourceId).toBe('test-agent')
      expect(parsed.config.name).toBe('Test Agent')
      expect(parsed.contract.inputSchema).toHaveProperty('type', 'object')
      expect(parsed.contract.outputSchema).toHaveProperty('type', 'object')
    })
  })
})
