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

test.group('DataDomain.addProperty()', () => {
  test('addProperty adds a property 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 property = dataDomain.addProperty(entity.key, { key: 'test-property' })
    assert.instanceOf(property, DomainProperty)
    assert.isTrue(dataDomain.graph.hasNode(property.key))
  }).tags(['@modeling', '@property', '@add'])

  test('addProperty adds a property 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 property = dataDomain.addProperty(entity.key)
    assert.instanceOf(property, DomainProperty)
    assert.isTrue(dataDomain.graph.hasNode(property.key))
    assert.equal(property.info.name, 'new_property')
    assert.equal(property.type, 'string')
  }).tags(['@modeling', '@property', '@add'])

  test('addProperty throws an error if parent entity does not exist', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.throws(() => {
      dataDomain.addProperty('non-existent-entity', { key: 'test-property' })
    }, 'Parent entity non-existent-entity does not exist')
  }).tags(['@modeling', '@property', '@add'])

  test('addProperty throws an error if parent is not an entity', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    assert.throws(() => {
      dataDomain.addProperty(model.key)
    }, `Parent entity ${model.key} not found`)
  }).tags(['@modeling', '@property', '@add'])

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

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

  test('addProperty adds a property to a parent entity - checks items list', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = dataDomain.addEntity(model.key)
    const property = dataDomain.addProperty(entity.key)
    assert.deepInclude([...entity.listProperties()], property)
  }).tags(['@modeling', '@property', '@add'])

  test('addProperty throws an error if no parent is provided', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.throws(() => {
      // @ts-expect-error For testing purposes, we are passing undefined
      dataDomain.addProperty(undefined)
    }, 'A property expects a DomainEntity parent')
  }).tags(['@modeling', '@property', '@add'])

  test('addProperty throws when adding property to a foreign entity', ({ assert }) => {
    const domain1 = new DataDomain()
    const m1 = domain1.addModel({ key: 'model1' })
    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)

    assert.throws(() => {
      domain1.addProperty(`${domain2.key}:${e2.key}`)
    }, 'Cannot add a property to a foreign domain')
  }).tags(['@modeling', '@property', '@add'])
})

test.group('DataDomain.removeProperty()', () => {
  test('removes a property from the graph', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = dataDomain.addEntity(model.key)
    const property = dataDomain.addProperty(entity.key)
    dataDomain.removeProperty(property.key)
    assert.isFalse(dataDomain.graph.hasNode(property.key))
  }).tags(['@modeling', '@property', '@remove'])

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

  test('removes the property from the parent entity', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = dataDomain.addEntity(model.key)
    const property = dataDomain.addProperty(entity.key)
    dataDomain.removeProperty(property.key)
    assert.isUndefined(entity.listProperties().next().value)
  }).tags(['@modeling', '@property', '@remove'])

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

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

  test('throws an error if the property is not of type DomainProperty', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    dataDomain.graph.setNode(model.key, model)
    assert.throws(() => {
      dataDomain.removeProperty(model.key)
    }, `Property ${model.key} not found`)
  }).tags(['@modeling', '@property', '@remove'])

  test('throws an error if trying to remove a property from a foreign domain', ({ assert }) => {
    const domain1 = new DataDomain()
    const m1 = domain1.addModel()
    domain1.addEntity(m1.key)

    const domain2 = new DataDomain()
    const m2 = domain2.addModel()
    const e2 = domain2.addEntity(m2.key)
    const property = domain2.addProperty(e2.key)

    domain2.info.version = '1.0.0'
    domain1.registerForeignDomain(domain2)
    assert.throws(() => {
      domain1.removeProperty(`${domain2.key}:${property.key}`)
    }, 'Cannot remove a property from a foreign domain')
  }).tags(['@modeling', '@property', '@remove'])

  test('removes the property from the fields list', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = dataDomain.addEntity(model.key)
    const property = dataDomain.addProperty(entity.key)
    dataDomain.removeProperty(property.key)
    assert.deepEqual([...entity.listFields()], [])
  }).tags(['@modeling', '@property', '@remove'])
})

test.group('DataDomain.findProperty()', () => {
  test('finds a property by its key', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = dataDomain.addEntity(model.key)
    const property = dataDomain.addProperty(entity.key)
    const foundProperty = dataDomain.findProperty(property.key)
    assert.deepEqual(foundProperty, property)
  }).tags(['@modeling', '@property', '@find'])

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

  test('returns undefined if key is not a property', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const foundProperty = dataDomain.findProperty(model.key)
    assert.isUndefined(foundProperty)
  }).tags(['@modeling', '@property', '@find'])

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

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

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

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

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

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

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

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

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

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

    const foundProperty = domain1.findProperty(`${domain2.key}:${property.key}`)
    assert.deepEqual(foundProperty, property)
  }).tags(['@modeling', '@property', '@find'])
})
