import { test } from '@japa/runner'
import { DataDomain } from '../../../src/modeling/DataDomain.js'
import { DomainAssociation } from '../../../src/modeling/DomainAssociation.js'

test.group('DataDomain.addAssociation()', () => {
  test('addAssociation adds an association to an entity', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = dataDomain.addEntity(model.key, { key: 'test-entity' })
    const association = dataDomain.addAssociation(entity.key)
    assert.instanceOf(association, DomainAssociation)
    assert.isTrue(dataDomain.graph.hasNode(association.key))
  }).tags(['@modeling', '@associations', '@add'])

  test('addAssociation adds an association to an entity with a target', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity1 = dataDomain.addEntity(model.key, { key: 'entity1' })
    const entity2 = dataDomain.addEntity(model.key, { key: 'entity2' })
    const association = dataDomain.addAssociation(entity1.key, { key: entity2.key })
    assert.instanceOf(association, DomainAssociation)
    assert.isTrue(dataDomain.graph.hasNode(association.key))
  }).tags(['@modeling', '@associations', '@add'])

  test('addAssociation throws an error if source entity does not exist', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.throws(() => {
      dataDomain.addAssociation('non-existent-entity')
    }, 'Source entity non-existent-entity not found')
  }).tags(['@modeling', '@associations', '@add'])

  test('addAssociation adds an association with default values', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = dataDomain.addEntity(model.key, { key: 'test-entity' })
    const association = dataDomain.addAssociation(entity.key)
    assert.instanceOf(association, DomainAssociation)
    assert.isTrue(dataDomain.graph.hasNode(association.key))
    assert.typeOf(association.key, 'string')
    assert.equal(association.info.name, 'new_association')
  }).tags(['@modeling', '@associations', '@add'])

  test('addAssociation notifies change', async ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = dataDomain.addEntity(model.key, { key: 'test-entity' })
    dataDomain.addAssociation(entity.key)
    await assert.dispatches(dataDomain, 'change', { timeout: 20 })
  }).tags(['@modeling', '@associations', '@add'])

  test('addAssociation adds an association to an entity - checks parent-child relationship', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = dataDomain.addEntity(model.key, { key: 'test-entity' })
    const association = dataDomain.addAssociation(entity.key)
    assert.isTrue(dataDomain.graph.hasEdge(entity.key, association.key))
  }).tags(['@modeling', '@associations', '@add'])

  test('addAssociation adds an association to an entity - checks items list', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = dataDomain.addEntity(model.key, { key: 'test-entity' })
    const association = dataDomain.addAssociation(entity.key)
    assert.deepInclude([...entity.listAssociations()], association)
  }).tags(['@modeling', '@associations', '@add'])

  test('addAssociation throws an error if no source is provided', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.throws(() => {
      // @ts-expect-error For testing purposes, we are passing undefined
      dataDomain.addAssociation(undefined)
    }, 'Source entity undefined not found')
  }).tags(['@modeling', '@associations', '@add'])

  test('addAssociation adds an association to a foreign entity', ({ assert }) => {
    const domain1 = new DataDomain()
    const m1 = domain1.addModel({ key: 'model1' })
    const e1 = domain1.addEntity(m1.key, { key: 'entity1' })

    const domain2 = new DataDomain()
    const m2 = domain2.addModel({ key: 'model2' })
    domain2.addEntity(m2.key, { key: 'entity2' })
    domain2.info.version = '1.0.0'

    domain1.registerForeignDomain(domain2)

    const association = domain1.addAssociation(e1.key, { key: 'entity2', domain: domain2.key })
    assert.instanceOf(association, DomainAssociation)
    assert.isTrue(domain1.graph.hasNode(association.key))
  }).tags(['@modeling', '@associations', '@add'])

  test('addAssociation throws an error if foreign entity does not exist', ({ assert }) => {
    const domain1 = new DataDomain()
    const m1 = domain1.addModel({ key: 'model1' })
    const e1 = domain1.addEntity(m1.key, { key: 'entity1' })

    const domain2 = new DataDomain()
    domain2.info.version = '1.0.0'
    domain1.registerForeignDomain(domain2)

    assert.throws(() => {
      domain1.addAssociation(e1.key, { key: 'non-existent-entity', domain: 'domain2' })
    }, 'Foreign entity domain2:non-existent-entity not found')
  }).tags(['@modeling', '@associations', '@add'])
})

test.group('DataDomain.removeAssociation()', () => {
  test('removes an association from the graph', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = dataDomain.addEntity(model.key, { key: 'test-entity' })
    const association = dataDomain.addAssociation(entity.key)
    dataDomain.removeAssociation(association.key)
    assert.isFalse(dataDomain.graph.hasNode(association.key))
  }).tags(['@modeling', '@associations', '@remove'])

  test('throws an error if the association does not exist', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.throws(() => {
      dataDomain.removeAssociation('non-existent-association')
    }, 'Association non-existent-association does not exist')
  }).tags(['@modeling', '@associations', '@remove'])

  test('removes the association from the parent entity', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = dataDomain.addEntity(model.key, { key: 'test-entity' })
    const association = dataDomain.addAssociation(entity.key)
    dataDomain.removeAssociation(association.key)
    assert.isFalse(entity.hasAssociations())
  }).tags(['@modeling', '@associations', '@remove'])

  test('throws an error when the parent entity is not found', ({ assert }) => {
    const dataDomain = new DataDomain()
    const association = new DomainAssociation(dataDomain, 'non-existent-entity')
    dataDomain.graph.setNode(association.key, association)
    assert.throws(() => {
      dataDomain.removeAssociation(association.key)
    }, `Parent entity not found for association ${association.key}`)
  }).tags(['@modeling', '@associations', '@remove'])

  test('notifies change', async ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = dataDomain.addEntity(model.key, { key: 'test-entity' })
    const association = dataDomain.addAssociation(entity.key)
    dataDomain.removeAssociation(association.key)
    await assert.dispatches(dataDomain, 'change', { timeout: 20 })
  }).tags(['@modeling', '@associations', '@remove'])

  test('removes an association to a foreign entity', ({ assert }) => {
    const domain1 = new DataDomain()
    const m1 = domain1.addModel({ key: 'model1' })
    const e1 = domain1.addEntity(m1.key, { key: 'entity1' })

    const domain2 = new DataDomain()
    const m2 = domain2.addModel({ key: 'model2' })
    const e2 = domain2.addEntity(m2.key, { key: 'entity2' })
    domain2.info.version = '1.0.0'
    domain1.registerForeignDomain(domain2)

    const association = domain1.addAssociation(e1.key, { key: e2.key, domain: domain2.key })
    domain1.removeAssociation(association.key)
    assert.isFalse(domain1.graph.hasNode(association.key))
  }).tags(['@modeling', '@associations', '@remove'])

  test('throws an error if the association is not of type DomainAssociation', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    dataDomain.graph.setNode(model.key, model)
    assert.throws(() => {
      dataDomain.removeAssociation(model.key)
    }, 'Association test-model not found')
  }).tags(['@modeling', '@associations', '@remove'])

  test('removes the association from the fields list', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = dataDomain.addEntity(model.key, { key: 'test-entity' })
    const association = dataDomain.addAssociation(entity.key)
    dataDomain.removeAssociation(association.key)
    assert.deepEqual([...entity.listFields()], [])
  }).tags(['@modeling', '@associations', '@remove'])

  test('keeps both source and target entities in the graph after removing an association', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity1 = dataDomain.addEntity(model.key, { key: 'entity1' })
    const entity2 = dataDomain.addEntity(model.key, { key: 'entity2' })
    const association = dataDomain.addAssociation(entity1.key, { key: entity2.key })
    dataDomain.removeAssociation(association.key)
    assert.isTrue(dataDomain.graph.hasNode(entity1.key))
    assert.isTrue(dataDomain.graph.hasNode(entity2.key))
  }).tags(['@modeling', '@associations', '@remove'])
})

test.group('DataDomain.findAssociation()', () => {
  test('finds an association by its key', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const entity = dataDomain.addEntity(model.key, { key: 'test-entity' })
    const association = dataDomain.addAssociation(entity.key)
    const foundAssociation = dataDomain.findAssociation(association.key)
    assert.deepEqual(foundAssociation, association)
  }).tags(['@modeling', '@associations', '@find'])

  test('returns undefined if association does not exist', ({ assert }) => {
    const dataDomain = new DataDomain()
    const foundAssociation = dataDomain.findAssociation('non-existent-association')
    assert.isUndefined(foundAssociation)
  }).tags(['@modeling', '@associations', '@find'])

  test('returns undefined if key is not an association', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    const foundAssociation = dataDomain.findAssociation(model.key)
    assert.isUndefined(foundAssociation)
  }).tags(['@modeling', '@associations', '@find'])

  test('returns undefined if key is undefined', ({ assert }) => {
    const dataDomain = new DataDomain()
    // @ts-expect-error For testing purposes
    const foundAssociation = dataDomain.findAssociation(undefined)
    assert.isUndefined(foundAssociation)
  }).tags(['@modeling', '@associations', '@find'])

  test('returns undefined if key is null', ({ assert }) => {
    const dataDomain = new DataDomain()
    // @ts-expect-error For testing purposes
    const foundAssociation = dataDomain.findAssociation(null)
    assert.isUndefined(foundAssociation)
  }).tags(['@modeling', '@associations', '@find'])

  test('returns undefined if key is an empty string', ({ assert }) => {
    const dataDomain = new DataDomain()
    const foundAssociation = dataDomain.findAssociation('')
    assert.isUndefined(foundAssociation)
  }).tags(['@modeling', '@associations', '@find'])

  test('returns undefined if key is a number', ({ assert }) => {
    const dataDomain = new DataDomain()
    // @ts-expect-error For testing purposes
    const foundAssociation = dataDomain.findAssociation(123)
    assert.isUndefined(foundAssociation)
  }).tags(['@modeling', '@associations', '@find'])

  test('returns undefined if key is a boolean', ({ assert }) => {
    const dataDomain = new DataDomain()
    // @ts-expect-error For testing purposes
    const foundAssociation = dataDomain.findAssociation(true)
    assert.isUndefined(foundAssociation)
  }).tags(['@modeling', '@associations', '@find'])

  test('returns undefined if key is an object', ({ assert }) => {
    const dataDomain = new DataDomain()
    // @ts-expect-error For testing purposes
    const foundAssociation = dataDomain.findAssociation({})
    assert.isUndefined(foundAssociation)
  }).tags(['@modeling', '@associations', '@find'])

  test('returns undefined if key is an array', ({ assert }) => {
    const dataDomain = new DataDomain()
    // @ts-expect-error For testing purposes
    const foundAssociation = dataDomain.findAssociation([])
    assert.isUndefined(foundAssociation)
  }).tags(['@modeling', '@associations', '@find'])

  test('returns undefined if key is a function', ({ assert }) => {
    const dataDomain = new DataDomain()
    // @ts-expect-error For testing purposes
    const foundAssociation = dataDomain.findAssociation(() => {})
    assert.isUndefined(foundAssociation)
  }).tags(['@modeling', '@associations', '@find'])

  test('finds an association to a foreign domain', ({ assert }) => {
    const domain1 = new DataDomain()
    const m1 = domain1.addModel({ key: 'model1' })
    const e1 = domain1.addEntity(m1.key, { key: 'entity1' })

    const domain2 = new DataDomain()
    const m2 = domain2.addModel({ key: 'model2' })
    const e2 = domain2.addEntity(m2.key, { key: 'entity2' })
    domain2.info.version = '1.0.0'
    domain1.registerForeignDomain(domain2)
    const association = domain1.addAssociation(e1.key, { key: e2.key, domain: domain2.key })

    const foundAssociation = domain1.findAssociation(association.key)
    assert.deepEqual(foundAssociation, association)
  }).tags(['@modeling', '@associations', '@find'])
})
