import { test } from '@japa/runner'
import { DataDomain, DomainNamespace } from '../../../src/index.js'

test.group('DomainNamespace.addNamespace()', () => {
  test('adds a namespace to the data domain', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const childNamespace = namespace.addNamespace()
    assert.instanceOf(childNamespace, DomainNamespace)
    assert.isTrue(dataDomain.graph.hasNode(childNamespace.key))
    assert.equal(dataDomain.graph.parent(childNamespace.key), namespace.key)
  })

  test('adds a namespace with default values', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const childNamespace = namespace.addNamespace()
    assert.instanceOf(childNamespace, DomainNamespace)
    assert.isTrue(dataDomain.graph.hasNode(childNamespace.key))
    assert.equal(childNamespace.info.name, 'New namespace')
  })

  test('throws an error if a namespace with the same key already exists', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    assert.throws(() => {
      namespace.addNamespace({ key: namespace.key })
    }, `Trying to add a namespace to ${namespace.key}, but it already exists`)
  })

  test('notifies change', async ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    namespace.addNamespace()
    await assert.dispatches(dataDomain, 'change', { timeout: 20 })
  })

  test('adds a namespace to the root of the graph', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const childNamespace = namespace.addNamespace()
    assert.instanceOf(childNamespace, DomainNamespace)
    assert.isTrue(dataDomain.graph.hasNode(childNamespace.key))
    assert.equal(dataDomain.graph.parent(childNamespace.key), namespace.key)
  })

  test('adds a namespace to a parent namespace - checks parent-child relationship', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const childNamespace = parentNamespace.addNamespace()
    assert.isTrue(dataDomain.graph.hasParent(childNamespace.key, parentNamespace.key))
  })

  test('adds a namespace to a parent namespace - checks items list', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const childNamespace = parentNamespace.addNamespace()
    assert.deepInclude([...parentNamespace.listNamespaces()], childNamespace)
  })
})

test.group('DomainNamespace.removeNamespace()', () => {
  test('removes a child namespace', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const childNamespace = namespace.addNamespace()
    namespace.removeNamespace(childNamespace.key)
    assert.isUndefined(dataDomain.findNamespace(childNamespace.key))
    assert.isFalse(dataDomain.graph.hasNode(childNamespace.key))
    assert.deepEqual([...namespace.listNamespaces()], [])
  })

  test('throws an error if the namespace does not exist', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    assert.throws(() => {
      namespace.removeNamespace('non-existent-ns')
    }, "Trying to remove the non-existent-ns namespace, but it doesn't exist")
  })

  test('throws an error when deleting not a namespace', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const m1 = n1.addModel()
    assert.throws(() => {
      n1.removeNamespace(m1.key)
    }, `Trying to remove the ${m1.key} namespace, but it is not a namespace`)
  })

  test('removes a namespace with children', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = n1.addNamespace()
    const m1 = n2.addModel()
    n1.removeNamespace(n2.key)
    assert.isUndefined(root.findNamespace(n2.key))
    assert.isFalse(root.graph.hasNode(n2.key))
    assert.isFalse(root.graph.hasNode(m1.key))
    assert.deepEqual([...n1.listNamespaces()], [])
  })

  test('removes a namespace with models', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = n1.addNamespace()
    const m1 = n2.addModel()
    n1.removeNamespace(n2.key)
    assert.isUndefined(root.findModel(m1.key))
    assert.isFalse(root.graph.hasNode(m1.key))
  })

  test('removes a namespace with entities', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = n1.addNamespace()
    const m1 = n2.addModel()
    const entity = m1.addEntity()
    n1.removeNamespace(n2.key)
    assert.isUndefined(root.findEntity(entity.key))
    assert.isFalse(root.graph.hasNode(entity.key))
  })

  test('removes a namespace with properties', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = n1.addNamespace()
    const m1 = n2.addModel()
    const e1 = m1.addEntity()
    const property = e1.addProperty()
    n1.removeNamespace(n2.key)
    assert.isUndefined(root.findProperty(property.key))
    assert.isFalse(root.graph.hasNode(property.key))
  })

  test('removes a namespace with associations', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = n1.addNamespace()
    const m1 = n2.addModel()
    const e1 = m1.addEntity()
    const a1 = e1.addAssociation()
    n1.removeNamespace(n2.key)
    assert.isUndefined(root.findAssociation(a1.key))
    assert.isFalse(root.graph.hasNode(a1.key))
  })

  test('removes a namespace with children - checks parent-child relationship', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = n1.addNamespace()
    n1.removeNamespace(n2.key)
    assert.isFalse(root.graph.hasParent(n2.key, n1.key))
  })

  test('removes a namespace with children - checks items list', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = n1.addNamespace()
    n1.removeNamespace(n2.key)
    assert.notDeepInclude([...n1.listNamespaces()], n2)
  })

  test('removes a namespace with models - checks parent-child relationship', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = n1.addNamespace()
    const m1 = n2.addModel()
    n1.removeNamespace(n2.key)
    assert.isFalse(root.graph.hasParent(m1.key, n2.key))
  })

  test('removes a namespace with models - checks items list', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = n1.addNamespace()
    const m1 = n2.addModel()
    n1.removeNamespace(n2.key)
    assert.notDeepInclude([...n2.listModels()], m1)
  })
})

test.group('DomainNamespace.attachNamespace()', () => {
  test('attaches a child namespace', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = root.addNamespace()
    n1.attachNamespace(n2.key)
    assert.deepInclude([...n1.listNamespaces()], n2)
  })

  test('throws an error if the namespace does not exist', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    assert.throws(() => {
      n1.attachNamespace('non-existent-ns')
    }, "Trying to attach the non-existent-ns namespace, but it doesn't exist")
  })

  test('attaches a namespace - checks parent-child relationship', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = root.addNamespace()
    n1.attachNamespace(n2.key)
    assert.isTrue(root.graph.hasParent(n2.key, n1.key))
  })

  test('attaches a namespace - checks items list', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = root.addNamespace()
    n1.attachNamespace(n2.key)
    assert.deepInclude([...n1.listNamespaces()], n2)
  })

  test('attaches a namespace - does notify change', async ({ assert, sleep }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = root.addNamespace()
    // addNamespace dispatches the change event
    await sleep(0)
    n1.attachNamespace(n2.key)
    await assert.dispatches(root, 'change', { timeout: 20 })
  })

  test('throws an error when attaching not a namespace', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const m1 = n1.addModel()
    assert.throws(() => {
      n1.attachNamespace(m1.key)
    }, `Trying to attach the ${m1.key} namespace, but it is not a namespace`)
  })

  test('attaches a namespace and removes it from the previous parent', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = n1.addNamespace()
    const n3 = root.addNamespace()
    n3.attachNamespace(n2.key)
    assert.deepEqual(n1.fields, [])
  })

  test('attaches a namespace from root', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = root.addNamespace()
    n1.attachNamespace(n2.key)
    assert.deepEqual(n1.fields, [{ key: n2.key, type: 'namespace' }])
  })
})

test.group('DomainNamespace.detachNamespace()', () => {
  test('detaches a child namespace', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = n1.addNamespace()
    n1.detachNamespace(n2.key)
    assert.ok(root.findNamespace(n2.key))
    assert.isTrue(root.graph.hasNode(n2.key))
    assert.deepEqual([...n1.listNamespaces()], [])
  })

  test('throws an error if the namespace does not exist', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    assert.throws(() => {
      n1.detachNamespace('non-existent-ns')
    }, `Trying to detach the non-existent-ns namespace, but it doesn't exist`)
  })

  test('detaches a namespace - checks parent-child relationship', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = n1.addNamespace()
    n1.detachNamespace(n2.key)
    assert.isFalse(root.graph.hasParent(n2.key, n1.key))
  })

  test('detaches a namespace - checks items list', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = n1.addNamespace()
    n1.detachNamespace(n2.key)
    assert.notDeepInclude([...n1.listNamespaces()], n2)
  })

  test('detaches a namespace - does not notify change', async ({ assert, sleep }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const n2 = n1.addNamespace()
    await sleep(0)
    await assert.notDispatches(root, 'change', { timeout: 20 })
    n1.detachNamespace(n2.key)
  })

  test('throws an error when detaching not a namespace', ({ assert }) => {
    const root = new DataDomain()
    const n1 = root.addNamespace()
    const m1 = n1.addModel()
    assert.throws(() => {
      n1.detachNamespace(m1.key)
    }, `Trying to detach the ${m1.key} namespace, but it is not a namespace`)
  })
})

test.group('DomainNamespace.listNamespaces()', () => {
  test('lists all child namespaces', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const childNamespace1 = namespace.addNamespace()
    const childNamespace2 = namespace.addNamespace()
    const namespaces = [...namespace.listNamespaces()]
    assert.lengthOf(namespaces, 2)
    assert.deepInclude(namespaces, childNamespace1)
    assert.deepInclude(namespaces, childNamespace2)
  })

  test('returns an empty array if there are no child namespaces', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const namespaces = [...namespace.listNamespaces()]
    assert.lengthOf(namespaces, 0)
  })

  test('returns only namespaces', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const childNamespace = namespace.addNamespace()
    namespace.addModel()
    const namespaces = [...namespace.listNamespaces()]
    assert.lengthOf(namespaces, 1)
    assert.deepInclude(namespaces, childNamespace)
  })

  test('namespaces in the list are of type DomainNamespace', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    namespace.addNamespace()
    const namespaces = [...namespace.listNamespaces()]
    assert.instanceOf(namespaces[0], DomainNamespace)
  })

  test('lists namespaces in the order they were added', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const childNamespace1 = namespace.addNamespace()
    const childNamespace2 = namespace.addNamespace()
    const namespaces = [...namespace.listNamespaces()]
    assert.deepEqual(namespaces, [childNamespace1, childNamespace2])
  })

  test('lists namespaces when there are no namespaces', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const namespaces = [...namespace.listNamespaces()]
    assert.deepEqual(namespaces, [])
  })

  test('lists namespaces when there are other domain elements', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const childNamespace = namespace.addNamespace()
    namespace.addModel()
    const namespaces = [...namespace.listNamespaces()]
    assert.deepEqual(namespaces, [childNamespace])
  })
})

test.group('DomainNamespace.hasNamespaces()', () => {
  test('returns true if there are child namespaces', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    namespace.addNamespace()
    assert.isTrue(namespace.hasNamespaces())
  })

  test('returns false if there are no child namespaces', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    assert.isFalse(namespace.hasNamespaces())
  })

  test('returns false if there are only models', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    namespace.addModel()
    assert.isFalse(namespace.hasNamespaces())
  })

  test('returns true if there are both namespaces and models', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    namespace.addNamespace()
    namespace.addModel()
    assert.isTrue(namespace.hasNamespaces())
  })

  test('returns false if the namespace has no fields', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    namespace.fields = []
    assert.isFalse(namespace.hasNamespaces())
  })
})
