/**
 * RMInventoryPiece Model Tests
 * Tests for dimension-based RM inventory piece tracking
 */

const fc = require('fast-check');

// Mock Sequelize for testing
const mockSequelize = {
  authenticate: jest.fn().mockResolvedValue(true),
  close: jest.fn().mockResolvedValue(true),
  sync: jest.fn().mockResolvedValue(true),
  Sequelize: {
    Op: {
      gte: Symbol('gte'),
      or: Symbol('or'),
      ne: Symbol('ne')
    },
    literal: jest.fn((str) => ({ literal: str }))
  }
};

// Mock RMInventoryPiece model
const mockRMInventoryPiece = {
  sequelize: mockSequelize,
  build: jest.fn(),
  create: jest.fn(),
  findByPk: jest.fn(),
  findAll: jest.fn(),
  bulkCreate: jest.fn(),
  destroy: jest.fn().mockResolvedValue(true),
  findSuitablePieces: jest.fn()
};

// Mock Product model
const mockProduct = {
  id: 1,
  name: 'Test Cotton Fabric',
  product_type: 'RM',
  track_inventory: true,
  track_by_dimensions: true,
  unit_of_measure: 'm'
};

// Create mock instance with validation
const createMockRMInventoryPieceInstance = (data) => {
  const instance = { ...data };
  
  // Add validation logic
  const validate = () => {
    if (!instance.product_id || !instance.piece_number || !instance.length || !instance.width || !instance.unit) {
      throw new Error('Missing required fields');
    }
    
    if (instance.length <= 0 || instance.width <= 0) {
      throw new Error('Dimensions must be positive');
    }
    
    if (!['FULL', 'USABLE', 'WASTE', 'SCRAP'].includes(instance.status)) {
      throw new Error('Invalid status');
    }
    
    if (!['inch', 'cm', 'm'].includes(instance.unit)) {
      throw new Error('Invalid unit');
    }
    
    // Status-based validation
    if (instance.status === 'FULL') {
      if (instance.usable_length !== null || instance.usable_width !== null) {
        throw new Error('FULL pieces should not have usable dimensions set');
      }
    } else {
      if (instance.usable_length === null || instance.usable_width === null) {
        throw new Error('Non-FULL pieces must have usable dimensions set');
      }
      if (instance.usable_length < 0 || instance.usable_width < 0) {
        throw new Error('Usable dimensions cannot be negative');
      }
      if (instance.usable_length > instance.length || instance.usable_width > instance.width) {
        throw new Error('Usable dimensions cannot exceed original dimensions');
      }
    }
  };
  
  // Add instance methods
  instance.getArea = function() {
    return parseFloat(this.length) * parseFloat(this.width);
  };
  
  instance.getUsableArea = function() {
    if (this.status === 'FULL') {
      return this.getArea();
    }
    return parseFloat(this.usable_length || 0) * parseFloat(this.usable_width || 0);
  };
  
  instance.getScrapArea = function() {
    return parseFloat(this.scrap_length || 0) * parseFloat(this.scrap_width || 0);
  };
  
  instance.canFitDimensions = function(requiredLength, requiredWidth) {
    const availableLength = this.status === 'FULL' ? this.length : this.usable_length;
    const availableWidth = this.status === 'FULL' ? this.width : this.usable_width;
    
    return availableLength >= requiredLength && availableWidth >= requiredWidth;
  };
  
  instance.validate = jest.fn().mockImplementation(validate);
  instance.save = jest.fn().mockResolvedValue(instance);
  instance.update = jest.fn().mockImplementation(async (updates) => {
    Object.assign(instance, updates);
    validate();
    return instance;
  });
  instance.destroy = jest.fn().mockResolvedValue(true);
  instance.id = Math.floor(Math.random() * 1000);
  instance.created_at = new Date();
  instance.updated_at = new Date();
  
  // Set defaults
  instance.status = instance.status || 'FULL';
  instance.scrap_length = instance.scrap_length || 0;
  instance.scrap_width = instance.scrap_width || 0;
  
  return instance;
};

// Setup mock implementations
mockRMInventoryPiece.create.mockImplementation(async (data) => {
  const instance = createMockRMInventoryPieceInstance(data);
  await instance.validate();
  return instance;
});

mockRMInventoryPiece.bulkCreate.mockImplementation(async (dataArray) => {
  const instances = dataArray.map(data => {
    const instance = createMockRMInventoryPieceInstance(data);
    instance.validate();
    return instance;
  });
  return instances;
});

mockRMInventoryPiece.findSuitablePieces.mockImplementation(async (productId, requiredLength, requiredWidth, unit = 'm') => {
  // Mock implementation of findSuitablePieces
  const mockPieces = [
    createMockRMInventoryPieceInstance({
      product_id: productId,
      piece_number: 1,
      length: 6.0,
      width: 4.0,
      unit: unit,
      status: 'FULL'
    }),
    createMockRMInventoryPieceInstance({
      product_id: productId,
      piece_number: 2,
      length: 5.0,
      width: 3.0,
      unit: unit,
      status: 'USABLE',
      usable_length: 4.0,
      usable_width: 2.5
    }),
    createMockRMInventoryPieceInstance({
      product_id: productId,
      piece_number: 3,
      length: 3.0,
      width: 2.0,
      unit: unit,
      status: 'WASTE',
      usable_length: 2.5,
      usable_width: 1.5
    })
  ];
  
  // Filter pieces that can fit the required dimensions
  const suitablePieces = mockPieces.filter(piece => {
    if (piece.status === 'SCRAP') return false;
    return piece.canFitDimensions(requiredLength, requiredWidth);
  });
  
  // Sort by priority: FULL > USABLE > WASTE, then by area (smaller first)
  suitablePieces.sort((a, b) => {
    const statusPriority = { 'FULL': 1, 'USABLE': 2, 'WASTE': 3 };
    if (statusPriority[a.status] !== statusPriority[b.status]) {
      return statusPriority[a.status] - statusPriority[b.status];
    }
    return a.getArea() - b.getArea();
  });
  
  return suitablePieces;
});

describe('RMInventoryPiece Model', () => {
  let testProduct;

  beforeAll(async () => {
    testProduct = mockProduct;
  });

  afterAll(async () => {
    // No cleanup needed for mocks
  });

  beforeEach(async () => {
    // Reset mocks before each test
    jest.clearAllMocks();
  });

  describe('Basic Model Operations', () => {
    test('should create RM inventory piece with valid data', async () => {
      const piece = await mockRMInventoryPiece.create({
        product_id: testProduct.id,
        piece_number: 1,
        length: 6.0,
        width: 4.0,
        unit: 'm',
        status: 'FULL'
      });

      expect(piece).toBeDefined();
      expect(piece.product_id).toBe(testProduct.id);
      expect(piece.length).toBe(6.0);
      expect(piece.width).toBe(4.0);
      expect(piece.status).toBe('FULL');
      expect(piece.usable_length).toBeNull();
      expect(piece.usable_width).toBeNull();
    });

    test('should enforce required fields', async () => {
      await expect(mockRMInventoryPiece.create({
        // Missing required fields
      })).rejects.toThrow();
    });

    test('should enforce positive dimensions', async () => {
      await expect(mockRMInventoryPiece.create({
        product_id: testProduct.id,
        piece_number: 1,
        length: -1.0, // Invalid negative length
        width: 4.0,
        unit: 'm'
      })).rejects.toThrow();
    });

    test('should enforce valid status enum', async () => {
      await expect(mockRMInventoryPiece.create({
        product_id: testProduct.id,
        piece_number: 1,
        length: 6.0,
        width: 4.0,
        unit: 'm',
        status: 'INVALID_STATUS'
      })).rejects.toThrow();
    });

    test('should enforce valid unit enum', async () => {
      await expect(mockRMInventoryPiece.create({
        product_id: testProduct.id,
        piece_number: 1,
        length: 6.0,
        width: 4.0,
        unit: 'invalid_unit'
      })).rejects.toThrow();
    });
  });

  describe('Status-Based Validation', () => {
    test('FULL pieces should not have usable dimensions', async () => {
      await expect(mockRMInventoryPiece.create({
        product_id: testProduct.id,
        piece_number: 1,
        length: 6.0,
        width: 4.0,
        unit: 'm',
        status: 'FULL',
        usable_length: 5.0, // Should not be set for FULL pieces
        usable_width: 3.0
      })).rejects.toThrow('FULL pieces should not have usable dimensions set');
    });

    test('Non-FULL pieces must have usable dimensions', async () => {
      await expect(mockRMInventoryPiece.create({
        product_id: testProduct.id,
        piece_number: 1,
        length: 6.0,
        width: 4.0,
        unit: 'm',
        status: 'USABLE'
        // Missing usable_length and usable_width
      })).rejects.toThrow('Non-FULL pieces must have usable dimensions set');
    });

    test('Usable dimensions cannot exceed original dimensions', async () => {
      await expect(mockRMInventoryPiece.create({
        product_id: testProduct.id,
        piece_number: 1,
        length: 6.0,
        width: 4.0,
        unit: 'm',
        status: 'USABLE',
        usable_length: 7.0, // Exceeds original length
        usable_width: 3.0
      })).rejects.toThrow('Usable dimensions cannot exceed original dimensions');
    });

    test('should create valid USABLE piece', async () => {
      const piece = await mockRMInventoryPiece.create({
        product_id: testProduct.id,
        piece_number: 1,
        length: 6.0,
        width: 4.0,
        unit: 'm',
        status: 'USABLE',
        usable_length: 5.0,
        usable_width: 3.0
      });

      expect(piece.status).toBe('USABLE');
      expect(piece.usable_length).toBe(5.0);
      expect(piece.usable_width).toBe(3.0);
    });
  });

  describe('Instance Methods', () => {
    test('getArea should calculate total area correctly', async () => {
      const piece = await mockRMInventoryPiece.create({
        product_id: testProduct.id,
        piece_number: 1,
        length: 6.0,
        width: 4.0,
        unit: 'm',
        status: 'FULL'
      });

      expect(piece.getArea()).toBe(24.0);
    });

    test('getUsableArea should return total area for FULL pieces', async () => {
      const piece = await mockRMInventoryPiece.create({
        product_id: testProduct.id,
        piece_number: 1,
        length: 6.0,
        width: 4.0,
        unit: 'm',
        status: 'FULL'
      });

      expect(piece.getUsableArea()).toBe(24.0);
    });

    test('getUsableArea should return usable area for non-FULL pieces', async () => {
      const piece = await mockRMInventoryPiece.create({
        product_id: testProduct.id,
        piece_number: 1,
        length: 6.0,
        width: 4.0,
        unit: 'm',
        status: 'USABLE',
        usable_length: 5.0,
        usable_width: 3.0
      });

      expect(piece.getUsableArea()).toBe(15.0);
    });

    test('getScrapArea should calculate scrap area correctly', async () => {
      const piece = await mockRMInventoryPiece.create({
        product_id: testProduct.id,
        piece_number: 1,
        length: 6.0,
        width: 4.0,
        unit: 'm',
        status: 'SCRAP',
        usable_length: 0,
        usable_width: 0,
        scrap_length: 2.0,
        scrap_width: 1.5
      });

      expect(piece.getScrapArea()).toBe(3.0);
    });

    test('canFitDimensions should work for FULL pieces', async () => {
      const piece = await mockRMInventoryPiece.create({
        product_id: testProduct.id,
        piece_number: 1,
        length: 6.0,
        width: 4.0,
        unit: 'm',
        status: 'FULL'
      });

      expect(piece.canFitDimensions(5.0, 3.0)).toBe(true);
      expect(piece.canFitDimensions(7.0, 3.0)).toBe(false);
      expect(piece.canFitDimensions(5.0, 5.0)).toBe(false);
    });

    test('canFitDimensions should work for non-FULL pieces', async () => {
      const piece = await mockRMInventoryPiece.create({
        product_id: testProduct.id,
        piece_number: 1,
        length: 6.0,
        width: 4.0,
        unit: 'm',
        status: 'USABLE',
        usable_length: 4.0,
        usable_width: 3.0
      });

      expect(piece.canFitDimensions(3.0, 2.0)).toBe(true);
      expect(piece.canFitDimensions(5.0, 2.0)).toBe(false);
      expect(piece.canFitDimensions(3.0, 4.0)).toBe(false);
    });
  });

  describe('Class Methods', () => {
    test('findSuitablePieces should find pieces that can fit required dimensions', async () => {
      const pieces = await mockRMInventoryPiece.findSuitablePieces(testProduct.id, 3.0, 2.0, 'm');
      
      expect(pieces.length).toBe(2); // FULL piece (6x4) and USABLE piece (4x2.5 usable)
      expect(pieces[0].status).toBe('FULL'); // Should prioritize FULL pieces
    });

    test('findSuitablePieces should exclude SCRAP pieces', async () => {
      const pieces = await mockRMInventoryPiece.findSuitablePieces(testProduct.id, 1.0, 0.5, 'm');
      
      // Should not include SCRAP piece even though it has the dimensions
      expect(pieces.every(p => p.status !== 'SCRAP')).toBe(true);
    });

    test('findSuitablePieces should order by priority (FULL > USABLE > WASTE)', async () => {
      const pieces = await mockRMInventoryPiece.findSuitablePieces(testProduct.id, 2.0, 1.0, 'm');
      
      const statuses = pieces.map(p => p.status);
      const fullIndex = statuses.indexOf('FULL');
      const usableIndex = statuses.indexOf('USABLE');
      const wasteIndex = statuses.indexOf('WASTE');
      
      if (fullIndex !== -1 && usableIndex !== -1) {
        expect(fullIndex).toBeLessThan(usableIndex);
      }
      if (usableIndex !== -1 && wasteIndex !== -1) {
        expect(usableIndex).toBeLessThan(wasteIndex);
      }
    });
  });

  /**
   * Property 2: Dimension-Based Piece Creation
   * Validates: Requirements 1.2, 1.3
   * 
   * This property ensures that RM inventory pieces are created correctly
   * with proper dimension tracking and status management.
   */
  describe('Property 2: Dimension-Based Piece Creation', () => {
    test('should maintain dimension consistency across all operations', () => {
      return fc.assert(fc.asyncProperty(
        fc.record({
          length: fc.float({ min: 0.001, max: 100.0 }),
          width: fc.float({ min: 0.001, max: 100.0 }),
          unit: fc.constantFrom('inch', 'cm', 'm'),
          piece_number: fc.integer({ min: 1, max: 100 }),
          status: fc.constantFrom('FULL', 'USABLE', 'WASTE', 'SCRAP'),
          usable_length_factor: fc.float({ min: 0.1, max: 1.0 }), // Factor of original length
          usable_width_factor: fc.float({ min: 0.1, max: 1.0 }), // Factor of original width
          scrap_length: fc.float({ min: 0, max: 10.0 }),
          scrap_width: fc.float({ min: 0, max: 10.0 })
        }),
        async (data) => {
          // Calculate usable dimensions based on status
          let usable_length = null;
          let usable_width = null;
          
          if (data.status !== 'FULL') {
            usable_length = data.length * data.usable_length_factor;
            usable_width = data.width * data.usable_width_factor;
          }

          const pieceData = {
            product_id: testProduct.id,
            piece_number: data.piece_number,
            length: data.length,
            width: data.width,
            unit: data.unit,
            status: data.status,
            usable_length,
            usable_width,
            scrap_length: data.scrap_length,
            scrap_width: data.scrap_width
          };

          const piece = await mockRMInventoryPiece.create(pieceData);

          // Property 2.1: Piece is created with correct dimensions
          expect(parseFloat(piece.length)).toBeCloseTo(data.length, 3);
          expect(parseFloat(piece.width)).toBeCloseTo(data.width, 3);
          expect(piece.unit).toBe(data.unit);

          // Property 2.2: Status-based dimension consistency
          if (data.status === 'FULL') {
            expect(piece.usable_length).toBeNull();
            expect(piece.usable_width).toBeNull();
          } else {
            expect(parseFloat(piece.usable_length)).toBeCloseTo(usable_length, 3);
            expect(parseFloat(piece.usable_width)).toBeCloseTo(usable_width, 3);
          }

          // Property 2.3: Area calculations are consistent
          const totalArea = piece.getArea();
          const usableArea = piece.getUsableArea();
          const scrapArea = piece.getScrapArea();

          expect(totalArea).toBeCloseTo(data.length * data.width, 3);
          expect(scrapArea).toBeCloseTo(data.scrap_length * data.scrap_width, 3);

          if (data.status === 'FULL') {
            expect(usableArea).toBeCloseTo(totalArea, 3);
          } else {
            expect(usableArea).toBeCloseTo(usable_length * usable_width, 3);
          }

          // Property 2.4: Dimension fitting logic is correct
          const canFitSelf = piece.canFitDimensions(data.length, data.width);
          if (data.status === 'FULL') {
            expect(canFitSelf).toBe(true);
          }

          // Property 2.5: Usable dimensions never exceed original dimensions
          if (data.status !== 'FULL') {
            expect(parseFloat(piece.usable_length)).toBeLessThanOrEqual(parseFloat(piece.length));
            expect(parseFloat(piece.usable_width)).toBeLessThanOrEqual(parseFloat(piece.width));
          }

          // Clean up
          await piece.destroy();
        }
      ), { numRuns: 100 });
    });

    test('should handle edge cases in dimension calculations', () => {
      return fc.assert(fc.asyncProperty(
        fc.record({
          length: fc.constantFrom(0.001, 0.1, 1.0, 10.0, 100.0), // Edge case dimensions
          width: fc.constantFrom(0.001, 0.1, 1.0, 10.0, 100.0),
          unit: fc.constantFrom('inch', 'cm', 'm')
        }),
        async (data) => {
          const piece = await mockRMInventoryPiece.create({
            product_id: testProduct.id,
            piece_number: 1,
            length: data.length,
            width: data.width,
            unit: data.unit,
            status: 'FULL'
          });

          // Property 2.6: Edge case dimensions are handled correctly
          expect(piece.getArea()).toBeCloseTo(data.length * data.width, 6);
          expect(piece.getUsableArea()).toBeCloseTo(data.length * data.width, 6);
          expect(piece.canFitDimensions(data.length, data.width)).toBe(true);
          expect(piece.canFitDimensions(data.length + 0.001, data.width)).toBe(false);
          expect(piece.canFitDimensions(data.length, data.width + 0.001)).toBe(false);

          // Clean up
          await piece.destroy();
        }
      ), { numRuns: 50 });
    });

    test('should maintain consistency when updating piece status', () => {
      return fc.assert(fc.asyncProperty(
        fc.record({
          initial_length: fc.float({ min: 1.0, max: 10.0 }),
          initial_width: fc.float({ min: 1.0, max: 10.0 }),
          new_usable_length_factor: fc.float({ min: 0.1, max: 0.9 }),
          new_usable_width_factor: fc.float({ min: 0.1, max: 0.9 })
        }),
        async (data) => {
          // Create FULL piece
          const piece = await mockRMInventoryPiece.create({
            product_id: testProduct.id,
            piece_number: 1,
            length: data.initial_length,
            width: data.initial_width,
            unit: 'm',
            status: 'FULL'
          });

          const originalArea = piece.getArea();

          // Update to USABLE status
          const new_usable_length = data.initial_length * data.new_usable_length_factor;
          const new_usable_width = data.initial_width * data.new_usable_width_factor;

          await piece.update({
            status: 'USABLE',
            usable_length: new_usable_length,
            usable_width: new_usable_width
          });

          // Property 2.7: Status update maintains dimension consistency
          expect(piece.getArea()).toBeCloseTo(originalArea, 3); // Original area unchanged
          expect(piece.getUsableArea()).toBeCloseTo(new_usable_length * new_usable_width, 3);
          expect(parseFloat(piece.usable_length)).toBeLessThanOrEqual(parseFloat(piece.length));
          expect(parseFloat(piece.usable_width)).toBeLessThanOrEqual(parseFloat(piece.width));

          // Clean up
          await piece.destroy();
        }
      ), { numRuns: 100 });
    });
  });
});