// 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 type { Node, Edge, Connection } from '@xyflow/react'
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
import type {PhaseNode, PhaseNodeData} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode";
import {act, getByTestId, render} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import VisProgPage from '../../../../../src/pages/VisProgPage/VisProg';
import {mockReactFlow} from "../../../../setupFlowTests.ts";


class ResizeObserver {
  observe() {}
  unobserve() {}
  disconnect() {}
}
window.ResizeObserver = ResizeObserver;

jest.mock('@neodrag/react', () => ({
  useDraggable: (ref: React.RefObject<HTMLElement>, options: any) => {
    // We access the real useEffect from React to attach a listener
    // This bridges the gap between the test's userEvent and the component's logic
    const { useEffect } = jest.requireActual('react');
    
    useEffect(() => {
      const element = ref.current;
      if (!element) return;

      // When the test fires a "pointerup" (end of click/drag), 
      // we manually trigger the library's onDragEnd callback.
      const handlePointerUp = (e: PointerEvent) => {
        if (options.onDragEnd) {
          options.onDragEnd({ event: e });
        }
      };

      element.addEventListener('pointerup', handlePointerUp as EventListener);
      return () => {
        element.removeEventListener('pointerup', handlePointerUp as EventListener);
      };
    }, [ref, options]);
  },
}));

describe('PhaseNode', () => {
  it('each created phase gets its own children array, not the same reference ', async () => {
    const user = userEvent.setup();

    const { container } = render(<VisProgPage />);

    // --- Mock ReactFlow bounding box ---
    // Your DndToolbar checks these values:
    const flowEl = container.querySelector('.react-flow');
    jest.spyOn(flowEl!, 'getBoundingClientRect').mockReturnValue({
      left: 0,
      right: 800,
      top: 0,
      bottom: 600,
      width: 800,
      height: 600,
      x: 0,
      y: 0,
      toJSON: () => {},
    });
    
    // Find the draggable norm node in the toolbar
    const phaseButton = getByTestId(container, 'draggable-phase')
    
    // Simulate dropping phase down in graph (twice)
    for (let i = 0; i < 2; i++) {
    await user.pointer([
      // touch the screen at element1
      {keys: '[TouchA>]', target: phaseButton},
      // move the touch pointer to element2
      {pointerName: 'TouchA', coords: {x: 300, y: 250}},
      // release the touch pointer at the last position (element2)
      {keys: '[/TouchA]'},
    ]);
    }

    // Find nodes
    const nodes = useFlowStore.getState().nodes;
    const phaseNodes = nodes.filter((x) => x.type === 'phase');
    const p1 = phaseNodes[0];
    const p2 = phaseNodes[1];


    // expect same value, not same reference
    expect(p1.data.children).not.toBe(p2.data.children);
    expect(p1.data.children).toEqual(p2.data.children);

    // Add nodes to children
    const p1_data = p1.data as PhaseNodeData;
    const p2_data = p2.data as PhaseNodeData;
    p1_data.children.push("norm-1");
    p2_data.children.push("norm-2");
    p2_data.children.push("goal-1");

    // check that after adding, its not the same reference, and its not the same children
    expect(p1.data.children).not.toBe(p2.data.children);
    expect(p1.data.children).not.toEqual(p2.data.children);
    
    // expect them to have the correct length.
    expect(p1_data.children.length == 1);
    expect(p2_data.children.length == 2);
  });
});

// --| Helper functions |--

function createPhaseNode(
  id: string,
  overrides: Partial<PhaseNodeData> = {},
): Node<PhaseNodeData> {
  return {
    id: id,
    type: 'phase',
    position: { x: 0, y: 0 },
    data: {
      label: 'Phase',
      droppable: true,
      children: [],
      hasReduce: true,
      nextPhaseId: null,
      isFirstPhase: false,
      ...overrides,
    },
  }
}

function createNode(id: string, type: string): Node {
  return {
    id: id,
    type: type,
    position: { x: 0, y: 0 },
    data: {},
  }
}

function connect(source: string, target: string): Connection {
  return {
    source: source,
    target: target,
    sourceHandle: null,
    targetHandle: null
  };
}

function edge(source: string, target: string): Edge {
  return {
    id: `${source}-${target}`,
    source: source,
    target: target,
  }
}

// --| Connection Tests |--

describe('PhaseNode Connection logic', () => {
  beforeAll(() => {
    mockReactFlow();
  });

  describe('PhaseConnections', () => {
    test('connecting start => phase sets isFirstPhase to true', () => {
      const phase = createPhaseNode('phase-1')
      const start = createNode('start', 'start')

      useFlowStore.setState({ nodes: [phase, start] })

      // verify it starts of false
      expect(phase.data.isFirstPhase).toBe(false);

      act(() => {
        useFlowStore.getState().onConnect(connect('start', 'phase-1'))
      })

      const updatedPhase = useFlowStore
        .getState()
        .nodes.find((n) => n.id === 'phase-1') as PhaseNode

      expect(updatedPhase.data.isFirstPhase).toBe(true)
    })

    test('connecting task => phase adds child', () => {
      const phase = createPhaseNode('phase-1')
      const norm = createNode('norm-1', 'norm')

      useFlowStore.setState({ nodes: [phase, norm] })

      act(() => {
        useFlowStore.getState().onConnect(connect('norm-1', 'phase-1'))
      })

      const updatedPhase = useFlowStore
        .getState()
        .nodes.find((n) => n.id === 'phase-1') as PhaseNode

      expect(updatedPhase.data.children).toEqual(['norm-1'])
    })

    test('connecting phase => phase sets nextPhaseId', () => {
      const p1 = createPhaseNode('phase-1')
      const p2 = createPhaseNode('phase-2')

      useFlowStore.setState({ nodes: [p1, p2] })

      act(() => {
        useFlowStore.getState().onConnect(connect('phase-1', 'phase-2'))
      })

      const updatedP1 = useFlowStore
        .getState()
        .nodes.find((n) => n.id === 'phase-1') as PhaseNode

      expect(updatedP1.data.nextPhaseId).toBe('phase-2')
    })

    test('connecting phase to end => phase sets nextPhaseId to "end"', () => {
      const phase = createPhaseNode('phase-1')
      const end = createNode('end', 'end')

      useFlowStore.setState({ nodes: [phase, end] })

      act(() => {
        useFlowStore.getState().onConnect(connect('phase-1', 'end'))
      })

      const updatedPhase = useFlowStore
        .getState()
        .nodes.find((n) => n.id === 'phase-1') as PhaseNode

      expect(updatedPhase.data.nextPhaseId).toBe('end')
    })
  })

  describe('PhaseDisconnections', () => {
    test('disconnecting task => phase removes child', () => {
      const phase = createPhaseNode('phase-1', { children: ['norm-1'] })
      const norm = createNode('norm-1', 'norm')

      useFlowStore.setState({
        nodes: [phase, norm],
        edges: [edge('norm-1', 'phase-1')]
      })

      act(() => {
        useFlowStore.getState().onEdgesDelete([edge('norm-1', 'phase-1')])
      })

      const updatedPhase = useFlowStore
        .getState()
        .nodes.find((n) => n.id === 'phase-1') as PhaseNode

      expect(updatedPhase.data.children).toEqual([])
    })

    test('disconnecting start => phase sets isFirstPhase to false', () => {
      const phase = createPhaseNode('phase-1', { isFirstPhase: true })
      const start = createNode('start', 'start')

      useFlowStore.setState({
        nodes: [phase, start],
        edges: [edge('start', 'phase-1')]
      })

      act(() => {
        useFlowStore.getState().onEdgesDelete([edge('start', 'phase-1')])
      })

      const updatedPhase = useFlowStore
        .getState()
        .nodes.find((n) => n.id === 'phase-1') as PhaseNode

      expect(updatedPhase.data.isFirstPhase).toBe(false)
    })

    test('disconnecting phase => phase sets nextPhaseId to null', () => {
      const p1 = createPhaseNode('phase-1', { nextPhaseId: 'phase-2' })
      const p2 = createPhaseNode('phase-2')

      useFlowStore.setState({
        nodes: [p1, p2],
        edges: [edge('phase-1', 'phase-2')]
      })

      act(() => {
        useFlowStore.getState().onEdgesDelete([edge('phase-1', 'phase-2')])
      })

      const updatedP1 = useFlowStore
        .getState()
        .nodes.find((n) => n.id === 'phase-1') as PhaseNode

      expect(updatedP1.data.nextPhaseId).toBeNull()
    })
  })
})