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

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

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

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

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

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

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

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

test.group('DomainNamespace.removeModel()', () => {
  test('removes a child model', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model = namespace.addModel()
    namespace.removeModel(model.key)
    assert.isUndefined(dataDomain.findModel(model.key))
    assert.isFalse(dataDomain.graph.hasNode(model.key))
    assert.deepEqual([...namespace.listModels()], [])
  })

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

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

  test('removes a model with entities', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model = namespace.addModel()
    const entity = dataDomain.addEntity(model.key)
    namespace.removeModel(model.key)
    assert.isUndefined(dataDomain.findModel(model.key))
    assert.isFalse(dataDomain.graph.hasNode(model.key))
    assert.isUndefined(dataDomain.findEntity(entity.key))
    assert.isFalse(dataDomain.graph.hasNode(entity.key))
  })

  test('removes a model - checks parent-child relationship', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model = namespace.addModel()
    namespace.removeModel(model.key)
    assert.isFalse(dataDomain.graph.hasParent(model.key, namespace.key))
  })

  test('removes a model - checks items list', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model = namespace.addModel()
    namespace.removeModel(model.key)
    assert.notDeepInclude([...namespace.listModels()], model)
  })
})

test.group('DomainNamespace.attachModel()', () => {
  test('attaches a child model', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model = dataDomain.addModel()
    namespace.attachModel(model.key)
    assert.deepInclude([...namespace.listModels()], model)
  })

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

  test('attaches a model - checks parent-child relationship', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const model = dataDomain.addModel()
    parentNamespace.attachModel(model.key)
    assert.isTrue(dataDomain.graph.hasParent(model.key, parentNamespace.key))
  })

  test('attaches a model - checks items list', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const model = dataDomain.addModel()
    parentNamespace.attachModel(model.key)
    assert.deepInclude([...parentNamespace.listModels()], model)
  })

  test('attaches a model - notifies a change', async ({ assert, sleep }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const model = dataDomain.addModel()
    await sleep(0)
    parentNamespace.attachModel(model.key)
    await assert.dispatches(dataDomain, 'change', { timeout: 1 })
  })

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

test.group('DomainNamespace.detachModel()', () => {
  test('detaches a child model', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model = namespace.addModel()
    namespace.detachModel(model.key)
    assert.ok(dataDomain.findModel(model.key))
    assert.isTrue(dataDomain.graph.hasNode(model.key))
    assert.deepEqual([...namespace.listModels()], [])
  })

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

  test('detaches a model - checks parent-child relationship', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const model = parentNamespace.addModel()
    parentNamespace.detachModel(model.key)
    assert.isFalse(dataDomain.graph.hasParent(model.key, parentNamespace.key))
  })

  test('detaches a model - checks items list', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const model = parentNamespace.addModel()
    parentNamespace.detachModel(model.key)
    assert.notDeepInclude([...parentNamespace.listModels()], model)
  })

  test('detaches a model - does not notify change', async ({ assert, sleep }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const model = parentNamespace.addModel()
    await sleep(0)
    parentNamespace.detachModel(model.key)
    await assert.notDispatches(dataDomain, 'change', { timeout: 20 })
  })

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

test.group('DomainNamespace.listModels()', () => {
  test('lists all child models', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model1 = namespace.addModel()
    const model2 = namespace.addModel()
    const models = [...namespace.listModels()]
    assert.lengthOf(models, 2)
    assert.deepInclude(models, model1)
    assert.deepInclude(models, model2)
  })

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

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

  test('models in the list are of type DomainModel', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    namespace.addModel()
    const models = [...namespace.listModels()]
    assert.instanceOf(models[0], DomainModel)
  })

  test('lists models in the order they were added', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model1 = namespace.addModel()
    const model2 = namespace.addModel()
    const models = [...namespace.listModels()]
    assert.deepEqual(models, [model1, model2])
  })

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

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

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

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

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

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

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