// 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, it, beforeEach } from '@jest/globals';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
import NormNode, {
  NormReduce,
  type NormNodeData,
  NormConnectionSource, NormConnectionTarget
} from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode';
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
import type { Node } from '@xyflow/react';
import '@testing-library/jest-dom'
import { NormNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.default.ts';
import { BasicBeliefNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.default.ts';
import BasicBeliefNode, { BasicBeliefConnectionSource } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx';

describe('NormNode', () => {
  let user: ReturnType<typeof userEvent.setup>;

  beforeEach(() => {
    user = userEvent.setup();
  });

  describe('Rendering', () => {
    it('should render the norm node with default data', () => {
      const mockNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {...JSON.parse(JSON.stringify(NormNodeDefaults))},
      };

      renderWithProviders(
        <NormNode
          id={mockNode.id}
          type={mockNode.type as string}
          data={mockNode.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
      );

      expect(screen.getByPlaceholderText('Pepper should ...')).toBeInTheDocument();
    });

    it('should render with pre-populated norm text', () => {
      const mockNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Test Norm',
          droppable: true,
          norm: 'Be respectful to humans',
          hasReduce: true,
        },
      };

      renderWithProviders(
        <NormNode
          id={mockNode.id}
          type={mockNode.type as string}
          data={mockNode.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
      );

      const input = screen.getByDisplayValue('Be respectful to humans');
      expect(input).toBeInTheDocument();
    });

    it('should render with selected state', () => {
      const mockNode: Node<NormNodeData> = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Test Norm',
          droppable: true,
          conditions: [],
          norm: '',
          hasReduce: true,
          critical: false
        },
      };

      renderWithProviders( 
        <NormNode
          id={mockNode.id}
          type={mockNode.type as string}
          data={mockNode.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
      );

      const norm = screen.getByText("Norm :")
      expect(norm).toBeInTheDocument();
    });

    it('should render with dragging state', () => {
      const mockNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Test Norm',
          droppable: true,
          norm: 'Dragged norm',
          hasReduce: true,
        },
      };

      renderWithProviders(
        <NormNode
          id={mockNode.id}
          type={mockNode.type as string}
          data={mockNode.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
      );

      const input = screen.getByDisplayValue('Dragged norm');
      expect(input).toBeInTheDocument();
    });
  });

  describe('User Interactions', () => {
    it('should update norm text when user types in the input field', async () => {
      const mockNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Test Norm',
          droppable: true,
          norm: '',
          hasReduce: true,
        },
      };

      useFlowStore.setState({
        nodes: [mockNode],
        edges: [],
      });

      renderWithProviders(
        <NormNode
          id={mockNode.id}
          type={mockNode.type as string}
          data={mockNode.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
      );

      const input = screen.getByPlaceholderText('Pepper should ...');
      await user.type(input, 'Be polite to guests{enter}');

      await waitFor(() => {
        const state = useFlowStore.getState();
        const updatedNode = state.nodes.find(n => n.id === 'norm-1');
        expect(updatedNode?.data.norm).toBe('Be polite to guests');
      });
    });

    it('should handle clearing the norm text', async () => {
      const mockNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Test Norm',
          droppable: true,
          norm: 'Initial norm text',
          hasReduce: true,
        },
      };

      useFlowStore.setState({
        nodes: [mockNode],
        edges: [],
      });

      renderWithProviders(
        <NormNode
          id={mockNode.id}
          type={mockNode.type as string}
          data={mockNode.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
      );

      const input = screen.getByDisplayValue('Initial norm text') as HTMLInputElement;

      // clearing the norm text is the same as just deleting all characters one by one
      // TODO: FIGURE OUT A MORE EFFICIENT WAY, I"M SORRY, user.clear() just doesn't work:/
      for (let a = 0; a < 'Initial norm text'.length; a++){
        await user.type(input, '{backspace}')
      }
      await user.type(input,'{enter}')

      await waitFor(() => {
        const state = useFlowStore.getState();
        const updatedNode = state.nodes.find(n => n.id === 'norm-1');
        expect(updatedNode?.data.norm).toBe('');
      });
    });

    it('should update norm text multiple times', async () => {
      const mockNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Test Norm',
          droppable: true,
          norm: '',
          hasReduce: true,
        },
      };

      useFlowStore.setState({
        nodes: [mockNode],
        edges: [],
      });

      renderWithProviders(
        <NormNode
          id={mockNode.id}
          type={mockNode.type as string}
          data={mockNode.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
      );

      const input = screen.getByPlaceholderText('Pepper should ...');
      await user.type(input, 'First norm{enter}');
      await waitFor(() => {
        expect(useFlowStore.getState().nodes[0].data.norm).toBe('First norm');
      });


      // TODO: FIGURE OUT A MORE EFFICIENT WAY, I"M SORRY, user.clear() just doesn't work:/
      for (let a = 0; a < 'First norm'.length; a++){
        await user.type(input, '{backspace}')
      }

      await user.type(input, 'Second norm{enter}');
      await waitFor(() => {
        expect(useFlowStore.getState().nodes[0].data.norm).toBe('Second norm');
      });
    });

    it('should handle special characters in norm text', async () => {
      const mockNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Test Norm',
          droppable: true,
          norm: '',
          hasReduce: true,
        },
      };

      useFlowStore.setState({
        nodes: [mockNode],
        edges: [],
      });

      renderWithProviders(
        <NormNode
          id={mockNode.id}
          type={mockNode.type as string}
          data={mockNode.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
      );

      const input = screen.getByPlaceholderText('Pepper should ...');
      await user.type(input, "Don't harm & be nice!{enter}" );

      await waitFor(() => {
        expect(useFlowStore.getState().nodes[0].data.norm).toBe("Don't harm & be nice!");
      });
    });

    it('should handle long norm text', async () => {
      const longText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
      const mockNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Test Norm',
          droppable: true,
          norm: '',
          hasReduce: true,
        },
      };

      useFlowStore.setState({
        nodes: [mockNode],
        edges: [],
      });

      renderWithProviders(
        <NormNode
          id={mockNode.id}
          type={mockNode.type as string}
          data={mockNode.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
      );

      const input = screen.getByPlaceholderText('Pepper should ...');
      await user.type(input, longText);
      await user.type(input, "{enter}")

      await waitFor(() => {
        expect(useFlowStore.getState().nodes[0].data.norm).toBe(longText);
      });
    });
  });

  describe('NormReduce Function', () => {
    it('should reduce a norm node to its essential data', () => {

      const condition: Node = {
        id: "belief-1",
        type: 'basic_belief',
        position: {x: 10, y: 10},
        data: {
          ...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults))
        }
      }
      
      const normNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Safety Norm',
          droppable: true,
          norm: 'Never harm humans',
          hasReduce: true,
          condition: "belief-1"
        },
      };

      const allNodes: Node[] = [normNode, condition];
      const result = NormReduce(normNode, allNodes);
      expect(result).toEqual({
        id: 'norm-1',
        label: 'Safety Norm',
        norm: 'Never harm humans',
        critical: false,
        condition: {
          id: "belief-1", 
          keyword: ""
        },
      });
    });

    it('should reduce multiple norm nodes independently', () => {
      const norm1: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Norm 1',
          droppable: true,
          norm: 'Be helpful',
          hasReduce: true,
        },
      };

      const norm2: Node = {
        id: 'norm-2',
        type: 'norm',
        position: { x: 100, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Norm 2',
          droppable: true,
          norm: 'Be honest',
          hasReduce: true,
        },
      };

      const allNodes: Node[] = [norm1, norm2];

      const result1 = NormReduce(norm1, allNodes);
      const result2 = NormReduce(norm2, allNodes);

      expect(result1.id).toBe('norm-1');
      expect(result1.norm).toBe('Be helpful');
      expect(result2.id).toBe('norm-2');
      expect(result2.norm).toBe('Be honest');
    });

    it('should handle empty norm text', () => {
      const normNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Empty Norm',
          droppable: true,
          norm: '',
          hasReduce: true,
        },
      };

      const result = NormReduce(normNode, [normNode]);

      expect(result.norm).toBe('');
      expect(result.id).toBe('norm-1');
    });

    it('should preserve node label in reduction', () => {
      const normNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Custom Label',
          droppable: false,
          norm: 'Test norm',
          hasReduce: false,
        },
      };

      const result = NormReduce(normNode, [normNode]);

      expect(result.label).toBe('Custom Label');
    });
  });

  describe('NormConnects Function', () => {
    it('should handle connection without errors', () => {
      const normNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Test Norm',
          droppable: true,
          norm: 'Test',
          hasReduce: true,
        },
      };

      const phaseNode: Node = {
        id: 'phase-1',
        type: 'phase',
        position: { x: 100, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Phase 1',
          droppable: true,
          children: [],
          hasReduce: true,
        },
      };

      expect(() => {
        NormConnectionSource(normNode, phaseNode.id);
      }).not.toThrow();
    });

    it('should handle connection when norm is target', () => {
      const normNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Test Norm',
          droppable: true,
          norm: 'Test',
          hasReduce: true,
        },
      };

      const phaseNode: Node = {
        id: 'phase-1',
        type: 'phase',
        position: { x: 100, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Phase 1',
          droppable: true,
          children: [],
          hasReduce: true,
        },
      };

      expect(() => {
        NormConnectionTarget(normNode, phaseNode.id);
      }).not.toThrow();
    });

    it('should handle self-connection', () => {
      const normNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...NormNodeDefaults,
          label: 'Test Norm',
          droppable: true,
          norm: 'Test',
          hasReduce: true,
        },
      };

      expect(() => {
        NormConnectionTarget(normNode, normNode.id);
        NormConnectionSource(normNode, normNode.id);
      }).not.toThrow();
    });
  });

  describe('Integration with Store', () => {
    it('should properly update the store when editing norm text', async () => {
      const mockNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Test Norm',
          droppable: true,
          norm: '',
          hasReduce: true,
        },
      };

      useFlowStore.setState({
        nodes: [mockNode],
        edges: [],
      });

      renderWithProviders(
        <NormNode
          id={mockNode.id}
          type={mockNode.type as string}
          data={mockNode.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
      );

      const input = screen.getByPlaceholderText('Pepper should ...');

      // TODO: FIGURE OUT A MORE EFFICIENT WAY, I"M SORRY, user.clear() just doesn't work:/
      for (let a = 0; a < 20; a++){
        await user.type(input, '{backspace}')
      }
      await user.type(input, 'New norm value{enter}');

      await waitFor(() => {
        const state = useFlowStore.getState();
        expect(state.nodes).toHaveLength(1);
        expect(state.nodes[0].id).toBe('norm-1');
        expect(state.nodes[0].data.norm).toBe('New norm value');
      });
    });

    it('should properly update the store when editing critical checkbox', async () => {
      const mockNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Test Norm',
          droppable: true,
          norm: '',
          hasReduce: true,
          critical: false,
        },
      };

      useFlowStore.setState({
        nodes: [mockNode],
        edges: [],
      });

      renderWithProviders(
        <NormNode
          id={mockNode.id}
          type={mockNode.type as string}
          data={mockNode.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
      );

      await waitFor(() => {
        const state = useFlowStore.getState();
        expect(state.nodes).toHaveLength(1);
        expect(state.nodes[0].id).toBe('norm-1');
        expect(state.nodes[0].data.norm).toBe('');

      });
    });

    it('should not affect other nodes when updating one norm node', async () => {
      const norm1: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Norm 1',
          droppable: true,
          norm: 'Original norm 1',
          hasReduce: true,
        },
      };

      const norm2: Node = {
        id: 'norm-2',
        type: 'norm',
        position: { x: 100, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Norm 2',
          droppable: true,
          norm: 'Original norm 2',
          hasReduce: true,
        },
      };

      useFlowStore.setState({
        nodes: [norm1, norm2],
        edges: [],
      });

      renderWithProviders(
        <NormNode
          id={norm1.id}
          type={norm1.type as string}
          data={norm1.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
      );

      const input = screen.getByDisplayValue('Original norm 1') as HTMLInputElement;


      // TODO: FIGURE OUT A MORE EFFICIENT WAY, I"M SORRY, user.clear() just doesn't work:/
      for (let a = 0; a < 20; a++){
        await user.type(input, '{backspace}')
      }
      await user.type(input, 'Updated norm 1{enter}');

      await waitFor(() => {
        const state = useFlowStore.getState();
        const updatedNorm1 = state.nodes.find(n => n.id === 'norm-1');
        const unchangedNorm2 = state.nodes.find(n => n.id === 'norm-2');

        expect(updatedNorm1?.data.norm).toBe('Updated norm 1');
        expect(unchangedNorm2?.data.norm).toBe('Original norm 2');
      });
    });

    it('should maintain data consistency with multiple rapid updates', async () => {
      const mockNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...NormNodeDefaults,
          label: 'Test Norm',
          droppable: true,
          norm: 'haa haa fuyaaah - link',
          hasReduce: true,
        },
      };

      useFlowStore.setState({
        nodes: [mockNode],
        edges: [],
      });

      renderWithProviders(
        <NormNode
          id={mockNode.id}
          type={mockNode.type as string}
          data={mockNode.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
      );

      const input = screen.getByPlaceholderText('Pepper should ...');
      expect(input).toBeDefined()

      await user.type(input, 'a{enter}');
      await waitFor(() => {
        expect(useFlowStore.getState().nodes[0].data.norm).toBe('haa haa fuyaaah - linka');
      });

      await user.type(input, 'b{enter}');
      await waitFor(() => {
        expect(useFlowStore.getState().nodes[0].data.norm).toBe('haa haa fuyaaah - linkab');
      });

      await user.type(input, 'c{enter}');
      await waitFor(() => {
        expect(useFlowStore.getState().nodes[0].data.norm).toBe('haa haa fuyaaah - linkabc');
      });
    });
  });

  describe('Integration beliefs', () => { 
    it('should update visually when adding beliefs', async () => {
      // Setup state
      const mockNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Test Norm',
          droppable: true,
          norm: 'haa haa fuyaaah - link',
          hasReduce: true,
        }
      };

      const mockBelief: Node = {
        id: 'basic_belief-1',
        type: 'basic_belief',
        position: {x:100, y:100},
        data: {
          ...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults))
        }
      };

      useFlowStore.setState({
        nodes: [mockNode, mockBelief],
        edges: [],
      });
       
      // Simulate connecting
      NormConnectionTarget(mockNode, mockBelief.id);
      BasicBeliefConnectionSource(mockBelief, mockNode.id)
      
      renderWithProviders(
        <div>
        <NormNode
          id={mockNode.id}
          type={mockNode.type as string}
          data={mockNode.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
        <BasicBeliefNode
          id={mockBelief.id}
          type={mockBelief.type as string}
          data={mockBelief.data as any}
          selected={false}
          isConnectable={true}
          zIndex={0}
          dragging={false}
          selectable={true}
          deletable={true}
          draggable={true}
          positionAbsoluteX={0}
          positionAbsoluteY={0}
        />
        </div>
      ); 

      await waitFor(() => {
        expect(screen.getByTestId('norm-condition-information')).toBeInTheDocument();
      });


    });
  
  it('should update the data when adding beliefs', async () => {
      // Setup state
      const mockNode: Node = {
        id: 'norm-1',
        type: 'norm',
        position: { x: 0, y: 0 },
        data: {
          ...JSON.parse(JSON.stringify(NormNodeDefaults)),
          label: 'Test Norm',
          droppable: true,
          norm: 'haa haa fuyaaah - link',
          hasReduce: true,
        }
      };

      const mockBelief1: Node = {
        id: 'basic_belief-1',
        type: 'basic_belief',
        position: {x:100, y:100},
        data: {
          ...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults))
        }
      };

      useFlowStore.setState({
        nodes: [mockNode, mockBelief1],
        edges: [],
      });
       
      // Simulate connecting
      useFlowStore.getState().onConnect({
        source: 'basic_belief-1',
        target: 'norm-1',
        sourceHandle: null,
        targetHandle: null,
      });
   
    
      const state = useFlowStore.getState();
      const updatedNorm = state.nodes.find(n => n.id === 'norm-1');
      expect(updatedNorm?.data.condition).toEqual("basic_belief-1");
    });
  });
});