import {
  parseModelData,
  exportModelData,
  autoLayout,
  getComponentTypeFromCategory,
  getFitViewport,
} from '../modelUtils';
import { Node, Edge } from '../../store/store';

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

  describe('getComponentTypeFromCategory', () => {
    it('should return "1" for Events category', () => {
      expect(getComponentTypeFromCategory('Events')).toBe('1');
    });

    it('should return "1" for Triggers category', () => {
      expect(getComponentTypeFromCategory('Triggers')).toBe('1');
    });

    it('should return "4" for Actions category', () => {
      expect(getComponentTypeFromCategory('Actions')).toBe('4');
    });

    it('should return "4" for Tasks category', () => {
      expect(getComponentTypeFromCategory('Tasks')).toBe('4');
    });

    it('should return "5" for Conditions category', () => {
      expect(getComponentTypeFromCategory('Conditions')).toBe('5');
    });

    it('should return "5" for Decisions category', () => {
      expect(getComponentTypeFromCategory('Decisions')).toBe('5');
    });

    it('should return "6" for Gateways category', () => {
      expect(getComponentTypeFromCategory('Gateways')).toBe('6');
    });

    it('should return "4" (element) as default for unknown category', () => {
      expect(getComponentTypeFromCategory('Unknown')).toBe('4');
      expect(getComponentTypeFromCategory('')).toBe('4');
    });
  });

  describe('parseModelData', () => {
    it('should return empty arrays and null for null input', () => {
      const result = parseModelData(null);
      expect(result.nodes).toEqual([]);
      expect(result.edges).toEqual([]);
      expect(result.modelData).toBeNull();
    });

    it('should parse JSON string input', () => {
      const jsonString = JSON.stringify({
        nodes: [{ id: 'node1', type: 'element', position: { x: 100, y: 200 } }],
        edges: [],
      });

      const result = parseModelData(jsonString);
      expect(result.nodes).toHaveLength(1);
      expect(result.nodes[0].id).toBe('node1');
    });

    it('should parse object input', () => {
      const data = {
        nodes: [{ id: 'node1', type: 'start', position: { x: 100, y: 200 } }],
        edges: [],
      } as any;

      const result = parseModelData(data);
      expect(result.nodes).toHaveLength(1);
      expect(result.nodes[0].type).toBe('start');
    });

    it('should ensure node IDs are strings', () => {
      const data = {
        nodes: [{ id: 123, type: 'element', position: { x: 0, y: 0 } }],
        edges: [],
      } as any;

      const result = parseModelData(data);
      expect(typeof result.nodes[0].id).toBe('string');
      expect(result.nodes[0].id).toBe('123');
    });

    it('should set default position if not provided', () => {
      const data = {
        nodes: [{ id: 'node1', type: 'element' }],
        edges: [],
      } as any;

      const result = parseModelData(data);
      expect(result.nodes[0].position).toBeDefined();
      expect(result.nodes[0].position.x).toBeDefined();
      expect(result.nodes[0].position.y).toBeDefined();
    });

    it('should use node ID as label if no label provided', () => {
      const data = {
        nodes: [{ id: 'node1', type: 'element', position: { x: 0, y: 0 } }],
        edges: [],
      } as any;

      const result = parseModelData(data);
      expect(result.nodes[0].data.label).toBe('node1');
    });

    it('should preserve node configuration', () => {
      const config = { key1: 'value1', key2: 'value2' };
      const data = {
        nodes: [{ id: 'node1', type: 'element', position: { x: 0, y: 0 }, configuration: config }],
        edges: [],
      } as any;

      const result = parseModelData(data);
      expect(result.nodes[0].data.configuration).toEqual(config);
      expect(result.nodes[0].data.configured).toBe(true);
    });

    it('should set configured to false for empty configuration', () => {
      const data = {
        nodes: [{ id: 'node1', type: 'element', position: { x: 0, y: 0 }, configuration: {} }],
        edges: [],
      } as any;

      const result = parseModelData(data);
      expect(result.nodes[0].data.configured).toBe(false);
    });

    it('should preserve locked property', () => {
      const data = {
        nodes: [{ id: 'node1', type: 'element', position: { x: 0, y: 0 }, locked: true }],
        edges: [],
      } as any;

      const result = parseModelData(data);
      expect(result.nodes[0].data.locked).toBe(true);
    });

    it('should filter out edges with non-existent source nodes', () => {
      const data = {
        nodes: [{ id: 'node1', type: 'element', position: { x: 0, y: 0 } }],
        edges: [{ id: 'edge1', source: 'nonexistent', target: 'node1' }],
      } as any;

      const result = parseModelData(data);
      expect(result.edges).toHaveLength(0);
    });

    it('should filter out edges with non-existent target nodes', () => {
      const data = {
        nodes: [{ id: 'node1', type: 'element', position: { x: 0, y: 0 } }],
        edges: [{ id: 'edge1', source: 'node1', target: 'nonexistent' }],
      } as any;

      const result = parseModelData(data);
      expect(result.edges).toHaveLength(0);
    });

    it('should set correct edge type for condition edges', () => {
      const data = {
        nodes: [
          { id: 'node1', type: 'element', position: { x: 0, y: 0 } },
          { id: 'node2', type: 'element', position: { x: 100, y: 0 } },
        ],
        edges: [{ id: 'edge1', source: 'node1', target: 'node2', condition: 'test_condition' }],
      } as any;

      const result = parseModelData(data);
      expect(result.edges[0].type).toBe('condition');
    });

    it('should set correct edge type for annotation edges', () => {
      const data = {
        nodes: [
          { id: 'node1', type: 'element', position: { x: 0, y: 0 } },
          { id: 'node2', type: 'element', position: { x: 100, y: 0 } },
        ],
        edges: [{ id: 'edge1', source: 'node1', target: 'node2', annotation: 'Some annotation' }],
      } as any;

      const result = parseModelData(data);
      expect(result.edges[0].type).toBe('annotation');
    });

    it('should assign correct handles for gateway nodes', () => {
      const data = {
        nodes: [
          { id: 'gateway1', type: 'gateway', position: { x: 0, y: 0 } },
          { id: 'node2', type: 'element', position: { x: 100, y: 0 } },
          { id: 'node3', type: 'element', position: { x: 100, y: 100 } },
        ],
        edges: [
          { id: 'edge1', source: 'gateway1', target: 'node2' },
          { id: 'edge2', source: 'gateway1', target: 'node3' },
        ],
      } as any;

      const result = parseModelData(data);
      expect(result.edges[0].sourceHandle).toBe('true');
      expect(result.edges[1].sourceHandle).toBe('false');
    });

    it('should preserve edge locked property', () => {
      const data = {
        nodes: [
          { id: 'node1', type: 'element', position: { x: 0, y: 0 } },
          { id: 'node2', type: 'element', position: { x: 100, y: 0 } },
        ],
        edges: [{ id: 'edge1', source: 'node1', target: 'node2', locked: true }],
      } as any;

      const result = parseModelData(data);
      expect(result.edges[0].data.locked).toBe(true);
    });

    it('should handle empty nodes and edges arrays', () => {
      const data = { nodes: [], edges: [] };
      const result = parseModelData(data);
      expect(result.nodes).toEqual([]);
      expect(result.edges).toEqual([]);
    });

    it('should handle missing nodes array', () => {
      const data = { edges: [] };
      const result = parseModelData(data as any);
      expect(result.nodes).toEqual([]);
    });

    it('should handle missing edges array', () => {
      const data = { nodes: [] };
      const result = parseModelData(data as any);
      expect(result.edges).toEqual([]);
    });
  });

  describe('exportModelData', () => {
    const mockNodes: Node[] = [
      {
        id: 'node1',
        type: 'element',
        position: { x: 100.5, y: 200.7 },
        data: {
          label: 'Test Node',
          plugin: 'test_plugin',
          configuration: { key: 'value' },
          locked: true,
          annotation: 'Test annotation',
        },
      },
    ];

    const mockEdges: Edge[] = [
      {
        id: 'edge1',
        source: 'node1',
        target: 'node2',
        sourceHandle: 'output',
        targetHandle: 'input',
        data: {
          condition: 'test_condition',
          conditionLabel: 'Test Condition',
          conditionConfiguration: { config: 'value' },
          locked: true,
          annotation: 'Edge annotation',
          controlOffset: { x: 10, y: 20 },
        },
      },
    ];

    it('should export basic model structure', () => {
      const result = exportModelData([], []);
      expect(result.id).toBeDefined();
      expect(result.version).toBe('1.0.0');
      expect(result.metadata).toBeDefined();
      expect(result.nodes).toEqual([]);
      expect(result.edges).toEqual([]);
    });

    it('should export with custom metadata', () => {
      const metadata = {
        id: 'custom-id',
        label: 'Custom Model',
        description: 'A test model',
        tags: ['test', 'demo'],
      };

      const result = exportModelData([], [], metadata);
      expect(result.id).toBe('custom-id');
      expect(result.metadata.label).toBe('Custom Model');
      expect(result.metadata.description).toBe('A test model');
      expect(result.metadata.tags).toEqual(['test', 'demo']);
    });

    it('should round node positions to integers', () => {
      const result = exportModelData(mockNodes, []);
      expect(result.nodes[0].position.x).toBe(101);
      expect(result.nodes[0].position.y).toBe(201);
    });

    it('should include node properties in export', () => {
      const result = exportModelData(mockNodes, []);
      const exportedNode = result.nodes[0];

      expect(exportedNode.id).toBe('node1');
      expect(exportedNode.type).toBe('element');
      expect(exportedNode.plugin).toBe('test_plugin');
      expect(exportedNode.label).toBe('Test Node');
      expect(exportedNode.configuration).toEqual({ key: 'value' });
      expect(exportedNode.locked).toBe(true);
      expect(exportedNode.annotation).toBe('Test annotation');
    });

    it('should include edge properties in export', () => {
      const result = exportModelData([], mockEdges);
      const exportedEdge = result.edges[0];

      expect(exportedEdge.id).toBe('edge1');
      expect(exportedEdge.source).toBe('node1');
      expect(exportedEdge.target).toBe('node2');
      expect(exportedEdge.sourceHandle).toBe('output');
      expect(exportedEdge.targetHandle).toBe('input');
      expect(exportedEdge.condition).toBe('test_condition');
      expect(exportedEdge.conditionLabel).toBe('Test Condition');
      expect(exportedEdge.conditionConfiguration).toEqual({ config: 'value' });
      expect(exportedEdge.locked).toBe(true);
      expect(exportedEdge.annotation).toBe('Edge annotation');
      expect(exportedEdge.controlOffset).toEqual({ x: 10, y: 20 });
    });

    it('should handle edges without data', () => {
      const edgesWithoutData: Edge[] = [
        { id: 'edge1', source: 'node1', target: 'node2' },
      ];

      const result = exportModelData([], edgesWithoutData);
      expect(result.edges[0].condition).toBe('');
      expect(result.edges[0].locked).toBe(false);
    });

    it('should set executable to true by default', () => {
      const result = exportModelData([], []);
      expect(result.metadata.executable).toBe(true);
    });

    it('should allow executable to be set to false', () => {
      const result = exportModelData([], [], { executable: false });
      expect(result.metadata.executable).toBe(false);
    });

    it('should filter out internal properties starting with underscore from configuration', () => {
      const nodesWithInternalProps: Node[] = [
        {
          id: 'node1',
          type: 'element',
          position: { x: 100, y: 200 },
          data: {
            label: 'Test Node',
            plugin: 'test_plugin',
            configuration: {
              validKey: 'value',
              anotherKey: 123,
              _componentLabel: 'Internal Label',
              _componentLocked: true,
              _internalState: 'something',
            },
          },
        },
      ];

      const result = exportModelData(nodesWithInternalProps, []);
      const exportedConfig = result.nodes[0].configuration;

      expect(exportedConfig.validKey).toBe('value');
      expect(exportedConfig.anotherKey).toBe(123);
      expect(exportedConfig._componentLabel).toBeUndefined();
      expect(exportedConfig._componentLocked).toBeUndefined();
      expect(exportedConfig._internalState).toBeUndefined();
    });

    it('should handle undefined configuration when filtering', () => {
      const nodesWithoutConfig: Node[] = [
        {
          id: 'node1',
          type: 'element',
          position: { x: 100, y: 200 },
          data: {
            label: 'Test Node',
            plugin: 'test_plugin',
          },
        },
      ];

      const result = exportModelData(nodesWithoutConfig, []);
      expect(result.nodes[0].configuration).toEqual({});
    });
  });

  describe('autoLayout', () => {
    it('should return null for null nodes', () => {
      expect(autoLayout(null as any, [])).toBeNull();
    });

    it('should return null for empty nodes array', () => {
      expect(autoLayout([], [])).toBeNull();
    });

    it('should handle undefined edges', () => {
      const nodes: Node[] = [
        { id: 'node1', type: 'element', position: { x: 0, y: 0 }, data: {} },
      ];

      const result = autoLayout(nodes, undefined as any);
      expect(result).toBeDefined();
    });

    it('should preserve locked nodes', () => {
      const nodes: Node[] = [
        { id: 'locked', type: 'element', position: { x: 500, y: 500 }, data: { locked: true } },
        { id: 'unlocked', type: 'element', position: { x: 0, y: 0 }, data: { locked: false } },
      ];

      const result = autoLayout(nodes, []);
      const lockedNode = result?.find((n: Node) => n.id === 'locked');
      expect(lockedNode?.position.x).toBe(500);
      expect(lockedNode?.position.y).toBe(500);
    });

    it('should layout unlocked nodes', () => {
      const nodes: Node[] = [
        { id: 'node1', type: 'start', position: { x: 0, y: 0 }, data: {} },
        { id: 'node2', type: 'element', position: { x: 0, y: 0 }, data: {} },
      ];

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

      const result = autoLayout(nodes, edges);
      expect(result).toBeDefined();
      expect(result?.length).toBe(2);
    });
  });

  describe('getFitViewport', () => {
    it('should return default viewport for empty nodes', () => {
      const result = getFitViewport([], 800, 600);
      expect(result).toEqual({ x: 0, y: 0, zoom: 1 });
    });

    it('should calculate viewport to fit nodes', () => {
      const nodes: Node[] = [
        { id: 'node1', position: { x: 0, y: 0 }, data: {} },
        { id: 'node2', position: { x: 1000, y: 800 }, data: {} },
      ];

      const result = getFitViewport(nodes, 800, 600);
      expect(result.zoom).toBeLessThanOrEqual(4);
      expect(result.zoom).toBeGreaterThan(0);
    });

    it('should respect max zoom limit', () => {
      const nodes: Node[] = [
        { id: 'node1', position: { x: 100, y: 100 }, data: {} },
      ];

      // Small nodes in large viewport should trigger max zoom
      const result = getFitViewport(nodes, 8000, 6000);
      expect(result.zoom).toBeLessThanOrEqual(4);
    });

    it('should apply padding factor', () => {
      const nodes: Node[] = [
        { id: 'node1', position: { x: 0, y: 0 }, data: {} },
        { id: 'node2', position: { x: 400, y: 300 }, data: {} },
      ];

      const withPadding = getFitViewport(nodes, 800, 600, 0.2);
      const withoutPadding = getFitViewport(nodes, 800, 600, 0);

      // With padding should have lower zoom to fit more
      expect(withPadding.zoom).toBeLessThanOrEqual(withoutPadding.zoom);
    });

    it('should internally compute node bounds for viewport calculation', () => {
      // getNodesBounds is now private, but we can verify its behavior
      // through getFitViewport by checking that spread-out nodes produce
      // a lower zoom than tightly grouped nodes
      const spreadNodes: Node[] = [
        { id: 'node1', position: { x: 0, y: 0 }, data: {} },
        { id: 'node2', position: { x: 2000, y: 1500 }, data: {} },
      ];

      const tightNodes: Node[] = [
        { id: 'node1', position: { x: 100, y: 100 }, data: {} },
        { id: 'node2', position: { x: 150, y: 150 }, data: {} },
      ];

      const spreadResult = getFitViewport(spreadNodes, 800, 600);
      const tightResult = getFitViewport(tightNodes, 800, 600);

      expect(spreadResult.zoom).toBeLessThan(tightResult.zoom);
    });
  });
});
