// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { describe, beforeEach } from '@jest/globals';
import { screen } from '@testing-library/react';
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
import type { XYPosition } from '@xyflow/react';
import { NodeTypes, NodeDefaults, NodeConnections, NodeReduces, NodesInPhase } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/NodeRegistry';
import '@testing-library/jest-dom'
import { createElement } from 'react';
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';


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

  function createNode(id: string, type: string, position: XYPosition, data: Record<string, unknown>, deletable? : boolean) {
    const defaultData = NodeDefaults[type as keyof typeof NodeDefaults]

    return {
      id: id,
      type: type,
      position: position,
      data: {...defaultData, ...data},
      deletable: deletable
    }
  }


    /**
   * Reduces the graph into its phases' information and recursively calls their reducing function
   */ 
  function graphReducer() {
    const { nodes } = useFlowStore.getState();
    return nodes
      .filter((n) => n.type == 'phase')
      .map((n) => {
        const reducer = NodeReduces['phase'];
        return reducer(n, nodes)
      }); 
  }

  function getAllTypes() {
    return Object.entries(NodeTypes).map(([t])=>t)
  }
    
  describe('Rendering', () => {
    test.each(getAllTypes())('it should render %s node with the default data', (nodeType) => {
      const lengthBefore = screen.getAllByText(/.*/).length;

      const newNode = createNode(nodeType + "1", nodeType, {x: 200, y:200}, {});

      const found = Object.entries(NodeTypes).find(([t]) => t === nodeType);
      const uiElement = found ? found[1] : null;

      expect(uiElement).not.toBeNull();
      const props = {
        id: newNode.id,
        type: newNode.type as string,
        data: newNode.data as any,
        selected: false,
        isConnectable: true,
        zIndex: 0,
        dragging: false,
        selectable: true,
        deletable: true,
        draggable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      };

      renderWithProviders(createElement(uiElement as React.ComponentType<any>, props));
      const lengthAfter = screen.getAllByText(/.*/).length;

      expect(lengthBefore + 1 === lengthAfter);
    });
  
  });


  describe('Connecting', () => {
    test.each(getAllTypes())('it should call the connect function when %s node is connected', (nodeType) => {
      // Create two nodes - one of the current type and one to connect to
      const sourceNode = createNode('source-1', nodeType, {x: 100, y: 100}, {});
      const targetNode = createNode('target-1', 'end', {x: 300, y: 100}, {});
      
      // Add nodes to store
      useFlowStore.setState({ nodes: [sourceNode, targetNode] });

      // Spy on the connect functions
      const sourceConnectSpy = jest.spyOn(NodeConnections.Sources, nodeType as keyof typeof NodeConnections.Sources);
      const targetConnectSpy = jest.spyOn(NodeConnections.Targets, 'end');

      // Simulate connection
      useFlowStore.getState().onConnect({
        source: 'source-1',
        target: 'target-1',
        sourceHandle: null,
        targetHandle: null,
      });

      // Verify the connect functions were called
      expect(sourceConnectSpy).toHaveBeenCalledWith(sourceNode, targetNode.id);
      expect(targetConnectSpy).toHaveBeenCalledWith(targetNode, sourceNode.id);

      sourceConnectSpy.mockRestore();
      targetConnectSpy.mockRestore();
    }); 
  });

  describe('Disconnecting', () => {
    test.each(getAllTypes())('it should remove the correct data when something is disconnected on a %s node.', (nodeType) => {
      // Create two nodes - one of the current type and one to connect to
      const sourceNode = createNode('source-1', nodeType, {x: 100, y: 100}, {});
      const targetNode = createNode('target-1', 'basic_belief', {x: 300, y: 100}, {});
      
      // Add nodes to store
      useFlowStore.setState({ nodes: [sourceNode, targetNode] });

      // Spy on the connect functions
      const sourceConnectSpy = jest.spyOn(NodeConnections.Sources, nodeType as keyof typeof NodeConnections.Sources);
      const targetConnectSpy = jest.spyOn(NodeConnections.Targets, 'basic_belief');

      // Simulate connection
      useFlowStore.getState().onConnect({
        source: 'source-1', 
        target: 'target-1',
        sourceHandle: null,
        targetHandle: null,
      });

    
      // Verify the connect functions were called
      expect(sourceConnectSpy).toHaveBeenCalledWith(sourceNode, targetNode.id);
      expect(targetConnectSpy).toHaveBeenCalledWith(targetNode, sourceNode.id);

      // Find this connection, and delete it
      const edge = useFlowStore.getState().edges[0];
      useFlowStore.getState().onEdgesDelete([edge]);

      // Find the nodes in the flow
      const newSourceNode = useFlowStore.getState().nodes.find((node) => node.id == "source-1");
      const newTargetNode = useFlowStore.getState().nodes.find((node) => node.id == "target-1");

      // Expect them to be the same after deleting the edges
      expect(newSourceNode).toBe(sourceNode);
      expect(newTargetNode).toBe(targetNode);

      // Restore our spies
      sourceConnectSpy.mockRestore();
      targetConnectSpy.mockRestore();
    }); 
  });

  describe('Reducing', () => {
    test.each(getAllTypes())('it should correctly call/ not call the reduce function when %s node is in a phase', (nodeType) => {
      // Create a phase node and a node of the current type
      const phaseNode = createNode('phase-1', 'phase', {x: 200, y: 100}, { label: 'Test Phase', children: [] });
      const testNode = createNode('node-1', nodeType, {x: 100, y: 100}, {});
      
      // Add the test node as a child of the phase
      (phaseNode.data as any).children.push(testNode.id);
      
      // Add nodes to store
      useFlowStore.setState({ nodes: [phaseNode, testNode] });

      // Spy on the reduce functions
      const phaseReduceSpy = jest.spyOn(NodeReduces, 'phase');
      const nodeReduceSpy = jest.spyOn(NodeReduces, nodeType as keyof typeof NodeReduces);

      // Simulate reducing - using the graphReducer
      const result = graphReducer();

      // Verify the reduce functions were called
      expect(phaseReduceSpy).toHaveBeenCalledWith(phaseNode, [phaseNode, testNode]);
      // Check if this node type is in NodesInPhase and returns false
      const nodesInPhaseFunc = NodesInPhase[nodeType as keyof typeof NodesInPhase];
      if (nodesInPhaseFunc && !nodesInPhaseFunc() && nodeType !== 'phase') {
        // Node is NOT in phase, so it should NOT be called
        expect(nodeReduceSpy).not.toHaveBeenCalled();
      } else {
        // Node IS in phase, so it SHOULD be called
        expect(nodeReduceSpy).toHaveBeenCalled();
      }

      // Verify the correct structure is present using NodesInPhase
      expect(result).toHaveLength(nodeType !== 'phase' ? 1 : 2);
      expect(result[0]).toHaveProperty('id', 'phase-1');
      expect(result[0]).toHaveProperty('name', 'Test Phase');

      // Restore mocks
      phaseReduceSpy.mockRestore();
      nodeReduceSpy.mockRestore();
    }); 
  });
});