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

test.group('DomainEntity.listParents()', () => {
  test('lists all parent entities', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity()
    const entity2 = model.addEntity()
    const entity3 = model.addEntity()
    entity2.addParent(entity1.key)
    entity2.addParent(entity3.key)
    const parents = [...entity2.listParents()]
    assert.lengthOf(parents, 2)
    assert.deepInclude(parents, entity1)
    assert.deepInclude(parents, entity3)
  })

  test('returns an empty array if there are no parents', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = model.addEntity()
    const parents = [...entity.listParents()]
    assert.lengthOf(parents, 0)
  })

  test('returns only entities', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity()
    const entity2 = model.addEntity()
    entity2.addParent(entity1.key)
    entity2.addProperty({ key: 'test-property', type: 'string' })
    const parents = [...entity2.listParents()]
    assert.lengthOf(parents, 1)
    assert.deepInclude(parents, entity1)
  })

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

  test('lists parents when there are multiple levels of inheritance', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity()
    const entity2 = model.addEntity()
    const entity3 = model.addEntity()
    entity3.addParent(entity2.key)
    entity2.addParent(entity1.key)
    const parents = [...entity3.listParents()]
    assert.deepEqual(parents, [entity2])
  })
})

test.group('DomainEntity.addParent()', () => {
  test('adds a parent to the entity', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    entity2.addParent(entity1.key)
    assert.isTrue(dataDomain.graph.hasEdge(entity2.key, entity1.key))
    assert.deepEqual([...entity2.listParents()], [entity1])
  })

  test('throws an error if the parent does not exist', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = model.addEntity()
    assert.throws(() => {
      entity.addParent('non-existent-entity')
    }, 'Entity with key "non-existent-entity" not found')
  })

  test('throws an error if adding self as parent', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = model.addEntity()
    assert.throws(() => {
      entity.addParent(entity.key)
    }, 'Entity cannot be a parent of itself')
  })

  test('throws an error if circular dependency is detected', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    entity1.addParent(entity2.key)
    assert.throws(() => {
      entity2.addParent(entity1.key)
    }, 'Circular inheritance detected. Cannot add entity1 as a parent')
  })

  test('throws an error if circular dependency is detected - indirect', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    const entity3 = model.addEntity({ key: 'entity3' })
    entity1.addParent(entity2.key)
    entity2.addParent(entity3.key)
    assert.throws(() => {
      entity3.addParent(entity1.key)
    }, 'Circular inheritance detected. Cannot add entity1 as a parent')
  })

  test('throws an error if parent already exists', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    entity2.addParent(entity1.key)
    assert.throws(() => {
      entity2.addParent(entity1.key)
    }, 'Parent entity1 already exists')
  })

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

  test('creates a graph edge', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    entity2.addParent(entity1.key)
    assert.isTrue(dataDomain.graph.hasEdge(entity2.key, entity1.key))
    assert.deepEqual(dataDomain.graph.edge(entity2.key, entity1.key), { type: 'parent' })
  })

  test('adds a foreign domain parent to the entity', ({ assert }) => {
    const dataDomain = new DataDomain()
    const foreignDomain = new DataDomain({ key: 'external-domain' })
    const model = dataDomain.addModel()
    const entity = model.addEntity({ key: 'child-entity' })
    const foreignModel = foreignDomain.addModel()
    const foreignEntity = foreignModel.addEntity({ key: 'parent-entity' })
    const foreignKey = `${foreignDomain.key}:${foreignEntity.key}`

    // Register foreign domain and add foreign parent
    foreignDomain.info.version = '1.0.0'
    dataDomain.registerForeignDomain(foreignDomain)
    entity.addParent('parent-entity', 'external-domain')

    // Verify it was added correctly
    assert.isTrue(dataDomain.graph.hasEdge(entity.key, foreignKey))
    assert.deepEqual(dataDomain.graph.edge(entity.key, foreignKey), {
      type: 'parent',
      domain: 'external-domain',
      foreign: true,
    })
    const parents = [...entity.listParents()]
    assert.lengthOf(parents, 1)
    assert.deepEqual(parents[0], foreignEntity)
  })

  test('throws an error if foreign domain parent does not exist', ({ assert }) => {
    const dataDomain = new DataDomain()
    const foreignDomain = new DataDomain({ key: 'external-domain' })
    const model = dataDomain.addModel()
    const entity = model.addEntity({ key: 'child-entity' })

    // Register foreign domain without the parent entity
    foreignDomain.info.version = '1.0.0'
    dataDomain.registerForeignDomain(foreignDomain)

    assert.throws(() => {
      entity.addParent('non-existent-parent', 'external-domain')
    }, 'Entity with key "non-existent-parent" not found in domain "external-domain"')
  })

  test('throws an error if foreign domain is not registered', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = model.addEntity({ key: 'child-entity' })

    assert.throws(() => {
      entity.addParent('parent-entity', 'non-existent-domain')
    }, 'Foreign domain non-existent-domain not found')
  })

  test('can add both local and foreign parents', ({ assert }) => {
    const dataDomain = new DataDomain()
    const foreignDomain = new DataDomain({ key: 'external-domain' })
    const model = dataDomain.addModel()
    const localParent = model.addEntity({ key: 'local-parent' })
    const entity = model.addEntity({ key: 'child-entity' })
    const foreignModel = foreignDomain.addModel()
    const foreignEntity = foreignModel.addEntity({ key: 'foreign-parent' })
    const foreignKey = `${foreignDomain.key}:${foreignEntity.key}`

    // Register foreign domain and add both parents
    foreignDomain.info.version = '1.0.0'
    dataDomain.registerForeignDomain(foreignDomain)
    entity.addParent(localParent.key)
    entity.addParent('foreign-parent', 'external-domain')

    // Verify both parents were added
    assert.isTrue(dataDomain.graph.hasEdge(entity.key, localParent.key))
    assert.isTrue(dataDomain.graph.hasEdge(entity.key, foreignKey))

    const parents = [...entity.listParents()]
    assert.lengthOf(parents, 2)
    assert.deepInclude(parents, localParent)
    assert.deepInclude(parents, foreignEntity)
  })

  test('throws an error if trying to add the same foreign parent twice', ({ assert }) => {
    const dataDomain = new DataDomain()
    const foreignDomain = new DataDomain({ key: 'external-domain' })
    const model = dataDomain.addModel()
    const entity = model.addEntity({ key: 'child-entity' })
    const foreignModel = foreignDomain.addModel()
    foreignModel.addEntity({ key: 'parent-entity' })

    // Register foreign domain and add foreign parent
    foreignDomain.info.version = '1.0.0'
    dataDomain.registerForeignDomain(foreignDomain)
    entity.addParent('parent-entity', 'external-domain')

    assert.throws(() => {
      entity.addParent('parent-entity', 'external-domain')
    }, 'Parent parent-entity already exists')
  })
})

test.group('DomainEntity.removeParent()', () => {
  test('removes a parent from the entity', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    entity2.addParent(entity1.key)
    entity2.removeParent(entity1.key)
    assert.isFalse(dataDomain.graph.hasEdge(entity2.key, entity1.key))
    assert.deepEqual([...entity2.listParents()], [])
  })

  test('throws an error if the parent does not exist', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = model.addEntity()
    assert.throws(() => {
      entity.removeParent('non-existent-entity')
    }, `Trying to remove a parent non-existent-entity from ${entity.key}, but it doesn't exist`)
  })

  test('throws an error if foreign domain parent does not exist', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = model.addEntity({ key: 'child-entity' })
    assert.throws(() => {
      entity.removeParent('non-existent-entity', 'external-domain')
    }, `Foreign domain external-domain not found`)
  })

  test('throws an error if edge exists but is not a parent relationship', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    // Create a non-parent edge
    dataDomain.graph.setEdge(entity2.key, entity1.key, { type: 'association' })
    assert.throws(() => {
      entity2.removeParent(entity1.key)
    }, `Edge between ${entity2.key} and entity1 exists but is not a parent relationship`)
  })

  test('throws an error if foreign domain edge exists but is not a parent relationship', ({ assert }) => {
    const dataDomain = new DataDomain()
    const foreignDomain = new DataDomain({ key: 'external-domain' })
    const model = dataDomain.addModel()
    const entity = model.addEntity({ key: 'child-entity' })
    const foreignModel = foreignDomain.addModel()
    const foreignEntity = foreignModel.addEntity({ key: 'parent-entity' })

    // Register foreign domain and create a non-parent edge
    foreignDomain.info.version = '1.0.0'
    dataDomain.registerForeignDomain(foreignDomain)
    const foreignKey = `${foreignDomain.key}:${foreignEntity.key}`
    dataDomain.graph.setEdge(entity.key, foreignKey, { type: 'association' })

    assert.throws(() => {
      entity.removeParent('parent-entity', 'external-domain')
    }, `Edge between child-entity and parent-entity in domain "external-domain" exists but is not a parent relationship`)
  })

  test('removes a foreign domain parent from the entity', ({ assert }) => {
    const dataDomain = new DataDomain()
    const foreignDomain = new DataDomain({ key: 'external-domain' })
    const model = dataDomain.addModel()
    const entity = model.addEntity({ key: 'child-entity' })
    const foreignModel = foreignDomain.addModel()
    const foreignEntity = foreignModel.addEntity({ key: 'parent-entity' })
    const foreignKey = `${foreignDomain.key}:${foreignEntity.key}`

    // Register foreign domain and add foreign parent
    foreignDomain.info.version = '1.0.0'
    dataDomain.registerForeignDomain(foreignDomain)
    entity.addParent('parent-entity', 'external-domain')

    // Verify it was added
    assert.isTrue(dataDomain.graph.hasEdge(entity.key, foreignKey))
    assert.deepEqual(dataDomain.graph.edge(entity.key, foreignKey), {
      type: 'parent',
      domain: 'external-domain',
      foreign: true,
    })

    // Remove the foreign parent
    entity.removeParent('parent-entity', 'external-domain')
    assert.isFalse(dataDomain.graph.hasEdge(entity.key, foreignKey))
  })

  test('removes local parent while keeping foreign parent', ({ assert }) => {
    const dataDomain = new DataDomain()
    const foreignDomain = new DataDomain({ key: 'external-domain' })
    const model = dataDomain.addModel()
    const localParent = model.addEntity({ key: 'local-parent' })
    const entity = model.addEntity({ key: 'child-entity' })
    const foreignModel = foreignDomain.addModel()
    const foreignEntity = foreignModel.addEntity({ key: 'foreign-parent' })
    const foreignKey = `${foreignDomain.key}:${foreignEntity.key}`

    // Register foreign domain and add both parents
    foreignDomain.info.version = '1.0.0'
    dataDomain.registerForeignDomain(foreignDomain)
    entity.addParent(localParent.key)
    entity.addParent('foreign-parent', 'external-domain')

    // Remove only the local parent
    entity.removeParent(localParent.key)

    // Verify local parent is removed but foreign remains
    assert.isFalse(dataDomain.graph.hasEdge(entity.key, localParent.key))
    assert.isTrue(dataDomain.graph.hasEdge(entity.key, foreignKey))
  })

  test('removes foreign parent while keeping local parent', ({ assert }) => {
    const dataDomain = new DataDomain()
    const foreignDomain = new DataDomain({ key: 'external-domain' })
    const model = dataDomain.addModel()
    const localParent = model.addEntity({ key: 'local-parent' })
    const entity = model.addEntity({ key: 'child-entity' })
    const foreignModel = foreignDomain.addModel()
    const foreignEntity = foreignModel.addEntity({ key: 'foreign-parent' })
    const foreignKey = `${foreignDomain.key}:${foreignEntity.key}`

    // Register foreign domain and add both parents
    foreignDomain.info.version = '1.0.0'
    dataDomain.registerForeignDomain(foreignDomain)
    entity.addParent(localParent.key)
    entity.addParent('foreign-parent', 'external-domain')

    // Remove only the foreign parent
    entity.removeParent('foreign-parent', 'external-domain')

    // Verify foreign parent is removed but local remains
    assert.isTrue(dataDomain.graph.hasEdge(entity.key, localParent.key))
    assert.isFalse(dataDomain.graph.hasEdge(entity.key, foreignKey))
  })

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

  test('notifies change when removing foreign domain parent', async ({ assert }) => {
    const dataDomain = new DataDomain()
    const foreignDomain = new DataDomain({ key: 'external-domain' })
    const model = dataDomain.addModel()
    const entity = model.addEntity({ key: 'child-entity' })
    const foreignModel = foreignDomain.addModel()
    foreignModel.addEntity({ key: 'parent-entity' })

    // Register foreign domain and add foreign parent
    foreignDomain.info.version = '1.0.0'
    dataDomain.registerForeignDomain(foreignDomain)
    entity.addParent('parent-entity', 'external-domain')
    entity.removeParent('parent-entity', 'external-domain')
    await assert.dispatches(dataDomain, 'change', { timeout: 20 })
  })

  test('removes the graph edge', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    entity2.addParent(entity1.key)
    entity2.removeParent(entity1.key)
    assert.isFalse(dataDomain.graph.hasEdge(entity2.key, entity1.key))
  })

  test('removes the foreign domain graph edge', ({ assert }) => {
    const dataDomain = new DataDomain()
    const foreignDomain = new DataDomain({ key: 'external-domain' })
    const model = dataDomain.addModel()
    const entity = model.addEntity({ key: 'child-entity' })
    const foreignModel = foreignDomain.addModel()
    foreignModel.addEntity({ key: 'parent-entity' })
    const foreignKey = `${foreignDomain.key}:parent-entity`

    // Register foreign domain and add foreign parent
    foreignDomain.info.version = '1.0.0'
    dataDomain.registerForeignDomain(foreignDomain)
    entity.addParent('parent-entity', 'external-domain')
    entity.removeParent('parent-entity', 'external-domain')
    assert.isFalse(dataDomain.graph.hasEdge(entity.key, foreignKey))
  })
})

test.group('DomainEntity.hasCircularParent()', () => {
  test('returns false when no parents', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    assert.isFalse(entity1.hasCircularParent(entity2.key))
  })

  test('returns true when direct circular dependency', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    entity1.addParent(entity2.key)
    assert.isTrue(entity2.hasCircularParent(entity1.key))
  })

  test('returns true when indirect circular dependency', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity()
    const entity2 = model.addEntity()
    const entity3 = model.addEntity()
    entity1.addParent(entity2.key)
    entity2.addParent(entity3.key)
    assert.isTrue(entity3.hasCircularParent(entity1.key))
  })

  test('returns true when indirect circular dependency - longer chain', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    const entity3 = model.addEntity({ key: 'entity3' })
    const entity4 = model.addEntity({ key: 'entity4' })
    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 }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    const entity3 = model.addEntity({ key: 'entity3' })
    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 }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    const entity3 = model.addEntity({ key: 'entity3' })
    const entity4 = model.addEntity({ key: 'entity4' })
    entity1.addParent(entity2.key)
    entity1.addParent(entity3.key)
    entity2.addParent(entity4.key)
    assert.isTrue(entity4.hasCircularParent(entity1.key))
  })

  test('returns false when there is a parent, but not in the chain', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    const entity3 = model.addEntity({ key: 'entity3' })
    entity3.addParent(entity2.key)
    assert.isFalse(entity1.hasCircularParent(entity3.key))
  })
})

test.group('DomainEntity.hasParents()', () => {
  test('returns true if the entity has parents', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    entity2.addParent(entity1.key)
    assert.isTrue(entity2.hasParents())
  })

  test('returns false if the entity has no parents', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = model.addEntity()
    assert.isFalse(entity.hasParents())
  })

  test('returns false if the entity has other edges but not parent', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = model.addEntity({ key: 'entity1' })
    const entity2 = model.addEntity({ key: 'entity2' })
    dataDomain.graph.setEdge(entity2.key, entity1.key, { type: 'association' })
    assert.isFalse(entity2.hasParents())
  })
})
