import "reflect-metadata";
import * as fc from "fast-check";
import { 
  MetadataRegistry,
  SchemaBuilder,
  Entity, 
  Property,
  Id,
  OneToOne,
  OneToMany,
  ManyToOne,
  ManyToMany
} from "../../core";

describe("Property-based Metadata Tests", () => {
  let registry: MetadataRegistry;
  let builder: SchemaBuilder;

  beforeEach(() => {
    registry = new MetadataRegistry();
    builder = new SchemaBuilder(registry);
  });

  test("should correctly register entities with randomly generated properties", () => {
    // Generate random class names
    const classNameArb = fc.string({ 
      minLength: 3, 
      maxLength: 20 
    }).map(s => s.replace(/[^a-zA-Z]/g, 'a')); // Replace non-letters with 'a'
    
    // Generate random property names
    const propNameArb = fc.string({ 
      minLength: 2, 
      maxLength: 15
    }).map(s => s.replace(/[^a-zA-Z]/g, 'a')); // Replace non-letters with 'a'
    
    // Generate random property options
    const propOptionsArb = fc.record({
      required: fc.boolean(),
      unique: fc.boolean(),
      default: fc.oneof(fc.string(), fc.integer(), fc.constant(null))
    });
    
    // Generate arrays of property definitions with unique names
    const propertiesArb = fc.uniqueArray(
      fc.tuple(propNameArb, propOptionsArb),
      { 
        minLength: 1, 
        maxLength: 10,
        comparator: (a, b) => a[0] === b[0] // Compare by property name
      }
    );
    
    // Generate random entity metadata
    const entityMetadataArb = fc.record({
      className: classNameArb,
      properties: propertiesArb
    });
    
    // Run the test
    fc.assert(
      fc.property(entityMetadataArb, (entityMetadata) => {
        // Create a dynamic class with the entity decorator
        const entityClass = createDynamicEntity(
          entityMetadata.className,
          entityMetadata.properties
        );
        
        // Register the entity
        builder.registerEntity(entityClass);
        
        // Verify registration was successful
        const registeredMetadata = registry.getEntityMetadata(entityClass);
        if (!registeredMetadata) return false;
        
        // Verify all properties were registered
        let allPropertiesRegistered = true;
        for (const [propName, options] of entityMetadata.properties) {
          const propMetadata = registry.getPropertyMetadata(entityClass, propName);
          if (!propMetadata) {
            allPropertiesRegistered = false;
            break;
          }
          
          // Check if required and unique options match
          if (propMetadata.required !== options.required ||
              propMetadata.unique !== options.unique) {
            allPropertiesRegistered = false;
            break;
          }
        }
        
        return allPropertiesRegistered;
      }),
      { numRuns: 50 }
    );
  });

  test("should handle all relationship types correctly", () => {
    // Define types of relationships to test
    const relationshipTypes = ["one-to-one", "one-to-many", "many-to-one", "many-to-many"];
    
    // Generate random relationship metadata
    const relationshipArb = fc.record({
      sourceClass: fc.string({ minLength: 3, maxLength: 10 }).map(s => s.replace(/[^a-zA-Z]/g, 'a')),
      targetClass: fc.string({ minLength: 3, maxLength: 10 }).map(s => s.replace(/[^a-zA-Z]/g, 'a')),
      relType: fc.constantFrom(...relationshipTypes),
      relName: fc.string({ minLength: 2, maxLength: 10 }).map(s => s.replace(/[^a-zA-Z]/g, 'a')),
      inverseProp: fc.string({ minLength: 2, maxLength: 10 }).map(s => s.replace(/[^a-zA-Z]/g, 'a')),
      required: fc.boolean()
    });
    
    // Run the test
    fc.assert(
      fc.property(relationshipArb, (relData) => {
        // Create source and target classes
        const TargetClass = createDynamicEntity(relData.targetClass, []);
        const SourceClass = createDynamicEntityWithRelationship(
          relData.sourceClass,
          relData.relName,
          TargetClass,
          relData.relType,
          relData.inverseProp,
          relData.required
        );
        
        // Register classes
        builder.registerEntities([SourceClass, TargetClass]);
        
        // Verify relationship registration
        const relationship = registry.getRelationshipMetadata(SourceClass, relData.relName);
        if (!relationship) return false;
        
        // Check relationship properties
        const correctTarget = relationship.target === TargetClass;
        const correctInverse = relationship.inverse === relData.inverseProp;
        const correctRequired = relationship.required === relData.required;
        
        // Check cardinality based on relationship type
        let correctCardinality = false;
        if (relData.relType === "one-to-one" || relData.relType === "many-to-one") {
          correctCardinality = relationship.cardinality === "one";
        } else if (relData.relType === "one-to-many" || relData.relType === "many-to-many") {
          correctCardinality = relationship.cardinality === "many";
        }
        
        return correctTarget && correctInverse && correctRequired && correctCardinality;
      }),
      { numRuns: 50 }
    );
  });

  test("should handle cloning and clearing registry with random data", () => {
    // Generate a random set of entities to register
    const entitiesCountArb = fc.integer({ min: 1, max: 10 });
    
    fc.assert(
      fc.property(entitiesCountArb, (count) => {
        // Create multiple random entities
        const entities = [];
        for (let i = 0; i < count; i++) {
          const className = `TestEntity${i}`;
          const entityClass = createDynamicEntity(className, [
            [`prop${i}`, { required: true }]
          ]);
          entities.push(entityClass);
        }
        
        // Register all entities
        builder.registerEntities(entities);
        
        // Verify all entities are registered
        const allRegistered = entities.every(e => 
          registry.getEntityMetadata(e) !== undefined
        );
        if (!allRegistered) return false;
        
        // Clone the registry
        const clonedRegistry = registry.clone();
        
        // Verify all entities exist in the clone
        const allCloned = entities.every(e => 
          clonedRegistry.getEntityMetadata(e) !== undefined
        );
        if (!allCloned) return false;
        
        // Clear the original registry
        registry.clear();
        
        // Verify original registry is empty but clone still has data
        const originalEmpty = entities.every(e => 
          registry.getEntityMetadata(e) === undefined
        );
        const cloneIntact = entities.every(e => 
          clonedRegistry.getEntityMetadata(e) !== undefined
        );
        
        return originalEmpty && cloneIntact;
      }),
      { numRuns: 10 }
    );
  });
});

// Helper functions to dynamically create entity classes
function createDynamicEntity(
  className: string, 
  properties: [string, any][]
): any {
  // Create a class with the specified name
  const entityClass = class {};
  Object.defineProperty(entityClass, 'name', { value: className });
  
  // Apply entity decorator
  Entity()(entityClass);
  
  // Add properties with decorators
  properties.forEach(([propName, options]) => {
    // Define the property on the prototype
    Object.defineProperty(entityClass.prototype, propName, {
      writable: true,
      enumerable: true,
      configurable: true
    });
    
    // Apply property decorator
    Property(options)(entityClass.prototype, propName);
  });
  
  // Add an ID property
  Object.defineProperty(entityClass.prototype, 'id', {
    writable: true,
    enumerable: true,
    configurable: true
  });
  Id()(entityClass.prototype, 'id');
  
  return entityClass;
}

function createDynamicEntityWithRelationship(
  className: string,
  relationshipName: string,
  targetClass: any,
  relationType: string,
  inverseProp: string,
  required: boolean
): any {
  // Create a class with the specified name
  const entityClass = class {};
  Object.defineProperty(entityClass, 'name', { value: className });
  
  // Apply entity decorator
  Entity()(entityClass);
  
  // Define the relationship property
  Object.defineProperty(entityClass.prototype, relationshipName, {
    writable: true,
    enumerable: true,
    configurable: true
  });
  
  // Apply the appropriate relationship decorator
  const relationshipOptions = {
    target: () => targetClass,
    inverse: inverseProp,
    required
  };
  
  switch (relationType) {
    case "one-to-one":
      OneToOne(relationshipOptions)(entityClass.prototype, relationshipName);
      break;
    case "one-to-many":
      OneToMany(relationshipOptions)(entityClass.prototype, relationshipName);
      break;
    case "many-to-one":
      ManyToOne(relationshipOptions)(entityClass.prototype, relationshipName);
      break;
    case "many-to-many":
      ManyToMany(relationshipOptions)(entityClass.prototype, relationshipName);
      break;
  }
  
  // Add an ID property
  Object.defineProperty(entityClass.prototype, 'id', {
    writable: true,
    enumerable: true,
    configurable: true
  });
  Id()(entityClass.prototype, 'id');
  
  return entityClass;
}