import { describe, it, expect, beforeEach } from 'vitest';
import { PropertyExtractor } from '@/parsers/property-extractor';
import {
  programmaticNodeFactory,
  declarativeNodeFactory,
  versionedNodeClassFactory,
  versionedNodeTypeClassFactory,
  nodeClassFactory,
  propertyFactory,
  stringPropertyFactory,
  numberPropertyFactory,
  booleanPropertyFactory,
  optionsPropertyFactory,
  collectionPropertyFactory,
  nestedPropertyFactory,
  resourcePropertyFactory,
  operationPropertyFactory,
  aiToolNodeFactory
} from '@tests/fixtures/factories/parser-node.factory';

describe('PropertyExtractor', () => {
  let extractor: PropertyExtractor;

  beforeEach(() => {
    extractor = new PropertyExtractor();
  });

  describe('extractProperties', () => {
    it('should extract properties from programmatic node', () => {
      const nodeDefinition = programmaticNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toHaveLength(nodeDefinition.properties.length);
      expect(properties).toEqual(expect.arrayContaining(
        nodeDefinition.properties.map(prop => expect.objectContaining({
          displayName: prop.displayName,
          name: prop.name,
          type: prop.type,
          default: prop.default
        }))
      ));
    });

    it('should extract properties from versioned node latest version', () => {
      const versionedDef = versionedNodeClassFactory.build();
      const NodeClass = class {
        nodeVersions = versionedDef.nodeVersions;
        baseDescription = versionedDef.baseDescription;
      };
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      // Should get properties from version 2 (latest)
      expect(properties).toHaveLength(versionedDef.nodeVersions![2].description.properties.length);
    });

    it('should extract properties from instance with nodeVersions', () => {
      const NodeClass = class {
        description = { name: 'test' };
        constructor() {
          (this as any).nodeVersions = {
            1: {
              description: {
                properties: [propertyFactory.build({ name: 'v1prop' })]
              }
            },
            2: {
              description: {
                properties: [
                  propertyFactory.build({ name: 'v2prop1' }),
                  propertyFactory.build({ name: 'v2prop2' })
                ]
              }
            }
          };
        }
      };
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toHaveLength(2);
      expect(properties[0].name).toBe('v2prop1');
      expect(properties[1].name).toBe('v2prop2');
    });

    it('should normalize properties to consistent structure', () => {
      const rawProperties = [
        {
          displayName: 'Field 1',
          name: 'field1',
          type: 'string',
          default: 'value',
          description: 'Test field',
          required: true,
          displayOptions: { show: { resource: ['user'] } },
          typeOptions: { multipleValues: true },
          noDataExpression: false,
          extraField: 'should be removed'
        }
      ];
      
      const NodeClass = nodeClassFactory.build({
        description: { 
          name: 'test',
          properties: rawProperties 
        }
      });
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties[0]).toEqual({
        displayName: 'Field 1',
        name: 'field1',
        type: 'string',
        default: 'value',
        description: 'Test field',
        options: undefined,
        required: true,
        displayOptions: { show: { resource: ['user'] } },
        typeOptions: { multipleValues: true },
        noDataExpression: false
      });
      
      expect(properties[0]).not.toHaveProperty('extraField');
    });

    it('should handle nodes without properties', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          displayName: 'Test'
          // No properties field
        }
      });
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toEqual([]);
    });

    it('should handle failed instantiation', () => {
      const NodeClass = class {
        static description = {
          name: 'test',
          properties: [propertyFactory.build()]
        };
        constructor() {
          throw new Error('Cannot instantiate');
        }
      };
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toHaveLength(1); // Should get static description property
    });

    it('should extract from baseDescription when main description is missing', () => {
      const NodeClass = class {
        baseDescription = {
          properties: [
            stringPropertyFactory.build({ name: 'baseProp' })
          ]
        };
      };
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toHaveLength(1);
      expect(properties[0].name).toBe('baseProp');
    });

    it('should handle complex nested properties', () => {
      const nestedProp = nestedPropertyFactory.build();
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [nestedProp]
        }
      });
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toHaveLength(1);
      expect(properties[0].type).toBe('collection');
      expect(properties[0].options).toBeDefined();
    });

    it('should handle non-function node classes', () => {
      const nodeInstance = {
        description: {
          properties: [propertyFactory.build()]
        }
      };

      const properties = extractor.extractProperties(nodeInstance as any);

      expect(properties).toHaveLength(1);
    });
  });

  describe('extractOperations', () => {
    it('should extract operations from declarative node routing', () => {
      const nodeDefinition = declarativeNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      // Declarative node has 2 resources with 2 operations each = 4 total
      expect(operations.length).toBe(4);
      
      // Check that we have operations for each resource
      const userOps = operations.filter(op => op.resource === 'user');
      const postOps = operations.filter(op => op.resource === 'post');
      
      expect(userOps.length).toBe(2); // Create and Get
      expect(postOps.length).toBe(2); // Create and List
      
      // Verify operation structure
      expect(userOps[0]).toMatchObject({
        resource: 'user',
        operation: expect.any(String),
        name: expect.any(String),
        action: expect.any(String)
      });
    });

    it('should extract operations when node has programmatic properties', () => {
      const operationProp = operationPropertyFactory.build();
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [operationProp]
        }
      });
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      expect(operations.length).toBe(operationProp.options!.length);
      operations.forEach((op, idx) => {
        expect(op).toMatchObject({
          operation: operationProp.options![idx].value,
          name: operationProp.options![idx].name,
          description: operationProp.options![idx].description
        });
      });
    });

    it('should extract operations when routing.operations structure exists', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          routing: {
            operations: {
              create: { displayName: 'Create Item' },
              update: { displayName: 'Update Item' },
              delete: { displayName: 'Delete Item' }
            }
          }
        }
      });
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      // routing.operations is not currently extracted by the property extractor
      // It only extracts from routing.request structure
      expect(operations).toHaveLength(0);
    });

    it('should handle operations when programmatic nodes have resource-based structure', () => {
      const resourceProp = resourcePropertyFactory.build();
      const operationProp = {
        displayName: 'Operation',
        name: 'operation',
        type: 'options',
        displayOptions: {
          show: {
            resource: ['user', 'post']
          }
        },
        options: [
          { name: 'Create', value: 'create', action: 'Create item' },
          { name: 'Delete', value: 'delete', action: 'Delete item' }
        ]
      };
      
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [resourceProp, operationProp]
        }
      });
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      // PropertyExtractor only extracts operations, not resources
      // It should find the operation property and extract its options
      expect(operations).toHaveLength(operationProp.options.length);
      expect(operations[0]).toMatchObject({
        operation: 'create',
        name: 'Create',
        description: undefined // action field is not mapped to description
      });
      expect(operations[1]).toMatchObject({
        operation: 'delete',
        name: 'Delete',
        description: undefined
      });
    });

    it('should return empty array when node has no operations', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [stringPropertyFactory.build()]
        }
      });
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      expect(operations).toEqual([]);
    });

    it('should extract operations when node has version structure', () => {
      const NodeClass = class {
        nodeVersions = {
          1: {
            description: {
              properties: []
            }
          },
          2: {
            description: {
              routing: {
                request: {
                  resource: {
                    options: [
                      { name: 'User', value: 'user' }
                    ]
                  },
                  operation: {
                    options: {
                      user: [
                        { name: 'Get', value: 'get', action: 'Get a user' }
                      ]
                    }
                  }
                }
              }
            }
          }
        };
      };
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      expect(operations).toHaveLength(1);
      expect(operations[0]).toMatchObject({
        resource: 'user',
        operation: 'get',
        name: 'User - Get',
        action: 'Get a user'
      });
    });

    it('should handle extraction when property is named action instead of operation', () => {
      const actionProp = {
        displayName: 'Action',
        name: 'action',
        type: 'options',
        options: [
          { name: 'Send', value: 'send' },
          { name: 'Receive', value: 'receive' }
        ]
      };
      
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [actionProp]
        }
      });
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      expect(operations).toHaveLength(2);
      expect(operations[0].operation).toBe('send');
    });
  });

  describe('detectAIToolCapability', () => {
    it('should detect AI capability when usableAsTool property is true', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          usableAsTool: true
        }
      });
      
      const isAITool = extractor.detectAIToolCapability(NodeClass as any);
      
      expect(isAITool).toBe(true);
    });

    it('should detect AI capability when actions contain usableAsTool', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          actions: [
            { name: 'action1', usableAsTool: false },
            { name: 'action2', usableAsTool: true }
          ]
        }
      });
      
      const isAITool = extractor.detectAIToolCapability(NodeClass as any);
      
      expect(isAITool).toBe(true);
    });

    it('should detect AI capability when versioned node has usableAsTool', () => {
      const NodeClass = {
        nodeVersions: {
          1: {
            description: { usableAsTool: false }
          },
          2: {
            description: { usableAsTool: true }
          }
        }
      };
      
      const isAITool = extractor.detectAIToolCapability(NodeClass as any);
      
      expect(isAITool).toBe(true);
    });

    it('should detect AI capability when node name contains AI-related terms', () => {
      const aiNodeNames = ['openai', 'anthropic', 'huggingface', 'cohere', 'myai'];
      
      aiNodeNames.forEach(name => {
        const NodeClass = nodeClassFactory.build({
          description: { name }
        });
        
        const isAITool = extractor.detectAIToolCapability(NodeClass as any);
        
        expect(isAITool).toBe(true);
      });
    });

    it('should return false when node is not AI-related', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'slack',
          usableAsTool: false
        }
      });
      
      const isAITool = extractor.detectAIToolCapability(NodeClass as any);
      
      expect(isAITool).toBe(false);
    });

    it('should return false when node has no description', () => {
      const NodeClass = class {};
      
      const isAITool = extractor.detectAIToolCapability(NodeClass as any);
      
      expect(isAITool).toBe(false);
    });
  });

  describe('extractCredentials', () => {
    it('should extract credentials when node description contains them', () => {
      const credentials = [
        { name: 'apiKey', required: true },
        { name: 'oauth2', required: false }
      ];
      
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          credentials
        }
      });
      
      const extracted = extractor.extractCredentials(NodeClass as any);
      
      expect(extracted).toEqual(credentials);
    });

    it('should extract credentials when node has version structure', () => {
      const NodeClass = class {
        nodeVersions = {
          1: {
            description: {
              credentials: [{ name: 'basic', required: true }]
            }
          },
          2: {
            description: {
              credentials: [
                { name: 'oauth2', required: true },
                { name: 'apiKey', required: false }
              ]
            }
          }
        };
      };
      
      const credentials = extractor.extractCredentials(NodeClass as any);
      
      expect(credentials).toHaveLength(2);
      expect(credentials[0].name).toBe('oauth2');
      expect(credentials[1].name).toBe('apiKey');
    });

    it('should return empty array when node has no credentials', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test'
          // No credentials field
        }
      });
      
      const credentials = extractor.extractCredentials(NodeClass as any);
      
      expect(credentials).toEqual([]);
    });

    it('should extract credentials when only baseDescription has them', () => {
      const NodeClass = class {
        baseDescription = {
          credentials: [{ name: 'token', required: true }]
        };
      };
      
      const credentials = extractor.extractCredentials(NodeClass as any);
      
      expect(credentials).toHaveLength(1);
      expect(credentials[0].name).toBe('token');
    });

    it('should extract credentials when they are defined at instance level', () => {
      const NodeClass = class {
        constructor() {
          (this as any).description = {
            credentials: [
              { name: 'jwt', required: true }
            ]
          };
        }
      };
      
      const credentials = extractor.extractCredentials(NodeClass as any);
      
      expect(credentials).toHaveLength(1);
      expect(credentials[0].name).toBe('jwt');
    });

    it('should return empty array when instantiation fails', () => {
      const NodeClass = class {
        constructor() {
          throw new Error('Cannot instantiate');
        }
      };
      
      const credentials = extractor.extractCredentials(NodeClass as any);
      
      expect(credentials).toEqual([]);
    });
  });

  describe('edge cases', () => {
    it('should handle extraction when properties are deeply nested', () => {
      const deepProperty = {
        displayName: 'Deep Options',
        name: 'deepOptions',
        type: 'collection',
        options: [
          {
            displayName: 'Level 1',
            name: 'level1',
            type: 'collection',
            options: [
              {
                displayName: 'Level 2',
                name: 'level2',
                type: 'collection',
                options: [
                  stringPropertyFactory.build({ name: 'deepValue' })
                ]
              }
            ]
          }
        ]
      };
      
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [deepProperty]
        }
      });
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toHaveLength(1);
      expect(properties[0].name).toBe('deepOptions');
      expect(properties[0].options[0].options[0].options).toBeDefined();
    });

    it('should not throw when node structure has circular references', () => {
      const NodeClass = class {
        description: any = { name: 'test' };
        constructor() {
          this.description.properties = [
            {
              name: 'prop1',
              type: 'string',
              parentRef: this.description // Circular reference
            }
          ];
        }
      };
      
      // Should not throw or hang
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toBeDefined();
    });

    it('should extract from all sources when multiple operation types exist', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          routing: {
            request: {
              resource: {
                options: [{ name: 'Resource1', value: 'res1' }]
              }
            },
            operations: {
              custom: { displayName: 'Custom Op' }
            }
          },
          properties: [
            operationPropertyFactory.build()
          ]
        }
      });
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      // Should extract from all sources
      expect(operations.length).toBeGreaterThan(1);
    });
  });
});