import {
  classifyNodes,
  findStartNodes,
  calculateLockedBounds,
  createNodeCollisionBounds,
  isPositionOccupied,
  findNonCollidingPosition,
  buildGraphData,
  calculateIdealXPosition,
  determineStartPosition,
  findNearestEdge,
  LAYOUT_CONFIG,
} from '../layoutHelpers';
import { Node, Edge } from '../../store/store';

describe('layoutHelpers', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('classifyNodes', () => {
    it('should separate locked and unlocked nodes', () => {
      const nodes: Node[] = [
        { id: 'locked1', position: { x: 0, y: 0 }, data: { locked: true } },
        { id: 'locked2', position: { x: 0, y: 0 }, data: { locked: true } },
        { id: 'unlocked1', position: { x: 0, y: 0 }, data: { locked: false } },
        { id: 'unlocked2', position: { x: 0, y: 0 }, data: {} },
      ];

      const result = classifyNodes(nodes);

      expect(result.lockedNodes).toHaveLength(2);
      expect(result.unlockedNodes).toHaveLength(2);
      expect(result.lockedNodes.map(n => n.id)).toContain('locked1');
      expect(result.lockedNodes.map(n => n.id)).toContain('locked2');
      expect(result.unlockedNodes.map(n => n.id)).toContain('unlocked1');
      expect(result.unlockedNodes.map(n => n.id)).toContain('unlocked2');
    });

    it('should handle all locked nodes', () => {
      const nodes: Node[] = [
        { id: 'locked1', position: { x: 0, y: 0 }, data: { locked: true } },
        { id: 'locked2', position: { x: 0, y: 0 }, data: { locked: true } },
      ];

      const result = classifyNodes(nodes);
      expect(result.lockedNodes).toHaveLength(2);
      expect(result.unlockedNodes).toHaveLength(0);
    });

    it('should handle all unlocked nodes', () => {
      const nodes: Node[] = [
        { id: 'unlocked1', position: { x: 0, y: 0 }, data: {} },
        { id: 'unlocked2', position: { x: 0, y: 0 }, data: { locked: false } },
      ];

      const result = classifyNodes(nodes);
      expect(result.lockedNodes).toHaveLength(0);
      expect(result.unlockedNodes).toHaveLength(2);
    });

    it('should handle empty array', () => {
      const result = classifyNodes([]);
      expect(result.lockedNodes).toHaveLength(0);
      expect(result.unlockedNodes).toHaveLength(0);
    });

    it('should treat missing data property as unlocked', () => {
      const nodes: Node[] = [
        { id: 'node1', position: { x: 0, y: 0 } } as Node,
      ];

      const result = classifyNodes(nodes);
      expect(result.unlockedNodes).toHaveLength(1);
    });
  });

  describe('findStartNodes', () => {
    it('should find start type nodes', () => {
      const nodes: Node[] = [
        { id: 'start1', type: 'start', position: { x: 0, y: 0 }, data: {} },
        { id: 'element1', type: 'element', position: { x: 0, y: 0 }, data: {} },
      ];

      const inDegree = new Map([['start1', 0], ['element1', 1]]);
      const result = findStartNodes(nodes, inDegree);

      expect(result).toHaveLength(1);
      expect(result[0].id).toBe('start1');
    });

    it('should find nodes with zero incoming edges', () => {
      const nodes: Node[] = [
        { id: 'node1', type: 'element', position: { x: 0, y: 0 }, data: {} },
        { id: 'node2', type: 'element', position: { x: 0, y: 0 }, data: {} },
      ];

      const inDegree = new Map([['node1', 0], ['node2', 1]]);
      const result = findStartNodes(nodes, inDegree);

      expect(result.some(n => n.id === 'node1')).toBe(true);
    });

    it('should find nodes with event plugin', () => {
      const nodes: Node[] = [
        { id: 'event1', type: 'element', position: { x: 0, y: 0 }, data: { plugin: 'entity_create_event' } },
        { id: 'action1', type: 'element', position: { x: 0, y: 0 }, data: { plugin: 'entity_save' } },
      ];

      const inDegree = new Map([['event1', 0], ['action1', 1]]);
      const result = findStartNodes(nodes, inDegree);

      expect(result.some(n => n.id === 'event1')).toBe(true);
    });

    it('should prioritize start type over other criteria', () => {
      const nodes: Node[] = [
        { id: 'event1', type: 'element', position: { x: 0, y: 0 }, data: { plugin: 'event' } },
        { id: 'start1', type: 'start', position: { x: 0, y: 0 }, data: {} },
      ];

      const inDegree = new Map([['event1', 0], ['start1', 0]]);
      const result = findStartNodes(nodes, inDegree);

      expect(result[0].id).toBe('start1');
    });

    it('should return first node if no start nodes found', () => {
      const nodes: Node[] = [
        { id: 'node1', type: 'element', position: { x: 0, y: 0 }, data: {} },
        { id: 'node2', type: 'element', position: { x: 0, y: 0 }, data: {} },
      ];

      const inDegree = new Map([['node1', 1], ['node2', 1]]);
      const result = findStartNodes(nodes, inDegree);

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

    it('should return empty array for empty nodes', () => {
      const result = findStartNodes([], new Map());
      expect(result).toHaveLength(0);
    });
  });

  describe('calculateLockedBounds', () => {
    it('should return null for empty array', () => {
      const result = calculateLockedBounds([]);
      expect(result).toBeNull();
    });

    it('should calculate padded bounds', () => {
      const nodes: Node[] = [
        { id: 'node1', position: { x: 100, y: 200 }, data: {} },
      ];

      const result = calculateLockedBounds(nodes);
      expect(result).not.toBeNull();
      expect(result!.x1).toBeLessThan(100);
      expect(result!.y1).toBeLessThan(200);
      expect(result!.x2).toBeGreaterThan(100);
      expect(result!.y2).toBeGreaterThan(200);
    });
  });

  describe('createNodeCollisionBounds', () => {
    it('should create bounds for each node', () => {
      const nodes: Node[] = [
        { id: 'node1', position: { x: 100, y: 200 }, data: {} },
        { id: 'node2', position: { x: 300, y: 400 }, data: {} },
      ];

      const result = createNodeCollisionBounds(nodes);
      expect(result).toHaveLength(2);
    });

    it('should calculate bounds based on node dimensions', () => {
      const nodes: Node[] = [
        { id: 'node1', position: { x: 100, y: 200 }, data: {} },
      ];

      const result = createNodeCollisionBounds(nodes);
      const bounds = result[0];

      expect(bounds.id).toBe('node1');
      expect(bounds.x1).toBeLessThan(100);
      expect(bounds.x2).toBeGreaterThan(100);
      expect(bounds.y1).toBeLessThan(200);
      expect(bounds.y2).toBeGreaterThan(200);
    });

    it('should filter out nodes without position', () => {
      const nodes: Node[] = [
        { id: 'node1', data: {} } as Node,
        { id: 'node2', position: { x: 100, y: 200 }, data: {} },
      ];

      const result = createNodeCollisionBounds(nodes);
      expect(result).toHaveLength(1);
      expect(result[0].id).toBe('node2');
    });

    it('should return empty array for empty input', () => {
      const result = createNodeCollisionBounds([]);
      expect(result).toEqual([]);
    });
  });

  describe('isPositionOccupied', () => {
    it('should return false when no locked bounds exist', () => {
      const result = isPositionOccupied(100, 200, []);
      expect(result).toBe(false);
    });

    it('should return true when position overlaps with locked node', () => {
      const lockedBounds = [
        { id: 'locked1', x1: 50, y1: 150, x2: 150, y2: 250 },
      ];

      const result = isPositionOccupied(100, 200, lockedBounds);
      expect(result).toBe(true);
    });

    it('should return false when position does not overlap', () => {
      const lockedBounds = [
        { id: 'locked1', x1: 0, y1: 0, x2: 50, y2: 50 },
      ];

      const result = isPositionOccupied(500, 500, lockedBounds);
      expect(result).toBe(false);
    });

    it('should account for collision padding', () => {
      const lockedBounds = [
        { id: 'locked1', x1: 100, y1: 100, x2: 200, y2: 200 },
      ];

      // Position just outside the bounds but within padding
      const nodeWidth = LAYOUT_CONFIG.NODE_WIDTH;
      const padding = LAYOUT_CONFIG.COLLISION_PADDING;
      const nearX = 200 + nodeWidth / 2 + padding - 1;

      const result = isPositionOccupied(nearX, 150, lockedBounds);
      expect(result).toBe(true);
    });
  });

  describe('findNonCollidingPosition', () => {
    it('should return target position when not occupied', () => {
      const result = findNonCollidingPosition(100, 200, [], { x: 0, y: 0 });
      expect(result).toEqual({ x: 100, y: 200 });
    });

    it('should find alternative position when target is occupied', () => {
      const lockedBounds = [
        { id: 'locked1', x1: 50, y1: 150, x2: 150, y2: 250 },
      ];

      const result = findNonCollidingPosition(100, 200, lockedBounds, { x: 0, y: 0 });
      expect(result.x).not.toBe(100);
    });

    it('should eventually find a position within max attempts', () => {
      // Create a small grid of locked bounds
      const lockedBounds = [
        { id: 'locked1', x1: 0, y1: 0, x2: 200, y2: 200 },
      ];

      const result = findNonCollidingPosition(100, 100, lockedBounds, { x: 0, y: 0 });
      expect(result).toBeDefined();
    });

    it('should try moving down when moving right is blocked (attempts 11-20)', () => {
      // Create bounds that block the right direction (attempts 1-10) but leave down open
      const lockedBounds: { id: string; x1: number; y1: number; x2: number; y2: number }[] = [];
      // Block a wide horizontal area
      for (let i = 0; i < 15; i++) {
        lockedBounds.push({
          id: `blocked${i}`,
          x1: 50 + i * 100,
          y1: 50,
          x2: 150 + i * 100,
          y2: 150,
        });
      }

      const result = findNonCollidingPosition(100, 100, lockedBounds, { x: 0, y: 0 });
      // Position should be different (moved down)
      expect(result).toBeDefined();
      expect(result.y).toBeGreaterThanOrEqual(100);
    });

    it('should try moving left when right and down are blocked (attempts 21-30)', () => {
      // Create bounds that block right and down directions
      const lockedBounds: { id: string; x1: number; y1: number; x2: number; y2: number }[] = [];
      // Block a large area to force left movement
      for (let row = 0; row < 30; row++) {
        for (let col = 0; col < 30; col++) {
          lockedBounds.push({
            id: `blocked${row}_${col}`,
            x1: col * 50,
            y1: row * 50,
            x2: col * 50 + 40,
            y2: row * 50 + 40,
          });
        }
      }

      const result = findNonCollidingPosition(100, 100, lockedBounds, { x: 0, y: 0 });
      expect(result).toBeDefined();
    });
  });

  describe('buildGraphData', () => {
    it('should build adjacency map for nodes and edges', () => {
      const nodes: Node[] = [
        { id: 'node1', position: { x: 0, y: 0 }, data: {} },
        { id: 'node2', position: { x: 0, y: 0 }, data: {} },
        { id: 'node3', position: { x: 0, y: 0 }, data: {} },
      ];

      const edges: Edge[] = [
        { id: 'edge1', source: 'node1', target: 'node2' },
        { id: 'edge2', source: 'node2', target: 'node3' },
      ];

      const result = buildGraphData(nodes, edges);

      expect(result.adjacencyMap.get('node1')).toContain('node2');
      expect(result.adjacencyMap.get('node2')).toContain('node3');
    });

    it('should calculate in-degree correctly', () => {
      const nodes: Node[] = [
        { id: 'node1', position: { x: 0, y: 0 }, data: {} },
        { id: 'node2', position: { x: 0, y: 0 }, data: {} },
        { id: 'node3', position: { x: 0, y: 0 }, data: {} },
      ];

      const edges: Edge[] = [
        { id: 'edge1', source: 'node1', target: 'node2' },
        { id: 'edge2', source: 'node1', target: 'node3' },
        { id: 'edge3', source: 'node2', target: 'node3' },
      ];

      const result = buildGraphData(nodes, edges);

      expect(result.inDegree.get('node1')).toBe(0);
      expect(result.inDegree.get('node2')).toBe(1);
      expect(result.inDegree.get('node3')).toBe(2);
    });

    it('should calculate out-degree correctly', () => {
      const nodes: Node[] = [
        { id: 'node1', position: { x: 0, y: 0 }, data: {} },
        { id: 'node2', position: { x: 0, y: 0 }, data: {} },
        { id: 'node3', position: { x: 0, y: 0 }, data: {} },
      ];

      const edges: Edge[] = [
        { id: 'edge1', source: 'node1', target: 'node2' },
        { id: 'edge2', source: 'node1', target: 'node3' },
      ];

      const result = buildGraphData(nodes, edges);

      expect(result.outDegree.get('node1')).toBe(2);
      expect(result.outDegree.get('node2')).toBe(0);
      expect(result.outDegree.get('node3')).toBe(0);
    });

    it('should ignore edges with missing source or target', () => {
      const nodes: Node[] = [
        { id: 'node1', position: { x: 0, y: 0 }, data: {} },
        { id: 'node2', position: { x: 0, y: 0 }, data: {} },
      ];

      const edges: Edge[] = [
        { id: 'edge1', source: 'node1', target: 'missing' },
        { id: 'edge2', source: 'missing', target: 'node2' },
      ];

      const result = buildGraphData(nodes, edges);

      expect(result.inDegree.get('node1')).toBe(0);
      expect(result.inDegree.get('node2')).toBe(0);
    });

    it('should handle empty arrays', () => {
      const result = buildGraphData([], []);

      expect(result.adjacencyMap.size).toBe(0);
      expect(result.inDegree.size).toBe(0);
      expect(result.outDegree.size).toBe(0);
    });
  });

  describe('calculateIdealXPosition', () => {
    it('should return null when no connections exist', () => {
      const nodes: Node[] = [
        { id: 'node1', position: { x: 100, y: 0 }, data: {} },
      ];

      const result = calculateIdealXPosition('node1', [], nodes);
      expect(result).toBeNull();
    });

    it('should calculate average of parent positions', () => {
      const nodes: Node[] = [
        { id: 'parent1', position: { x: 100, y: 0 }, data: {} },
        { id: 'parent2', position: { x: 200, y: 0 }, data: {} },
        { id: 'node1', position: { x: 0, y: 100 }, data: {} },
      ];

      const edges: Edge[] = [
        { id: 'edge1', source: 'parent1', target: 'node1' },
        { id: 'edge2', source: 'parent2', target: 'node1' },
      ];

      const result = calculateIdealXPosition('node1', edges, nodes);
      expect(result).toBe(150); // (100 + 200) / 2
    });

    it('should include child positions in calculation', () => {
      const nodes: Node[] = [
        { id: 'node1', position: { x: 0, y: 0 }, data: {} },
        { id: 'child1', position: { x: 200, y: 100 }, data: {} },
      ];

      const edges: Edge[] = [
        { id: 'edge1', source: 'node1', target: 'child1' },
      ];

      const result = calculateIdealXPosition('node1', edges, nodes);
      expect(result).toBe(200);
    });

    it('should handle nodes without positions', () => {
      const nodes: Node[] = [
        { id: 'parent1', data: {} } as Node,
        { id: 'node1', position: { x: 0, y: 0 }, data: {} },
      ];

      const edges: Edge[] = [
        { id: 'edge1', source: 'parent1', target: 'node1' },
      ];

      const result = calculateIdealXPosition('node1', edges, nodes);
      expect(result).toBeNull();
    });
  });

  describe('determineStartPosition', () => {
    it('should return default position when no locked bounds', () => {
      const result = determineStartPosition(null);
      expect(result.x).toBeDefined();
      expect(result.y).toBeDefined();
    });

    it('should position to the right of locked bounds', () => {
      const lockedBounds = { x1: 0, y1: 0, x2: 500, y2: 400 };

      const result = determineStartPosition(lockedBounds);
      expect(result.x).toBeGreaterThan(500);
    });

    it('should respect minimum y position', () => {
      const lockedBounds = { x1: 0, y1: 1000, x2: 500, y2: 1200 };

      const result = determineStartPosition(lockedBounds);
      expect(result.y).toBeGreaterThanOrEqual(0);
    });
  });

  describe('LAYOUT_CONFIG', () => {
    it('should have positive node dimensions', () => {
      expect(LAYOUT_CONFIG.NODE_WIDTH).toBeGreaterThan(0);
      expect(LAYOUT_CONFIG.NODE_HEIGHT).toBeGreaterThan(0);
    });

    it('should have positive spacing values', () => {
      expect(LAYOUT_CONFIG.HORIZONTAL_SPACING).toBeGreaterThan(0);
      expect(LAYOUT_CONFIG.VERTICAL_SPACING).toBeGreaterThan(0);
    });

    it('should have positive collision padding', () => {
      expect(LAYOUT_CONFIG.COLLISION_PADDING).toBeGreaterThan(0);
    });

    it('should have minimum node distance greater than node width', () => {
      expect(LAYOUT_CONFIG.MIN_NODE_DISTANCE).toBeGreaterThan(LAYOUT_CONFIG.NODE_WIDTH);
    });
  });

  describe('findNearestEdge', () => {
    const nodes: Node[] = [
      { id: 'n1', position: { x: 0, y: 0 }, data: {}, width: 200, height: 100 },
      { id: 'n2', position: { x: 400, y: 0 }, data: {}, width: 200, height: 100 },
      { id: 'n3', position: { x: 0, y: 400 }, data: {}, width: 200, height: 100 },
    ];

    const edges: Edge[] = [
      { id: 'e1', source: 'n1', target: 'n2', data: {} },
      { id: 'e2', source: 'n1', target: 'n3', data: {} },
    ];

    it('should find the nearest edge to a position', () => {
      // Midpoint of e1 is approximately (300, 50) (centers of n1 and n2)
      const result = findNearestEdge({ x: 300, y: 50 }, edges, nodes);
      expect(result).not.toBeNull();
      expect(result!.id).toBe('e1');
    });

    it('should find e2 when position is closer to it', () => {
      // Midpoint of e2 is approximately (100, 250) (centers of n1 and n3)
      const result = findNearestEdge({ x: 100, y: 250 }, edges, nodes);
      expect(result).not.toBeNull();
      expect(result!.id).toBe('e2');
    });

    it('should return null when no edge is within maxDistance', () => {
      const result = findNearestEdge({ x: 1000, y: 1000 }, edges, nodes, 80);
      expect(result).toBeNull();
    });

    it('should respect maxDistance parameter', () => {
      // Position very close to e1 midpoint but with tiny maxDistance
      const result = findNearestEdge({ x: 300, y: 50 }, edges, nodes, 1);
      // Since the exact midpoint depends on node dimensions, this may or may not find it
      // The test verifies the maxDistance parameter is respected
      if (result) {
        expect(result.id).toBe('e1');
      }
    });

    it('should return null for empty edges', () => {
      const result = findNearestEdge({ x: 0, y: 0 }, [], nodes);
      expect(result).toBeNull();
    });

    it('should return null for empty nodes', () => {
      const result = findNearestEdge({ x: 0, y: 0 }, edges, []);
      expect(result).toBeNull();
    });

    it('should skip edges with missing source or target nodes', () => {
      const orphanEdges: Edge[] = [
        { id: 'orphan', source: 'missing1', target: 'missing2', data: {} },
      ];
      const result = findNearestEdge({ x: 0, y: 0 }, orphanEdges, nodes, 1000);
      expect(result).toBeNull();
    });

    it('should use default maxDistance of 80', () => {
      // Position is right at the midpoint of e1 (~300, 50)
      const result = findNearestEdge({ x: 300, y: 50 }, edges, nodes);
      expect(result).not.toBeNull();
    });
  });
});
