import { test } from '@japa/runner'
import { Thing, DataNamespaceKind } from '../../../../src/index.js'
import { DataEntity, DataItem, DataModel, DataNamespace, IDataNamespace } from '../../../../src/legacy.js'

test.group('constructor() defaults', () => {
  test('sets the kind', ({ assert }) => {
    const ns = new DataNamespace()
    assert.equal(ns.kind, DataNamespaceKind)
  })

  test('sets the key', ({ assert }) => {
    const ns = new DataNamespace()
    assert.typeOf(ns.key, 'string')
    assert.isNotEmpty(ns.key)
  })

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

  test('sets the default "items"', ({ assert }) => {
    const ns = new DataNamespace()
    assert.deepEqual(ns.items, [])
  })

  test('has no "root" when no parent', ({ assert }) => {
    const ns = new DataNamespace()
    assert.isUndefined(ns.root)
  })

  test('sets the "root" when passed parent', ({ assert }) => {
    const parent = new DataNamespace()
    const ns = new DataNamespace(undefined, parent)
    assert.deepEqual(ns.root, parent)
  })

  test('sets the default "definitions"', ({ assert }) => {
    const ns = new DataNamespace()
    assert.typeOf(ns.definitions, 'object')
  })

  test('sets the default "definitions.models"', ({ assert }) => {
    const ns = new DataNamespace()
    assert.deepEqual(ns.definitions.models, [])
  })

  test('sets the default "definitions.tags"', ({ assert }) => {
    const ns = new DataNamespace()
    assert.deepEqual(ns.definitions.tags, [])
  })

  test('sets the default "definitions.associations"', ({ assert }) => {
    const ns = new DataNamespace()
    assert.deepEqual(ns.definitions.associations, [])
  })

  test('sets the default "definitions.entities"', ({ assert }) => {
    const ns = new DataNamespace()
    assert.deepEqual(ns.definitions.entities, [])
  })

  test('sets the default "definitions.properties"', ({ assert }) => {
    const ns = new DataNamespace()
    assert.deepEqual(ns.definitions.properties, [])
  })

  test('sets the default "definitions.namespaces"', ({ assert }) => {
    const ns = new DataNamespace()
    assert.deepEqual(ns.definitions.namespaces, [])
  })
})

test.group('constructor() from schema initialization', () => {
  test('sets the key', ({ assert }) => {
    const orig = new DataNamespace().toJSON()
    orig.key = 'test'
    const instance = new DataNamespace(orig)
    assert.equal(instance.key, 'test')
  })

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

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

test.group('fromName()', () => {
  test('sets the name', ({ assert }) => {
    const ns = DataNamespace.fromName('test')
    assert.equal(ns.info.name, 'test')
  })
})

test.group('new()', (group) => {
  let base: IDataNamespace
  group.each.setup(() => {
    base = {
      info: Thing.fromName('test name').toJSON(),
      key: 'test123',
      kind: DataNamespaceKind,
      items: [],
      definitions: {},
    }
  })

  test('sets the key', ({ assert }) => {
    const ns = new DataNamespace()
    ns.new(base)
    assert.equal(ns.key, 'test123')
  })

  test('sets the info', ({ assert }) => {
    const ns = new DataNamespace()
    ns.new(base)
    assert.equal(ns.info.name, 'test name')
  })

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

  test('throws when unknown input', ({ assert }) => {
    const ns = new DataNamespace()
    assert.throws(() => {
      // @ts-expect-error Used in testing
      ns.new({})
    }, 'Not a data namespace.')
  })

  test('sets the "definitions.namespaces"', ({ assert }) => {
    const ns = new DataNamespace()
    ns.addNamespace('n1')
    const instance = new DataNamespace()
    instance.new(ns.toJSON())
    assert.lengthOf(instance.definitions.namespaces, 1)
    instance.new(base)
    assert.lengthOf(instance.definitions.namespaces, 0)
  })

  test('sets the "definitions.models"', ({ assert }) => {
    const ns = new DataNamespace()
    ns.addDataModel('d1')
    const instance = new DataNamespace()
    instance.new(ns.toJSON())
    assert.lengthOf(instance.definitions.models, 1)
    instance.new(base)
    assert.lengthOf(instance.definitions.models, 0)
  })

  test('sets the "definitions.tags"', ({ assert }) => {
    const ns = new DataNamespace()
    ns.definitions.tags = ['a']
    const instance = new DataNamespace()
    instance.new(ns.toJSON())
    assert.lengthOf(instance.definitions.tags, 1)
    instance.new(base)
    assert.lengthOf(instance.definitions.tags, 0)
  })

  test('sets the "definitions.entities"', ({ assert }) => {
    const ns = new DataNamespace()
    const d1 = ns.addDataModel('d1')
    d1.addEntity('e1')
    const instance = new DataNamespace()
    instance.new(ns.toJSON())
    assert.lengthOf(instance.definitions.entities, 1)
    instance.new(base)
    assert.lengthOf(instance.definitions.entities, 0)
  })

  test('sets entities on the corresponding data model', ({ assert }) => {
    const ns = new DataNamespace()
    const d1 = ns.addDataModel('d1')
    const e1 = d1.addEntity('e1')
    const instance = new DataNamespace()
    instance.new(ns.toJSON())
    const model = instance.findDataModel(d1.key)!
    const [entity] = model.entities
    assert.deepEqual(entity, e1)
  })

  test('sets the "definitions.properties"', ({ assert }) => {
    const ns = new DataNamespace()
    const d1 = ns.addDataModel('d1')
    const e1 = d1.addEntity('e1')
    e1.addNamedProperty('p1')
    const instance = new DataNamespace()
    instance.new(ns.toJSON())
    assert.lengthOf(instance.definitions.properties, 1)
    instance.new(base)
    assert.lengthOf(instance.definitions.properties, 0)
  })

  test('sets the properties on the corresponding entity', ({ assert }) => {
    const ns = new DataNamespace()
    const d1 = ns.addDataModel('d1')
    const e1 = d1.addEntity('e1')
    const p1 = e1.addNamedProperty('p1')
    const instance = new DataNamespace()
    instance.new(ns.toJSON())

    const model = instance.findDataModel(d1.key)!
    const [entity] = model.entities
    assert.lengthOf(entity.properties, 1, 'has the properties')
    assert.deepEqual(entity.properties[0], p1, 'has the property')
  })

  test('sets the "definitions.associations"', ({ assert }) => {
    const ns = new DataNamespace()
    const d1 = ns.addDataModel('d1')
    const e1 = d1.addEntity('e1')
    e1.addNamedAssociation('a1')
    const instance = new DataNamespace()
    instance.new(ns.toJSON())
    assert.lengthOf(instance.definitions.associations, 1)
    instance.new(base)
    assert.lengthOf(instance.definitions.associations, 0)
  })

  test('sets the associations on the corresponding entity', ({ assert }) => {
    const ns = new DataNamespace()
    const d1 = ns.addDataModel('d1')
    const e1 = d1.addEntity('e1')
    const a1 = e1.addNamedAssociation('a1')
    const instance = new DataNamespace()
    instance.new(ns.toJSON())

    const model = instance.findDataModel(d1.key)!
    const [entity] = model.entities
    assert.lengthOf(entity.associations, 1, 'has the associations')
    assert.deepEqual(entity.associations[0], a1, 'has the association')
  })
})

test.group('toJSON()', (group) => {
  let base: DataNamespace
  group.each.setup(() => {
    base = new DataNamespace({
      info: Thing.fromName('test name').toJSON(),
      key: 'test123',
      kind: DataNamespaceKind,
      definitions: {},
      items: [],
    })
  })

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

  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('serializes the "items"', ({ assert }) => {
    const created = base.addNamespace('n1')
    const result = base.toJSON()
    assert.lengthOf(result.items, 1)
    assert.equal(result.items[0].key, created.key)
  })

  test('serializes the "definitions"', ({ assert }) => {
    const result = base.toJSON()
    assert.typeOf(result.definitions, 'object')
  })

  test('serializes the "definitions.namespaces"', ({ assert }) => {
    const created = base.addNamespace('n1')
    const result = base.toJSON()
    assert.typeOf(result.definitions.namespaces, 'array', 'has the array')
    assert.lengthOf(result.definitions.namespaces!, 1, 'has a single namespace item')
    assert.deepEqual(result.definitions.namespaces, [created.toJSON()], 'has the namespace item')
  })

  test('serializes the "definitions.models"', ({ assert }) => {
    const created = base.addDataModel('d1')
    const result = base.toJSON()
    assert.typeOf(result.definitions.models, 'array', 'has the array')
    assert.lengthOf(result.definitions.models!, 1, 'has a single models item')
    assert.deepEqual(result.definitions.models, [created.toJSON()], 'has the models item')
  })

  test('serializes the "definitions.tags"', ({ assert }) => {
    base.definitions.tags = ['a']
    const result = base.toJSON()
    assert.typeOf(result.definitions.tags, 'array', 'has the array')
    assert.lengthOf(result.definitions.tags!, 1, 'has a single tags item')
    assert.deepEqual(result.definitions.tags, ['a'], 'has the tags item')
  })

  test('serializes the "definitions.entities"', ({ assert }) => {
    const d1 = base.addDataModel('d1')
    const created = d1.addEntity('e1')
    const result = base.toJSON()
    assert.typeOf(result.definitions.entities, 'array', 'has the array')
    assert.lengthOf(result.definitions.entities!, 1, 'has a single entity item')
    assert.deepEqual(result.definitions.entities, [created.toJSON()], 'has the entity item')
  })

  test('serializes the "definitions.properties"', ({ assert }) => {
    const d1 = base.addDataModel('d1')
    const e1 = d1.addEntity('e1')
    const created = e1.addNamedProperty('p1')
    const result = base.toJSON()
    assert.typeOf(result.definitions.properties, 'array', 'has the array')
    assert.lengthOf(result.definitions.properties!, 1, 'has a single property item')
    assert.deepEqual(result.definitions.properties, [created.toJSON()], 'has the property item')
  })

  test('serializes the "definitions.associations"', ({ assert }) => {
    const d1 = base.addDataModel('d1')
    const e1 = d1.addEntity('e1')
    const created = e1.addNamedAssociation('p1')
    const result = base.toJSON()
    assert.typeOf(result.definitions.associations, 'array', 'has the array')
    assert.lengthOf(result.definitions.associations!, 1, 'has a single association item')
    assert.deepEqual(result.definitions.associations, [created.toJSON()], 'has the association item')
  })

  test('has no "definitions.models" when empty', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.definitions.models)
  })

  test('has no "definitions.tags" when empty', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.definitions.tags)
  })

  test('has no "definitions.entities" when empty', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.definitions.entities)
  })

  test('has no "definitions.properties" when empty', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.definitions.properties)
  })

  test('has no "definitions.associations" when empty', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.definitions.associations)
  })
})

test.group('remove()', (group) => {
  let root: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('removes a child namespace', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    n1.remove()
    assert.deepEqual(root.definitions.namespaces, [])
  })

  test('removes a child namespace and its contents', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    const d1 = n1.addDataModel('d1')
    d1.addEntity('e1')
    n1.remove()
    assert.deepEqual(root.definitions.namespaces, [])
    assert.deepEqual(root.definitions.models, [])
    assert.deepEqual(root.definitions.entities, [])
  })

  test('throws when trying to remove the root namespace', ({ assert }) => {
    assert.throws(() => {
      root.remove()
    }, 'Unable to remove the root namespace this way.')
  })
})

test.group('findParent()', (group) => {
  let root: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('returns undefined when requesting self', ({ assert }) => {
    const result = root.findParent(root.key)
    assert.isUndefined(result)
  })

  test('returns self when a direct child', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    const result = root.findParent(n1.key)
    assert.isTrue(result === root)
  })

  test('returns the parent for a child', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    const n2 = n1.addNamespace('n2')
    const result = root.findParent(n2.key)
    assert.isTrue(result === n1)
  })
})

test.group('addNamespace()', (group) => {
  let root: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('adds a namespace to a root by the name', ({ assert }) => {
    const result = root.addNamespace('an ns')
    assert.typeOf(result, 'object', 'returns the created namespace')
    assert.deepEqual(root.definitions.namespaces, [result], 'has the ns in definitions')
    assert.lengthOf(root.items, 1, 'the root has an item')
    assert.equal(root.items[0].key, result.key, 'the root has the created item')
    assert.equal(root.items[0].kind, result.kind, 'the root item has the proper kind')
  })

  test('adds a namespace to a root by the instance', ({ assert }) => {
    const instance = new DataNamespace()
    const result = root.addNamespace(instance)
    assert.deepEqual(result, instance, 'returns the same namespace')
    assert.deepEqual(root.definitions.namespaces, [instance], 'has the ns in definitions')
    assert.lengthOf(root.items, 1, 'the root has an item')
    assert.equal(root.items[0].key, result.key, 'the root has the created item')
    assert.equal(root.items[0].kind, result.kind, 'the root item has the proper kind')
  })

  test('adds a namespace to a root by the schema', ({ assert }) => {
    const instance = new DataNamespace().toJSON()
    const result = root.addNamespace(instance)
    assert.deepEqual(result.toJSON(), instance, 'returns the created namespace')
  })

  test('adds a namespace to a parent namespace', ({ assert }) => {
    const parent = root.addNamespace('parent')
    const child = parent.addNamespace('child')
    assert.typeOf(child, 'object', 'returns the child namespace')
    assert.lengthOf(root.definitions.namespaces, 2, 'root has both namespaces')
    assert.deepEqual(root.definitions.namespaces, [parent, child], 'has both namespace definitions')
    assert.lengthOf(root.items, 1, 'the root has only one item')
    assert.equal(root.items[0].key, parent.key, 'the root has its own item')
    assert.lengthOf(parent.items, 1, 'the parent has only one item')
    assert.equal(parent.items[0].key, child.key, 'the parent has its own item')
  })
})

test.group('findNamespace()', (group) => {
  let root: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('finds a direct child namespace', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    const result = root.findNamespace(n1.key)
    assert.deepEqual(result, n1)
  })

  test('finds a sub-ns child namespace', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    const n2 = n1.addNamespace('n2')
    const result = root.findNamespace(n2.key)
    assert.deepEqual(result, n2)
  })
})

test.group('removeNamespace()', (group) => {
  let root: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('removes a child namespace', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    root.removeNamespace(n1.key)
    assert.deepEqual(root.definitions.namespaces, [])
  })

  test('removes a child namespace and its contents', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    const d1 = n1.addDataModel('d1')
    d1.addEntity('e1')
    root.removeNamespace(n1.key)
    assert.deepEqual(root.definitions.namespaces, [])
    assert.deepEqual(root.definitions.models, [])
    assert.deepEqual(root.definitions.entities, [])
  })

  test('throws when trying to remove the root namespace', ({ assert }) => {
    assert.throws(() => {
      root.removeNamespace(root.key)
    }, 'Unable to remove the root namespace this way.')
  })
})

test.group('addDataModel()', (group) => {
  let root: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('adds data model to the root ns by name', ({ assert }) => {
    const d1 = root.addDataModel('d1')
    assert.typeOf(d1, 'object', 'returns the created model')
    assert.deepEqual(root.definitions.models, [d1], 'adds the definition')
    assert.lengthOf(root.items, 1, 'adds the item')
    assert.equal(root.items[0].key, d1.key, 'the item has the key')
    assert.equal(root.items[0].kind, d1.kind, 'the item has the kind')
  })

  test('adds data model to the root ns by instance', ({ assert }) => {
    const m1 = DataModel.fromName(root, 'm1')
    const d1 = root.addDataModel(m1)
    assert.deepEqual(d1, m1, 'returns the same model')
    assert.deepEqual(root.definitions.models, [m1], 'adds the definition')
    assert.lengthOf(root.items, 1, 'adds the item')
    assert.equal(root.items[0].key, d1.key, 'the item has the key')
    assert.equal(root.items[0].kind, d1.kind, 'the item has the kind')
  })

  test('adds data model to the root ns by schema', ({ assert }) => {
    const m1 = DataModel.fromName(root, 'm1').toJSON()
    const d1 = root.addDataModel(m1)
    assert.deepEqual(d1.toJSON(), m1, 'returns the same model')
    assert.deepEqual(root.definitions.models[0].toJSON(), m1, 'adds the definition')
    assert.lengthOf(root.items, 1, 'adds the item')
    assert.equal(root.items[0].key, d1.key, 'the item has the key')
    assert.equal(root.items[0].kind, d1.kind, 'the item has the kind')
  })

  test('adds a data model to the child namespace', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    const d1 = n1.addDataModel('d1')
    assert.typeOf(d1, 'object', 'returns the created model')
    assert.deepEqual(root.definitions.models, [d1], 'adds the definition')
    assert.lengthOf(n1.items, 1, 'adds the item')
    assert.equal(n1.items[0].key, d1.key, 'the item has the key')
    assert.equal(n1.items[0].kind, d1.kind, 'the item has the kind')
    assert.lengthOf(root.items, 1, 'the root has only 1 item')
  })
})

test.group('findDataModel()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let m2: DataModel

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    const parent = root.addNamespace('n1')
    m2 = parent.addDataModel('m2')
  })

  test('returns the data model', ({ assert }) => {
    const result = root.findDataModel(m1.key)
    assert.deepEqual(result, m1)
  })

  test('returns child data model', ({ assert }) => {
    const result = root.findDataModel(m2.key)
    assert.deepEqual(result, m2)
  })

  test('returns undefined when not found', ({ assert }) => {
    const result = root.findDataModel('other')
    assert.isUndefined(result)
  })
})

test.group('removeDataModel()', (group) => {
  let root: DataNamespace
  let m1: DataModel
  let m2: DataModel

  group.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    const parent = root.addNamespace('n1')
    m2 = parent.addDataModel('m2')
  })

  test('removes the root data model', ({ assert }) => {
    root.removeDataModel(m1.key)
    assert.lengthOf(root.definitions.models, 1, 'has the remaining data model')
  })

  test('returns child data model', ({ assert }) => {
    root.removeDataModel(m2.key)
    assert.lengthOf(root.definitions.models, 1, 'has the remaining data model')
  })

  test('does nothing when not found', ({ assert }) => {
    root.removeDataModel('other')
    assert.lengthOf(root.definitions.models, 2, 'has all data models')
  })
})

test.group('associationGraph()', (group) => {
  let root: DataNamespace
  let d1: DataModel
  let d2: DataModel

  group.each.setup(() => {
    root = new DataNamespace()
    d1 = root.addDataModel('d1')
    d2 = root.addDataModel('d2')
  })

  test('returns empty object when no entities and associations', ({ assert }) => {
    const result = root.associationGraph()
    assert.deepEqual(result, {})
  })

  test('returns empty object when no associations', ({ assert }) => {
    d1.addEntity('e1')
    d2.addEntity('e2')
    const result = root.associationGraph()
    assert.deepEqual(result, {})
  })

  test('returns simple association graph', ({ assert }) => {
    const e1 = d1.addEntity('e1')
    const e2 = d2.addEntity('e2')
    e1.addTargetAssociation(e2.key)
    const result = root.associationGraph()
    assert.deepEqual(result, {
      [e1.key]: [e2.key],
    })
  })

  test('returns multiple associations from the same entity', ({ assert }) => {
    const e1 = d1.addEntity('e1')
    const e2 = d2.addEntity('e2')
    const e3 = d2.addEntity('e3')
    e1.addTargetAssociation(e2.key)
    e1.addTargetAssociation(e3.key)
    const result = root.associationGraph()
    assert.deepEqual(result, {
      [e1.key]: [e3.key, e2.key],
    })
  })

  test('returns multiple associations to the same entity', ({ assert }) => {
    const e1 = d1.addEntity('e1')
    const e2 = d2.addEntity('e2')
    const e3 = d2.addEntity('e3')
    e2.addTargetAssociation(e1.key)
    e3.addTargetAssociation(e1.key)
    const result = root.associationGraph()
    assert.deepEqual(result, {
      [e2.key]: [e1.key],
      [e3.key]: [e1.key],
    })
  })

  test('captures self-association', ({ assert }) => {
    const e1 = d1.addEntity('e1')
    e1.addTargetAssociation(e1.key)
    const result = root.associationGraph()
    assert.deepEqual(result, {
      [e1.key]: [e1.key],
    })
  })
})

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 graph = root.associationGraph()
    const paths: string[][] = []
    for (const path of root.associationPath(e1.key, e2.key, graph)) {
      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 graph = root.associationGraph()
    const paths: string[][] = []
    for (const path of root.associationPath(e1.key, e3.key, graph)) {
      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 graph = root.associationGraph()
    const paths: string[][] = []
    for (const path of root.associationPath(e1.key, e1.key, graph)) {
      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 graph = root.associationGraph()
    const paths: string[][] = []
    for (const path of root.associationPath(e1.key, e5.key, graph)) {
      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 graph = root.associationGraph()
    const paths: string[][] = []
    for (const path of root.associationPath(e1.key, e5.key, graph)) {
      paths.push(path)
    }
    assert.lengthOf(paths, 1, 'has both paths')
    assert.deepEqual(paths[0], [e1.key, e5.key], 'has the 1st path')
  })

  test('respects the order of nodes in paths', ({ assert }) => {
    e1.addTargetAssociation(e2.key)
    e2.addTargetAssociation(e1.key)
    const paths: string[][] = []
    const graph = root.associationGraph()
    for (const path of root.associationPath(e1.key, e2.key, graph)) {
      paths.push(path)
    }
    for (const path of root.associationPath(e2.key, e1.key, graph)) {
      paths.push(path)
    }
    assert.lengthOf(paths, 2, 'has only 2 paths')
    const [p1, p2] = paths
    assert.deepEqual(p1, [e1.key, e2.key], 'has ordered paths from e1 to e2')
    assert.deepEqual(p2, [e2.key, e1.key], 'has ordered paths from e2 to e1')
  })
})

test.group('computeForeignNamespaceKeys()', () => {
  test('returns an empty array when no associations', ({ assert }) => {
    const n1 = new DataNamespace()
    const result = n1.computeForeignNamespaceKeys()
    assert.deepEqual(result, [])
  })

  test('returns an empty array when no foreign associations', ({ assert }) => {
    const n1 = new DataNamespace()
    const d1 = n1.addDataModel('d1')
    const e1 = d1.addEntity('e1')
    const e2 = d1.addEntity('e2')
    e1.addTargetAssociation(e2.key)
    const result = n1.computeForeignNamespaceKeys()
    assert.deepEqual(result, [])
  })

  test('returns ids of foreign namespaces', ({ assert }) => {
    const n1 = new DataNamespace()
    const d1 = n1.addDataModel('d1')
    const e1 = d1.addEntity('e1')
    const n2 = new DataNamespace()
    const d2 = n2.addDataModel('d2')
    const e2 = d2.addEntity('e2')
    n1.addForeign(n2)
    e1.addForeignAssociation(e2.key, n2.key)

    const result = n1.computeForeignNamespaceKeys()
    assert.deepEqual(result, [n2.key])
  })
})

test.group('findAssociatedEntity()', (group) => {
  let root: DataNamespace
  let foreign: DataNamespace
  let e1: DataEntity
  let e2: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    foreign = new DataNamespace()
    root.addForeign(foreign)
    const model1 = root.addDataModel('m1')
    const model2 = foreign.addDataModel('m2')
    e1 = model1.addEntity('e1')
    e2 = model2.addEntity('e2')
  })

  test('finds an entity in the current namespace', ({ assert }) => {
    const result = root.findAssociatedEntity(e1.key)
    assert.deepEqual(result, e1)
  })

  test('finds an entity in a foreign namespace', ({ assert }) => {
    const result = root.findAssociatedEntity(e2.key, foreign.key)
    assert.deepEqual(result, e2)
  })

  test('returns undefined when the entity is not found in the current namespace', ({ assert }) => {
    const result = root.findAssociatedEntity('non-existent-key')
    assert.isUndefined(result)
  })

  test('returns undefined when the entity is not found in a foreign namespace', ({ assert }) => {
    const result = root.findAssociatedEntity('non-existent-key', foreign.key)
    assert.isUndefined(result)
  })

  test('returns undefined when the foreign namespace does not exist', ({ assert }) => {
    const result = root.findAssociatedEntity(e2.key, 'non-existent-namespace')
    assert.isUndefined(result)
  })

  test('returns undefined when no namespace is provided and the entity is in a foreign namespace', ({ assert }) => {
    const result = root.findAssociatedEntity(e2.key)
    assert.isUndefined(result)
  })
})

test.group('addForeign()', (group) => {
  let root: DataNamespace
  let foreign: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
    foreign = new DataNamespace()
  })

  test('adds a foreign namespace', ({ assert }) => {
    root.addForeign(foreign)
    assert.deepEqual(root.foreign, [foreign])
  })

  test('does not add the same foreign namespace twice', ({ assert }) => {
    root.addForeign(foreign)
    root.addForeign(foreign)
    assert.deepEqual(root.foreign, [foreign])
  })
})

test.group('removeForeign()', (group) => {
  let root: DataNamespace
  let foreign: DataNamespace
  let foreign2: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
    foreign = new DataNamespace()
    foreign2 = new DataNamespace()
    root.addForeign(foreign)
    root.addForeign(foreign2)
  })

  test('removes a foreign namespace', ({ assert }) => {
    root.removeForeign(foreign)
    assert.deepEqual(root.foreign, [foreign2])
  })

  test('does nothing when the foreign namespace does not exist', ({ assert }) => {
    const other = new DataNamespace()
    root.removeForeign(other)
    assert.deepEqual(root.foreign, [foreign, foreign2])
  })
})

test.group('hasForeignNamespace()', (group) => {
  let root: DataNamespace
  let foreign: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
    foreign = new DataNamespace()
    root.addForeign(foreign)
  })

  test('returns true when the foreign namespace exists', ({ assert }) => {
    const result = root.hasForeignNamespace(foreign.key)
    assert.isTrue(result)
  })

  test('returns false when the foreign namespace does not exist', ({ assert }) => {
    const result = root.hasForeignNamespace('non-existent-key')
    assert.isFalse(result)
  })
})

test.group('listNamespaces()', (group) => {
  let root: DataNamespace
  let n1: DataNamespace
  let n2: DataNamespace
  let m1: DataModel

  group.each.setup(() => {
    root = new DataNamespace()
    n1 = root.addNamespace('n1')
    n2 = root.addNamespace('n2')
    m1 = root.addDataModel('m1')
  })

  test('lists all namespaces', ({ assert }) => {
    const result = root.listNamespaces()
    assert.deepEqual(result, [n1, n2])
  })

  test('lists namespaces in a sub-namespace', ({ assert }) => {
    const n3 = n1.addNamespace('n3')
    const result = n1.listNamespaces()
    assert.deepEqual(result, [n3])
  })

  test('does not list data models', ({ assert }) => {
    const result = root.listNamespaces()
    assert.notDeepInclude(result, m1)
  })
})

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

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

  test('lists all data models', ({ assert }) => {
    const result = root.listDataModels()
    assert.deepEqual(result, [m1])
  })

  test('lists data models in a sub-namespace', ({ assert }) => {
    const m3 = n1.addDataModel('m3')
    const result = n1.listDataModels()
    assert.deepEqual(result, [m2, m3])
  })

  test('does not list entities', ({ assert }) => {
    const result = root.listDataModels()
    assert.notDeepInclude(result, e1)
  })
})

test.group('getRoot()', (group) => {
  let root: DataNamespace
  let n1: DataNamespace
  let n2: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
    n1 = root.addNamespace('n1')
    n2 = n1.addNamespace('n2')
  })

  test('returns self when called on the root namespace', ({ assert }) => {
    const result = root.getRoot()
    assert.deepEqual(result, root)
  })

  test('returns the root namespace when called on a sub-namespace', ({ assert }) => {
    const result = n1.getRoot()
    assert.deepEqual(result, root)
  })

  test('returns the root namespace when called on a sub-sub-namespace', ({ assert }) => {
    const result = n2.getRoot()
    assert.deepEqual(result, root)
  })
})

test.group('getParentInstance()', (group) => {
  let root: DataNamespace
  let n1: DataNamespace
  let n2: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
    n1 = root.addNamespace('n1')
    n2 = n1.addNamespace('n2')
  })

  test('returns undefined when called on the root namespace', ({ assert }) => {
    const result = root.getParentInstance()
    assert.isUndefined(result)
  })

  test('returns the parent namespace when called on a sub-namespace', ({ assert }) => {
    const result = n2.getParentInstance()
    assert.deepEqual(result, n1)
  })
})

test.group('isRoot()', (group) => {
  let root: DataNamespace
  let n1: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
    n1 = root.addNamespace('n1')
  })

  test('returns true when called on the root namespace', ({ assert }) => {
    const result = root.isRoot()
    assert.isTrue(result)
  })

  test('returns false when called on a sub-namespace', ({ assert }) => {
    const result = n1.isRoot()
    assert.isFalse(result)
  })
})

test.group('adaptNamespace()', (group) => {
  let root: DataNamespace
  let n1: DataNamespace
  let n2: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
    n1 = root.addNamespace('n1')
    n2 = root.addNamespace('n2')
  })

  test('moves a namespace to a new parent', ({ assert }) => {
    const n3 = n1.addNamespace('n3')
    n2.adaptNamespace(n3)
    assert.deepEqual(n1.items, [], 'removes from the old parent')
    const item = DataItem.dataNamespace(root, n3.key)
    assert.deepEqual(n2.items, [item], 'adds to the new parent')
    assert.deepEqual(root.definitions.namespaces, [n1, n2, n3], 'keeps the namespace in the root definitions')
  })

  test('moves a namespace to a new index', ({ assert }) => {
    const n3 = n1.addNamespace('n3')
    const n4 = n1.addNamespace('n4')
    const n5 = n2.addNamespace('n5')
    n2.adaptNamespace(n3, { index: 0 })
    const item3 = DataItem.dataNamespace(root, n3.key)
    const item4 = DataItem.dataNamespace(root, n4.key)
    const item5 = DataItem.dataNamespace(root, n5.key)
    assert.deepEqual(n1.items, [item4], 'removes from the old parent')
    assert.deepEqual(n2.items, [item3, item5], 'adds to the new parent')
    assert.deepEqual(root.definitions.namespaces, [n1, n2, n3, n4, n5], 'keeps the namespace in the root definitions')
  })

  test('throws when adapting self', ({ assert }) => {
    assert.throws(() => n1.adaptNamespace(n1), 'Unable to adapt a namespace that is self.')
  })

  test('throws when adapting a namespace from another root', ({ assert }) => {
    const otherRoot = new DataNamespace()
    const otherNs = otherRoot.addNamespace('other')
    assert.throws(
      () => n1.adaptNamespace(otherNs),
      `The namespace ${otherNs.key} is not in the same namespace as this data namespace.`
    )
  })

  test('throws when adapting a namespace that is already adapted', ({ assert }) => {
    const n3 = n1.addNamespace('n3')
    n2.adaptNamespace(n3)
    assert.throws(() => n2.adaptNamespace(n3), `The namespace ${n3.key} is already adapted by this data namespace.`)
  })

  test('throws when index is out of range (minimum)', ({ assert }) => {
    assert.throws(() => n1.adaptNamespace(n2, { index: -1 }), `The index -1 cannot be below 0.`)
  })

  test('throws when index is out of range (maximum)', ({ assert }) => {
    assert.throws(() => n1.adaptNamespace(n2, { index: 1 }), `The index 1 is not valid.`)
  })
})

test.group('adaptNamespace() - Circular Dependency', (group) => {
  let root: DataNamespace
  let n1: DataNamespace
  let n2: DataNamespace
  let n3: DataNamespace
  let n4: DataNamespace

  // Root
  // ├── n1
  // │   ├── n3
  // │   │   └── n4
  // └── n2

  group.each.setup(() => {
    root = new DataNamespace()
    n1 = root.addNamespace('n1')
    n2 = root.addNamespace('n2')
    n3 = n1.addNamespace('n3')
    n4 = n3.addNamespace('n4')
  })

  test('throws when adapting a namespace under itself', ({ assert }) => {
    assert.throws(() => n1.adaptNamespace(n1), 'Unable to adapt a namespace that is self.')
  })

  test('throws when adapting a namespace under its child', ({ assert }) => {
    assert.throws(
      () => n3.adaptNamespace(n1),
      `Unable to adapt namespace ${n1.key} under itself or one of its children.`
    )
  })

  test('throws when adapting a namespace under its grandchild', ({ assert }) => {
    assert.throws(
      () => n4.adaptNamespace(n1),
      `Unable to adapt namespace ${n1.key} under itself or one of its children.`
    )
  })

  test('does not throw when adapting a namespace under its sibling', ({ assert }) => {
    const n5 = n1.addNamespace('n5')
    assert.doesNotThrow(() => n2.adaptNamespace(n5))
  })

  test("does not throw when adapting a namespace under its sibling's child", ({ assert }) => {
    const n5 = n1.addNamespace('n5')
    const n6 = n5.addNamespace('n6')
    assert.doesNotThrow(() => n2.adaptNamespace(n6))
  })

  test("does not throw when adapting a namespace under its sibling's parent", ({ assert }) => {
    const n5 = n1.addNamespace('n5')
    // Root (before)
    // ├── n1
    // │   ├── n3
    // │   │   └── n4
    // │   └── n5
    // └── n2
    // Root (after)
    // └── n1
    //     ├── n3
    //     │   └── n4
    //     └── n5
    //         └── n2
    assert.doesNotThrow(() => n5.adaptNamespace(n2))
  })
})

test.group('adaptDataModel()', (group) => {
  let root: DataNamespace
  let n1: DataNamespace
  let n2: DataNamespace
  let m1: DataModel
  let m2: DataModel

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

  test('moves a data model to a new parent', ({ assert }) => {
    n2.adaptDataModel(m1)
    const item1 = DataItem.dataModel(root, m1.key)
    const item2 = DataItem.dataModel(root, m2.key)
    assert.deepEqual(n1.items, [item2], 'removes from the old parent')
    assert.deepEqual(n2.items, [item1], 'adds to the new parent')
    assert.deepEqual(root.definitions.models, [m1, m2], 'keeps the data model in the root definitions')
  })

  test('moves a data model to a new index', ({ assert }) => {
    const m3 = n2.addDataModel('m3')
    n2.adaptDataModel(m1, { index: 0 })
    const item1 = DataItem.dataModel(root, m1.key)
    const item2 = DataItem.dataModel(root, m2.key)
    const item3 = DataItem.dataModel(root, m3.key)
    assert.deepEqual(n1.items, [item2], 'removes from the old parent')
    assert.deepEqual(n2.items, [item1, item3], 'adds to the new parent')
    assert.deepEqual(root.definitions.models, [m1, m2, m3], 'keeps the data model in the root definitions')
  })

  test('throws when adapting a data model from another root', ({ assert }) => {
    const otherRoot = new DataNamespace()
    const otherNs = otherRoot.addNamespace('other')
    const otherModel = otherNs.addDataModel('Other Data Model')
    assert.throws(
      () => n1.adaptDataModel(otherModel),
      `The data model ${otherModel.key} is not in the same namespace as this data namespace.`
    )
  })

  test('throws when adapting a data model that is already adapted', ({ assert }) => {
    n2.adaptDataModel(m1)
    assert.throws(() => n2.adaptDataModel(m1), `The data model ${m1.key} is already adapted by this data namespace.`)
  })

  test('throws when index is out of range (minimum)', ({ assert }) => {
    assert.throws(() => n2.adaptDataModel(m1, { index: -1 }), `The index -1 cannot be below 0.`)
  })

  test('throws when index is out of range (maximum)', ({ assert }) => {
    assert.throws(() => n2.adaptDataModel(m1, { index: 1 }), `The index 1 is not valid.`)
  })

  test('throws when adapting a data model from another namespace', ({ assert }) => {
    const otherRoot = new DataNamespace()
    const otherModel = new DataModel(otherRoot)
    assert.throws(
      () => n1.adaptDataModel(otherModel),
      `The data model ${otherModel.key} is not in the same namespace as this data namespace.`
    )
  })
})

test.group('isChildOf()', (group) => {
  let root: DataNamespace
  let n1: DataNamespace
  let n2: DataNamespace
  let n3: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
    n1 = root.addNamespace('n1')
    n2 = n1.addNamespace('n2')
    n3 = n2.addNamespace('n3')
  })

  test('returns false when called on the root namespace', ({ assert }) => {
    const result = root.isChildOf('some-key')
    assert.isFalse(result)
  })

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

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

  test('returns false when called on a sibling', ({ assert }) => {
    const n4 = root.addNamespace('n4')
    const result = n1.isChildOf(n4.key)
    assert.isFalse(result)
  })

  test('returns false when called on a parent', ({ assert }) => {
    const result = root.isChildOf(n1.key)
    assert.isFalse(result)
  })

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