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

test.group('DomainModel.addEntity()', () => {
  test('adds an entity to the model', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = model.addEntity({ key: 'test-entity' })
    assert.instanceOf(entity, DomainEntity)
    assert.equal(entity.kind, DomainEntityKind)
    assert.equal(entity.key, 'test-entity')
    assert.deepInclude(entity.info, { name: 'new_entity' })
    assert.isTrue(dataDomain.graph.hasNode(entity.key))
    assert.equal(dataDomain.graph.parent(entity.key), model.key)
  })

  test('adds an entity with default values', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = model.addEntity()
    assert.instanceOf(entity, DomainEntity)
    assert.equal(entity.kind, DomainEntityKind)
    assert.typeOf(entity.key, 'string')
    assert.isNotEmpty(entity.key)
    assert.deepInclude(entity.info, { name: 'new_entity' })
    assert.isTrue(dataDomain.graph.hasNode(entity.key))
    assert.equal(dataDomain.graph.parent(entity.key), model.key)
  })

  test('throws an error if an entity with the same key already exists', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    model.addEntity({ key: 'test-entity' })
    assert.throws(() => {
      model.addEntity({ key: 'test-entity' })
    }, 'Entity with key test-entity already exists')
  })

  test('notifies change', async ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    model.addEntity({ key: 'test-entity' })
    await assert.dispatches(dataDomain, 'change', { timeout: 20 })
  })

  test('adds an entity to the fields array', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = model.addEntity({ key: 'test-entity' })
    assert.deepEqual(model.fields, [{ key: entity.key, type: 'entity' }])
  })

  test('adds an entity to the root graph', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = model.addEntity({ key: 'test-entity' })
    assert.isTrue(dataDomain.graph.hasNode(entity.key))
  })

  test('adds an entity with a parent relationship', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = model.addEntity({ key: 'test-entity' })
    assert.equal(dataDomain.graph.parent(entity.key), model.key)
  })
})

test.group('DomainModel.removeEntity()', () => {
  test('removes an entity from the model', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = model.addEntity({ key: 'test-entity' })
    model.removeEntity(entity.key)
    assert.isFalse(dataDomain.graph.hasNode(entity.key))
    assert.deepEqual(model.fields, [])
  })

  test('throws an error if the entity does not exist', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    assert.throws(() => {
      model.removeEntity('non-existent-entity')
    }, "Trying to remove the non-existent-entity entity, but it doesn't exist")
  })

  test('throws an error if the key is not an entity', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    assert.throws(() => {
      model.removeEntity(model.key)
    }, `Trying to remove the ${model.key} entity, but it is not an entity`)
  })

  test('throws an error if the entity belongs to another parent', ({ assert }) => {
    const root = new DataDomain()
    const m1 = root.addModel()
    const m2 = root.addModel()
    const e1 = m1.addEntity()
    assert.throws(() => {
      m2.removeEntity(e1.key)
    }, `Trying to remove the ${e1.key} entity, but it is not a child of this model`)
  })

  test('notifies change', async ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = model.addEntity({ key: 'test-entity' })
    model.removeEntity(entity.key)
    await assert.dispatches(dataDomain, 'change', { timeout: 20 })
  })

  test('removes the entity from the fields array', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = model.addEntity({ key: 'test-entity' })
    model.removeEntity(entity.key)
    assert.deepEqual(model.fields, [])
  })

  test('removes the entity from the root graph', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = model.addEntity({ key: 'test-entity' })
    model.removeEntity(entity.key)
    assert.isFalse(dataDomain.graph.hasNode(entity.key))
  })

  test('removes the parent relationship', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = model.addEntity({ key: 'test-entity' })
    model.removeEntity(entity.key)
    assert.isUndefined(dataDomain.graph.parent(entity.key))
    assert.deepEqual([...dataDomain.graph.children(model.key)], [])
  })

  test('removes an entity with properties', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = model.addEntity({ key: 'test-entity' })
    entity.addProperty({ key: 'test-property', type: 'string' })
    model.removeEntity(entity.key)
    assert.isFalse(dataDomain.graph.hasNode(entity.key))
    assert.isFalse(dataDomain.graph.hasNode('test-property'))
  })

  test('removes an entity with associations', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = model.addEntity({ key: 'test-entity' })
    const association = entity.addAssociation()
    model.removeEntity(entity.key)
    assert.isFalse(dataDomain.graph.hasNode(entity.key))
    assert.isFalse(dataDomain.graph.hasNode(association.key))
  })
})

test.group('DomainModel.attachEntity()', () => {
  test('attaches an entity to the model', ({ assert }) => {
    const root = new DataDomain()
    const m1 = root.addModel()
    const m2 = root.addModel()
    const e1 = m1.addEntity()
    m2.attachEntity(e1.key)
    assert.isTrue(root.graph.hasNode(e1.key))
    assert.equal(root.graph.parent(e1.key), m2.key)
    assert.deepEqual(m2.fields, [{ key: e1.key, type: 'entity' }])
  })

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

  test('throws an error if the key is not an entity', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    assert.throws(() => {
      model.attachEntity(model.key)
    }, `Trying to attach the ${model.key} entity, but it is not a entity`)
  })

  test('throws an error if the entity is already a child of this model', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = model.addEntity()
    assert.throws(() => {
      model.attachEntity(entity.key)
    }, `Trying to attach the ${entity.key} entity, but it is already a child of this model`)
  })

  test('throws an error if the entity is not in the same domain', ({ assert }) => {
    const dataDomain1 = new DataDomain()
    const model1 = dataDomain1.addModel()

    const dataDomain2 = new DataDomain()
    const model2 = dataDomain2.addModel()
    const entity = model2.addEntity()
    dataDomain2.info.version = '1.0.0'
    dataDomain1.registerForeignDomain(dataDomain2)

    assert.throws(() => {
      model1.attachEntity(`${dataDomain2.key}:${entity.key}`)
    }, `Trying to attach the ${dataDomain2.key}:${entity.key} entity, but it is not in the same domain`)
  })

  test('attaches an entity to the fields array', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model1 = dataDomain.addModel({ key: 'test-model1' })
    const model2 = dataDomain.addModel({ key: 'test-model2' })
    const entity = model1.addEntity({ key: 'test-entity' })
    model2.attachEntity(entity.key)
    assert.deepEqual(model2.fields, [{ key: entity.key, type: 'entity' }])
  })

  test('attaches an entity to the root graph', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model1 = dataDomain.addModel({ key: 'test-model1' })
    const model2 = dataDomain.addModel({ key: 'test-model2' })
    const entity = model1.addEntity({ key: 'test-entity' })
    model2.attachEntity(entity.key)
    assert.isTrue(dataDomain.graph.hasNode(entity.key))
  })

  test('attaches an entity with a parent relationship', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model1 = dataDomain.addModel({ key: 'test-model1' })
    const model2 = dataDomain.addModel({ key: 'test-model2' })
    const entity = model1.addEntity({ key: 'test-entity' })
    model2.attachEntity(entity.key)
    assert.equal(dataDomain.graph.parent(entity.key), model2.key)
  })

  test('attaches an entity and removes it from the previous parent', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model1 = dataDomain.addModel()
    const model2 = dataDomain.addModel()
    const entity = model1.addEntity()
    model2.attachEntity(entity.key)
    assert.deepEqual(model1.fields, [])
  })
})

test.group('DomainModel.detachEntity()', () => {
  test('detaches an entity from the model', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = model.addEntity()
    model.detachEntity(entity.key)
    assert.isFalse(dataDomain.graph.hasParent(entity.key, model.key))
    assert.deepEqual(model.fields, [])
  })

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

  test('throws an error if the key is not an entity', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    model.fields.push({ key: model.key, type: 'entity' })
    assert.throws(() => {
      model.detachEntity(model.key)
    }, `Trying to detach the ${model.key} entity, but it is not an entity`)
  })

  test('throws an error if the entity is not a child of this model', ({ assert }) => {
    const root = new DataDomain()
    const m1 = root.addModel()
    const m2 = root.addModel()
    const e1 = m1.addEntity()
    m2.fields.push({ key: e1.key, type: 'entity' })
    assert.throws(() => {
      m2.detachEntity(e1.key)
    }, `Trying to detach the ${e1.key} entity, but it is not a child of this model`)
  })

  test('detaches an entity from the fields array', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = model.addEntity()
    model.detachEntity(entity.key)
    assert.deepEqual(model.fields, [])
  })

  test('adds an entity from the root graph', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = model.addEntity()
    model.detachEntity(entity.key)
    assert.isTrue(dataDomain.graph.hasNode(entity.key))
  })

  test('detaches an entity and sets its parent to the root graph', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = model.addEntity()
    model.detachEntity(entity.key)
    assert.equal(dataDomain.graph.parent(entity.key), undefined)
  })
})

test.group('DomainModel.listEntities()', () => {
  test('lists all entities in the model', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    const entities = [...model.listEntities()]
    assert.lengthOf(entities, 2)
    assert.deepInclude(entities, entity1)
    assert.deepInclude(entities, entity2)
  })

  test('returns an empty array if there are no entities', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entities = [...model.listEntities()]
    assert.lengthOf(entities, 0)
  })

  test('returns only entities', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity1 = model.addEntity({ key: 'entity1' })
    dataDomain.addNamespace({ key: 'ns1' })
    const entities = [...model.listEntities()]
    assert.lengthOf(entities, 1)
    assert.deepInclude(entities, entity1)
  })

  test('entities in the list are of type DomainEntity', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    model.addEntity({ key: 'entity1' })
    const entities = [...model.listEntities()]
    assert.instanceOf(entities[0], DomainEntity)
  })

  test('lists entities in the order they were added', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    const entities = [...model.listEntities()]
    assert.deepEqual(entities, [entity1, entity2])
  })

  test('lists entities when there are no entities', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entities = [...model.listEntities()]
    assert.deepEqual(entities, [])
  })

  test('lists entities when there are other domain elements', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity1 = model.addEntity({ key: 'entity1' })
    dataDomain.addNamespace({ key: 'ns1' })
    const entities = [...model.listEntities()]
    assert.deepEqual(entities, [entity1])
  })
})

test.group('DomainModel.hasEntities()', () => {
  test('returns true if there are child entities', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    model.addEntity({ key: 'test-entity' })
    assert.isTrue(model.hasEntities())
  })

  test('returns false if there are no child entities', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    assert.isFalse(model.hasEntities())
  })

  test('returns false if there are only other domain elements', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    dataDomain.addNamespace({ key: 'test-namespace' })
    assert.isFalse(model.hasEntities())
  })

  test('returns true if there are both entities and other domain elements', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    model.addEntity({ key: 'test-entity' })
    dataDomain.addNamespace({ key: 'test-namespace' })
    assert.isTrue(model.hasEntities())
  })

  test('returns false if the model has no fields', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    model.fields = []
    assert.isFalse(model.hasEntities())
  })
})
