/* eslint-disable prettier/prettier */
import { mutations, createLink, filterVisibleNodes, validateDataset } from '../../src/store/modules/data.store.js'
import { cloneDeep, random } from 'lodash'

import aggregationRandomizedTestDataset from '@/datasets/data-harry.js'

describe('store/data/ADD_NODE', () => {
  it('adds one node', () => {
    let node = { name: 'test' }
    const state = { nodes: [] }

    mutations.ADD_NODE(state, node)

    expect(state).toEqual({ nodes: [{ id: 0, name: 'test' }] })
  })
})

describe('store/data/UPDATE_NODES', () => {
  it('fails with empty or undefined arr', () => {
    for (const payload of [{}, [], undefined])
      expect(() => {
        mutations.UPDATE_NODES({}, payload)
      }).toThrow()
  })

  it('fails with entry without node id', () => {
    expect(() => mutations.UPDATE_NODES({}, [{ label: 'new' }])).toThrow()
  })

  it('change single node', () => {
    const payload = [{ id: 0, label: 'b', num: 42 }],
      state = { nodes: [{ id: 0, label: 'a' }] },
      expectedState = { nodes: [{ id: 0, label: 'b', num: 42 }] }

    mutations.UPDATE_NODES(state, payload)

    expect(state).toEqual(expectedState)
  })

  it('change multiple nodes', () => {
    const payload = [
        { id: 0 },
        { id: 1, label: 'b' },
        { id: 2, new1: 1, new2: { new3: false } },
        { id: 3, vis: { graph: { x: 42 } } }
      ],
      state = {
        nodes: [
          { id: 0, label: 'a' },
          { id: 1, label: 'a' },
          { id: 2 },
          { id: 3 }
        ]
      },
      expectedState = {
        nodes: [
          { id: 0, label: 'a' },
          { id: 1, label: 'b' },
          { id: 2, new1: 1, new2: { new3: false } },
          { id: 3, vis: { graph: { x: 42 } } }
        ]
      }

    mutations.UPDATE_NODES(state, payload)

    expect(state).toEqual(expectedState)
  })
})

// FIXME: test exceptions, may not be correct, not decided yet how to handle original data entries
describe.skip('store/data/REMOVE_NODES', () => {
  it('fails with empty or undefined arr', () => {
    for (const arr of [[], undefined])
      expect(() => {
        mutations.REMOVE_NODES({}, { nodeIds: arr })
      }).toThrow()
  })

  it('fails when node doesnt exist', () => {
    expect(() =>
      mutations.REMOVE_NODES({ nodes: [{ id: 2, label: 'c' }] }, { nodeIds: [0] })
    ).toThrow()
  })

  it('fails when node is not user created', () => {
    expect(() =>
      mutations.REMOVE_NODES({ nodes: [{ id: 2, label: 'c' }] }, { nodeIds: [2] })
    ).toThrow()
  })

  // FIXME
  it.skip('remove single node', () => {
    const state = {
      nodes: [
        { id: 0, label: 'a' },
        { id: 1, label: 'b' },
        { id: 2, label: 'c' }
      ],
      links: [
        { id: 0, source: 0, target: 1 },
        { id: 1, source: 1, target: 0 },
        { id: 2, source: 1, target: 2 }
      ]
    }
    const expectedState = {
      nodes: [{ id: 0, label: 'a' }, { id: 2, label: 'c' }],
      links: []
    }

    const payload = { nodeIds: [1] }
    const expectedPayload = { nodeIds: [1], removedNodeIds: [1], removedLinkIds: [0, 1, 2] }

    mutations.REMOVE_NODES(state, payload)

    expect(state).toEqual(expectedState)
    expect(payload).toEqual(expectedPayload)
  })

  // FIXME
  it.skip('remove multiple nodes with multilinks', () => {
    const state = {
      nodes: [
        { id: 0, label: 'a' },
        { id: 1, label: 'b' },
        { id: 2, label: 'c' },
        { id: 3, label: 'd' },
        { id: 4, label: 'e' }
      ],
      links: [
        { id: 0, source: 0, target: 1, vis: { inMultilink: 2 } },
        { id: 1, source: 1, target: 0, vis: { inMultilink: 2 } },
        { id: 2, source: 0, target: 1, vis: { isMultilink: true } },
        { id: 3, source: 2, target: 0 },
        { id: 4, source: 2, target: 4 },
        { id: 5, source: 3, target: 2 },
        { id: 6, source: 4, target: 3 }
      ]
    }
    const expectedState = {
      nodes: [{ id: 3, label: 'd' }, { id: 4, label: 'e' }],
      links: [{ id: 6, source: 4, target: 3 }]
    }

    const payload = { nodeIds: [0, 1, 2] }
    const expectedPayload = {
      nodeIds: [0, 1, 2],
      removedNodeIds: [0, 1, 2],
      removedLinkIds: [2, 3, 4, 5]
    }

    mutations.REMOVE_NODES(state, payload)

    expect(state).toEqual(expectedState)
    expect(payload).toEqual(expectedPayload)
  })
})

describe('store/data/AGGREGATE_NODES', () => {
  it('fails with < 2 nodes', () => {
    const state = { nodes: [{ id: 0 }, { id: 1 }], links: [] }
    const org_state = { ...state }

    expect(() => {
      mutations.AGGREGATE_NODES(state, {
        nodeIds: [0],
        targetGroupNode: { type: 'group' }
      })
    }).toThrow()
    expect(state).toEqual(org_state)
  })

  it('links only inside group, simple dataset', () => {
    const state = {
      nodes: [
        { id: 0, dataClass: 'person', type: 'regular' },
        { id: 1, dataClass: 'document', type: 'regular' },
        { id: 2, dataClass: 'sth', type: 'regular' }
      ],
      links: [{ id: 0, source: 0, target: 1 }, { id: 1, source: 1, target: 0 }]
    }
    const expectedState = {
      nodes: [
        { id: 0, dataClass: 'person', type: 'regular', vis: { replacedBy: 3 } },
        { id: 1, dataClass: 'document', type: 'regular', vis: { replacedBy: 3 } },
        { id: 2, dataClass: 'sth', type: 'regular' },
        { id: 3, type: 'group' }
      ],
      links: [
        { id: 0, source: 0, target: 1, vis: { newSource: 3, newTarget: 3 } },
        { id: 1, source: 1, target: 0, vis: { newSource: 3, newTarget: 3 } }
      ]
    }
    const payload = {
      nodeIds: [0, 1],
      targetGroupNode: { type: 'group' }
    }
    const expectedPayload = {
      nodeIds: [0, 1],
      targetGroupNode: { id: 3, type: 'group' },
      newVisibleLinks: [],
      removedLinkIds: [0, 1]
    }

    mutations.AGGREGATE_NODES(state, payload)

    expect(state).toEqual(expectedState)
    expect(payload).toMatchObject(expectedPayload)
  })

  it('group nodes which have links with non-group members (no multilinks in result)', () => {
    const state = {
      nodes: [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }],
      links: [
        // inside group
        { id: 0, source: 0, target: 1 },
        { id: 1, source: 1, target: 0 },
        // outside group
        { id: 2, source: 2, target: 3 },
        { id: 3, source: 3, target: 4 },
        // between
        { id: 4, source: 2, target: 0 },
        { id: 5, source: 0, target: 3 },
        { id: 6, source: 4, target: 1 }
      ]
    }

    mutations.AGGREGATE_NODES(state, {
      nodeIds: [0, 1],
      targetGroupNode: { type: 'group' }
    })

    const expectedState = {
      nodes: [
        { id: 0, vis: { replacedBy: 5 } },
        { id: 1, vis: { replacedBy: 5 } },
        { id: 2 },
        { id: 3 },
        { id: 4 },
        { id: 5, type: 'group' }
      ],
      links: [
        { id: 0, source: 0, target: 1, vis: { newSource: 5, newTarget: 5 } },
        { id: 1, source: 1, target: 0, vis: { newSource: 5, newTarget: 5 } },
        // outside
        { id: 2, source: 2, target: 3 },
        { id: 3, source: 3, target: 4 },
        // new links connecting the group
        { id: 4, source: 2, target: 0, vis: { newSource: 2, newTarget: 5 } },
        { id: 5, source: 0, target: 3, vis: { newSource: 5, newTarget: 3 } },
        { id: 6, source: 4, target: 1, vis: { newSource: 4, newTarget: 5 } }
      ]
    }
    expect(state).toEqual(expectedState)
  })

  it('multilink 1 A', () => {
    const state = {
      nodes: [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }],
      links: [
        {
          id: 0,
          source: 0,
          target: 1,
          direction: true,
          vis: { inMultilink: 2 }
        },
        {
          id: 1,
          source: 1,
          target: 0,
          direction: true,
          vis: { inMultilink: 2 }
        },
        {
          id: 2,
          source: 0,
          target: 1,
          direction: {
            nonDirectional: false,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: { isMultilink: true }
        },
        {
          id: 3,
          source: 0,
          target: 2,
          direction: false
        },
        {
          id: 4,
          source: 0,
          target: 3,
          direction: false
        }
      ]
    }

    const payload = {
      nodeIds: [1, 2],
      targetGroupNode: { type: 'group' }
    }
    const expectedPayload = {
      nodeIds: [1, 2],
      targetGroupNode: { id: 4, type: 'group' },
      newVisibleLinks: [
        {
          id: 5,
          source: 4,
          target: 0
        }
      ],
      removedLinkIds: [2, 3]
    }

    const expectedState = {
      nodes: [
        { id: 0 },
        { id: 1, vis: { replacedBy: 4 } },
        { id: 2, vis: { replacedBy: 4 } },
        { id: 3 },
        { id: 4, type: 'group' }
      ],
      links: [
        {
          id: 0,
          source: 0,
          target: 1,
          direction: true,
          vis: { inMultilink: 5, newSource: 0, newTarget: 4 }
        },
        {
          id: 1,
          source: 1,
          target: 0,
          direction: true,
          vis: { inMultilink: 5, newSource: 4, newTarget: 0 }
        },
        {
          id: 2,
          source: 0,
          target: 1,
          direction: {
            nonDirectional: false,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: {
            isArchived: true,
            isMultilink: true
          }
        },
        {
          id: 3,
          source: 0,
          target: 2,
          direction: false,
          vis: { inMultilink: 5, newSource: 0, newTarget: 4 }
        },
        {
          id: 4,
          source: 0,
          target: 3,
          direction: false
        },
        {
          id: 5,
          source: 4, // multilinks always start from new group nodes
          target: 0,
          direction: {
            nonDirectional: true,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: { isMultilink: true }
        }
      ]
    }

    mutations.AGGREGATE_NODES(state, payload)

    expect(state).toEqual(expectedState)
    expect(payload).toEqual(expectedPayload)
  })

  it('multilink 1 B', () => {
    const state = {
      nodes: [
        { id: 0 },
        { id: 1, vis: { replacedBy: 4 } },
        { id: 2, vis: { replacedBy: 4 } },
        { id: 3 },
        { id: 4, type: 'group' }
      ],
      links: [
        {
          id: 0,
          source: 0,
          target: 1,
          direction: true,
          vis: { inMultilink: 5, newSource: 0, newTarget: 4 }
        },
        {
          id: 1,
          source: 1,
          target: 0,
          direction: true,
          vis: { inMultilink: 5, newSource: 4, newTarget: 0 }
        },
        {
          id: 2,
          source: 0,
          target: 1,
          direction: {
            nonDirectional: false,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: {
            isArchived: true,
            isMultilink: true
          }
        },
        {
          id: 3,
          source: 0,
          target: 2,
          direction: false,
          vis: { inMultilink: 5, newSource: 0, newTarget: 4 }
        },
        {
          id: 4,
          source: 0,
          target: 3,
          direction: false
        },
        {
          id: 5,
          source: 4, // multilinks always start from new group nodes
          target: 0,
          direction: {
            nonDirectional: true,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: { isMultilink: true }
        }
      ]
    }

    const payload = {
      nodeIds: [0, 3],
      targetGroupNode: { type: 'group' }
    }
    const expectedPayload = {
      nodeIds: [0, 3],
      targetGroupNode: { id: 5, type: 'group' },
      newVisibleLinks: [
        {
          id: 6,
          source: 5,
          target: 4
        }
      ],
      removedLinkIds: [4, 5]
    }

    const expectedState = {
      nodes: [
        { id: 0, vis: { replacedBy: 5 } },
        { id: 1, vis: { replacedBy: 4 } },
        { id: 2, vis: { replacedBy: 4 } },
        { id: 3, vis: { replacedBy: 5 } },
        { id: 4, type: 'group' },
        { id: 5, type: 'group' }
      ],
      links: [
        {
          id: 0,
          source: 0,
          target: 1,
          direction: true,
          vis: { inMultilink: 6, newSource: 5, newTarget: 4 }
        },
        {
          id: 1,
          source: 1,
          target: 0,
          direction: true,
          vis: { inMultilink: 6, newSource: 4, newTarget: 5 }
        },
        {
          id: 2,
          source: 0,
          target: 1,
          direction: {
            nonDirectional: false,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: {
            isArchived: true,
            isMultilink: true
          }
        },
        {
          id: 3,
          source: 0,
          target: 2,
          direction: false,
          vis: { inMultilink: 6, newSource: 5, newTarget: 4 }
        },
        {
          id: 4,
          source: 0,
          target: 3,
          direction: false,
          vis: { newSource: 5, newTarget: 5 }
        },
        {
          id: 5,
          source: 4, // multilinks always start from new group nodes
          target: 0,
          direction: {
            nonDirectional: true,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: { isArchived: true, isMultilink: true }
        },
        {
          id: 6,
          source: 5, // multilinks always start from new group nodes
          target: 4,
          direction: {
            nonDirectional: true,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: { isMultilink: true }
        }
      ]
    }

    mutations.AGGREGATE_NODES(state, payload)

    expect(state).toEqual(expectedState)
    expect(payload).toEqual(expectedPayload)
  })

  it('multilink 2', () => {
    const state = {
      nodes: [
        {
          id: 0,
          type: 'regular'
        },
        {
          id: 1,
          type: 'regular'
        },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' }
      ],
      links: [
        {
          id: 0,
          source: 0,
          target: 2,
          direction: true
        },
        {
          id: 1,
          source: 2,
          target: 1,
          direction: true
        },
        {
          id: 2,
          source: 3,
          target: 0,
          direction: true
        },
        {
          id: 3,
          source: 3,
          target: 1,
          direction: false
        }
      ]
    }

    const payload = {
      nodeIds: [0, 1],
      targetGroupNode: { type: 'group' }
    }
    const expectedPayload = {
      nodeIds: [0, 1],
      targetGroupNode: { id: 4, type: 'group' },
      newVisibleLinks: [
        {
          id: 4,
          source: 4,
          target: 2
        },
        {
          id: 5,
          source: 4,
          target: 3
        }
      ],
      removedLinkIds: [0, 1, 2, 3]
    }

    mutations.AGGREGATE_NODES(state, payload)

    const expectedState = {
      nodes: [
        {
          id: 0,
          type: 'regular',
          vis: { replacedBy: 4 }
        },
        {
          id: 1,
          type: 'regular',
          vis: { replacedBy: 4 }
        },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' },
        { id: 4, type: 'group' }
      ],
      links: [
        {
          id: 0,
          source: 0,
          target: 2,
          direction: true,
          vis: { inMultilink: 4, newSource: 4, newTarget: 2 }
        },
        {
          id: 1,
          source: 2,
          target: 1,
          direction: true,
          vis: { inMultilink: 4, newSource: 2, newTarget: 4 }
        },
        {
          id: 2,
          source: 3,
          target: 0,
          direction: true,
          vis: { inMultilink: 5, newSource: 3, newTarget: 4 }
        },
        {
          id: 3,
          source: 3,
          target: 1,
          direction: false,
          vis: { inMultilink: 5, newSource: 3, newTarget: 4 }
        },
        {
          id: 4,
          source: 4,
          target: 2,
          direction: {
            nonDirectional: false,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: {
            isMultilink: true
          }
        },
        {
          id: 5,
          source: 4,
          target: 3,
          direction: {
            nonDirectional: true,
            sourceToTarget: false,
            targetToSource: true
          },
          vis: {
            isMultilink: true
          }
        }
      ]
    }

    expect(state).toEqual(expectedState)
    expect(payload).toMatchObject(expectedPayload)
  })

  it('repeated grouping (with multilinks) 1', () => {
    const state = {
      nodes: [
        { id: 0, type: 'regular', vis: { replacedBy: 4 } },
        { id: 1, type: 'regular', vis: { replacedBy: 4 } },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' },
        { id: 4, type: 'group' }
      ],
      links: [
        {
          id: 0,
          source: 0,
          target: 2,
          direction: true,
          vis: { newSource: 4, newTarget: 2, inMultilink: 6 }
        },
        {
          id: 1,
          source: 2,
          target: 1,
          direction: true,
          vis: { newSource: 2, newTarget: 4, inMultilink: 6 }
        },
        {
          id: 2,
          source: 3,
          target: 0,
          direction: true,
          vis: { newSource: 3, newTarget: 4, inMultilink: 7 }
        },
        {
          id: 3,
          source: 3,
          target: 1,
          direction: false,
          vis: { newSource: 3, newTarget: 4, inMultilink: 7 }
        },
        {
          id: 4,
          source: 3,
          target: 1,
          direction: false,
          vis: { newSource: 3, newTarget: 4, inMultilink: 7 }
        },
        {
          id: 5,
          source: 3,
          target: 1,
          direction: {
            nonDirectional: true,
            sourceToTarget: true,
            targetToSource: false
          },
          vis: {
            isArchived: true,
            isMultilink: true
          }
        },
        {
          id: 6,
          source: 4,
          target: 2,
          direction: {
            nonDirectional: false,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: {
            isMultilink: true
          }
        },
        {
          id: 7,
          source: 4,
          target: 3,
          direction: {
            nonDirectional: false,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: {
            isMultilink: true
          }
        }
      ]
    }

    const expectedState = {
      nodes: [
        { id: 0, type: 'regular', vis: { replacedBy: 4 } },
        { id: 1, type: 'regular', vis: { replacedBy: 4 } },
        { id: 2, type: 'regular', vis: { replacedBy: 5 } },
        { id: 3, type: 'regular' },
        { id: 4, type: 'group', vis: { replacedBy: 5 } },
        { id: 5, type: 'group' }
      ],
      links: [
        {
          id: 0,
          source: 0,
          target: 2,
          direction: true,
          vis: { newSource: 5, newTarget: 5 }
        },
        {
          id: 1,
          source: 2,
          target: 1,
          direction: true,
          vis: { newSource: 5, newTarget: 5 }
        },
        {
          id: 2,
          source: 3,
          target: 0,
          direction: true,
          vis: { newSource: 3, newTarget: 5, inMultilink: 8 }
        },
        {
          id: 3,
          source: 3,
          target: 1,
          direction: false,
          vis: { newSource: 3, newTarget: 5, inMultilink: 8 }
        },
        {
          id: 4,
          source: 3,
          target: 1,
          direction: false,
          vis: { newSource: 3, newTarget: 5, inMultilink: 8 }
        },
        {
          id: 5,
          source: 3,
          target: 1,
          direction: {
            nonDirectional: true,
            sourceToTarget: true,
            targetToSource: false
          },
          vis: {
            isArchived: true,
            isMultilink: true
          }
        },
        {
          id: 6,
          source: 4,
          target: 2,
          direction: {
            nonDirectional: false,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: {
            isArchived: true,
            isMultilink: true
          }
        },
        {
          id: 7,
          source: 4,
          target: 3,
          direction: {
            nonDirectional: false,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: {
            isArchived: true,
            isMultilink: true
          }
        },
        {
          id: 8,
          source: 5,
          target: 3,
          direction: {
            nonDirectional: true,
            sourceToTarget: false,
            targetToSource: true
          },
          vis: {
            isMultilink: true
          }
        }
      ]
    }

    const payload = {
      nodeIds: [2, 4],
      targetGroupNode: { type: 'group' }
    }
    const expectedPayload = {
      nodeIds: [2, 4],
      targetGroupNode: { id: 5, type: 'group' },
      newVisibleLinks: [
        {
          id: 8,
          source: 5,
          target: 3
        }
      ],
      removedLinkIds: [6, 7]
    }

    mutations.AGGREGATE_NODES(state, payload)

    expect(state).toEqual(expectedState)
    expect(payload).toMatchObject(expectedPayload)
  })

  it('bundle', () => {
    const state = {
      nodes: [
        { id: 0, dataClass: 'person', type: 'regular' },
        { id: 1, dataClass: 'document', type: 'regular' },
        { id: 2, dataClass: 'sth', type: 'regular' }
      ],
      links: [{ id: 0, source: 1, target: 2 }, { id: 1, source: 1, target: 0 }]
    }
    const expectedState = {
      nodes: [
        { id: 0, dataClass: 'person', type: 'regular', vis: { bundledNodes: [1] } },
        { id: 1, dataClass: 'document', type: 'regular', vis: { bundledIn: 0 } },
        { id: 2, dataClass: 'sth', type: 'regular' }
      ],
      links: [
        { id: 0, source: 1, target: 2, vis: { newSource: 0, newTarget: 2 } },
        { id: 1, source: 1, target: 0, vis: { newSource: 0, newTarget: 0 } }
      ]
    }
    const payload = {
      action: 'bundle',
      targetGroupNode: { id: 0, type: 'regular' },
      nodeIds: [1]
    }
    const expectedPayload = {
      action: 'bundle',
      targetGroupNode: { id: 0, type: 'regular' },
      nodeIds: [1],
      newVisibleLinks: [{ id: 0, source: 0, target: 2 }],
      removedLinkIds: [0, 1]
    }

    mutations.AGGREGATE_NODES(state, payload)

    expect(state).toEqual(expectedState)
    expect(payload).toMatchObject(expectedPayload)
  })

  it('bundle forming multilinks', () => {
    const state = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' },
        { id: 4, type: 'regular' }
      ],
      links: [
        { id: 0, source: 3, target: 1, direction: false },
        { id: 1, source: 3, target: 0, direction: true },
        { id: 2, source: 4, target: 3, direction: false },
        { id: 3, source: 2, target: 3, direction: false },
        { id: 4, source: 0, target: 1, direction: true },
        { id: 5, source: 0, target: 2, direction: true }
      ],
    }

    const expectedState = {
      nodes: [
        { id: 0, type: 'regular', vis: { bundledIn: 3 } },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular', vis: { bundledNodes: [0] } },
        { id: 4, type: 'regular' }
      ],
      links: [
        { id: 0, source: 3, target: 1, direction: false, vis: { inMultilink: 6 } },
        { id: 1, source: 3, target: 0, direction: true, vis: { newSource: 3, newTarget: 3 } },
        { id: 2, source: 4, target: 3, direction: false },
        { id: 3, source: 2, target: 3, direction: false, vis: { inMultilink: 7 } },
        { id: 4, source: 0, target: 1, direction: true, vis: { newSource: 3, newTarget: 1, inMultilink: 6 } },
        { id: 5, source: 0, target: 2, direction: true, vis: { newSource: 3, newTarget: 2, inMultilink: 7 } },
        { id: 6, source: 3, target: 1, direction: { 
          nonDirectional: true, sourceToTarget: true, targetToSource: false 
        }, vis: { isMultilink: true } },
        { id: 7, source: 3, target: 2, direction: { 
          nonDirectional: true, sourceToTarget: true, targetToSource: false 
        }, vis: { isMultilink: true } },
      ],
    }

    const payload = {
      action: 'bundle',
      targetGroupNode: { id: 3, type: 'regular' },
      nodeIds: [0]
    }
    const expectedPayload = {
      action: 'bundle',
      targetGroupNode: { id: 3, type: 'regular' },
      nodeIds: [0],
      newVisibleLinks: [{ id: 2, source: 4, target: 3 }, { id: 6, source: 3, target: 1 }, {id: 7, source: 3, target: 2}],
      removedLinkIds: [0, 1, 2, 3, 4, 5]
    }

    mutations.AGGREGATE_NODES(state, payload)

    expect(state).toEqual(expectedState)
    expect(payload).toMatchObject(expectedPayload)
  })

  it('bundling to a group', () => {
    const state = {
      nodes: [
        { id: 0, type: 'regular' }, // to bundle
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular', vis: { replacedBy: 5 } },
        { id: 3, type: 'regular', vis: { replacedBy: 5 } },
        { id: 4, type: 'regular' },  // to bundle
        { id: 5, type: 'group' }  // target
      ],
      links: [
        { id: 0, source: 3, target: 1, direction: false, vis: { newSource: 5, newTarget: 1 } },
        { id: 1, source: 3, target: 0, direction: true, vis: { newSource: 5, newTarget: 0, inMultilink: 6 } },
        { id: 2, source: 4, target: 3, direction: false, vis: { newSource: 4, newTarget: 5 } },
        { id: 3, source: 2, target: 3, direction: false, vis: { newSource: 5, newTarget: 5 } },
        { id: 4, source: 0, target: 1, direction: true },
        { id: 5, source: 0, target: 2, direction: true, vis: { newSource: 0, newTarget: 5, inMultilink: 6 } },
        { id: 6, source: 5, target: 0, vis: { isMultilink: true }, direction: { nonDirectional: false, sourceToTarget: true, targetToSource: true }}
      ]
    }

    const expectedState = {
      nodes: [
        { id: 0, type: 'regular', vis: { bundledIn: 5 } },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular', vis: { replacedBy: 5 } },
        { id: 3, type: 'regular', vis: { replacedBy: 5 } },
        { id: 4, type: 'regular', vis: { bundledIn: 5 } },
        { id: 5, type: 'group', vis: { bundledNodes: [0, 4] } }
      ],
      links: [
        { id: 0, source: 3, target: 1, direction: false, vis: { newSource: 5, newTarget: 1, inMultilink: 7 } },
        { id: 1, source: 3, target: 0, direction: true, vis: { newSource: 5, newTarget: 5 } },
        { id: 2, source: 4, target: 3, direction: false, vis: { newSource: 5, newTarget: 5 } },
        { id: 3, source: 2, target: 3, direction: false, vis: { newSource: 5, newTarget: 5 } },
        { id: 4, source: 0, target: 1, direction: true, vis: { newSource: 5, newTarget: 1, inMultilink: 7 } },
        { id: 5, source: 0, target: 2, direction: true, vis: { newSource: 5, newTarget: 5 } },
        { id: 6, source: 5, target: 0, vis: { isMultilink: true, isArchived: true }, direction: { nonDirectional: false, sourceToTarget: true, targetToSource: true }},
        { id: 7, source: 5, target: 1, vis: { isMultilink: true }, direction: { nonDirectional: true, sourceToTarget: true, targetToSource: false }}
      ]
    }

    const payload = {
      action: 'bundle',
      targetGroupNode: { id: 5, type: 'group' },
      nodeIds: [0, 4]
    }
    const expectedPayload = {
      action: 'bundle',
      targetGroupNode: { id: 5, type: 'group' },
      nodeIds: [0, 4],
      newVisibleLinks: [{ id: 7, source: 5, target: 1 }],
      removedLinkIds: [0, 2, 4, 6]
    }

    mutations.AGGREGATE_NODES(state, payload)

    expect(state).toEqual(expectedState)
    expect(payload).toMatchObject(expectedPayload)
  })
})

describe('store/data/DISAGGREGATE_NODE', () => {
  it('fails with invalid node id', () => {
    for (const testId of [{}, [1], NaN, -1]) {
      expect(() => {
        mutations.DISAGGREGATE_NODE({ nodes: [{ id: 0 }] }, { nodeId: testId })
      }).toThrow()
    }
  })

  it('fails if node does not exist', () => {
    expect(() => {
      mutations.DISAGGREGATE_NODE({ nodes: [{ id: 0 }] }, { nodeId: 1 })
    }).toThrow()
  })

  it('fails if not group', () => {
    expect(() => {
      mutations.DISAGGREGATE_NODE({ nodes: [{ id: 0 }], links: [] }, { revertAction: 'group', nodeId: 0 })
    }).toThrow()

    expect(() => {
      mutations.DISAGGREGATE_NODE({ nodes: [{ id: 0, type: 'regular' }], links: [] }, { revertAction: 'group', nodeId: 0 })
    }).toThrow()
  })

  it('links only inside group, simple dataset', () => {
    const state = {
      nodes: [
        { id: 0, dataClass: 'person', type: 'regular' },
        { id: 1, dataClass: 'document', type: 'regular' },
        { id: 2, dataClass: 'sth', type: 'regular' }
      ],
      links: [{ id: 0, source: 0, target: 1 }, { id: 1, source: 1, target: 0 }]
    }
    const expectedState = {
      nodes: [
        { id: 0, dataClass: 'person', type: 'regular', vis: { replacedBy: 3 } },
        { id: 1, dataClass: 'document', type: 'regular', vis: { replacedBy: 3 } },
        { id: 2, dataClass: 'sth', type: 'regular' },
        { id: 3, type: 'group' }
      ],
      links: [
        { id: 0, source: 0, target: 1, vis: { newSource: 3, newTarget: 3 } },
        { id: 1, source: 1, target: 0, vis: { newSource: 3, newTarget: 3 } }
      ]
    }
    const payload = {
      nodeIds: [0, 1],
      targetGroupNode: { type: 'group' }
    }
    const expectedPayload = {
      nodeIds: [0, 1],
      targetGroupNode: { id: 3, type: 'group' },
      newVisibleLinks: [],
      removedLinkIds: [0, 1]
    }

    mutations.AGGREGATE_NODES(state, payload)

    expect(state).toEqual(expectedState)
    expect(payload).toMatchObject(expectedPayload)
  })

  it('simple, no multilinks', () => {
    const state = {
      nodes: [
        { id: 0, type: 'regular', vis: { replacedBy: 2 } },
        { id: 1, type: 'regular', vis: { replacedBy: 2 } },
        { id: 2, type: 'group' },
        { id: 3, type: 'regular' }
      ],
      links: [
        {
          id: 0,
          source: 0,
          target: 1,
          direction: true,
          vis: { newSource: 2, newTarget: 2 }
        },
        {
          id: 1,
          source: 1,
          target: 3,
          direction: true,
          vis: { newSource: 2, newTarget: 3 }
        }
      ]
    }

    const expectedState = {
      nodes: [{ id: 0, type: 'regular' }, { id: 1, type: 'regular' }, { id: 3, type: 'regular' }],
      links: [
        { id: 0, source: 0, target: 1, direction: true },
        { id: 1, source: 1, target: 3, direction: true }
      ]
    }

    const payload = {
      revertAction: 'group',
      nodeId: 2
    }
    const expectedPayload = {
      revertAction: 'group',
      nodeId: 2,
      removeNode: true,
      newVisibleLinks: [{ id: 0, source: 0, target: 1 }, { id: 1, source: 1, target: 3 }],
      newVisibleNodes: [{ id: 0 }, { id: 1 }],
      removedLinkIds: [1]
    }

    mutations.DISAGGREGATE_NODE(state, payload)

    expect(state).toEqual(expectedState)
    expect(payload).toEqual(expectedPayload)
  })

  it('multilinks 1', () => {
    const state = {
      nodes: [
        { id: 0, type: 'regular', vis: { replacedBy: 4 } },
        { id: 1, type: 'regular', vis: { replacedBy: 4 } },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' },
        { id: 4, type: 'group' }
      ],
      links: [
        {
          id: 0,
          source: 0,
          target: 2,
          direction: true,
          vis: { newSource: 4, newTarget: 2, inMultilink: 4 }
        },
        {
          id: 1,
          source: 2,
          target: 1,
          direction: true,
          vis: { newSource: 2, newTarget: 4, inMultilink: 4 }
        },
        {
          id: 2,
          source: 0,
          target: 1,
          direction: true,
          vis: { newSource: 4, newTarget: 4 }
        },
        {
          id: 4,
          source: 4,
          target: 2,
          direction: {
            nonDirectional: true,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: { isMultilink: true }
        },
        { id: 5, source: 3, target: 2, direction: true },
        { id: 6, source: 3, target: 4, direction: false }
      ]
    }

    const expectedState = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' }
      ],
      links: [
        { id: 0, source: 0, target: 2, direction: true },
        { id: 1, source: 2, target: 1, direction: true },
        { id: 2, source: 0, target: 1, direction: true },
        { id: 5, source: 3, target: 2, direction: true }
      ]
    }

    const payload = {
      revertAction: 'group',
      nodeId: 4
    }
    const expectedPayload = {
      revertAction: 'group',
      nodeId: 4,
      removeNode: true,
      newVisibleLinks: [
        { id: 0, source: 0, target: 2 },
        { id: 1, source: 2, target: 1 },
        { id: 2, source: 0, target: 1 }
      ],
      newVisibleNodes: [{ id: 0 }, { id: 1 }],
      removedLinkIds: [4, 6]
    }

    mutations.DISAGGREGATE_NODE(state, payload)

    expect(state).toEqual(expectedState)
    expect(payload).toEqual(expectedPayload)
  })

  it('ungrouping in different order 3', () => {
    const state = {
      nodes: [
        { id: 0, type: 'regular', vis: { replacedBy: 4 } },
        { id: 1, type: 'regular', vis: { replacedBy: 4 } },
        { id: 2, type: 'regular', vis: { replacedBy: 5 } },
        { id: 3, type: 'regular', vis: { replacedBy: 5 } },
        { id: 4, type: 'group' },
        { id: 5, type: 'group' }
      ],
      links: [
        {
          id: 0,
          source: 0,
          target: 1,
          direction: true,
          vis: { newSource: 4, newTarget: 4 }
        },
        {
          id: 1,
          source: 0,
          target: 2,
          direction: true,
          vis: { newSource: 4, newTarget: 5, inMultilink: 5 }
        },
        {
          id: 2,
          source: 1,
          target: 3,
          direction: false,
          vis: { newSource: 4, newTarget: 5, inMultilink: 5 }
        },
        {
          id: 3,
          source: 3,
          target: 0,
          direction: true,
          vis: { newSource: 5, newTarget: 4, inMultilink: 5 }
        },
        {
          id: 4,
          source: 4,
          target: 3,
          direction: {
            nonDirectional: true,
            sourceToTarget: false,
            targetToSource: true
          },
          vis: {
            isArchived: true,
            isMultilink: true
          }
        },
        {
          id: 5,
          source: 5,
          target: 4,
          direction: {
            nonDirectional: true,
            sourceToTarget: true,
            targetToSource: false
          },
          vis: { isMultilink: true }
        }
      ]
    }
    const expectedStateAfterDissagregating4 = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular', vis: { replacedBy: 5 } },
        { id: 3, type: 'regular', vis: { replacedBy: 5 } },
        { id: 5, type: 'group' }
      ],
      links: [
        { id: 0, source: 0, target: 1, direction: true },
        {
          id: 1,
          source: 0,
          target: 2,
          direction: true,
          vis: { newSource: 0, newTarget: 5, inMultilink: 4 }
        },
        {
          id: 2,
          source: 1,
          target: 3,
          direction: false,
          vis: { newSource: 1, newTarget: 5 }
        },
        {
          id: 3,
          source: 3,
          target: 0,
          direction: true,
          vis: { newSource: 5, newTarget: 0, inMultilink: 4 }
        },
        {
          id: 4,
          source: 5,
          target: 0,
          direction: {
            nonDirectional: false,
            sourceToTarget: true,
            targetToSource: true
          },
          vis: { isMultilink: true }
        }
      ]
    }
    // original state
    const expectedStateAfterDissagregating5 = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' }
      ],
      links: [
        { id: 0, source: 0, target: 1, direction: true },
        { id: 1, source: 0, target: 2, direction: true },
        { id: 2, source: 1, target: 3, direction: false },
        { id: 3, source: 3, target: 0, direction: true }
      ]
    }

    // dissagregate 4
    const payload1 = { revertAction: 'group', nodeId: 4 }
    mutations.DISAGGREGATE_NODE(state, payload1)

    const expectedPayloadAfterDissagregating4 = {
      revertAction: 'group',
      nodeId: 4,
      removeNode: true,
      newVisibleLinks: [
        { id: 0, source: 0, target: 1 },
        { id: 2, source: 1, target: 5 },
        { id: 4, source: 5, target: 0 }
      ],
      newVisibleNodes: [{ id: 0 }, { id: 1 }],
      removedLinkIds: [5]
    }
    expect(state).toEqual(expectedStateAfterDissagregating4)
    expect(payload1).toEqual(expectedPayloadAfterDissagregating4)

    // dissagregate 5
    const payload2 = { revertAction: 'group', nodeId: 5 }
    mutations.DISAGGREGATE_NODE(state, payload2)

    const expectedPayloadAfterDissagregating5 = {
      revertAction: 'group',
      nodeId: 5,
      removeNode: true,
      newVisibleLinks: [
        { id: 1, source: 0, target: 2 },
        { id: 2, source: 1, target: 3 },
        { id: 3, source: 3, target: 0 }
      ],
      newVisibleNodes: [{ id: 2 }, { id: 3 }],
      removedLinkIds: [2, 4]
    }
    expect(state).toEqual(expectedStateAfterDissagregating5)
    expect(payload2).toEqual(expectedPayloadAfterDissagregating5)
  })

  it('bundle with multilinks, two nodes were bundled inside', () => {
    const state = {
      nodes: [
        { id: 0, type: 'regular', vis: { bundledIn: 3 } },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular', vis: { bundledIn: 3 } },
        { id: 3, type: 'regular', vis: { bundledNodes: [0, 2] } },
        { id: 4, type: 'regular' }
      ],
      links: [
        { id: 0, source: 3, target: 1, direction: false, vis: { inMultilink: 6 } },
        { id: 1, source: 3, target: 0, direction: true, vis: { newSource: 3, newTarget: 3 } },
        { id: 2, source: 4, target: 3, direction: false },
        { id: 3, source: 2, target: 3, direction: false, vis: { newSource: 3, newTarget: 3 } },
        { id: 4, source: 0, target: 1, direction: true, vis: { newSource: 3, newTarget: 1, inMultilink: 6 } },
        { id: 5, source: 0, target: 2, direction: true, vis: { newSource: 3, newTarget: 3 } },
        { id: 6, source: 3, target: 1, direction: { 
          nonDirectional: true, sourceToTarget: true, targetToSource: false 
        }, vis: { isMultilink: true } }
      ]
    }

    const expectedState = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' },
        { id: 4, type: 'regular' }
      ],
      links: [
        { id: 0, source: 3, target: 1, direction: false },
        { id: 1, source: 3, target: 0, direction: true },
        { id: 2, source: 4, target: 3, direction: false },
        { id: 3, source: 2, target: 3, direction: false },
        { id: 4, source: 0, target: 1, direction: true },
        { id: 5, source: 0, target: 2, direction: true }
      ]
    }

    const payload = {
      revertAction: 'bundle',
      nodeId: 3,
      nodeIds: [0, 2] // FIXME
    }
    const expectedPayload = {
      revertAction: 'bundle',
      nodeId: 3,
      nodeIds: [0, 2],
      removeNode: false,
      newVisibleLinks: [
        { id: 0, source: 3, target: 1 },
        // FIXME: missing 2?
        { id: 1, source: 3, target: 0 },
        { id: 3, source: 2, target: 3 },
        { id: 4, source: 0, target: 1 },
        { id: 5, source: 0, target: 2 }
       ],
      removedLinkIds: [6] // FIXME: can be 2, 6?
    }

    mutations.DISAGGREGATE_NODE(state, payload)

    expect(state).toEqual(expectedState)
    expect(payload).toMatchObject(expectedPayload)
  })
})

describe('store/data/AGGREGATE_NODES && store/data/DISAGGREGATE_NODE', () => {
  it('ungrouping with a hierarchy', () => {
    const state = {
      nodes: [
        {
          id: 0,
          name: 'Danielle Cowan',
          type: 'regular',
          label: 'Danielle'
        },
        {
          id: 1,
          name: 'Malcolm Wang',
          type: 'regular',
          label: 'Malcolm'
        },
        {
          id: 2,
          name: 'Paula Walter',
          type: 'regular',
          label: 'Paula'
        },
        {
          id: 3,
          name: 'Steven Kang',
          type: 'regular',
          label: 'Steven'
        },
        {
          id: 4,
          name: 'Drill & co.',
          type: 'regular',
          label: 'Drill & co.'
        }
      ],
      links: [
        {
          id: 1,
          source: 3,
          target: 1,
          direction: false
        },
        {
          id: 2,
          source: 3,
          target: 0,
          direction: true
        },
        {
          id: 4,
          source: 4,
          target: 3,
          direction: false
        },
        {
          id: 6,
          source: 2,
          target: 3,
          direction: false
        },
        {
          id: 7,
          source: 0,
          target: 1,
          direction: true
        },
        {
          id: 8,
          source: 0,
          target: 2,
          direction: true
        },
        {
          id: 9,
          source: 0,
          target: 4,
          direction: false
        },
        {
          id: 10,
          source: 1,
          target: 4,
          direction: true
        }
      ]
    }
    const expectedState = cloneDeep(state)

    const groupNode0 = { type: 'group' }
    mutations.AGGREGATE_NODES(state, {
      nodeIds: [2, 3 /* paula, steven */],
      targetGroupNode: groupNode0
    })

    const groupNode1 = { type: 'group' }
    mutations.AGGREGATE_NODES(state, {
      nodeIds: [1, 4 /* malcolm, drill */],
      targetGroupNode: groupNode1
    })

    const groupNode2 = { type: 'group' }
    mutations.AGGREGATE_NODES(state, {
      nodeIds: [0, groupNode1.id /* danielle, group of malcolm+drill */],
      targetGroupNode: groupNode2
    })

    const payload0 = { revertAction: 'group', nodeId: groupNode0.id }
    mutations.DISAGGREGATE_NODE(state, payload0)

    const payload1 = { revertAction: 'group', nodeId: groupNode2.id }
    mutations.DISAGGREGATE_NODE(state, payload1)

    const payload2 = { revertAction: 'group', nodeId: groupNode1.id }
    mutations.DISAGGREGATE_NODE(state, payload2)

    expect(state).toEqual(expectedState)
  })

  it('complex 1', () => {
    const state = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' },
        { id: 4, type: 'regular' }
      ],
      links: [
        { id: 0, source: 0, target: 2, direction: true },
        { id: 1, source: 2, target: 1, direction: true },
        { id: 2, source: 1, target: 0 },
        { id: 3, source: 1, target: 3, direction: true },
        { id: 4, source: 2, target: 3, direction: false },
        { id: 5, source: 4, target: 0 }
      ]
    }
    const expectedFinalState = cloneDeep(state)

    const stateAfterGrouping0And3 = {
      nodes: [
        { id: 0, type: 'regular', vis: { replacedBy: 5 } },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular', vis: { replacedBy: 5 } },
        { id: 4, type: 'regular' },
        { id: 5, type: 'group' }
      ],
      links: [
        { id: 0, source: 0, target: 2, direction: true, vis: { newSource: 5, newTarget: 2, inMultilink: 7 } },
        { id: 1, source: 2, target: 1, direction: true },
        { id: 2, source: 1, target: 0, vis: { newSource: 1, newTarget: 5, inMultilink: 6 } },
        { id: 3, source: 1, target: 3, direction: true, vis: { newSource: 1, newTarget: 5, inMultilink: 6 } },
        { id: 4, source: 2, target: 3, direction: false, vis: { newSource: 2, newTarget: 5, inMultilink: 7 } },
        { id: 5, source: 4, target: 0, vis: { newSource: 4, newTarget: 5 } },
        { id: 6, source: 5, target: 1, direction: { nonDirectional: true, sourceToTarget: false, targetToSource: true }, vis: { isMultilink: true } },
        { id: 7, source: 5, target: 2, direction: { nonDirectional: true, sourceToTarget: true, targetToSource: false }, vis: { isMultilink: true } }
      ]
    }

    const stateAfterGrouping5And2 = {
      nodes: [
        { id: 0, type: 'regular', vis: { replacedBy: 5 } },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular', vis: { replacedBy: 6 } },
        { id: 3, type: 'regular', vis: { replacedBy: 5 } },
        { id: 4, type: 'regular' },
        { id: 5, type: 'group', vis: { replacedBy: 6 } },
        { id: 6, type: 'group' }
      ],
      links: [
        { id: 0, source: 0, target: 2, direction: true, vis: { newSource: 6, newTarget: 6 } },
        { id: 1, source: 2, target: 1, direction: true, vis: { newSource: 6, newTarget: 1, inMultilink: 8 } },
        { id: 2, source: 1, target: 0, vis: { newSource: 1, newTarget: 6, inMultilink: 8 } },
        { id: 3, source: 1, target: 3, direction: true, vis: { newSource: 1, newTarget: 6, inMultilink: 8 } },
        { id: 4, source: 2, target: 3, direction: false, vis: { newSource: 6, newTarget: 6 } },
        { id: 5, source: 4, target: 0, vis: { newSource: 4, newTarget: 6 } },
        { id: 6, source: 5, target: 1, direction: { nonDirectional: true, sourceToTarget: false, targetToSource: true }, vis: { isArchived: true, isMultilink: true } },
        { id: 7, source: 5, target: 2, direction: { nonDirectional: true, sourceToTarget: true, targetToSource: false }, vis: { isArchived: true, isMultilink: true } },
        { id: 8, source: 6, target: 1, direction: { nonDirectional: true, sourceToTarget: true, targetToSource: true }, vis: { isMultilink: true } }
      ]
    }

    const stateAfterGrouping1And4 = {
      nodes: [
        { id: 0, type: 'regular', vis: { replacedBy: 5 } },
        { id: 1, type: 'regular', vis: { replacedBy: 7 } },
        { id: 2, type: 'regular', vis: { replacedBy: 6 } },
        { id: 3, type: 'regular', vis: { replacedBy: 5 } },
        { id: 4, type: 'regular', vis: { replacedBy: 7 } },
        { id: 5, type: 'group', vis: { replacedBy: 6 } },
        { id: 6, type: 'group' },
        { id: 7, type: 'group' }
      ],
      links: [
        { id: 0, source: 0, target: 2, direction: true, vis: { newSource: 6, newTarget: 6 } },
        { id: 1, source: 2, target: 1, direction: true, vis: { newSource: 6, newTarget: 7, inMultilink: 9 } },
        { id: 2, source: 1, target: 0, vis: { newSource: 7, newTarget: 6, inMultilink: 9 } },
        { id: 3, source: 1, target: 3, direction: true, vis: { newSource: 7, newTarget: 6, inMultilink: 9 } },
        { id: 4, source: 2, target: 3, direction: false, vis: { newSource: 6, newTarget: 6 } },
        { id: 5, source: 4, target: 0, vis: { newSource: 7, newTarget: 6, inMultilink: 9 } },
        { id: 6, source: 5, target: 1, direction: { nonDirectional: true, sourceToTarget: false, targetToSource: true }, vis: { isArchived: true, isMultilink: true } },
        { id: 7, source: 5, target: 2, direction: { nonDirectional: true, sourceToTarget: true, targetToSource: false }, vis: { isArchived: true, isMultilink: true } },
        { id: 8, source: 6, target: 1, direction: { nonDirectional: true, sourceToTarget: true, targetToSource: true }, vis: { isArchived: true, isMultilink: true } },
        { id: 9, source: 7, target: 6, direction: { nonDirectional: true, sourceToTarget: true, targetToSource: true }, vis: { isMultilink: true } }
      ]
    }

    const stateAfterGrouping6And7 = {
      nodes: [
        { id: 0, type: 'regular', vis: { replacedBy: 5 } },
        { id: 1, type: 'regular', vis: { replacedBy: 7 } },
        { id: 2, type: 'regular', vis: { replacedBy: 6 } },
        { id: 3, type: 'regular', vis: { replacedBy: 5 } },
        { id: 4, type: 'regular', vis: { replacedBy: 7 } },
        { id: 5, type: 'group', vis: { replacedBy: 6 } },
        { id: 6, type: 'group', vis: { replacedBy: 8 } },
        { id: 7, type: 'group', vis: { replacedBy: 8 } },
        { id: 8, type: 'group' }
      ],
      links: [
        { id: 0, source: 0, target: 2, direction: true, vis: { newSource: 6, newTarget: 6 } },
        { id: 1, source: 2, target: 1, direction: true, vis: { newSource: 8, newTarget: 8 } },
        { id: 2, source: 1, target: 0, vis: { newSource: 8, newTarget: 8 } },
        { id: 3, source: 1, target: 3, direction: true, vis: { newSource: 8, newTarget: 8 } },
        { id: 4, source: 2, target: 3, direction: false, vis: { newSource: 6, newTarget: 6 } },
        { id: 5, source: 4, target: 0, vis: { newSource: 8, newTarget: 8 } },
        { id: 6, source: 5, target: 1, direction: { nonDirectional: true, sourceToTarget: false, targetToSource: true }, vis: { isArchived: true, isMultilink: true } },
        { id: 7, source: 5, target: 2, direction: { nonDirectional: true, sourceToTarget: true, targetToSource: false }, vis: { isArchived: true, isMultilink: true } },
        { id: 8, source: 6, target: 1, direction: { nonDirectional: true, sourceToTarget: true, targetToSource: true }, vis: { isArchived: true, isMultilink: true } },
        { id: 9, source: 7, target: 6, direction: { nonDirectional: true, sourceToTarget: true, targetToSource: true }, vis: { isArchived: true, isMultilink: true } }
      ]
    }

    const stateAfterUngrouping6 = {
      nodes: [
        { id: 0, type: 'regular', vis: { replacedBy: 5 } },
        { id: 1, type: 'regular', vis: { replacedBy: 7 } },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular', vis: { replacedBy: 5 } },
        { id: 4, type: 'regular', vis: { replacedBy: 7 } },
        { id: 5, type: 'group' },
        { id: 7, type: 'group' }
      ],
      links: [
        { id: 0, source: 0, target: 2, direction: true, vis: { newSource: 5, newTarget: 2, inMultilink: 7 } },
        { id: 1, source: 2, target: 1, direction: true, vis: { newSource: 2, newTarget: 7 } },
        { id: 2, source: 1, target: 0, vis: { newSource: 7, newTarget: 5, inMultilink: 8 } },
        { id: 3, source: 1, target: 3, direction: true, vis: { newSource: 7, newTarget: 5, inMultilink: 8 } },
        { id: 4, source: 2, target: 3, direction: false, vis: { newSource: 2, newTarget: 5, inMultilink: 7 } },
        { id: 5, source: 4, target: 0, vis: { newSource: 7, newTarget: 5, inMultilink: 8 } },
        { id: 6, source: 5, target: 1, direction: { nonDirectional: true, sourceToTarget: false, targetToSource: true }, vis: { isArchived: true, isMultilink: true } },
        { id: 7, source: 5, target: 2, direction: { nonDirectional: true, sourceToTarget: true, targetToSource: false }, vis: { isMultilink: true } },
        { id: 8, source: 7, target: 5, direction: { nonDirectional: true, sourceToTarget: true, targetToSource: false }, vis: { isMultilink: true } }
      ]
    }

    const stateAfterUngrouping5 = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular', vis: { replacedBy: 7 } },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' },
        { id: 4, type: 'regular', vis: { replacedBy: 7 } },
        { id: 7, type: 'group' }
      ],
      links: [
        { id: 0, source: 0, target: 2, direction: true },
        { id: 1, source: 2, target: 1, direction: true, vis: { newSource: 2, newTarget: 7 } },
        { id: 2, source: 1, target: 0, vis: { newSource: 7, newTarget: 0, inMultilink: 6 } },
        { id: 3, source: 1, target: 3, direction: true, vis: { newSource: 7, newTarget: 3 } },
        { id: 4, source: 2, target: 3, direction: false },
        { id: 5, source: 4, target: 0, vis: { newSource: 7, newTarget: 0, inMultilink: 6 } },
        { id: 6, source: 7, target: 0, direction: { nonDirectional: true, sourceToTarget: false, targetToSource: false }, vis: { isMultilink: true } }
      ]
    }

    // step 1: group 0+3
    const groupId5 = { type: 'group' }
    const payload = {
      nodeIds: [0, 3],
      targetGroupNode: groupId5
    }
    const expectedPayload = {
      nodeIds: [0, 3],
      targetGroupNode: { id: 5, ...groupId5 },
      newVisibleLinks: [
        { id: 5, source: 4, target: 5 },
        { id: 6, source: 5, target: 1 },
        { id: 7, source: 5, target: 2 }
      ],
      removedLinkIds: [0, 2, 3, 4, 5]
    }
    mutations.AGGREGATE_NODES(state, payload)
    expect(state).toEqual(stateAfterGrouping0And3)
    expect(payload).toEqual(expectedPayload)

    // step 2: group 5+2
    const groupId6 = { type: 'group' }
    const payload2 = {
      nodeIds: [2, 5],
      targetGroupNode: groupId6
    }
    const expectedPayload2 = {
      nodeIds: [2, 5],
      targetGroupNode: { id: 6, ...groupId6 },
      newVisibleLinks: [
        { id: 5, source: 4, target: 6 },
        { id: 8, source: 6, target: 1 }
      ],
      removedLinkIds: [1, 5, 6, 7]
    }
    mutations.AGGREGATE_NODES(state, payload2)
    expect(state).toEqual(stateAfterGrouping5And2)
    expect(payload2).toEqual(expectedPayload2)
    
    // step 3: group 1+4
    const groupId7 = { type: 'group' }
    const payload3 = {
      nodeIds: [1, 4],
      targetGroupNode: groupId7
    }
    const expectedPayload3 = {
      nodeIds: [1, 4],
      targetGroupNode: { id: 7, ...groupId7 },
      newVisibleLinks: [
        { id: 9, source: 7, target: 6 }
      ],
      removedLinkIds: [5, 8]
    }
    mutations.AGGREGATE_NODES(state, payload3)
    expect(state).toEqual(stateAfterGrouping1And4)
    expect(payload3).toEqual(expectedPayload3)

    // step 4: group 6+7
    const groupId8 = { type: 'group' }
    const payload4 = {
      nodeIds: [7, 6],
      targetGroupNode: groupId8
    }
    const expectedPayload4 = {
      nodeIds: [7, 6],
      targetGroupNode: { id: 8, ...groupId8 },
      newVisibleLinks: [
      ],
      removedLinkIds: [9]
    }
    mutations.AGGREGATE_NODES(state, payload4)
    expect(state).toEqual(stateAfterGrouping6And7)
    expect(payload4).toEqual(expectedPayload4)

    // step 5: ungroup 8
    const payload5 = {
      revertAction: 'group',
      nodeId: 8
    }
    const expectedPayload5 = {
      revertAction: 'group',
      nodeId: 8,
      removeNode: true,
      newVisibleLinks: [ { id: 9, source: 7, target: 6 } ],
      newVisibleNodes: [{ id: 6 }, { id: 7 }],
      removedLinkIds: []
    }
    mutations.DISAGGREGATE_NODE(state, payload5)
    expect(state).toEqual(stateAfterGrouping1And4)
    expect(payload5).toEqual(expectedPayload5)
  
    // step 6: ungroup 6
    const payload6 = {
      revertAction: 'group',
      nodeId: 6
    }
    const expectedPayload6 = {
      revertAction: 'group',
      nodeId: 6,
      removeNode: true,
      newVisibleLinks: [ { id: 1, source: 2, target: 7 }, { id: 7, source: 5, target: 2 }, { id: 8, source: 7, target: 5 } ],
      newVisibleNodes: [{ id: 2 }, { id: 5 }],
      removedLinkIds: [9]
    }
    mutations.DISAGGREGATE_NODE(state, payload6)
    expect(state).toEqual(stateAfterUngrouping6)
    expect(payload6).toEqual(expectedPayload6)

    // step 7: ungroup 5
    const payload7 = {
      revertAction: 'group',
      nodeId: 5
    }
    const expectedPayload7 = {
      revertAction: 'group',
      nodeId: 5,
      removeNode: true,
      newVisibleLinks: [ { id: 0, source: 0, target: 2 }, { id: 3, source: 7, target: 3 }, { id: 4, source: 2, target: 3 }, { id: 6, source: 7, target: 0 } ],
      newVisibleNodes: [{ id: 0 }, { id: 3 }],
      removedLinkIds: [7, 8]
    }
    mutations.DISAGGREGATE_NODE(state, payload7)
    expect(state).toEqual(stateAfterUngrouping5)
    expect(payload7).toEqual(expectedPayload7)

    // step 8: ungroup 7
    const payload8 = {
      revertAction: 'group',
      nodeId: 7
    }
    const expectedPayload8 = {
      revertAction: 'group',
      nodeId: 7,
      removeNode: true,
      newVisibleLinks: [ { id: 1, source: 2, target: 1 }, { id: 2, source: 1, target: 0 }, { id: 3, source: 1, target: 3 }, { id: 5, source: 4, target: 0 } ],
      newVisibleNodes: [{ id: 1 }, { id: 4 }],
      removedLinkIds: [1, 3, 6]
    }
    mutations.DISAGGREGATE_NODE(state, payload8)
    expect(state).toEqual(expectedFinalState)
    expect(payload8).toEqual(expectedPayload8)
  })
  
  it('bundle nodes, create group with the bundle, then ungroup', () => {
    const state = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' },
        { id: 4, type: 'regular' }
      ],
      links: [
        { id: 0, source: 3, target: 1, direction: false },
        { id: 1, source: 3, target: 0, direction: true },
        { id: 2, source: 4, target: 3, direction: false },
        { id: 3, source: 2, target: 3, direction: false },
        { id: 4, source: 0, target: 1, direction: true },
        { id: 5, source: 0, target: 2, direction: true }
      ]
    }
    const initialState = cloneDeep(state)
    const expectedStateAfterBundling0And2To3 = {
      nodes: [
        { id: 0, type: 'regular', vis: { bundledIn: 3 } },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular', vis: { bundledIn: 3 } },
        { id: 3, type: 'regular', vis: { bundledNodes: [0, 2] } },
        { id: 4, type: 'regular' }
      ],
      links: [
        { id: 0, source: 3, target: 1, direction: false, vis: { inMultilink: 6 } },
        { id: 1, source: 3, target: 0, direction: true, vis: { newSource: 3, newTarget: 3 } },
        { id: 2, source: 4, target: 3, direction: false },
        { id: 3, source: 2, target: 3, direction: false, vis: { newSource: 3, newTarget: 3 } },
        { id: 4, source: 0, target: 1, direction: true, vis: { newSource: 3, newTarget: 1, inMultilink: 6 } },
        { id: 5, source: 0, target: 2, direction: true, vis: { newSource: 3, newTarget: 3 } },
        { id: 6, source: 3, target: 1, vis: { isMultilink: true }, 
              direction: { nonDirectional: true, sourceToTarget: true, targetToSource: false }
        },
      ]
    }
    const expectedStateAfterGrouping3And4 = {
      nodes: [
        { id: 0, type: 'regular', vis: { bundledIn: 3 } },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular', vis: { bundledIn: 3 } },
        { id: 3, type: 'regular', vis: { bundledNodes: [0, 2], replacedBy: 5 } },
        { id: 4, type: 'regular', vis: { replacedBy: 5 } },
        { id: 5, type: 'group' }
      ],
      links: [
        { id: 0, source: 3, target: 1, direction: false, vis: { newSource: 5, newTarget: 1, inMultilink: 7 } },
        { id: 1, source: 3, target: 0, direction: true, vis: { newSource: 3, newTarget: 3 } },
        { id: 2, source: 4, target: 3, direction: false, vis: { newSource: 5, newTarget: 5 } },
        { id: 3, source: 2, target: 3, direction: false, vis: { newSource: 3, newTarget: 3 } },
        { id: 4, source: 0, target: 1, direction: true, vis: { newSource: 5, newTarget: 1, inMultilink: 7 } },
        { id: 5, source: 0, target: 2, direction: true, vis: { newSource: 3, newTarget: 3 } },
        { id: 6, source: 3, target: 1, vis: { isMultilink: true, isArchived: true }, 
              direction: { nonDirectional: true, sourceToTarget: true, targetToSource: false }
        },
        { id: 7, source: 5, target: 1, vis: { isMultilink: true}, 
              direction: { nonDirectional: true, sourceToTarget: true, targetToSource: false }
        }
      ]
    }

    // bundle 0,2 to 3, then create group 3+4
    mutations.AGGREGATE_NODES(state, { action: 'bundle', targetGroupNode: { id: 3, type: 'regular'}, nodeIds: [0, 2]})
    expect(state).toEqual(expectedStateAfterBundling0And2To3)
    
    mutations.AGGREGATE_NODES(state, { action: 'group', targetGroupNode: { type: 'group' }, nodeIds: [3, 4]})
    expect(state).toEqual(expectedStateAfterGrouping3And4)

    // dissagregate 5
    const payload = {
      revertAction: 'group',
      nodeId: 5
    }
    const expectedPayload = {
      revertAction: 'group',
      nodeId: 5,
      removeNode: true,
      newVisibleLinks: [
        { id: 2, source: 4, target: 3 },
        { id: 6, source: 3, target: 1 }
       ],
      removedLinkIds: [7],
      newVisibleNodes: [{ id: 3 }, { id: 4 }]
    }
    mutations.DISAGGREGATE_NODE(state, payload)
    expect(state).toEqual(expectedStateAfterBundling0And2To3)
    expect(payload).toEqual(expectedPayload)

    // unbundle 3
    const payload2 = {
      revertAction: 'bundle',
      nodeId: 3
    }
    const expectedPayload2 = {
      revertAction: 'bundle',
      nodeId: 3,
      removeNode: false,
      newVisibleLinks: [
        { id: 0, source: 3, target: 1 },
        { id: 1, source: 3, target: 0 },
        { id: 3, source: 2, target: 3 },
        { id: 4, source: 0, target: 1 },
        { id: 5, source: 0, target: 2 }
       ],
      removedLinkIds: [6], // maybe also 2?
      newVisibleNodes: [{ id: 0}, {id: 2}]
    }
    mutations.DISAGGREGATE_NODE(state, payload2)
    expect(state).toEqual(initialState)
    expect(payload2).toEqual(expectedPayload2)
  })
  
  it.skip('aggregate nodes in big dataset randomly into one group, then disaggregate all in random order; should match initial state; ignore payload for graph', () => {
    for (let i = 0; i < 100; i++) {
      const state = cloneDeep(aggregationRandomizedTestDataset) // load a big dataset
      const expectedState = cloneDeep(state)

      if (state.nodes.find(n => n.type === 'group'))
        throw Error('invalid input dataset, cannot contain groups')

      let nonGroupedNodeIds = []
      state.nodes.forEach(n => nonGroupedNodeIds.push(n.id))
      const groupedNodesIds = []

      // grouping
      while (nonGroupedNodeIds.length > 1) {
        let chooseCount = 0
        if (nonGroupedNodeIds.length < 6) chooseCount = nonGroupedNodeIds.length
        else chooseCount = random(2, 5)

        let chosenIds = []
        for (let i = 0; i < chooseCount; i++) {
          const chosenId = nonGroupedNodeIds[random(0, nonGroupedNodeIds.length - 1)]
          chosenIds.push(chosenId)
          nonGroupedNodeIds = nonGroupedNodeIds.filter(n => n !== chosenId)
        }

        if (chosenIds.length < 2) throw Error('chosen less then 2 nodes for grouping')
        const groupNode = { type: 'group' }
        mutations.AGGREGATE_NODES(state, {
          nodeIds: chosenIds,
          targetGroupNode: groupNode
        })
        //console.log('Grouped: ' + JSON.stringify(chosenIds) + ' in: ' + groupNode.id)

        chosenIds.forEach(id => groupedNodesIds.push(id))

        // sometimes, do random ungroup
        if (random(0, 100) < 40) {
          const visible = filterVisibleNodes(state.nodes).filter(n => n.type === 'group')
          const selectedId = visible[random(0, visible.length - 1)].id
          mutations.DISAGGREGATE_NODE(state, { revertAction: 'group', nodeId: selectedId })
        }
      }

      // ungrouping
      while (state.nodes.find(n => n.type === 'group')) {
        const visible = filterVisibleNodes(state.nodes).filter(n => n.type === 'group')
        const selectedId = visible[random(0, visible.length - 1)].id
        //console.log('Selected to ungroup: ' + selectedId)
        mutations.DISAGGREGATE_NODE(state, { revertAction: 'group', nodeId: selectedId })
      }

      expect(state).toEqual(expectedState)
    }
  })
})

describe('store/data/validateDataset', () => {
  it('succedes', () => {
    const data = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' },
        { id: 4, type: 'regular' }
      ],
      links: [
        { id: 0, source: 3, target: 1 },
        { id: 1, source: 3, target: 0 },
        { id: 2, source: 4, target: 3 },
        { id: 3, source: 2, target: 3 },
        { id: 4, source: 0, target: 1 }
      ]
    }
    expect(() => validateDataset(data)).not.toThrow()
  })

  it('succedes with multilinks', () => {
    const data = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' },
        { id: 4, type: 'regular' }
      ],
      links: [
        { id: 0, source: 3, target: 1 },
        { id: 1, source: 3, target: 0 },
        { id: 2, source: 4, target: 3 },
        { id: 3, source: 2, target: 3 },
        { id: 4, source: 0, target: 1, vis: { inMultilink: 6 } },
        { id: 5, source: 1, target: 0, vis: { inMultilink: 6 } },
        { id: 6, source: 0, target: 1, vis: { isMultilink: true } }
      ]
    }
    expect(() => validateDataset(data)).not.toThrow()
  })

  it('fails when node ID is not unique', () => {
    const data = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular' },
        { id: 3, type: 'group' },
        { id: 3, type: 'regular' },
        { id: 4, type: 'regular' }
      ],
      links: []
    }
    expect(() => validateDataset(data)).toThrow('ID of node is not unique')
  })

  it('fails when link ID is not unique', () => {
    const data = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' },
        { id: 4, type: 'regular' }
      ],
      links: [
        { id: 0, source: 3, target: 1 },
        { id: 1, source: 3, target: 0 },
        { id: 2, source: 4, target: 3 },
        { id: 2, source: 2, target: 3 },
        { id: 4, source: 0, target: 1 }
      ]
    }
    expect(() => validateDataset(data)).toThrow('ID of link is not unique')
  })

  it('fails when node is missing ID', () => {
    const data = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular' },
        { type: 'group' },
        { id: 3, type: 'regular' },
        { id: 4, type: 'regular' }
      ],
      links: []
    }
    expect(() => validateDataset(data)).toThrow('Node must have ID assigned')
  })

  it('fails when link is missing ID', () => {
    const data = {
      nodes: [],
      links: [
        { source: 4, target: 3 }
      ]
    }
    expect(() => validateDataset(data)).toThrow('Link must have ID assigned')
  })

  it('fails when link is connected to node which does not exist', () => {
    const data = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' }
      ],
      links: [
        { id: 0, source: 3, target: 1 },
        { id: 1, source: 3, target: 0 },
        { id: 2, source: 4, target: 3 },
        { id: 3, source: 7, target: 3 },
        { id: 4, source: 0, target: 1 }
      ]
    }
    expect(() => validateDataset(data)).toThrow('Link is connected to nonexistent node')
  })

  it('fails when links overlap and dont have multilink', () => {
    const data = {
      nodes: [
        { id: 0, type: 'regular' },
        { id: 1, type: 'regular' },
        { id: 2, type: 'regular' },
        { id: 3, type: 'regular' },
        { id: 4, type: 'regular' }
      ],
      links: [
        { id: 0, source: 3, target: 1 },
        { id: 1, source: 3, target: 0 },
        { id: 2, source: 4, target: 3 },
        { id: 3, source: 3, target: 4 },
        { id: 4, source: 2, target: 3 },
        { id: 5, source: 0, target: 1 }
      ]
    }
    expect(() => validateDataset(data)).toThrow('multilink')
  })
})

describe('helpers', () => {
  it('createLink', () => {
    const state = { links: [] }
    const expectedState = { links: [{ id: 0, source: 42, target: 24, vis: { inAggregation: 3 } }] }

    const link = createLink(0, 42, 24, { inAggregation: 3 })
    state.links.push(link)

    expect(state).toEqual(expectedState)
    expect(link).toEqual(state.links[0])
  })
})
