import { test } from '@japa/runner'
import {
  DomainNamespaceKind,
  DomainModelKind,
  DomainEntityKind,
  DataDomainKind,
  DomainPropertyKind,
  DomainAssociationKind,
} from '../../../src/models/kinds.js'
import { Thing, DataDomain, DomainNamespace, DomainModel } from '../../../src/index.js'

test.group('DataDomain', () => {
  test('constructor sets default values', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.equal(dataDomain.kind, DataDomainKind)
    assert.typeOf(dataDomain.key, 'string')
    assert.isNotEmpty(dataDomain.key)
    assert.isDefined(dataDomain.graph)
    assert.deepEqual(dataDomain.fields, [])
  }).tags(['@modeling', '@datadomain', '@core', '@unit', '@fast'])

  test('notifyChange dispatches a change event', async ({ assert }) => {
    const dataDomain = new DataDomain()
    dataDomain.notifyChange()
    await assert.dispatches(dataDomain, 'change', { timeout: 20 })
  }).tags(['@modeling', '@datadomain', '@core', '@unit', '@events', '@async'])
})

test.group('DataDomain.removeDomainElement()', () => {
  test('removes a namespace', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    dataDomain.removeDomainElement(namespace.key, DomainNamespaceKind)
    assert.isUndefined(dataDomain.findNamespace(namespace.key))
    assert.isFalse(dataDomain.graph.hasNode(namespace.key))
  }).tags(['@modeling', '@datadomain', '@crud', '@removal'])

  test('removes a data model', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    dataDomain.removeDomainElement(model.key, DomainModelKind)
    assert.isUndefined(dataDomain.findModel(model.key))
    assert.isFalse(dataDomain.graph.hasNode(model.key))
  }).tags(['@modeling', '@datadomain', '@crud', '@removal'])

  test('removes an entity', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = dataDomain.addEntity(model.key)
    dataDomain.removeDomainElement(entity.key, DomainEntityKind)
    assert.isUndefined(dataDomain.findEntity(entity.key))
    assert.isFalse(dataDomain.graph.hasNode(entity.key))
  }).tags(['@modeling', '@datadomain', '@crud', '@removal'])

  test('removes a property', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = dataDomain.addEntity(model.key)
    const property = dataDomain.addProperty(entity.key)
    dataDomain.removeDomainElement(property.key, DomainPropertyKind)
    assert.isUndefined(dataDomain.findProperty(property.key))
    assert.isFalse(dataDomain.graph.hasNode(property.key))
  }).tags(['@modeling', '@datadomain', '@crud', '@removal'])

  test('removes an association', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = dataDomain.addEntity(model.key)
    const association = dataDomain.addAssociation(entity.key)
    dataDomain.removeDomainElement(association.key, DomainAssociationKind)
    assert.isUndefined(dataDomain.findAssociation(association.key))
    assert.isFalse(dataDomain.graph.hasNode(association.key))
  }).tags(['@modeling', '@datadomain', '@crud', '@removal'])

  test('throws an error for an unknown kind', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.throws(() => {
      dataDomain.removeDomainElement('test-unknown', 'unknown-kind')
    }, 'Unknown kind unknown-kind for the object test-unknown')
  }).tags(['@modeling', '@datadomain', '@crud', '@removal'])

  test('throws an error when removing a non-existing namespace', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.throws(() => {
      dataDomain.removeDomainElement('non-existent-namespace', DomainNamespaceKind)
    }, `Namespace non-existent-namespace does not exist`)
  }).tags(['@modeling', '@datadomain', '@crud', '@removal'])

  test('throws an error when removing a non-existing model', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.throws(() => {
      dataDomain.removeDomainElement('non-existent-model', DomainModelKind)
    }, `Data model non-existent-model does not exist`)
  }).tags(['@modeling', '@datadomain', '@crud', '@removal'])

  test('throws an error when removing a non-existing entity', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.throws(() => {
      dataDomain.removeDomainElement('non-existent-entity', DomainEntityKind)
    }, `Entity non-existent-entity does not exist`)
  }).tags(['@modeling', '@datadomain', '@crud', '@removal'])

  test('throws an error when removing a non-existing property', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.throws(() => {
      dataDomain.removeDomainElement('non-existent-property', DomainPropertyKind)
    }, `Property non-existent-property does not exist`)
  }).tags(['@modeling', '@datadomain', '@crud', '@removal'])

  test('throws an error when removing a non-existing association', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.throws(() => {
      dataDomain.removeDomainElement('non-existent-association', DomainAssociationKind)
    }, `Association non-existent-association does not exist`)
  }).tags(['@modeling', '@datadomain', '@crud', '@removal'])

  test('removes a namespace with children', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const childNamespace = dataDomain.addNamespace({}, namespace.key)
    const model = dataDomain.addModel({}, namespace.key)
    const entity = dataDomain.addEntity(model.key)
    const property = dataDomain.addProperty(entity.key)
    const association = dataDomain.addAssociation(entity.key)

    dataDomain.removeDomainElement(namespace.key, DomainNamespaceKind)

    assert.isUndefined(dataDomain.findNamespace(namespace.key))
    assert.isUndefined(dataDomain.findNamespace(childNamespace.key))
    assert.isUndefined(dataDomain.findModel(model.key))
    assert.isUndefined(dataDomain.findEntity(entity.key))
    assert.isUndefined(dataDomain.findProperty(property.key))
    assert.isUndefined(dataDomain.findAssociation(association.key))

    assert.isFalse(dataDomain.graph.hasNode(namespace.key))
    assert.isFalse(dataDomain.graph.hasNode(childNamespace.key))
    assert.isFalse(dataDomain.graph.hasNode(model.key))
    assert.isFalse(dataDomain.graph.hasNode(entity.key))
    assert.isFalse(dataDomain.graph.hasNode(property.key))
    assert.isFalse(dataDomain.graph.hasNode(association.key))
  }).tags(['@modeling', '@datadomain', '@crud', '@removal'])
})

test.group('DataDomain.createSchema()', () => {
  test('creates a schema with default values', ({ assert }) => {
    const schema = DataDomain.createSchema()
    assert.equal(schema.kind, DataDomainKind)
    assert.typeOf(schema.key, 'string')
    assert.isNotEmpty(schema.key)
    assert.deepInclude(schema.info, { name: 'Unnamed domain' })
    assert.isUndefined(schema.graph)
    assert.isUndefined(schema.dependencyList)
    assert.isUndefined(schema.fields)
  }).tags(['@modeling', '@datadomain', '@schema', '@factory'])

  test('creates a schema with provided values', ({ assert }) => {
    const schema = DataDomain.createSchema({
      key: 'test-domain',
      info: { name: 'Test Domain', description: 'A test domain' },
      graph: { nodes: [], edges: [], options: {} },
      dependencyList: [{ key: 'dep1', version: '1.0.0' }],
      fields: [{ key: 'field1', type: 'namespace' }],
    })
    assert.equal(schema.kind, DataDomainKind)
    assert.equal(schema.key, 'test-domain')
    assert.deepInclude(schema.info, { name: 'Test Domain', description: 'A test domain' })
    assert.deepEqual(schema.graph, { nodes: [], edges: [], options: {} })
    assert.deepEqual(schema.dependencyList, [{ key: 'dep1', version: '1.0.0' }])
    assert.deepEqual(schema.fields, [{ key: 'field1', type: 'namespace' }])
  }).tags(['@modeling', '@datadomain', '@schema', '@factory'])

  test('creates a schema with partial info', ({ assert }) => {
    const schema = DataDomain.createSchema({
      info: { name: 'Test Domain' },
    })
    assert.equal(schema.kind, DataDomainKind)
    assert.typeOf(schema.key, 'string')
    assert.isNotEmpty(schema.key)
    assert.deepInclude(schema.info, { name: 'Test Domain' })
  }).tags(['@modeling', '@datadomain', '@schema', '@factory'])

  test('creates a schema with empty info', ({ assert }) => {
    const schema = DataDomain.createSchema({
      info: {},
    })
    assert.equal(schema.kind, DataDomainKind)
    assert.typeOf(schema.key, 'string')
    assert.isNotEmpty(schema.key)
    assert.deepInclude(schema.info, { name: 'Unnamed domain' })
  }).tags(['@modeling', '@datadomain', '@schema', '@factory'])

  test('creates a schema with dependencyList', ({ assert }) => {
    const schema = DataDomain.createSchema({
      dependencyList: [{ key: 'dep1', version: '1.0.0' }],
    })
    assert.equal(schema.kind, DataDomainKind)
    assert.typeOf(schema.key, 'string')
    assert.isNotEmpty(schema.key)
    assert.deepInclude(schema.info, { name: 'Unnamed domain' })
    assert.deepEqual(schema.dependencyList, [{ key: 'dep1', version: '1.0.0' }])
  }).tags(['@modeling', '@datadomain', '@schema', '@factory'])

  test('creates a schema with graph', ({ assert }) => {
    const schema = DataDomain.createSchema({
      graph: { nodes: [], edges: [], options: {} },
    })
    assert.equal(schema.kind, DataDomainKind)
    assert.typeOf(schema.key, 'string')
    assert.isNotEmpty(schema.key)
    assert.deepInclude(schema.info, { name: 'Unnamed domain' })
    assert.deepEqual(schema.graph, { nodes: [], edges: [], options: {} })
  }).tags(['@modeling', '@datadomain', '@schema', '@factory'])

  test('creates a schema with fields', ({ assert }) => {
    const schema = DataDomain.createSchema({
      fields: [{ key: 'field1', type: 'namespace' }],
    })
    assert.equal(schema.kind, DataDomainKind)
    assert.typeOf(schema.key, 'string')
    assert.isNotEmpty(schema.key)
    assert.deepInclude(schema.info, { name: 'Unnamed domain' })
    assert.deepEqual(schema.fields, [{ key: 'field1', type: 'namespace' }])
  }).tags(['@modeling', '@datadomain', '@schema', '@factory'])
})

test.group('DataDomain.constructor()', () => {
  test('creates a new DataDomain with default values', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.equal(dataDomain.kind, DataDomainKind)
    assert.typeOf(dataDomain.key, 'string')
    assert.isNotEmpty(dataDomain.key)
    assert.instanceOf(dataDomain.info, Thing)
    assert.deepInclude(dataDomain.info.toJSON(), { name: 'Unnamed domain' })
    assert.isDefined(dataDomain.graph)
    assert.deepEqual(dataDomain.dependencyList, [])
  }).tags(['@modeling', '@datadomain', '@constructor', '@initialization'])

  test('creates a new DataDomain with provided values', ({ assert }) => {
    const dataDomain = new DataDomain({
      key: 'test-domain',
      info: { name: 'Test Domain', description: 'A test domain' },
      dependencyList: [{ key: 'dep1', version: '1.0.0' }],
    })
    assert.equal(dataDomain.kind, DataDomainKind)
    assert.equal(dataDomain.key, 'test-domain')
    assert.instanceOf(dataDomain.info, Thing)
    assert.deepInclude(dataDomain.info.toJSON(), { name: 'Test Domain', description: 'A test domain' })
    assert.isDefined(dataDomain.graph)
    assert.deepEqual(dataDomain.dependencyList, [{ key: 'dep1', version: '1.0.0' }])
  }).tags(['@modeling', '@datadomain', '@constructor', '@initialization'])

  test('creates a new DataDomain with partial info', ({ assert }) => {
    const dataDomain = new DataDomain({
      info: { name: 'Test Domain' },
    })
    assert.equal(dataDomain.kind, DataDomainKind)
    assert.typeOf(dataDomain.key, 'string')
    assert.isNotEmpty(dataDomain.key)
    assert.instanceOf(dataDomain.info, Thing)
    assert.deepInclude(dataDomain.info.toJSON(), { name: 'Test Domain' })
  }).tags(['@modeling', '@datadomain', '@constructor', '@initialization'])

  test('creates a new DataDomain with empty info', ({ assert }) => {
    const dataDomain = new DataDomain({
      info: {},
    })
    assert.equal(dataDomain.kind, DataDomainKind)
    assert.typeOf(dataDomain.key, 'string')
    assert.isNotEmpty(dataDomain.key)
    assert.instanceOf(dataDomain.info, Thing)
    assert.deepInclude(dataDomain.info.toJSON(), { name: 'Unnamed domain' })
  }).tags(['@modeling', '@datadomain', '@constructor', '@initialization'])

  test('creates a new DataDomain with dependencyList', ({ assert }) => {
    const dataDomain = new DataDomain({
      dependencyList: [{ key: 'dep1', version: '1.0.0' }],
    })
    assert.equal(dataDomain.kind, DataDomainKind)
    assert.typeOf(dataDomain.key, 'string')
    assert.isNotEmpty(dataDomain.key)
    assert.instanceOf(dataDomain.info, Thing)
    assert.deepInclude(dataDomain.info.toJSON(), { name: 'Unnamed domain' })
    assert.deepEqual(dataDomain.dependencyList, [{ key: 'dep1', version: '1.0.0' }])
  }).tags(['@modeling', '@datadomain', '@constructor', '@initialization'])

  test('creates a new DataDomain with a custom key', ({ assert }) => {
    const dataDomain = new DataDomain({ key: 'custom-key' })
    assert.equal(dataDomain.key, 'custom-key')
  }).tags(['@modeling', '@datadomain', '@constructor', '@initialization'])

  test('creates a new DataDomain with custom info', ({ assert }) => {
    const customInfo = { name: 'Custom Name', description: 'Custom Description' }
    const dataDomain = new DataDomain({ info: customInfo })
    assert.deepInclude(dataDomain.info.toJSON(), customInfo)
  }).tags(['@modeling', '@datadomain', '@constructor', '@initialization'])

  test('initializes the graph', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.isDefined(dataDomain.graph)
  }).tags(['@modeling', '@datadomain', '@constructor', '@initialization'])

  test('initializes the dependencies map', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.instanceOf(dataDomain.dependencies, Map)
    assert.equal(dataDomain.dependencies.size, 0)
  }).tags(['@modeling', '@datadomain', '@constructor', '@initialization'])

  test('initializes the dependencyList array', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.isArray(dataDomain.dependencyList)
    assert.equal(dataDomain.dependencyList.length, 0)
  }).tags(['@modeling', '@datadomain', '@constructor', '@initialization'])

  test('does not notify change during initialization', async ({ assert }) => {
    const dataDomain = new DataDomain()
    await assert.notDispatches(dataDomain, 'change', { timeout: 20 })
  }).tags(['@modeling', '@datadomain', '@constructor', '@initialization'])

  test('restores the graph from a serialized state', ({ assert }) => {
    const dataDomain1 = new DataDomain()
    const model = dataDomain1.addModel()
    const entity = dataDomain1.addEntity(model.key)
    const serialized = dataDomain1.toJSON()

    const dataDomain2 = new DataDomain(serialized)
    const restoredEntity = dataDomain2.findEntity(entity.key)
    assert.isDefined(restoredEntity)
    assert.equal(restoredEntity!.key, entity.key)
  }).tags(['@modeling', '@datadomain', '@constructor', '@initialization'])

  test('restores the graph with dependencies', ({ assert }) => {
    const dataDomain1 = new DataDomain()
    const model = dataDomain1.addModel()
    const entity = dataDomain1.addEntity(model.key)
    dataDomain1.info.version = '1.0.0'
    const serialized = dataDomain1.toJSON()

    const dataDomain2 = new DataDomain(serialized, [dataDomain1])
    const restoredEntity = dataDomain2.findForeignEntity(entity.key, dataDomain1.key)
    assert.ok(restoredEntity)
  }).tags(['@modeling', '@datadomain', '@constructor', '@initialization'])
})

test.group('DataDomain.hasFields()', () => {
  test('returns true if there are child namespaces', ({ assert }) => {
    const dataDomain = new DataDomain()
    dataDomain.addNamespace()
    assert.isTrue(dataDomain.hasFields())
  }).tags(['@modeling', '@datadomain', '@fields', '@query'])

  test('returns true if there are child models', ({ assert }) => {
    const dataDomain = new DataDomain()
    dataDomain.addModel()
    assert.isTrue(dataDomain.hasFields())
  }).tags(['@modeling', '@datadomain', '@fields', '@query'])

  test('returns true if there are both namespaces and models', ({ assert }) => {
    const dataDomain = new DataDomain()
    dataDomain.addNamespace()
    dataDomain.addModel()
    assert.isTrue(dataDomain.hasFields())
  }).tags(['@modeling', '@datadomain', '@fields', '@query'])

  test('returns false if there are no child namespaces or models', ({ assert }) => {
    const dataDomain = new DataDomain()
    assert.isFalse(dataDomain.hasFields())
  }).tags(['@modeling', '@datadomain', '@fields', '@query'])

  test('returns false if the data domain has no fields', ({ assert }) => {
    const dataDomain = new DataDomain()
    dataDomain.fields = []
    assert.isFalse(dataDomain.hasFields())
  }).tags(['@modeling', '@datadomain', '@fields', '@query'])

  test('returns true if there are only foreign namespaces or models', ({ assert }) => {
    const dataDomain = new DataDomain()
    const foreignDomain = new DataDomain()
    foreignDomain.info.version = '1.0.0'
    dataDomain.registerForeignDomain(foreignDomain)
    foreignDomain.addNamespace()
    assert.isFalse(dataDomain.hasFields())
  }).tags(['@modeling', '@datadomain', '@fields', '@query'])
})

test.group('DataDomain.listFields()', () => {
  test('lists all child namespaces and models', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model = dataDomain.addModel()
    const fields = [...dataDomain.listFields()]
    assert.lengthOf(fields, 2)
    assert.deepInclude(fields, namespace)
    assert.deepInclude(fields, model)
  }).tags(['@modeling', '@datadomain', '@fields', '@listing'])

  test('returns an empty array if there are no child namespaces or models', ({ assert }) => {
    const dataDomain = new DataDomain()
    const fields = [...dataDomain.listFields()]
    assert.lengthOf(fields, 0)
  }).tags(['@modeling', '@datadomain', '@fields', '@listing'])

  test('returns only namespaces and models', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model = dataDomain.addModel()
    dataDomain.addEntity(model.key) // Add an entity to make sure it's not included
    const fields = [...dataDomain.listFields()]
    assert.lengthOf(fields, 2)
    assert.deepInclude(fields, namespace)
    assert.deepInclude(fields, model)
  }).tags(['@modeling', '@datadomain', '@fields', '@listing'])

  test('namespaces and models in the list are of type DomainNamespace and DomainModel', ({ assert }) => {
    const dataDomain = new DataDomain()
    dataDomain.addNamespace()
    dataDomain.addModel()
    const fields = [...dataDomain.listFields()]
    assert.instanceOf(fields[0], DomainNamespace)
    assert.instanceOf(fields[1], DomainModel)
  }).tags(['@modeling', '@datadomain', '@fields', '@listing'])

  test('lists namespaces and models in the order they were added', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model = dataDomain.addModel()
    const fields = [...dataDomain.listFields()]
    assert.deepEqual(fields, [namespace, model])
  }).tags(['@modeling', '@datadomain', '@fields', '@listing'])

  test('lists namespaces and models when there are no namespaces or models', ({ assert }) => {
    const dataDomain = new DataDomain()
    const fields = [...dataDomain.listFields()]
    assert.deepEqual(fields, [])
  }).tags(['@modeling', '@datadomain', '@fields', '@listing'])

  test('lists namespaces and models when there are other domain elements', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model = dataDomain.addModel()
    dataDomain.addEntity(model.key) // Add an entity to make sure it's not included
    const fields = [...dataDomain.listFields()]
    assert.deepEqual(fields, [namespace, model])
  }).tags(['@modeling', '@datadomain', '@fields', '@listing'])
})
