import { test } from '@japa/runner'
import {
  DataEntityKind,
  Thing,
  ValidationError,
  RemovePropertyException,
  RemoveAssociationException,
  RemoveEntityException,
  ForeignAssociationException,
} from '../../../../src/index.js'
import {
  DataNamespace,
  DataEntity,
  DataProperty,
  IDataEntity,
  DataModel,
  DataAssociation,
} from '../../../../src/legacy.js'

test.group('constructor() with defaults', (group) => {
  let root: DataNamespace
  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('sets the kind', ({ assert }) => {
    const assoc = new DataEntity(root)
    assert.equal(assoc.kind, DataEntityKind)
  })

  test('sets the key', ({ assert }) => {
    const assoc = new DataEntity(root)
    assert.typeOf(assoc.key, 'string')
    assert.isNotEmpty(assoc.key)
  })

  test('sets the default "info"', ({ assert }) => {
    const assoc = new DataEntity(root)
    assert.typeOf(assoc.info, 'EventTarget')
    assert.equal(assoc.info.name, '')
  })

  test('sets the default "tags"', ({ assert }) => {
    const assoc = new DataEntity(root)
    assert.deepEqual(assoc.tags, [])
  })

  test('sets the default "taxonomy"', ({ assert }) => {
    const assoc = new DataEntity(root)
    assert.deepEqual(assoc.taxonomy, [])
  })

  test('sets the default "properties"', ({ assert }) => {
    const assoc = new DataEntity(root)
    assert.deepEqual(assoc.properties, [])
  })

  test('sets the default "associations"', ({ assert }) => {
    const assoc = new DataEntity(root)
    assert.deepEqual(assoc.associations, [])
  })

  test('sets the default "parents"', ({ assert }) => {
    const assoc = new DataEntity(root)
    assert.deepEqual(assoc.parents, [])
  })

  test('does not set deprecated', ({ assert }) => {
    const assoc = new DataProperty(root)
    assert.isUndefined(assoc.deprecated)
  })
})

test.group('constructor() from schema initialization', (group) => {
  let root: DataNamespace
  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('sets the key', ({ assert }) => {
    const orig = new DataEntity(root).toJSON()
    orig.key = 'test'
    const instance = new DataEntity(root, orig)
    assert.equal(instance.key, 'test')
  })

  test('sets the info', ({ assert }) => {
    const orig = new DataEntity(root).toJSON()
    orig.info.name = 'test'
    const instance = new DataEntity(root, orig)
    assert.equal(instance.info.name, 'test')
  })

  test('sets the "tags"', ({ assert }) => {
    const orig = new DataEntity(root).toJSON()
    orig.tags = ['test']
    const assoc = new DataEntity(root, orig)
    assert.deepEqual(assoc.tags, ['test'])
  })

  test('sets the "taxonomy"', ({ assert }) => {
    const orig = new DataEntity(root).toJSON()
    orig.taxonomy = ['test']
    const assoc = new DataEntity(root, orig)
    assert.deepEqual(assoc.taxonomy, ['test'])
  })

  test('initializes from JSON schema ', ({ assert }) => {
    const orig = new DataEntity(root).toJSON()
    orig.key = 'test'
    const instance = new DataEntity(root, JSON.stringify(orig))
    assert.equal(instance.key, 'test')
  })

  test('sets the deprecated', ({ assert }) => {
    const orig = new DataEntity(root).toJSON()
    orig.deprecated = true
    const assoc = new DataEntity(root, orig)
    assert.isTrue(assoc.deprecated)
  })
})

test.group('fromName()', (group) => {
  let root: DataNamespace
  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('sets the name', ({ assert }) => {
    const assoc = DataEntity.fromName(root, 'test')
    assert.equal(assoc.info.name, 'test')
  })
})

test.group('new()', (group) => {
  let root: DataNamespace
  let base: IDataEntity
  group.each.setup(() => {
    root = new DataNamespace()
    base = {
      info: Thing.fromName('test name').toJSON(),
      key: 'test123',
      kind: DataEntityKind,
    }
  })

  test('sets the key', ({ assert }) => {
    const assoc = new DataEntity(root)
    assoc.new(base)
    assert.equal(assoc.key, 'test123')
  })

  test('sets the info', ({ assert }) => {
    const assoc = new DataEntity(root)
    assoc.new(base)
    assert.equal(assoc.info.name, 'test name')
  })

  test('sets default info', ({ assert }) => {
    const assoc = new DataEntity(root)
    // @ts-expect-error Used in testing
    delete base.info
    assoc.new(base)
    assert.equal(assoc.info.name, '')
  })

  test('sets the tags as a copy', ({ assert }) => {
    const assoc = new DataEntity(root)
    const init = { ...base, tags: ['a'] }
    assoc.new(init)
    assert.deepEqual(assoc.tags, ['a'])
    init.tags.push('b')
    assert.deepEqual(assoc.tags, ['a'])
  })

  test('resets tags when not in the input', ({ assert }) => {
    const assoc = new DataEntity(root)
    assoc.tags = ['a']
    assoc.new(base)
    assert.deepEqual(assoc.tags, [])
  })

  test('sets the parents as a copy', ({ assert }) => {
    const assoc = new DataEntity(root)
    const init = { ...base, parents: ['a'] }
    assoc.new(init)
    assert.deepEqual(assoc.parents, ['a'])
    init.parents.push('b')
    assert.deepEqual(assoc.parents, ['a'])
  })

  test('resets parents when not in the input', ({ assert }) => {
    const assoc = new DataEntity(root)
    assoc.parents = ['a']
    assoc.new(base)
    assert.deepEqual(assoc.parents, [])
  })

  test('sets the properties as a copy', ({ assert }) => {
    const src = new DataEntity(root)
    const p1 = src.addNamedProperty('p1')
    const assoc = new DataEntity(root)
    const init = src.toJSON()
    assoc.new(init)
    assert.deepEqual(assoc.properties, [p1])
    const p2 = src.addNamedProperty('p2')
    init.properties!.push(p2.key)
    assert.deepEqual(assoc.properties, [p1])
  })

  test('resets properties when not in the input', ({ assert }) => {
    const assoc = new DataEntity(root)
    assoc.addNamedProperty('test')
    assoc.new(base)
    assert.deepEqual(assoc.properties, [])
  })

  test('sets the associations as a copy', ({ assert }) => {
    const src = new DataEntity(root)
    const a1 = src.addNamedAssociation('a1')
    const assoc = new DataEntity(root)
    const init = src.toJSON()
    assoc.new(init)
    assert.deepEqual(assoc.associations, [a1])
    const a2 = src.addNamedAssociation('a2')
    init.associations!.push(a2.key)
    assert.deepEqual(assoc.associations, [a1])
  })

  test('resets associations when not in the input', ({ assert }) => {
    const assoc = new DataEntity(root)
    assoc.addNamedAssociation('test')
    assoc.new(base)
    assert.deepEqual(assoc.associations, [])
  })

  test('sets the taxonomy as a copy', ({ assert }) => {
    const assoc = new DataEntity(root)
    const init = { ...base, taxonomy: ['a'] }
    assoc.new(init)
    assert.deepEqual(assoc.taxonomy, ['a'])
    init.taxonomy.push('b')
    assert.deepEqual(assoc.taxonomy, ['a'])
  })

  test('resets taxonomy when not in the input', ({ assert }) => {
    const assoc = new DataEntity(root)
    assoc.taxonomy = ['a']
    assoc.new(base)
    assert.deepEqual(assoc.taxonomy, [])
  })

  test('sets the deprecated', ({ assert }) => {
    const assoc = new DataEntity(root)
    assoc.new({ ...base, deprecated: true })
    assert.isTrue(assoc.deprecated)
  })

  test('does not set deprecated when not in the input', ({ assert }) => {
    const assoc = new DataEntity(root)
    assoc.new(base)
    assert.isUndefined(assoc.deprecated)
  })

  test('throws when unknown input', ({ assert }) => {
    const assoc = new DataEntity(root)
    assert.throws(() => {
      // @ts-expect-error Used in testing
      assoc.new({})
    }, 'Invalid data entity definition.')
  })

  test('sets the fields as a copy', ({ assert }) => {
    const src = new DataEntity(root)
    const p1 = src.addNamedProperty('p1')
    const a1 = src.addNamedAssociation('a1')
    const assoc = new DataEntity(root)
    const init = src.toJSON()
    assoc.new(init)
    assert.deepEqual(assoc.fields, [
      { type: 'property', key: p1.key },
      { type: 'association', key: a1.key },
    ])
    const p2 = src.addNamedProperty('p2')
    init.fields!.push({ type: 'property', key: p2.key })
    assert.deepEqual(assoc.fields, [
      { type: 'property', key: p1.key },
      { type: 'association', key: a1.key },
    ])
  })

  test('resets fields when not in the input', ({ assert }) => {
    const assoc = new DataEntity(root)
    assoc.addNamedProperty('p1')
    assoc.addNamedAssociation('a1')
    assoc.new(base)
    assert.deepEqual(assoc.fields, [])
  })

  test('creates fields when missing', ({ assert }) => {
    const assoc = new DataEntity(root)
    const p1 = assoc.addNamedProperty('p1')
    const a1 = assoc.addNamedAssociation('a1')
    const init = assoc.toJSON()
    delete init.fields
    const instance = new DataEntity(root)
    instance.new(init)
    assert.deepEqual(instance.fields, [
      { type: 'property', key: p1.key },
      { type: 'association', key: a1.key },
    ])
  })
})

test.group('toJSON()', (group) => {
  let root: DataNamespace
  let base: DataEntity
  group.each.setup(() => {
    root = new DataNamespace()
    base = new DataEntity(root, {
      info: Thing.fromName('test name').toJSON(),
      key: 'test123',
      kind: DataEntityKind,
    })
  })

  test('serializes the kind', ({ assert }) => {
    const result = base.toJSON()
    assert.equal(result.kind, DataEntityKind)
  })

  test('serializes the key', ({ assert }) => {
    const result = base.toJSON()
    assert.equal(result.key, base.key)
  })

  test('serializes the info', ({ assert }) => {
    const result = base.toJSON()
    assert.equal(result.info.name, 'test name')
  })

  test('does not serialize taxonomy by default', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.taxonomy)
  })

  test('does not serialize tags by default', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.tags)
  })

  test('does not serialize parents by default', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.parents)
  })

  test('does not serialize properties by default', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.properties)
  })

  test('does not serialize associations by default', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.associations)
  })

  test('serializes tags as a copy', ({ assert }) => {
    base.tags = ['a']
    const result = base.toJSON()
    assert.deepEqual(result.tags, ['a'])
    base.tags.push('b')
    assert.deepEqual(result.tags, ['a'])
  })

  test('serializes taxonomy as a copy', ({ assert }) => {
    base.taxonomy = ['a']
    const result = base.toJSON()
    assert.deepEqual(result.taxonomy, ['a'])
    base.taxonomy.push('b')
    assert.deepEqual(result.taxonomy, ['a'])
  })

  test('serializes parents as a copy', ({ assert }) => {
    base.parents = ['a']
    const result = base.toJSON()
    assert.deepEqual(result.parents, ['a'])
    base.parents.push('b')
    assert.deepEqual(result.parents, ['a'])
  })

  test('serializes properties as a copy', ({ assert }) => {
    const p1 = base.addNamedProperty('p1')
    const result = base.toJSON()
    assert.deepEqual(result.properties, [p1.key])
    base.addNamedProperty('p2')
    assert.deepEqual(result.properties, [p1.key])
  })

  test('serializes associations as a copy', ({ assert }) => {
    const a1 = base.addNamedAssociation('a1')
    const result = base.toJSON()
    assert.deepEqual(result.associations, [a1.key])
    base.addNamedAssociation('a2')
    assert.deepEqual(result.associations, [a1.key])
  })

  test('serializes fields as a copy', ({ assert }) => {
    const p1 = base.addNamedProperty('p1')
    const a1 = base.addNamedAssociation('a1')
    const result = base.toJSON()
    assert.deepEqual(result.fields, [
      { type: 'property', key: p1.key },
      { type: 'association', key: a1.key },
    ])
    base.addNamedProperty('p2')
    assert.deepEqual(result.fields, [
      { type: 'property', key: p1.key },
      { type: 'association', key: a1.key },
    ])
  })

  test('does not serialize fields when empty', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.fields)
  })

  test('does not serialize deprecated by default', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.deprecated)
  })

  test('serialize the set deprecated', ({ assert }) => {
    base.deprecated = false
    const result = base.toJSON()
    assert.isFalse(result.deprecated)
  })
})

test.group('remove()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let e2: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    e2 = m1.addEntity('e2')
    e1.addNamedProperty('test prop')
    e1.addTargetAssociation(e2.key)
  })

  test('removes self from the parent data model', ({ assert }) => {
    e1.remove()
    assert.deepEqual(m1.entities, [e2])
  })

  test('removes self from the root definitions', ({ assert }) => {
    e1.remove()
    assert.deepEqual(root.definitions.entities, [e2])
  })

  test('removes own properties', ({ assert }) => {
    const p2 = e2.addNamedProperty('other')
    e1.remove()
    assert.deepEqual(e2.properties, [p2])
    assert.deepEqual(root.definitions.properties, [p2])
  })

  test('removes own associations', ({ assert }) => {
    const e3 = m1.addEntity('e3')
    const a2 = e2.addTargetAssociation(e3.key)
    e1.remove()
    assert.deepEqual(e2.associations, [a2])
    assert.deepEqual(root.definitions.associations, [a2])
  })

  test('throws when removing an entity that is a target', ({ assert }) => {
    e2.addTargetAssociation(e1.key)
    assert.throws(
      () => e1.remove(),
      RemoveEntityException as unknown as ErrorConstructor,
      `Cannot remove entity ${e1.info.name} because it is used as a target in associations in entities: e2.`
    )
  })

  test('force removes an entity that is a target', ({ assert }) => {
    e2.addTargetAssociation(e1.key)
    e1.remove({ force: true })
    assert.deepEqual(m1.entities, [e2])
    assert.deepEqual(root.definitions.entities, [e2])
    assert.deepEqual(e2.associations, [], 'removes the association from the target entity')
    assert.deepEqual(e2.fields, [], 'removes the association from the entity fields')
    assert.deepEqual(root.definitions.associations, [], 'removes the association from the root definitions')
  })

  test('throws when removing an entity that is a parent', ({ assert }) => {
    const e3 = m1.addEntity('e3')
    e3.addParent(e1.key)
    assert.throws(
      () => e1.remove(),
      RemoveEntityException as unknown as ErrorConstructor,
      `Cannot remove entity ${e1.info.name} because it is a parent for the following entities: e3.`
    )
  })

  test('force removes an entity that is a parent', ({ assert }) => {
    const e3 = m1.addEntity('e3')
    e3.addParent(e1.key)
    e1.remove({ force: true })
    assert.deepEqual(m1.entities, [e2, e3])
    assert.deepEqual(root.definitions.entities, [e2, e3])
    assert.deepEqual(e3.parents, [], 'removes the parent from the child entity')
  })

  test('force removes an entity that is a parent and a target', ({ assert }) => {
    const e3 = m1.addEntity('e3')
    e3.addParent(e1.key)
    e2.addTargetAssociation(e1.key)
    e1.remove({ force: true })
    assert.deepEqual(m1.entities, [e2, e3])
    assert.deepEqual(root.definitions.entities, [e2, e3])
    assert.deepEqual(e3.parents, [], 'removes the parent from the child entity')
    assert.deepEqual(e2.associations, [], 'removes the association from the target entity')
    assert.deepEqual(root.definitions.associations, [], 'removes the association from the root definitions')
  })
})

test.group('addTypedProperty()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let e2: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    e2 = m1.addEntity('e2')
  })

  test('adds the property to the entity', ({ assert }) => {
    const p1 = e1.addTypedProperty('boolean')
    assert.deepEqual(e1.properties, [p1])
  })

  test('adds the property type', ({ assert }) => {
    const p1 = e1.addTypedProperty('boolean')
    assert.equal(p1.type, 'boolean')
  })

  test('adds the property to the root definitions', ({ assert }) => {
    const p1 = e1.addTypedProperty('boolean')
    assert.deepEqual(root.definitions.properties, [p1])
  })

  test('does not change other entities', ({ assert }) => {
    e1.addTypedProperty('boolean')
    assert.deepEqual(e2.properties, [])
  })
})

test.group('addNamedProperty()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let e2: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    e2 = m1.addEntity('e2')
  })

  test('adds the property to the entity', ({ assert }) => {
    const p1 = e1.addNamedProperty('hello')
    assert.deepEqual(e1.properties, [p1])
  })

  test('adds the property type', ({ assert }) => {
    const p1 = e1.addNamedProperty('hello')
    assert.equal(p1.info.name, 'hello')
  })

  test('adds the property to the root definitions', ({ assert }) => {
    const p1 = e1.addNamedProperty('hello')
    assert.deepEqual(root.definitions.properties, [p1])
  })

  test('does not change other entities', ({ assert }) => {
    e1.addNamedProperty('hello')
    assert.deepEqual(e2.properties, [])
  })
})

test.group('removeProperty()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let e2: DataEntity
  let p1: DataProperty

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    e2 = m1.addEntity('e2')
    p1 = e1.addNamedProperty('p2')
  })

  test('removes the property from the entity', ({ assert }) => {
    e1.removeProperty(p1.key)
    assert.deepEqual(e1.properties, [])
  })

  test('removes the property from the root definitions', ({ assert }) => {
    e1.removeProperty(p1.key)
    assert.deepEqual(root.definitions.properties, [])
  })

  test('keeps other properties', ({ assert }) => {
    const p2 = e1.addNamedProperty('p2')
    const p3 = e2.addNamedProperty('p3')
    e1.removeProperty(p1.key)
    assert.deepEqual(e1.properties, [p2])
    assert.deepEqual(root.definitions.properties, [p2, p3])
  })

  test('throws when the property belongs to another data entity', ({ assert }) => {
    const p2 = e1.addNamedProperty('p2')
    const p3 = e2.addNamedProperty('p3')
    assert.throws(
      () => {
        e1.removeProperty(p3.key)
      },
      RemovePropertyException as unknown as ErrorConstructor,
      `Trying to remove the ${p3.key} property, but it doesn't exist.`
    )
    assert.deepEqual(e1.properties, [p1, p2])
    assert.deepEqual(root.definitions.properties, [p1, p2, p3])
  })

  test('throws when the property does not exist', ({ assert }) => {
    assert.throws(
      () => {
        e1.removeProperty('test')
      },
      RemovePropertyException as unknown as ErrorConstructor,
      `Trying to remove the test property, but it doesn't exist.`
    )
    assert.deepEqual(e1.properties, [p1])
    assert.deepEqual(root.definitions.properties, [p1])
  })
})

test.group('addNamedAssociation()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let e2: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    e2 = m1.addEntity('e2')
  })

  test('adds the association to the entity', ({ assert }) => {
    const a1 = e1.addNamedAssociation('test')
    assert.deepEqual(e1.associations, [a1])
  })

  test('adds the association to the root definitions', ({ assert }) => {
    const a1 = e1.addNamedAssociation('test')
    assert.deepEqual(root.definitions.associations, [a1])
  })

  test('adds the association name', ({ assert }) => {
    const a1 = e1.addNamedAssociation('test')
    assert.deepEqual(a1.info.name, 'test')
  })

  test('does not change other entities', ({ assert }) => {
    e1.addNamedAssociation('test')
    assert.deepEqual(e2.associations, [])
  })
})

test.group('addTargetAssociation()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let e2: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    e2 = m1.addEntity('e2')
  })

  test('adds the association to the entity', ({ assert }) => {
    const a1 = e1.addTargetAssociation(e2.key)
    assert.deepEqual(e1.associations, [a1])
  })

  test('adds the association to the root definitions', ({ assert }) => {
    const a1 = e1.addTargetAssociation(e2.key)
    assert.deepEqual(root.definitions.associations, [a1])
  })

  test('adds the association target', ({ assert }) => {
    const a1 = e1.addTargetAssociation(e2.key)
    assert.deepEqual(a1.targets, [{ key: e2.key }])
  })

  test('does not change other entities', ({ assert }) => {
    e1.addTargetAssociation(e2.key)
    assert.deepEqual(e2.associations, [])
  })
})

test.group('addForeignAssociation()', (group) => {
  let n1: DataNamespace
  let n2: DataNamespace
  let m1: DataModel
  let m2: DataModel
  let e1: DataEntity
  let e2: DataEntity

  group.each.setup(() => {
    n1 = new DataNamespace()
    n2 = new DataNamespace()
    n1.foreign.push(n2)
    m1 = n1.addDataModel('m1')
    m2 = n2.addDataModel('m2')
    e1 = m1.addEntity('e1')
    e2 = m2.addEntity('e2')
  })

  test('adds the association to the entity', ({ assert }) => {
    const a1 = e1.addForeignAssociation(e2.key, n2.key)
    assert.deepEqual(e1.associations, [a1])
  })

  test('adds the association to the root definitions', ({ assert }) => {
    const a1 = e1.addForeignAssociation(e2.key, n2.key)
    assert.deepEqual(n1.definitions.associations, [a1])
  })

  test('adds the association target', ({ assert }) => {
    const a1 = e1.addForeignAssociation(e2.key, n2.key)
    assert.deepEqual(a1.targets, [{ key: e2.key, namespace: n2.key }])
  })

  test('does not change other entities', ({ assert }) => {
    e1.addForeignAssociation(e2.key, n2.key)
    assert.deepEqual(e2.associations, [])
  })

  test('sets the association name', ({ assert }) => {
    const a1 = e1.addForeignAssociation(e2.key, n2.key, 'my name')
    assert.equal(a1.info.name, 'my name')
  })

  test('throws when the foreign namespace is not defined', ({ assert }) => {
    assert.throws(
      () => e1.addForeignAssociation(e2.key, 'unknown'),
      ForeignAssociationException as unknown as ErrorConstructor,
      'Trying to add a foreign association but the foreign namespace is not defined.'
    )
  })

  test('throws when adding a foreign association that targets the same entity', ({ assert }) => {
    assert.throws(
      () => e1.addForeignAssociation(e1.key, n1.key),
      ForeignAssociationException as unknown as ErrorConstructor,
      'Trying to add a foreign association but the foreign namespace is not defined.'
    )
  })
})

test.group('removeAssociation()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let e2: DataEntity
  let a1: DataAssociation

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    e2 = m1.addEntity('e2')
    a1 = e1.addTargetAssociation(e2.key)
  })

  test('removes the association from the entity', ({ assert }) => {
    e1.removeAssociation(a1.key)
    assert.deepEqual(e1.associations, [])
  })

  test('removes the association from the root definitions', ({ assert }) => {
    e1.removeAssociation(a1.key)
    assert.deepEqual(root.definitions.associations, [])
  })

  test('keeps other associations', ({ assert }) => {
    const e3 = m1.addEntity('e3')
    const a2 = e1.addTargetAssociation(e2.key)
    const a3 = e2.addTargetAssociation(e3.key)
    e1.removeAssociation(a1.key)
    assert.deepEqual(e1.associations, [a2])
    assert.deepEqual(root.definitions.associations, [a2, a3])
  })

  test('throws when the association belongs to another data entity', ({ assert }) => {
    const e3 = m1.addEntity('e3')
    const a2 = e1.addTargetAssociation(e2.key)
    const a3 = e2.addTargetAssociation(e3.key)
    assert.throws(
      () => e1.removeAssociation(a3.key),
      RemoveAssociationException as unknown as ErrorConstructor,
      `Trying to remove the ${a3.key} association, but it doesn't exist.`
    )
    assert.deepEqual(e1.associations, [a1, a2])
    assert.deepEqual(root.definitions.associations, [a1, a2, a3])
  })

  test('throws when the association does not exist', ({ assert }) => {
    assert.throws(
      () => e1.removeAssociation('test'),
      RemoveAssociationException as unknown as ErrorConstructor,
      `Trying to remove the test association, but it doesn't exist.`
    )
    assert.deepEqual(e1.associations, [a1])
    assert.deepEqual(root.definitions.associations, [a1])
  })
})

test.group('getComputedParents()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
  })

  test('returns empty array when no parents', ({ assert }) => {
    const result = e1.getComputedParents()
    assert.deepEqual(result, [])
  })

  test('returns a parent', ({ assert }) => {
    const e2 = m1.addEntity('e2')
    e1.parents.push(e2.key)
    const result = e1.getComputedParents()
    assert.deepEqual(result, [e2])
  })

  test('returns only direct parents by default', ({ assert }) => {
    const e2 = m1.addEntity('e2')
    const e3 = m1.addEntity('e3')
    e2.parents.push(e3.key)
    e1.parents.push(e2.key)
    const result = e1.getComputedParents()
    assert.deepEqual(result, [e2])
  })

  test('returns a parent with their parents', ({ assert }) => {
    const e2 = m1.addEntity('e2')
    const e3 = m1.addEntity('e3')
    e2.parents.push(e3.key)
    e1.parents.push(e2.key)
    const result = e1.getComputedParents(true)
    assert.deepEqual(result, [e2, e3])
  })

  test('returns all parents and their parents', ({ assert }) => {
    const e2 = m1.addEntity('e2')
    const e3 = m1.addEntity('e3')
    const e4 = m1.addEntity('e4')
    e2.parents.push(e3.key)
    e1.parents.push(e2.key)
    e1.parents.push(e4.key)
    const result = e1.getComputedParents(true)
    assert.deepEqual(result, [e2, e3, e4])
  })
})

test.group('getComputedChildren()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
  })

  test('returns empty array when no children', ({ assert }) => {
    const result = e1.getComputedChildren()
    assert.deepEqual(result, [])
  })

  test('returns the direct children', ({ assert }) => {
    const e2 = m1.addEntity('e2')
    const e3 = m1.addEntity('e3')
    e2.parents.push(e1.key)
    e3.parents.push(e2.key)

    const result = e1.getComputedChildren()
    assert.deepEqual(result, [e2])
  })

  test('returns grandchildren', ({ assert }) => {
    const e2 = m1.addEntity('e2')
    const e3 = m1.addEntity('e3')
    e2.addParent(e1.key)
    e3.addParent(e2.key)
    const result = e1.getComputedChildren()
    assert.deepEqual(result, [e2])
  })

  test('returns empty array when no direct children', ({ assert }) => {
    const e2 = m1.addEntity('e2')
    const e3 = m1.addEntity('e3')
    e3.addParent(e2.key)
    const result = e1.getComputedChildren()
    assert.deepEqual(result, [])
  })
})

test.group('getComputedAssociations()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
  })

  test('returns empty array when no association', ({ assert }) => {
    const result = e1.getComputedAssociations()
    assert.deepEqual(result, [])
  })

  test('returns the association targets', ({ assert }) => {
    const e2 = m1.addEntity('e2')
    const e3 = m1.addEntity('e3')
    e1.addTargetAssociation(e2.key)
    e2.addTargetAssociation(e3.key)

    const result = e1.getComputedAssociations()
    assert.deepEqual(result, [e2])
  })

  test('ignores when the target is not defined', ({ assert }) => {
    e1.addNamedAssociation('hello')
    const result = e1.getComputedAssociations()
    assert.deepEqual(result, [])
  })

  test('ignores when the target schema is not defined', ({ assert }) => {
    e1.addTargetAssociation('other')
    const result = e1.getComputedAssociations()
    assert.deepEqual(result, [])
  })

  test('throws for missing foreign entities', ({ assert }) => {
    assert.throws(() => {
      e1.addForeignAssociation('entity', 'namespace')
    }, `Trying to add a foreign association but the foreign namespace is not defined.`)
  })

  test('finds entities in a foreign namespaces', ({ assert }) => {
    const f1 = new DataNamespace()
    const fm1 = f1.addDataModel('fm1')
    const fe1 = fm1.addEntity('fe1')
    root.foreign.push(f1)
    e1.addForeignAssociation(fe1.key, f1.key)
    const result = e1.getComputedAssociations()
    assert.deepEqual(result, [fe1])
  })

  test('ignores unknown entities in a foreign namespaces', ({ assert }) => {
    const f1 = new DataNamespace()
    const fm1 = f1.addDataModel('fm1')
    fm1.addEntity('fe1')
    root.foreign.push(f1)
    e1.addForeignAssociation('other', f1.key)
    const result = e1.getComputedAssociations()
    assert.deepEqual(result, [])
  })

  test('returns multiple associations', ({ assert }) => {
    const e2 = m1.addEntity('e2')
    const e3 = m1.addEntity('e3')
    e1.addTargetAssociation(e2.key)
    e1.addTargetAssociation(e3.key)

    const result = e1.getComputedAssociations()
    assert.deepEqual(result.sort(), [e2, e3].sort())
  })

  test('returns indirect associations', ({ assert }) => {
    const e2 = m1.addEntity('e2')
    const e3 = m1.addEntity('e3')
    e1.addTargetAssociation(e2.key)
    e2.addTargetAssociation(e3.key)

    const result = e1.getComputedAssociations()
    assert.deepEqual(result, [e2])
  })

  test('returns multiple targets', ({ assert }) => {
    const e2 = m1.addEntity('e2')
    const e3 = m1.addEntity('e3')
    const a1 = e1.addNamedAssociation('a1')
    a1.addTarget(e2)
    a1.addTarget(e3)

    const result = e1.getComputedAssociations()
    assert.deepEqual(result.sort(), [e2, e3].sort())
  })
})

test.group('associationPath()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let e2: DataEntity
  let e3: DataEntity
  let e4: DataEntity
  let e5: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    e2 = m1.addEntity('e2')
    e3 = m1.addEntity('e3')
    e4 = m1.addEntity('e4')
    e5 = m1.addEntity('e5')
  })

  test('finds a direct connection', ({ assert }) => {
    e1.addTargetAssociation(e2.key)
    const paths: string[][] = []
    for (const path of e1.associationPath(e2.key)) {
      paths.push(path)
    }
    assert.lengthOf(paths, 1, 'has a single path')
    assert.deepEqual(paths[0], [e1.key, e2.key], 'has the path')
  })

  test('finds a connection through another entity', ({ assert }) => {
    e1.addTargetAssociation(e2.key)
    e2.addTargetAssociation(e3.key)
    const paths: string[][] = []
    for (const path of e1.associationPath(e3.key)) {
      paths.push(path)
    }
    assert.lengthOf(paths, 1, 'has a single path')
    assert.deepEqual(paths[0], [e1.key, e2.key, e3.key], 'has the path')
  })

  test('returns a connection to self', ({ assert }) => {
    e1.addTargetAssociation(e1.key)
    const paths: string[][] = []
    for (const path of e1.associationPath(e1.key)) {
      paths.push(path)
    }
    assert.lengthOf(paths, 1, 'has a single path')
    assert.deepEqual(paths[0], [e1.key], 'has the path')
  })

  test('yields multiple directions', ({ assert }) => {
    e1.addTargetAssociation(e5.key)
    e1.addTargetAssociation(e2.key)
    e2.addTargetAssociation(e3.key)
    e3.addTargetAssociation(e4.key)
    e4.addTargetAssociation(e5.key)
    const paths: string[][] = []
    for (const path of e1.associationPath(e5.key)) {
      paths.push(path)
    }
    assert.lengthOf(paths, 2, 'has both paths')
    assert.deepEqual(paths[0], [e1.key, e2.key, e3.key, e4.key, e5.key], 'has the 1st path')
    assert.deepEqual(paths[1], [e1.key, e5.key], 'has the 2nd path')
  })

  test('ignores broken paths', ({ assert }) => {
    e1.addTargetAssociation(e5.key)
    e1.addTargetAssociation(e2.key)
    e2.addTargetAssociation(e3.key)
    // no e3 -> e4
    e4.addTargetAssociation(e5.key)
    const paths: string[][] = []
    for (const path of e1.associationPath(e5.key)) {
      paths.push(path)
    }
    assert.lengthOf(paths, 1, 'has both paths')
    assert.deepEqual(paths[0], [e1.key, e5.key], 'has the 1st path')
  })
})

test.group('associationPath() - More Complex Scenarios', (group) => {
  let root: DataNamespace
  let model: DataModel
  let entityA: DataEntity
  let entityB: DataEntity
  let entityC: DataEntity
  let entityD: DataEntity
  let foreignRoot: DataNamespace
  let foreignEntity: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    model = root.addDataModel('model')
    entityA = model.addEntity('entityA')
    entityB = model.addEntity('entityB')
    entityC = model.addEntity('entityC')
    entityD = model.addEntity('entityD')

    foreignRoot = new DataNamespace()
    root.addForeign(foreignRoot)
    const foreignModel = foreignRoot.addDataModel('foreignModel')
    foreignEntity = foreignModel.addEntity('foreignEntity')
  })

  test('returns multiple paths when multiple indirect associations', ({ assert }) => {
    entityA.addTargetAssociation(entityB.key)
    entityA.addTargetAssociation(entityC.key)
    entityB.addTargetAssociation(entityD.key)
    entityC.addTargetAssociation(entityD.key)
    const result = [...entityA.associationPath(entityD.key)]
    assert.deepEqual(result, [
      [entityA.key, entityC.key, entityD.key],
      [entityA.key, entityB.key, entityD.key],
    ])
  })

  test('returns empty array when not associated', ({ assert }) => {
    entityA.addTargetAssociation(entityB.key)
    const result = [...entityA.associationPath(entityC.key)]
    assert.deepEqual(result, [])
  })

  test('returns empty array when no associations in foreign namespaces', ({ assert }) => {
    entityA.addForeignAssociation(foreignEntity.key, foreignRoot.key)
    const result = [...entityA.associationPath(entityB.key)]
    assert.deepEqual(result, [])
  })

  test('returns the path when foreign association', ({ assert }) => {
    entityA.addForeignAssociation(foreignEntity.key, foreignRoot.key)
    const result = [...entityA.associationPath(foreignEntity.key)]
    assert.deepEqual(result, [[entityA.key, foreignEntity.key]])
  })
})

test.group('isAssociated()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let e2: DataEntity
  let e3: DataEntity
  let e4: DataEntity

  let foreignRoot: DataNamespace
  let foreignEntity: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    e2 = m1.addEntity('e2')
    e3 = m1.addEntity('e3')
    e4 = m1.addEntity('e4')

    foreignRoot = new DataNamespace()
    root.addForeign(foreignRoot)
    const foreignModel = foreignRoot.addDataModel('foreignModel')
    foreignEntity = foreignModel.addEntity('foreignEntity')
  })

  test('returns true when testing self', ({ assert }) => {
    const result = e1.isAssociated(e1.key)
    assert.isTrue(result)
  })

  test('returns true when has direct association', ({ assert }) => {
    e1.addTargetAssociation(e2.key)
    const result = e1.isAssociated(e2.key)
    assert.isTrue(result)
  })

  test('returns true when has an association', ({ assert }) => {
    e1.addTargetAssociation(e2.key)
    e2.addTargetAssociation(e3.key)
    const result = e1.isAssociated(e3.key)
    assert.isTrue(result)
  })

  test('returns false when has no association', ({ assert }) => {
    e1.addTargetAssociation(e2.key)
    const result = e1.isAssociated(e3.key)
    assert.isFalse(result)
  })

  test('returns true when has multiple paths', ({ assert }) => {
    e1.addTargetAssociation(e2.key)
    e1.addTargetAssociation(e3.key)
    e3.addTargetAssociation(e4.key)
    e2.addTargetAssociation(e4.key)
    const result = e1.isAssociated(e4.key)
    assert.isTrue(result)
  })

  test('returns false when has no path', ({ assert }) => {
    e1.addTargetAssociation(e2.key)
    const result = e1.isAssociated(e3.key)
    assert.isFalse(result)
  })

  test('returns false when no association', ({ assert }) => {
    const result = DataEntity.isAssociated(e1, e2)
    assert.isFalse(result)
  })

  test('returns true when foreign association', ({ assert }) => {
    e1.addForeignAssociation(foreignEntity.key, foreignRoot.key)
    const result = DataEntity.isAssociated(e1, foreignEntity)
    assert.isTrue(result)
  })

  test('returns true when has multiple paths', ({ assert }) => {
    e1.addTargetAssociation(e2.key)
    e1.addTargetAssociation(e3.key)
    e3.addTargetAssociation(e4.key)
    e2.addTargetAssociation(e4.key)
    const result = DataEntity.isAssociated(e1, e4)
    assert.isTrue(result)
  })
})

test.group('getRelatedEntities()', (group) => {
  let n1: DataNamespace
  let n2: DataNamespace
  let m1: DataModel
  let m2: DataModel
  let e1a: DataEntity
  let e1b: DataEntity
  let e2a: DataEntity

  group.each.setup(() => {
    n1 = new DataNamespace()
    n2 = new DataNamespace()
    n1.foreign.push(n2)
    m1 = n1.addDataModel('m1')
    m2 = n2.addDataModel('m2')
    e1a = m1.addEntity('e1a')
    e1b = m1.addEntity('e1b')
    e2a = m2.addEntity('e2a')
    m2.addEntity('e2b')
    m2.addEntity('e2b')
  })

  test('reads entities from the same namespace', ({ assert }) => {
    // associate e1b with e1a and the result is the reverse
    e1b.addTargetAssociation(e1a.key)
    const result = e1a.getRelatedEntities()
    assert.deepEqual(result, [e1b])
  })

  test('reads entities from another namespace', ({ assert }) => {
    // associate e1b with e1a and the result is the reverse
    e2a.addTargetAssociation(e1a.key)
    const result = e1a.getRelatedEntities()
    assert.deepEqual(result, [e2a])
  })

  test('reads mixed entities from own and another namespace', ({ assert }) => {
    // associate e1b with e1a and the result is the reverse
    e1b.addTargetAssociation(e1a.key)
    e2a.addTargetAssociation(e1a.key)
    const result = e1a.getRelatedEntities()
    assert.deepEqual(result, [e1b, e2a])
  })

  test('returns multiple related entities', ({ assert }) => {
    e1b.addTargetAssociation(e1a)
    e2a.addTargetAssociation(e1a)
    const result = e1a.getRelatedEntities()
    assert.deepEqual(result.sort(), [e1b, e2a].sort())
  })
})

test.group('breadcrumbs()', (group) => {
  let root: DataNamespace
  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('adds root and self', ({ assert }) => {
    const e1 = new DataEntity(root)
    const result = e1.breadcrumbs()
    assert.lengthOf(result, 2, 'has the entire path')
    assert.equal(result[0].key, root.key, 'has the root as first')
    assert.equal(result[1].key, e1.key, 'has the entity as last')
  })

  test('adds root, data model, and self', ({ assert }) => {
    const m1 = root.addDataModel('m1')
    const e1 = m1.addEntity('e1')
    const result = e1.breadcrumbs()
    assert.lengthOf(result, 3, 'has the entire path')
    assert.equal(result[0].key, root.key, 'has the root as first')
    assert.equal(result[1].key, m1.key, 'has the model as parent')
    assert.equal(result[2].key, e1.key, 'has self last')
  })

  test('adds root, sub-ns, data model, and self', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    const m1 = n1.addDataModel('m1')
    const e1 = m1.addEntity('e1')
    const result = e1.breadcrumbs()
    assert.lengthOf(result, 4, 'has the entire path')
    assert.equal(result[0].key, root.key, 'has the root as first')
    assert.equal(result[1].key, n1.key, 'has the child-namespace')
    assert.equal(result[2].key, m1.key, 'has the model as parent')
    assert.equal(result[3].key, e1.key, 'has self last')
  })

  test('adds root, sub-ns, sub-ns, data model, and self', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    const n2 = n1.addNamespace('n2')
    const m1 = n2.addDataModel('m1')
    const e1 = m1.addEntity('e1')
    const result = e1.breadcrumbs()
    assert.lengthOf(result, 5, 'has the entire path')
    assert.equal(result[0].key, root.key, 'has the root as first')
    assert.equal(result[1].key, n1.key, 'has the child-namespace')
    assert.equal(result[2].key, n2.key, 'has the child-namespace')
    assert.equal(result[3].key, m1.key, 'has the model as parent')
    assert.equal(result[4].key, e1.key, 'has self last')
  })
})

test.group('addTag()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
  })

  test('ignores when empty', ({ assert }) => {
    e1.addTag('')
    assert.deepEqual(e1.tags, [])
  })

  test('adds a tag to the property', ({ assert }) => {
    e1.addTag('Test')
    assert.deepEqual(e1.tags, ['Test'])
  })

  test('ignores a tag case insensitive', ({ assert }) => {
    e1.addTag('Test')
    e1.addTag('teSt')
    assert.deepEqual(e1.tags, ['Test'])
  })

  test('adds a tag the root definitions', ({ assert }) => {
    e1.addTag('Test')
    assert.deepEqual(root.definitions.tags, ['Test'])
  })

  test('ignores adding to root definitions when tag exists case insensitive', ({ assert }) => {
    e1.addTag('Test')
    e1.addTag('TeSt')
    assert.deepEqual(root.definitions.tags, ['Test'])
  })
})

test.group('removeTag()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
  })

  test('ignores when empty', ({ assert }) => {
    e1.addTag('t1')
    e1.removeTag('')
    assert.deepEqual(e1.tags, ['t1'])
  })

  test('removes the tag from the property', ({ assert }) => {
    e1.addTag('t1')
    e1.removeTag('t1')
    assert.deepEqual(e1.tags, [])
  })

  test('removes only the selected tag', ({ assert }) => {
    e1.addTag('t1')
    e1.addTag('t2')
    e1.addTag('t3')
    e1.removeTag('t2')
    assert.deepEqual(e1.tags, ['t1', 't3'])
  })

  test('does not remove root tags', ({ assert }) => {
    e1.addTag('t1')
    e1.removeTag('t1')
    assert.deepEqual(root.definitions.tags, ['t1'])
  })
})

test.group('addTag() and removeTag() - More Scenarios', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
  })

  test('adds multiple tags to the property', ({ assert }) => {
    e1.addTag('Test1')
    e1.addTag('Test2')
    assert.deepEqual(e1.tags, ['Test1', 'Test2'])
  })

  test('removes multiple tags from the property', ({ assert }) => {
    e1.addTag('t1')
    e1.addTag('t2')
    e1.addTag('t3')
    e1.removeTag('t1')
    e1.removeTag('t3')
    assert.deepEqual(e1.tags, ['t2'])
  })

  test('removes a tag case insensitive', ({ assert }) => {
    e1.addTag('Test')
    e1.removeTag('teSt')
    assert.deepEqual(e1.tags, [])
  })
})

test.group('toApiShape()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
  })

  // these tests only check whether the AmfShapeGenerator is called.
  // specific tests are performed elsewhere

  test('returns an object', ({ assert }) => {
    const result = e1.toApiShape()
    assert.typeOf(result, 'object')
    assert.typeOf(result.inherits, 'array')
  })
})

test.group('hasClosedCycle()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let e2: DataEntity
  let e3: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    e2 = m1.addEntity('e2')
    e3 = m1.addEntity('e3')
  })

  test('returns false when no association', ({ assert }) => {
    const result = e1.hasClosedCycle(e1.key, e2.key)
    assert.isFalse(result)
  })

  test('returns false when not recursive', ({ assert }) => {
    e1.addTargetAssociation(e2.key)
    const result = e2.hasClosedCycle(e1.key, e3.key)
    assert.isFalse(result)
  })

  test('returns true when testing self-association', ({ assert }) => {
    const result = e1.hasClosedCycle(e1.key, e1.key)
    assert.isTrue(result)
  })

  test('returns true when referencing a closest neighbor', ({ assert }) => {
    e1.addTargetAssociation(e2.key)
    const result = e2.hasClosedCycle(e1.key, e1.key)
    assert.isTrue(result)
  })

  test('multiple cycles', ({ assert }) => {
    const e4 = m1.addEntity('e4')
    e1.addTargetAssociation(e2.key)
    e2.addTargetAssociation(e3.key)
    e3.addTargetAssociation(e4.key)

    assert.isTrue(e1.hasClosedCycle(e1.key, e1.key), 'e1 -> e1')
    assert.isFalse(e1.hasClosedCycle(e1.key, e2.key), 'e1 -> e2')
    assert.isFalse(e1.hasClosedCycle(e1.key, e3.key), 'e1 -> e3')
    assert.isFalse(e1.hasClosedCycle(e1.key, e4.key), 'e1 -> e4')

    assert.isTrue(e2.hasClosedCycle(e1.key, e1.key), 'e2 -> e1')
    assert.isTrue(e2.hasClosedCycle(e1.key, e2.key), 'e2 -> e2')
    assert.isFalse(e2.hasClosedCycle(e1.key, e3.key), 'e2 -> e3')
    // this does not create a forward cycle just yet. It is safe to render this connection in the UI
    // but it is unsafe to render connection e4 -> e2.
    assert.isFalse(e2.hasClosedCycle(e1.key, e4.key), 'e2 -> e4')

    assert.isTrue(e3.hasClosedCycle(e1.key, e1.key), 'e3 -> e1')
    assert.isTrue(e3.hasClosedCycle(e1.key, e2.key), 'e3 -> e2')
    assert.isTrue(e3.hasClosedCycle(e1.key, e3.key), 'e3 -> e3')
    assert.isFalse(e3.hasClosedCycle(e1.key, e4.key), 'e3 -> e4')

    assert.isTrue(e4.hasClosedCycle(e1.key, e1.key), 'e4 -> e1')
    assert.isTrue(e4.hasClosedCycle(e1.key, e2.key), 'e4 -> e2')
    assert.isTrue(e4.hasClosedCycle(e1.key, e3.key), 'e4 -> e3')
    assert.isTrue(e4.hasClosedCycle(e1.key, e4.key), 'e4 -> e4')
  })

  test('returns false when testing for the next associated node', ({ assert }) => {
    e1.addTargetAssociation(e2.key)
    e2.addTargetAssociation(e3.key)
    e3.addTargetAssociation(e2.key)

    assert.isFalse(e2.hasClosedCycle(e1.key, e3.key))
  })

  test('returns true when testing for last node going back to the parent', ({ assert }) => {
    e1.addTargetAssociation(e2.key)
    e2.addTargetAssociation(e3.key)
    e3.addTargetAssociation(e2.key)

    assert.isTrue(e3.hasClosedCycle(e1.key, e2.key))
  })
})

test.group('addParent()', (group) => {
  let root: DataNamespace
  let model: DataModel
  let entity1: DataEntity
  let entity2: DataEntity
  let entity3: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    model = root.addDataModel('model')
    entity1 = model.addEntity('entity1')
    entity2 = model.addEntity('entity2')
    entity3 = model.addEntity('entity3')
  })

  test('adds a valid parent', ({ assert }) => {
    entity2.addParent(entity1.key)
    assert.deepEqual(entity2.parents, [entity1.key])
  })

  test('throws an error when adding self as parent', ({ assert }) => {
    assert.throws(
      () => entity1.addParent(entity1.key),
      ValidationError as unknown as ErrorConstructor,
      'Entity cannot be a parent of itself.'
    )
  })

  test('throws an error when parent does not exist', ({ assert }) => {
    assert.throws(
      () => entity1.addParent('non-existent-key'),
      ValidationError as unknown as ErrorConstructor,
      'Entity with key "non-existent-key" not found.'
    )
  })

  test('throws an error when circular inheritance is detected - direct', ({ assert }) => {
    entity1.addParent(entity2.key)
    assert.throws(
      () => entity2.addParent(entity1.key),
      ValidationError as unknown as ErrorConstructor,
      'Circular inheritance detected.'
    )
  })

  test('throws an error when circular inheritance is detected - indirect', ({ assert }) => {
    entity1.addParent(entity2.key)
    entity2.addParent(entity3.key)
    assert.throws(
      () => entity3.addParent(entity1.key),
      ValidationError as unknown as ErrorConstructor,
      'Circular inheritance detected.'
    )
  })

  test('does not throw an error when entity1 has entity2 as a parent and entity 2 has entity3 as parent', ({
    assert,
  }) => {
    entity1.addParent(entity2.key)
    assert.doesNotThrow(() => entity2.addParent(entity3.key))
    assert.deepEqual(entity2.parents, [entity3.key])
  })

  test('does not throw an error when entity1 has entity3 as a parent and entity 2 has entity3 as parent', ({
    assert,
  }) => {
    entity1.addParent(entity3.key)
    assert.doesNotThrow(() => entity2.addParent(entity3.key))
    assert.deepEqual(entity2.parents, [entity3.key])
  })

  test('throws an error when adding the same parent twice', ({ assert }) => {
    entity2.addParent(entity1.key)
    assert.throws(
      () => entity2.addParent(entity1.key),
      ValidationError as unknown as ErrorConstructor,
      `Parent ${entity1.key} already exists.`
    )
  })

  test('adds multiple parents', ({ assert }) => {
    entity3.addParent(entity1.key)
    entity3.addParent(entity2.key)
    assert.deepEqual(entity3.parents.sort(), [entity1.key, entity2.key].sort())
  })
})

test.group('getParentInstance()', (group) => {
  let root: DataNamespace
  let model: DataModel
  let entity1: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    model = root.addDataModel('model')
    entity1 = model.addEntity('entity1')
  })

  test('returns undefined when the entity is not in a model', ({ assert }) => {
    const result = new DataEntity(root).getParentInstance()
    assert.isUndefined(result)
  })

  test('returns the parent data model', ({ assert }) => {
    const result = entity1.getParentInstance()
    assert.instanceOf(result, DataModel)
  })
})

test.group('hasCircularParent()', (group) => {
  let root: DataNamespace
  let model: DataModel
  let entity1: DataEntity
  let entity2: DataEntity
  let entity3: DataEntity
  let entity4: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    model = root.addDataModel('model')
    entity1 = model.addEntity('entity1')
    entity2 = model.addEntity('entity2')
    entity3 = model.addEntity('entity3')
    entity4 = model.addEntity('entity4')
  })

  test('returns false when no parents', ({ assert }) => {
    assert.isFalse(entity1.hasCircularParent(entity2.key))
  })

  test('returns false when not a circular dependency', ({ assert }) => {
    entity2.addParent(entity1.key)
    assert.isFalse(entity1.hasCircularParent(entity3.key))
  })

  test('returns true when direct circular dependency', ({ assert }) => {
    entity1.addParent(entity2.key)
    assert.isTrue(entity2.hasCircularParent(entity1.key))
  })

  test('returns true when indirect circular dependency', ({ assert }) => {
    entity1.addParent(entity2.key)
    entity2.addParent(entity3.key)
    assert.isTrue(entity3.hasCircularParent(entity1.key))
  })

  test('returns true when indirect circular dependency - longer chain', ({ assert }) => {
    entity1.addParent(entity2.key)
    entity2.addParent(entity3.key)
    entity3.addParent(entity4.key)
    assert.isTrue(entity4.hasCircularParent(entity1.key))
  })

  test('returns true when adding one of children as parent', ({ assert }) => {
    entity1.addParent(entity2.key)
    entity2.addParent(entity3.key)
    assert.isTrue(entity2.hasCircularParent(entity1.key))
    assert.isTrue(entity3.hasCircularParent(entity1.key))
  })

  test('returns true when indirect circular dependency - multiple parents', ({ assert }) => {
    entity1.addParent(entity2.key)
    entity1.addParent(entity3.key)
    entity2.addParent(entity4.key)
    assert.isTrue(entity4.hasCircularParent(entity1.key))
  })
})

test.group('associationPath()', (group) => {
  let root: DataNamespace
  let model: DataModel
  let entityA: DataEntity
  let entityB: DataEntity
  let entityC: DataEntity
  let entityD: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    model = root.addDataModel('model')
    entityA = model.addEntity('entityA')
    entityB = model.addEntity('entityB')
    entityC = model.addEntity('entityC')
    entityD = model.addEntity('entityD')
  })

  test('returns empty generator when no associations', ({ assert }) => {
    const result = [...entityA.associationPath(entityB.key)]
    assert.deepEqual(result, [])
  })

  test('returns the path when direct association', ({ assert }) => {
    entityA.addTargetAssociation(entityB.key)
    const result = [...entityA.associationPath(entityB.key)]
    assert.deepEqual(result, [[entityA.key, entityB.key]])
  })

  test('returns the path when indirect association', ({ assert }) => {
    entityA.addTargetAssociation(entityB.key)
    entityB.addTargetAssociation(entityC.key)
    const result = [...entityA.associationPath(entityC.key)]
    assert.deepEqual(result, [[entityA.key, entityB.key, entityC.key]])
  })

  test('returns multiple paths when multiple indirect associations', ({ assert }) => {
    entityA.addTargetAssociation(entityB.key)
    entityA.addTargetAssociation(entityC.key)
    entityB.addTargetAssociation(entityD.key)
    entityC.addTargetAssociation(entityD.key)
    const result = [...entityA.associationPath(entityD.key)]
    assert.deepEqual(result, [
      [entityA.key, entityC.key, entityD.key],
      [entityA.key, entityB.key, entityD.key],
    ])
  })

  test('returns empty array when not associated', ({ assert }) => {
    entityA.addTargetAssociation(entityB.key)
    const result = [...entityA.associationPath(entityC.key)]
    assert.deepEqual(result, [])
  })

  test('returns empty array when no associations in foreign namespaces', ({ assert }) => {
    const f1 = new DataNamespace()
    const fm1 = f1.addDataModel('fm1')
    const fe1 = fm1.addEntity('fe1')
    root.foreign.push(f1)
    entityA.addForeignAssociation(fe1.key, f1.key)
    const result = [...entityA.associationPath(entityB.key)]
    assert.deepEqual(result, [])
  })
})

test.group('getRelatedEntities()', (group) => {
  let root: DataNamespace
  let model: DataModel
  let entityA: DataEntity
  let entityB: DataEntity
  let entityC: DataEntity
  let entityD: DataEntity
  let foreignRoot: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
    model = root.addDataModel('model')
    entityA = model.addEntity('entityA')
    entityB = model.addEntity('entityB')
    entityC = model.addEntity('entityC')
    entityD = model.addEntity('entityD')

    foreignRoot = new DataNamespace()
    root.addForeign(foreignRoot)
  })

  test('returns empty array when no related entities', ({ assert }) => {
    const result = entityA.getRelatedEntities()
    assert.deepEqual(result, [])
  })

  test('returns related entities when direct association', ({ assert }) => {
    entityA.addTargetAssociation(entityB.key)
    const result = entityB.getRelatedEntities()
    assert.deepEqual(result, [entityA])
  })

  test('returns related entities when indirect association', ({ assert }) => {
    entityA.addTargetAssociation(entityB)
    entityB.addTargetAssociation(entityC)
    const result = entityC.getRelatedEntities()
    assert.deepEqual(result, [entityB])
  })

  test('returns multiple related entities', ({ assert }) => {
    entityA.addTargetAssociation(entityC)
    entityD.addTargetAssociation(entityC)
    const result = entityC.getRelatedEntities()
    assert.deepEqual(result, [entityA, entityD])
  })
})

test.group('listProperties()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let p1: DataProperty
  let p2: DataProperty

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    p1 = e1.addNamedProperty('p1')
    p2 = e1.addNamedProperty('p2')
  })

  test('lists all properties', ({ assert }) => {
    const result = e1.listProperties()
    assert.deepEqual(result, [p1, p2])
  })

  test('returns a copy of the array', ({ assert }) => {
    const result = e1.listProperties()
    result.pop()
    assert.lengthOf(e1.properties, 2)
  })
})

test.group('listAssociations()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let a1: DataAssociation
  let a2: DataAssociation

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    a1 = e1.addNamedAssociation('a1')
    a2 = e1.addNamedAssociation('a2')
  })

  test('lists all associations', ({ assert }) => {
    const result = e1.listAssociations()
    assert.deepEqual(result, [a1, a2])
  })

  test('returns a copy of the array', ({ assert }) => {
    const result = e1.listAssociations()
    result.pop()
    assert.lengthOf(e1.associations, 2)
  })
})

test.group('listFields()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let p1: DataProperty
  let p2: DataProperty
  let a1: DataAssociation
  let a2: DataAssociation

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    p1 = e1.addNamedProperty('p1')
    p2 = e1.addNamedProperty('p2')
    a1 = e1.addNamedAssociation('a1')
    a2 = e1.addNamedAssociation('a2')
  })

  test('lists all fields in order', ({ assert }) => {
    const result = e1.listFields()
    assert.deepEqual(result, [p1, p2, a1, a2])
  })

  test('returns a copy of the array', ({ assert }) => {
    const result = e1.listFields()
    result.pop()
    assert.lengthOf(e1.fields, 4)
  })

  test('returns fields in the order of the fields array', ({ assert }) => {
    e1.moveField(p1.key, 3)
    const result = e1.listFields()
    assert.deepEqual(result, [p2, a1, a2, p1])
  })
})

test.group('moveField()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let p1: DataProperty
  let p2: DataProperty
  let a1: DataAssociation
  let a2: DataAssociation

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    p1 = e1.addNamedProperty('p1')
    p2 = e1.addNamedProperty('p2')
    a1 = e1.addNamedAssociation('a1')
    a2 = e1.addNamedAssociation('a2')
  })

  test('moves the field to a new index', ({ assert }) => {
    e1.moveField(p1.key, 3)
    assert.deepEqual(
      e1.fields.map((i) => i.key),
      [p2.key, a1.key, a2.key, p1.key]
    )
  })

  test('Moving a field to the same index', ({ assert }) => {
    e1.moveField(p1.key, 0)
    assert.deepEqual(
      e1.fields.map((i) => i.key),
      [p1.key, p2.key, a1.key, a2.key]
    )
  })

  test('throws when the field does not exist', ({ assert }) => {
    assert.throws(
      () => e1.moveField('unknown', 0),
      ValidationError as unknown as ErrorConstructor,
      'Validation failure'
    )
  })

  test('throws when the index is out of bounds', ({ assert }) => {
    assert.throws(() => e1.moveField(p1.key, -1), ValidationError as unknown as ErrorConstructor, 'Validation failure')
    assert.throws(() => e1.moveField(p1.key, 4), ValidationError as unknown as ErrorConstructor, 'Validation failure')
  })
})

test.group('DataEntity.isChildOf()', (group) => {
  let root: DataNamespace
  let n1: DataNamespace
  let n2: DataNamespace
  let m1: DataModel
  let m2: DataModel
  let e1: DataEntity
  let e2: DataEntity
  let e3: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    n1 = root.addNamespace('n1')
    n2 = n1.addNamespace('n2')
    m1 = root.addDataModel('m1')
    m2 = n1.addDataModel('m2')
    e1 = m1.addEntity('e1')
    e2 = m2.addEntity('e2')
    e3 = n2.addDataModel('m3').addEntity('e3')
  })

  test('returns false when called on an entity not in a data model', ({ assert }) => {
    const e4 = new DataEntity(root)
    const result = e4.isChildOf('some-key')
    assert.isFalse(result)
  })

  test('returns false when called on self', ({ assert }) => {
    const result = e2.isChildOf(e2.key)
    assert.isFalse(result)
  })

  test('returns true when called on a direct parent', ({ assert }) => {
    const result = e2.isChildOf(m2.key)
    assert.isTrue(result)
  })

  test('returns true when called on a grandparent', ({ assert }) => {
    const result = e3.isChildOf(root.key)
    assert.isTrue(result)
  })

  test('returns false when called on a sibling', ({ assert }) => {
    const e4 = m1.addEntity('e4')
    const result = e1.isChildOf(e4.key)
    assert.isFalse(result)
  })

  test('returns false when called on a child', ({ assert }) => {
    const p1 = e2.addNamedProperty('p1')
    const result = e2.isChildOf(p1.key)
    assert.isFalse(result)
  })

  test('returns false when called on a non-existent namespace', ({ assert }) => {
    const result = e2.isChildOf('non-existent-key')
    assert.isFalse(result)
  })

  test('returns true when called on a grandparent', ({ assert }) => {
    const result = e3.isChildOf(n1.key)
    assert.isTrue(result)
  })

  test('returns true when called on a data model in the same namespace', ({ assert }) => {
    const result = e1.isChildOf(m1.key)
    assert.isTrue(result)
  })
})
