import {act} from '@testing-library/react';
import {
  type Connection,
  type Edge,
  type Node,
} from "@xyflow/react";
import type {HandleRule, RuleResult} from "../../../../src/pages/VisProgPage/visualProgrammingUI/HandleRuleLogic.ts";
import { NodeDisconnections } from "../../../../src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts";
import type {PhaseNodeData} from "../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx";
import useFlowStore from '../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
import { mockReactFlow } from '../../../setupFlowTests.ts';

beforeAll(() => {
  mockReactFlow();
});

// default state values for testing,
const normNode: Node = {
  id: 'norm-1',
  type: 'norm',
  position: { x: 0, y: 0 },
  data: {
    label: 'Test Norm',
    droppable: true,
    norm: 'Test',
    hasReduce: true,
  },
};

const phaseNode: Node = {
  id: 'phase-1',
  type: 'phase',
  position: { x: 100, y: 0 },
  data: {
    label: 'Phase 1',
    droppable: true,
    children: ["norm-1"],
    hasReduce: true,
  },
};

const testEdge: Edge = {
  id: 'xy-edge__1-2',
  source: 'norm-1',
  target: 'phase-1',
  sourceHandle: null,
  targetHandle: null,
}

const testStateReconnectEnd = {
  nodes: [phaseNode, normNode],
  edges: [testEdge],
}

const phaseNodeUnconnected = {
  id: 'phase-2',
  type: 'phase',
  position: { x: 100, y: 0 },
  data: {
    label: 'Phase 2',
    droppable: true,
    children: [],
    hasReduce: true,
  },
};

const testConnection: Connection = {
  source: 'norm-1',
  target: 'phase-2',
  sourceHandle: null,
  targetHandle: null,
}
const testStateOnConnect = {
  nodes: [phaseNodeUnconnected, normNode],
  edges: [],
}

describe('FlowStore Functionality', () => {
  describe('Node changes', () => {
    // currently just using a single function from the ReactFlow library,
    // so testing would mean we are testing already tested behavior.
    // if implementation gets modified tests should be added for custom behavior
  });
  describe('ReactFlow onEdgesDelete', () => {
    test('Deleted edge is reflected in removed phaseNode child', () => {
      const {onEdgesDelete} = useFlowStore.getState();

      useFlowStore.setState({
        nodes: [{
          id: 'phase-1',
          type: 'phase',
          position: { x: 100, y: 0 },
          data: {
            label: 'Phase 1',
            droppable: true,
            children: ["norm-1"],
            hasReduce: true,
          },
        },{
          id: 'norm-1',
          type: 'norm',
          position: { x: 0, y: 0 },
          data: {
            label: 'Test Norm',
            droppable: true,
            norm: 'Test',
            hasReduce: true,
          },
        }],
        edges: [], // edges is empty as onEdgesDelete is triggered after the edges are deleted
      })

      act(() => {
        onEdgesDelete([testEdge])
      });

      const outcome = useFlowStore.getState();
      expect((outcome.nodes[0].data as PhaseNodeData).children.length).toBe(0);
    })
    test('Deleted edge is reflected in phaseNode,even if normNode was already deleted and caused edge removal', () => {
      const { onEdgesDelete } = useFlowStore.getState();
      useFlowStore.setState({
        nodes: [{
          id: 'phase-1',
          type: 'phase',
          position: { x: 100, y: 0 },
          data: {
            label: 'Phase 1',
            droppable: true,
            children: ["norm-1"],
            hasReduce: true,
          },
        }],
        edges: [], // edges is empty as onEdgesDelete is triggered after the edges are deleted
      })

      act(() => {
        onEdgesDelete([testEdge]);
      })

      const outcome = useFlowStore.getState();
      expect((outcome.nodes[0].data as PhaseNodeData).children.length).toBe(0);
    })
    test('edge removal resulting from deletion of targetNode calls only the connection function for the sourceNode', () => {
      const { onEdgesDelete } = useFlowStore.getState();
      useFlowStore.setState({
        nodes: [{
          id: 'norm-1',
          type: 'norm',
          position: { x: 0, y: 0 },
          data: {
            label: 'Test Norm',
            droppable: true,
            norm: 'Test',
            hasReduce: true,
          },
        }],
        edges: [], // edges is empty as onEdgesDelete is triggered after the edges are deleted
      })

      const targetDisconnectSpy = jest.spyOn(NodeDisconnections.Targets, 'phase');
      const sourceDisconnectSpy = jest.spyOn(NodeDisconnections.Sources, 'norm');

      act(() => {
        onEdgesDelete([testEdge]);
      })

      expect(sourceDisconnectSpy).toHaveBeenCalledWith(normNode, 'phase-1');
      expect(targetDisconnectSpy).not.toHaveBeenCalled();

      sourceDisconnectSpy.mockRestore();
      targetDisconnectSpy.mockRestore();
    })
  })
  describe('Edge changes', () => {
    // currently just using a single function from the ReactFlow library,
    // so testing would mean we are testing already tested behavior.
    // if implementation gets modified tests should be added for custom behavior
  })
  describe('ReactFlow onConnect', () => {
    test('Adds connecting node to children of phaseNode', () => {
      const {onConnect} = useFlowStore.getState();
      useFlowStore.setState({
        nodes: testStateOnConnect.nodes,
        edges: testStateOnConnect.edges
      })

      act(() => {
        onConnect(testConnection);
      })

      const outcome = useFlowStore.getState();

      // phaseNode adds the normNode to its children
      expect((outcome.nodes[0].data as PhaseNodeData).children).toEqual(['norm-1']);

    })
    test('adds an edge when onConnect is triggered', () => {
      const {onConnect} = useFlowStore.getState();

      act(() => {
        onConnect({
          source: 'A',
          target: 'B',
          sourceHandle: null,
          targetHandle: null,
        });
      });

      const updatedEdges = useFlowStore.getState().edges;
      expect(updatedEdges).toHaveLength(1);
      expect(updatedEdges[0]).toMatchObject({
        source: 'A',
        target: 'B',
      });
    });
  });
  describe('ReactFlow onReconnect', () => {
    test('PhaseNodes correctly change their children', () => {
      const {onReconnect} = useFlowStore.getState();
      useFlowStore.setState({
        nodes: [{
          id: 'phase-1',
          type: 'phase',
          position: { x: 100, y: 0 },
          data: {
            label: 'Phase 1',
            droppable: true,
            children: ["norm-1"],
            hasReduce: true,
          },
        },{
          id: 'phase-2',
          type: 'phase',
          position: { x: 100, y: 0 },
          data: {
            label: 'Phase 2',
            droppable: true,
            children: [],
            hasReduce: true,
          },
        },{
          id: 'norm-1',
          type: 'norm',
          position: { x: 0, y: 0 },
          data: {
            label: 'Test Norm',
            droppable: true,
            norm: 'Test',
            hasReduce: true,
          },
        }],
        edges: [testEdge],
      })

      act(() => {
        onReconnect(testEdge, testConnection);
      })

      const outcome = useFlowStore.getState();

      // phaseNodes lose and gain children when norm node's connection is changed from phaseNode to PhaseNodeUnconnected
      expect((outcome.nodes[1].data as PhaseNodeData).children).toEqual(['norm-1']);
      expect((outcome.nodes[0].data as PhaseNodeData).children).toEqual([]);
    })
    test('reconnects an existing edge when onReconnect is triggered', () => {
      const {onReconnect} = useFlowStore.getState();
      const oldEdge = {
        id: 'xy-edge__A-B',
        source: 'A',
        target: 'B'
      };
      const newConnection = {
        source: 'A',
        target: 'C',
        sourceHandle: null,
        targetHandle: null,
      };
      act(() => {
        useFlowStore.setState({
          edges: [oldEdge]
        });
        onReconnect(oldEdge, newConnection);
      });

      const updatedEdges = useFlowStore.getState().edges;
      expect(updatedEdges).toHaveLength(1);
      expect(updatedEdges[0]).toMatchObject({
        id: 'xy-edge__A-C',
        source: 'A',
        target: 'C',
      });
    });
  });
  describe('ReactFlow onReconnectStart', () => {
    test('does correct setup for edge reconnection sequences', () => {
      const {onReconnectStart} = useFlowStore.getState();

      act(() => {
        onReconnectStart();
      });

      const updatedState = useFlowStore.getState().edgeReconnectSuccessful;
      expect(updatedState).toEqual(false);
    });
  });
  describe('ReactFlow onReconnectEnd', () => {
    // prepares the state to have an edge in the edge array
    beforeEach(() => {
      useFlowStore.setState({edges: [
          {
            id: 'xy-edge__A-B',
            source: 'A',
            target: 'B'
          }
        ]}
      );
    });



    test('successfully removes edge if no successful reconnect occurred', () => {
      const {onReconnectEnd} = useFlowStore.getState();
      useFlowStore.setState({
        edgeReconnectSuccessful: false,
        edges: testStateReconnectEnd.edges,
        nodes: testStateReconnectEnd.nodes
      });

      act(() => {
        onReconnectEnd(null, testEdge);
      });

      const updatedState = useFlowStore.getState();
      expect(updatedState.edgeReconnectSuccessful).toBe(true);
      expect(updatedState.edges).toHaveLength(0);
      expect(updatedState.nodes[0].data.children).toEqual([]);
    });

    test('does not remove reconnecting edge if successful reconnect occurred', () => {
      const {onReconnectEnd} = useFlowStore.getState();
      useFlowStore.setState({
        edgeReconnectSuccessful: true,
        edges: [testEdge],
        nodes: [{
          id: 'phase-1',
          type: 'phase',
          position: { x: 100, y: 0 },
          data: {
            label: 'Phase 1',
            droppable: true,
            children: ["norm-1"],
            hasReduce: true,
          },
        },{
          id: 'norm-1',
          type: 'norm',
          position: { x: 0, y: 0 },
          data: {
            label: 'Test Norm',
            droppable: true,
            norm: 'Test',
            hasReduce: true,
          },
        }]
      });

      act(() => {
        onReconnectEnd(null, testEdge);
      });

      const updatedState = useFlowStore.getState();
      expect(updatedState.edgeReconnectSuccessful).toBe(true);
      expect(updatedState.edges).toHaveLength(1);
      expect(updatedState.edges).toMatchObject([testEdge]);
      expect(updatedState.nodes[0].data.children).toEqual(["norm-1"]);
    });
  });
  describe('ReactFlow deleteNode', () => {
    // test deleting A and B, so we make sure the connecting edge gets deleted regardless of
    test.each([['A','B'],['B','A']])('deletes a node and its connected edges', (nodeId, undeletedNodeId) => {
      const {deleteNode} = useFlowStore.getState();
      useFlowStore.setState({
        nodes: [
          {
            id: 'A',
            type: 'default',
            position: {x: 0, y: 0},
            data: {label: 'A'}
          },
          {
            id: 'B',
            type: 'default',
            position: {x: 0, y: 300},
            data: {label: 'A'}
          }],
        edges: [
          {
            id: 'xy-edge__A-B',
            source: 'A',
            target: 'B'
          }]
      });


      act(()=> {
        deleteNode(nodeId);
      });

      const updatedState = useFlowStore.getState();
      expect(updatedState.edges).toHaveLength(0);
      expect(updatedState.nodes).toHaveLength(1);
      expect(updatedState.nodes[0].id).toBe(undeletedNodeId);
    });
  });
  describe('ReactFlow setNodes', () => {
    test('sets nodes to the provided list of nodes', () => {
      const {setNodes} = useFlowStore.getState();

      act(() => {
        setNodes([
          {
            id: 'start',
            type: 'start',
            position: {x: 0, y: 0},
            data: {label: 'start'}
          },
          {
            id: 'end',
            type: 'end',
            position: {x: 0, y: 300},
            data: {label: 'End'}
          }
        ]);
      });

      const updatedNodes = useFlowStore.getState().nodes;
      expect(updatedNodes).toHaveLength(2);
      expect(updatedNodes[0]).toMatchObject({
        id: 'start',
        type: 'start',
        position: {x: 0, y: 0},
        data: {label: 'start'}
      });
      expect(updatedNodes[1]).toMatchObject({
        id: 'end',
        type: 'end',
        position: {x: 0, y: 300},
        data: {label: 'End'}
      });
    });
  });
  describe('ReactFlow setEdges', () => {
   test('sets edges to the provided list of edges', () => {
      const {setEdges} = useFlowStore.getState();

      act(() => {
        setEdges([
          {
            id: 'start-end',
            source: 'start',
            target: 'end'
          }
        ]);
      });

      const updatedEdges = useFlowStore.getState().edges;
      expect(updatedEdges).toHaveLength(1);
      expect(updatedEdges[0]).toMatchObject({
        id: 'start-end',
        source: 'start',
        target: 'end'
      });
    });
  });
  describe('ReactFlow updateNodeData', () => {
    test.each([
      {
        state: {
          name: 'updateName',
          nodes: [{
            id: 'phase-1',
            type: 'phase',
            position: {x: 0, y: 300},
            data: {label: 'name', number: '2'}
          }]
        },
        input: {
          id: 'phase-1',
          changedData: {label: 'new name'}
        },
        expected: {
          node: {
            id: 'phase-1',
            type: 'phase',
            position: {x: 0, y: 300},
            data: {label: 'new name', number: '2'}
          }
        }
      },
      {
        state: {
          name: 'updateNumber',
          nodes: [{
            id: 'phase-1',
            type: 'phase',
            position: {x: 0, y: 300},
            data: {label: 'name', number: '2'}
          }]
        },
        input: {
          id: 'phase-1',
          changedData: {number: '3'}
        },
        expected: {
          node: {
            id: 'phase-1',
            type: 'phase',
            position: {x: 0, y: 300},
            data: {label: 'name', number: '3'}
          }
        }
      },
      {
        state: {
          name: 'updateNameAndNumber',
          nodes: [{
            id: 'phase-1',
            type: 'phase',
            position: {x: 0, y: 300},
            data: {label: 'name', number: '2'}
          }]
        },
        input: {
          id: 'phase-1',
          changedData: {label: 'new name', number: '3'}
        },
        expected: {
          node: {
            id: 'phase-1',
            type: 'phase',
            position: {x: 0, y: 300},
            data: {label: 'new name', number: '3'}
          }
        }
      },
      {
        state: {
          name: 'AddNewEntry',
          nodes: [{
            id: 'phase-1',
            type: 'phase',
            position: {x: 0, y: 300},
            data: {label: 'name', number: '2'}
          }]
        },
        input: {
          id: 'phase-1',
          changedData: {newEntry: 20}
        },
        expected: {
          node: {
            id: 'phase-1',
            type: 'phase',
            position: {x: 0, y: 300},
            data: {label: 'name', number: '2', newEntry: 20}
          }
        }
      },
      {
        state: {
          name: 'AddNewEntryAndUpdateOneValue_UnorderedInput',
          nodes: [{
            id: 'phase-1',
            type: 'phase',
            position: {x: 0, y: 300},
            data: {label: 'name', number: '2'}
          }]
        },
        input: {
          id: 'phase-1',
          changedData: {newEntry: 20, number: '3'}
        },
        expected: {
          node: {
            id: 'phase-1',
            type: 'phase',
            position: {x: 0, y: 300},
            data: {label: 'name', number: '3', newEntry: 20}
          }
        }
      }
    ])(`tests state: $state.name`, ({state, input,expected}) => {
      useFlowStore.setState({ nodes: state.nodes })
      const {updateNodeData} = useFlowStore.getState();
      act(() => {
        updateNodeData(input.id, input.changedData);
      })
      const updatedState = useFlowStore.getState();
      expect(updatedState.nodes).toHaveLength(1);
      expect(updatedState.nodes[0]).toMatchObject(expected.node);
    })
  describe('Handle Rule Registry', () => {
    it('should register and retrieve rules', () => {
      const mockRules: HandleRule[] = [() => ({ isSatisfied: true } as RuleResult)];

      useFlowStore.getState().registerRules('node1', 'handleA', mockRules);
      const rules = useFlowStore.getState().getTargetRules('node1', 'handleA');

      expect(rules).toEqual(mockRules);
    });

    it('should warn and return empty array if rules are missing', () => {
      const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});

      const rules = useFlowStore.getState().getTargetRules('missingNode', 'missingHandle');
      expect(rules).toEqual([]);
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No rules were registered'));

      consoleSpy.mockRestore();
    });

    it('should unregister a specific handle rule', () => {
      const mockRules: HandleRule[] = [() => ({ isSatisfied: true } as RuleResult)];
      useFlowStore.getState().registerRules('node1', 'handleA', mockRules);

      useFlowStore.getState().unregisterHandleRules('node1', 'handleA');
      const rules = useFlowStore.getState().getTargetRules('node1', 'handleA');

      expect(rules).toEqual([]);
    });

    it('should unregister all rules for a node', () => {
      const mockRules: HandleRule[] = [() => ({ isSatisfied: true } as RuleResult)];
      useFlowStore.getState().registerRules('node1', 'handleA', mockRules);
      useFlowStore.getState().registerRules('node1', 'handleB', mockRules);
      useFlowStore.getState().registerRules('node2', 'handleC', mockRules);

      useFlowStore.getState().unregisterNodeRules('node1');

      expect(useFlowStore.getState().getTargetRules('node1', 'handleA')).toEqual([]);
      expect(useFlowStore.getState().getTargetRules('node1', 'handleB')).toEqual([]);
      expect(useFlowStore.getState().getTargetRules('node2', 'handleC')).toEqual(mockRules);
    });
  });
})
});
