import { describe, it, expect, beforeEach } from 'vitest';
import { SimpleParser } from '@/parsers/simple-parser';
import {
  programmaticNodeFactory,
  declarativeNodeFactory,
  triggerNodeFactory,
  webhookNodeFactory,
  aiToolNodeFactory,
  versionedNodeClassFactory,
  versionedNodeTypeClassFactory,
  malformedNodeFactory,
  nodeClassFactory,
  propertyFactory,
  stringPropertyFactory,
  resourcePropertyFactory,
  operationPropertyFactory
} from '@tests/fixtures/factories/parser-node.factory';

describe('SimpleParser', () => {
  let parser: SimpleParser;

  beforeEach(() => {
    parser = new SimpleParser();
  });

  describe('parse method', () => {
    it('should parse a basic programmatic node', () => {
      const nodeDefinition = programmaticNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result).toMatchObject({
        style: 'programmatic',
        nodeType: nodeDefinition.name,
        displayName: nodeDefinition.displayName,
        description: nodeDefinition.description,
        category: nodeDefinition.group?.[0],
        properties: nodeDefinition.properties,
        credentials: nodeDefinition.credentials || [],
        isAITool: false,
        isWebhook: false,
        version: nodeDefinition.version?.toString() || '1',
        isVersioned: false,
        isTrigger: false,
        operations: expect.any(Array)
      });
    });

    it('should parse a declarative node', () => {
      const nodeDefinition = declarativeNodeFactory.build();
      // Fix the routing structure for simple parser - it expects operation.options to be an array
      nodeDefinition.routing.request!.operation = {
        options: [
          { name: 'Create User', value: 'createUser' },
          { name: 'Get User', value: 'getUser' }
        ]
      } as any;
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.style).toBe('declarative');
      expect(result.operations.length).toBeGreaterThan(0);
    });

    it('should detect trigger nodes', () => {
      const nodeDefinition = triggerNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.isTrigger).toBe(true);
    });

    it('should detect webhook nodes', () => {
      const nodeDefinition = webhookNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.isWebhook).toBe(true);
    });

    it('should detect AI tool nodes', () => {
      const nodeDefinition = aiToolNodeFactory.build();
      // Fix the routing structure for simple parser
      nodeDefinition.routing.request!.operation = {
        options: [
          { name: 'Create', value: 'create' }
        ]
      } as any;
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.isAITool).toBe(true);
    });

    it('should parse VersionedNodeType class', () => {
      const versionedDef = versionedNodeClassFactory.build();
      const VersionedNodeClass = class VersionedNodeType {
        baseDescription = versionedDef.baseDescription;
        nodeVersions = versionedDef.nodeVersions;
        currentVersion = versionedDef.baseDescription!.defaultVersion;
        
        constructor() {
          Object.defineProperty(this.constructor, 'name', {
            value: 'VersionedNodeType',
            configurable: true
          });
        }
      };
      
      const result = parser.parse(VersionedNodeClass as any);
      
      expect(result.isVersioned).toBe(true);
      expect(result.nodeType).toBe(versionedDef.baseDescription!.name);
      expect(result.displayName).toBe(versionedDef.baseDescription!.displayName);
      expect(result.version).toBe(versionedDef.baseDescription!.defaultVersion.toString());
    });

    it('should merge baseDescription with version-specific description', () => {
      const VersionedNodeClass = class VersionedNodeType {
        baseDescription = {
          name: 'mergedNode',
          displayName: 'Base Display Name',
          description: 'Base description'
        };
        
        nodeVersions = {
          1: {
            description: {
              displayName: 'Version 1 Display Name',
              properties: [propertyFactory.build()]
            }
          }
        };
        
        currentVersion = 1;
        
        constructor() {
          Object.defineProperty(this.constructor, 'name', {
            value: 'VersionedNodeType',
            configurable: true
          });
        }
      };
      
      const result = parser.parse(VersionedNodeClass as any);
      
      // Should merge baseDescription with version description
      expect(result.nodeType).toBe('mergedNode'); // From base
      expect(result.displayName).toBe('Version 1 Display Name'); // From version (overrides base)
      expect(result.description).toBe('Base description'); // From base
    });

    it('should throw error for nodes without name', () => {
      const nodeDefinition = malformedNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      expect(() => parser.parse(NodeClass as any)).toThrow('Node is missing name property');
    });

    it('should handle nodes that fail to instantiate', () => {
      const NodeClass = class {
        constructor() {
          throw new Error('Cannot instantiate');
        }
      };
      
      expect(() => parser.parse(NodeClass as any)).toThrow('Node is missing name property');
    });

    it('should handle static description property', () => {
      const nodeDefinition = programmaticNodeFactory.build();
      const NodeClass = class {
        static description = nodeDefinition;
      };
      
      // Since it can't instantiate and has no static description accessible,
      // it should throw for missing name
      expect(() => parser.parse(NodeClass as any)).toThrow();
    });

    it('should handle instance-based nodes', () => {
      const nodeDefinition = programmaticNodeFactory.build();
      const nodeInstance = {
        description: nodeDefinition
      };
      
      const result = parser.parse(nodeInstance as any);
      
      expect(result.displayName).toBe(nodeDefinition.displayName);
    });

    it('should use displayName fallback to name if not provided', () => {
      const nodeDefinition = programmaticNodeFactory.build();
      delete (nodeDefinition as any).displayName;
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.displayName).toBe(nodeDefinition.name);
    });

    it('should handle category extraction from different fields', () => {
      const testCases = [
        { 
          description: { group: ['transform'], categories: ['output'] },
          expected: 'transform' // group takes precedence
        },
        {
          description: { categories: ['output'] },
          expected: 'output'
        },
        {
          description: {},
          expected: undefined
        }
      ];
      
      testCases.forEach(({ description, expected }) => {
        const baseDefinition = programmaticNodeFactory.build();
        // Remove any existing group/categories from base definition to avoid conflicts
        delete baseDefinition.group;
        delete baseDefinition.categories;
        
        const nodeDefinition = { 
          ...baseDefinition,
          ...description,
          name: baseDefinition.name // Ensure name is preserved
        };
        const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
        
        const result = parser.parse(NodeClass as any);
        
        expect(result.category).toBe(expected);
      });
    });
  });

  describe('trigger detection', () => {
    it('should detect triggers by group', () => {
      const nodeDefinition = programmaticNodeFactory.build({
        group: ['trigger']
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.isTrigger).toBe(true);
    });

    it('should detect polling triggers', () => {
      const nodeDefinition = programmaticNodeFactory.build({
        polling: true
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.isTrigger).toBe(true);
    });

    it('should detect trigger property', () => {
      const nodeDefinition = programmaticNodeFactory.build({
        trigger: true
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.isTrigger).toBe(true);
    });

    it('should detect event triggers', () => {
      const nodeDefinition = programmaticNodeFactory.build({
        eventTrigger: true
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.isTrigger).toBe(true);
    });

    it('should detect triggers by name', () => {
      const nodeDefinition = programmaticNodeFactory.build({
        name: 'customTrigger'
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.isTrigger).toBe(true);
    });
  });

  describe('operations extraction', () => {
    it('should extract declarative operations from routing.request', () => {
      const nodeDefinition = declarativeNodeFactory.build();
      // Fix the routing structure for simple parser
      nodeDefinition.routing.request!.operation = {
        options: [
          { name: 'Create', value: 'create' },
          { name: 'Get', value: 'get' }
        ] as any
      };
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any);
      
      // Should have resource operations
      const resourceOps = result.operations.filter(op => op.resource);
      expect(resourceOps.length).toBeGreaterThan(0);
      
      // Should have operation entries
      const operationOps = result.operations.filter(op => op.operation && !op.resource);
      expect(operationOps.length).toBeGreaterThan(0);
    });

    it('should extract declarative operations from routing.operations', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          routing: {
            operations: {
              create: { displayName: 'Create Item' },
              read: { displayName: 'Read Item' },
              update: { displayName: 'Update Item' },
              delete: { displayName: 'Delete Item' }
            }
          }
        }
      });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.operations).toHaveLength(4);
      expect(result.operations).toEqual(expect.arrayContaining([
        { operation: 'create', name: 'Create Item' },
        { operation: 'read', name: 'Read Item' },
        { operation: 'update', name: 'Update Item' },
        { operation: 'delete', name: 'Delete Item' }
      ]));
    });

    it('should extract programmatic operations from resource property', () => {
      const resourceProp = resourcePropertyFactory.build();
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [resourceProp]
        }
      });
      
      const result = parser.parse(NodeClass as any);
      
      const resourceOps = result.operations.filter(op => op.type === 'resource');
      expect(resourceOps).toHaveLength(resourceProp.options!.length);
      resourceOps.forEach((op, idx) => {
        expect(op).toMatchObject({
          type: 'resource',
          resource: resourceProp.options![idx].value,
          name: resourceProp.options![idx].name
        });
      });
    });

    it('should extract programmatic operations with resource context', () => {
      const operationProp = operationPropertyFactory.build();
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [operationProp]
        }
      });
      
      const result = parser.parse(NodeClass as any);
      
      const operationOps = result.operations.filter(op => op.type === 'operation');
      expect(operationOps).toHaveLength(operationProp.options!.length);
      
      // Should extract resource context from displayOptions
      expect(operationOps[0].resources).toEqual(['user']);
    });

    it('should handle operations with multiple resource conditions', () => {
      const operationProp = {
        name: 'operation',
        type: 'options',
        displayOptions: {
          show: {
            resource: ['user', 'post', 'comment']
          }
        },
        options: [
          { name: 'Create', value: 'create', action: 'Create item' }
        ]
      };
      
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [operationProp]
        }
      });
      
      const result = parser.parse(NodeClass as any);
      
      const operationOps = result.operations.filter(op => op.type === 'operation');
      expect(operationOps[0].resources).toEqual(['user', 'post', 'comment']);
    });

    it('should handle single resource condition as array', () => {
      const operationProp = {
        name: 'operation',
        type: 'options',
        displayOptions: {
          show: {
            resource: 'user' // Single value, not array
          }
        },
        options: [
          { name: 'Get', value: 'get' }
        ]
      };
      
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [operationProp]
        }
      });
      
      const result = parser.parse(NodeClass as any);
      
      const operationOps = result.operations.filter(op => op.type === 'operation');
      expect(operationOps[0].resources).toEqual(['user']);
    });
  });

  describe('version extraction', () => {
    it('should prioritize currentVersion over description.defaultVersion', () => {
      const NodeClass = class {
        currentVersion = 2.2;  // Should be returned
        description = {
          name: 'test',
          displayName: 'Test',
          defaultVersion: 3  // Should be ignored when currentVersion exists
        };
      };

      const result = parser.parse(NodeClass as any);
      expect(result.version).toBe('2.2');
    });

    it('should extract version from description.defaultVersion', () => {
      const NodeClass = class {
        description = {
          name: 'test',
          displayName: 'Test',
          defaultVersion: 3
        };
      };

      const result = parser.parse(NodeClass as any);
      expect(result.version).toBe('3');
    });

    it('should NOT extract version from non-existent baseDescription (legacy bug)', () => {
      // This test verifies the bug fix from v2.17.4
      // baseDescription.defaultVersion doesn't exist on VersionedNodeType instances
      const NodeClass = class {
        baseDescription = {  // This property doesn't exist on VersionedNodeType!
          name: 'test',
          displayName: 'Test',
          defaultVersion: 3
        };
        // Constructor name trick to detect as VersionedNodeType
        constructor() {
          Object.defineProperty(this.constructor, 'name', {
            value: 'VersionedNodeType',
            configurable: true
          });
        }
      };

      const result = parser.parse(NodeClass as any);

      // Should fallback to default version '1' since baseDescription.defaultVersion doesn't exist
      expect(result.version).toBe('1');
    });

    it('should extract version from description.version', () => {
      // For this test, the version needs to be in the instantiated description
      const NodeClass = class {
        description = {
          name: 'test',
          version: 2
        };
      };
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.version).toBe('2');
    });

    it('should default to version 1', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test'
        }
      });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.version).toBe('1');
    });
  });

  describe('versioned node detection', () => {
    it('should detect nodes with baseDescription and nodeVersions', () => {
      // For simple parser, need to create a proper class structure
      const NodeClass = class {
        baseDescription = { 
          name: 'test',
          displayName: 'Test' 
        };
        nodeVersions = { 1: {}, 2: {} };
        
        constructor() {
          Object.defineProperty(this.constructor, 'name', {
            value: 'VersionedNodeType',
            configurable: true
          });
        }
      };
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.isVersioned).toBe(true);
    });

    it('should detect nodes with version array', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          version: [1, 1.1, 2]
        }
      });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.isVersioned).toBe(true);
    });

    it('should detect nodes with defaultVersion', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          defaultVersion: 2
        }
      });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.isVersioned).toBe(true);
    });

    it('should handle instance-level version detection', () => {
      const NodeClass = class {
        description = {
          name: 'test',
          version: [1, 2, 3]
        };
      };
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.isVersioned).toBe(true);
    });
  });

  describe('edge cases', () => {
    it('should handle empty routing object', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          routing: {}
        }
      });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.style).toBe('declarative');
      expect(result.operations).toEqual([]);
    });

    it('should handle missing properties array', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test'
        }
      });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.properties).toEqual([]);
    });

    it('should handle missing credentials', () => {
      const nodeDefinition = programmaticNodeFactory.build();
      delete (nodeDefinition as any).credentials;
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.credentials).toEqual([]);
    });

    it('should handle nodes with baseDescription but no name in main description', () => {
      const NodeClass = class {
        description = {};
        baseDescription = {
          name: 'baseNode',
          displayName: 'Base Node'
        };
      };
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.nodeType).toBe('baseNode');
      expect(result.displayName).toBe('Base Node');
    });

    it('should handle complex nested routing structures', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          routing: {
            request: {
              resource: {
                options: []
              },
              operation: {
                options: [] // Should be array, not object
              }
            },
            operations: {}
          }
        }
      });
      
      const result = parser.parse(NodeClass as any);
      
      expect(result.operations).toEqual([]);
    });

    it('should handle operations without displayName', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [
            {
              name: 'operation',
              type: 'options',
              displayOptions: {
                show: {}
              },
              options: [
                { value: 'create' }, // No name field
                { value: 'update', name: 'Update' }
              ]
            }
          ]
        }
      });
      
      const result = parser.parse(NodeClass as any);
      
      // Should handle missing names gracefully
      expect(result.operations).toHaveLength(2);
    });
  });
});