import { test } from '@japa/runner'
import {
  DataDomain,
  DomainNamespace,
  Thing,
  DomainNamespaceKind,
  DomainModel,
  type DomainNamespaceSchema,
  DomainModelKind,
} from '../../../src/index.js'

test.group('DomainNamespace - general', () => {
  test('constructor sets default values', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = new DomainNamespace(dataDomain, {})
    assert.equal(namespace.kind, DomainNamespaceKind)
    assert.typeOf(namespace.key, 'string')
    assert.isNotEmpty(namespace.key)
    assert.instanceOf(namespace.info, Thing)
    assert.equal(namespace.info.name, 'New namespace')
  })

  test('constructor sets values from schema', ({ assert }) => {
    const dataDomain = new DataDomain()
    const schema = {
      key: 'test-ns',
      info: { name: 'Test Namespace', description: 'A test namespace' },
    }
    const namespace = new DomainNamespace(dataDomain, schema)
    assert.equal(namespace.key, 'test-ns')
    assert.equal(namespace.info.name, 'Test Namespace')
    assert.equal(namespace.info.description, 'A test namespace')
  })

  test('toJSON returns a valid schema', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = new DomainNamespace(dataDomain, { key: 'test-ns', info: { name: 'Test Namespace' } })
    const json = namespace.toJSON()
    assert.equal(json.key, 'test-ns')
    assert.equal(json.kind, DomainNamespaceKind)
    assert.deepInclude(json.info, { name: 'Test Namespace' })
  })

  test('addNamespace adds a namespace to the data domain', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const childNamespace = namespace.addNamespace()
    assert.instanceOf(childNamespace, DomainNamespace)
    assert.equal(dataDomain.graph.parent(childNamespace.key), namespace.key)
  })

  test('addModel adds a model to the data domain', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model = namespace.addModel()
    assert.instanceOf(model, DomainModel)
    assert.equal(dataDomain.graph.parent(model.key), namespace.key)
  })

  test('dispatches the change event when the "info" property change', async ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace({})
    namespace.info.name = 'new-name'
    namespace.info.description = 'new-description'
    await assert.dispatches(dataDomain, 'change', { timeout: 20 })
    assert.equal(namespace.info.name, 'new-name')
    assert.equal(namespace.info.description, 'new-description')
  })
})

test.group('DomainNamespace#createSchema()', () => {
  test('creates a schema with default values', ({ assert }) => {
    const schema = DomainNamespace.createSchema()
    assert.equal(schema.kind, DomainNamespaceKind)
    assert.typeOf(schema.key, 'string')
    assert.isNotEmpty(schema.key)
    assert.deepInclude(schema.info, { name: 'New namespace' })
  })

  test('creates a schema with provided values', ({ assert }) => {
    const schema = DomainNamespace.createSchema({
      key: 'test-ns',
      info: { name: 'Test Namespace', description: 'A test namespace' },
      deprecated: true,
    })
    assert.equal(schema.kind, DomainNamespaceKind)
    assert.equal(schema.key, 'test-ns')
    assert.deepInclude(schema.info, { name: 'Test Namespace', description: 'A test namespace' })
    assert.isTrue(schema.deprecated)
  })

  test('creates a schema with partial info', ({ assert }) => {
    const schema = DomainNamespace.createSchema({
      info: { name: 'Test Namespace' },
    })
    assert.equal(schema.kind, DomainNamespaceKind)
    assert.typeOf(schema.key, 'string')
    assert.isNotEmpty(schema.key)
    assert.deepInclude(schema.info, { name: 'Test Namespace' })
  })

  test('creates a schema with empty info', ({ assert }) => {
    const schema = DomainNamespace.createSchema({
      info: {},
    })
    assert.equal(schema.kind, DomainNamespaceKind)
    assert.typeOf(schema.key, 'string')
    assert.isNotEmpty(schema.key)
    assert.deepInclude(schema.info, { name: 'New namespace' })
  })

  test('creates a schema with fields', ({ assert }) => {
    const schema = DomainNamespace.createSchema({
      fields: [{ key: 'test-model', type: 'model' }],
    })
    assert.equal(schema.kind, DomainNamespaceKind)
    assert.typeOf(schema.key, 'string')
    assert.isNotEmpty(schema.key)
    assert.deepInclude(schema.info, { name: 'New namespace' })
    assert.deepEqual(schema.fields, [{ key: 'test-model', type: 'model' }])
  })

  test('creates a schema with deprecated set to true', ({ assert }) => {
    const schema = DomainNamespace.createSchema({
      deprecated: true,
    })
    assert.equal(schema.kind, DomainNamespaceKind)
    assert.typeOf(schema.key, 'string')
    assert.isNotEmpty(schema.key)
    assert.deepInclude(schema.info, { name: 'New namespace' })
    assert.isTrue(schema.deprecated)
  })

  test('creates a schema with deprecated set to false', ({ assert }) => {
    const schema = DomainNamespace.createSchema({
      deprecated: false,
    })
    assert.equal(schema.kind, DomainNamespaceKind)
    assert.typeOf(schema.key, 'string')
    assert.isNotEmpty(schema.key)
    assert.deepInclude(schema.info, { name: 'New namespace' })
    assert.isFalse(schema.deprecated)
  })
})

test.group('DomainNamespace.constructor()', () => {
  test('constructor sets default values', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = new DomainNamespace(dataDomain, {})
    assert.equal(namespace.kind, DomainNamespaceKind)
    assert.typeOf(namespace.key, 'string')
    assert.isNotEmpty(namespace.key)
    assert.instanceOf(namespace.info, Thing)
    assert.equal(namespace.info.name, 'New namespace')
    assert.deepEqual(namespace.fields, [])
    assert.isUndefined(namespace.deprecated)
  })

  test('constructor sets values from schema', ({ assert }) => {
    const dataDomain = new DataDomain()
    const schema: Partial<DomainNamespaceSchema> = {
      key: 'test-ns',
      info: { name: 'Test Namespace', description: 'A test namespace' },
      fields: [{ key: 'test-model', type: 'model' }],
      deprecated: true,
    }
    const namespace = new DomainNamespace(dataDomain, schema)
    assert.equal(namespace.key, 'test-ns')
    assert.equal(namespace.info.name, 'Test Namespace')
    assert.equal(namespace.info.description, 'A test namespace')
    assert.deepEqual(namespace.fields, [{ key: 'test-model', type: 'model' }])
    assert.isTrue(namespace.deprecated)
  })

  test('constructor sets deprecated to undefined when not in schema', ({ assert }) => {
    const dataDomain = new DataDomain()
    const schema: Partial<DomainNamespaceSchema> = {
      key: 'test-ns',
      info: { name: 'Test Namespace', description: 'A test namespace' },
      fields: [{ key: 'test-model', type: 'model' }],
    }
    const namespace = new DomainNamespace(dataDomain, schema)
    assert.isUndefined(namespace.deprecated)
  })

  test('constructor sets fields to empty array when not in schema', ({ assert }) => {
    const dataDomain = new DataDomain()
    const schema = {
      key: 'test-ns',
      info: { name: 'Test Namespace', description: 'A test namespace' },
    }
    const namespace = new DomainNamespace(dataDomain, schema)
    assert.deepEqual(namespace.fields, [])
  })

  test('constructor sets deprecated to undefined when schema is undefined', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = new DomainNamespace(dataDomain)
    assert.isUndefined(namespace.deprecated)
  })

  test('constructor sets fields to empty array when schema is undefined', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = new DomainNamespace(dataDomain)
    assert.deepEqual(namespace.fields, [])
  })
})

test.group('DomainNamespace.toJSON()', () => {
  test('returns a valid schema with default values', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = new DomainNamespace(dataDomain, {})
    const json = namespace.toJSON()
    assert.equal(json.kind, DomainNamespaceKind)
    assert.equal(json.key, namespace.key)
    assert.deepInclude(json.info, { name: 'New namespace' })
    assert.isUndefined(json.fields)
    assert.isUndefined(json.deprecated)
  })

  test('returns a valid schema with provided values', ({ assert }) => {
    const dataDomain = new DataDomain()
    const schema: Partial<DomainNamespaceSchema> = {
      key: 'test-ns',
      info: { name: 'Test Namespace', description: 'A test namespace' },
      fields: [{ key: 'test-model', type: 'model' }],
      deprecated: true,
    }
    const namespace = new DomainNamespace(dataDomain, schema)
    const json = namespace.toJSON()
    assert.equal(json.kind, DomainNamespaceKind)
    assert.equal(json.key, 'test-ns')
    assert.deepInclude(json.info, { name: 'Test Namespace', description: 'A test namespace' })
    assert.deepEqual(json.fields, [{ key: 'test-model', type: 'model' }])
    assert.isTrue(json.deprecated)
  })

  test('includes child namespaces in the schema', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const childNamespace = namespace.addNamespace()
    const json = namespace.toJSON()
    assert.isArray(json.fields)
    assert.lengthOf(json.fields!, 1)
    assert.deepEqual(json.fields, [{ key: childNamespace.key, type: 'namespace' }])
  })

  test('includes data models in the schema', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model = namespace.addModel()
    const json = namespace.toJSON()
    assert.isArray(json.fields)
    assert.lengthOf(json.fields!, 1)
    assert.deepEqual(json.fields, [{ key: model.key, type: 'model' }])
  })

  test('includes deprecated flag when true', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace({ deprecated: true })
    const json = namespace.toJSON()
    assert.isTrue(json.deprecated)
  })

  test('includes deprecated flag when false', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace({ deprecated: false })
    const json = namespace.toJSON()
    assert.isFalse(json.deprecated)
  })

  test('includes multiple fields in the correct order', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const childNamespace = namespace.addNamespace()
    const model = namespace.addModel()
    const json = namespace.toJSON()
    assert.isArray(json.fields)
    assert.lengthOf(json.fields!, 2)
    assert.deepEqual(json.fields, [
      { key: childNamespace.key, type: 'namespace' },
      { key: model.key, type: 'model' },
    ])
  })

  test('serializes the "info" property', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace({
      info: { name: 'Test Name', description: 'Test Description' },
    })
    const json = namespace.toJSON()
    assert.deepInclude(json.info, { name: 'Test Name', description: 'Test Description' })
  })
})

test.group('DomainNamespace.getParentInstance()', () => {
  test('returns the data domain when the namespace is at the root', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const parent = namespace.getParentInstance()
    assert.deepEqual(parent, dataDomain)
  })

  test('returns the parent namespace when the namespace is a child', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const childNamespace = parentNamespace.addNamespace()
    const parent = childNamespace.getParentInstance()
    assert.deepEqual(parent, parentNamespace)
  })

  test('returns the correct parent when there are multiple levels of nesting', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const childNamespace = parentNamespace.addNamespace()
    const grandchildNamespace = childNamespace.addNamespace()
    const parent = grandchildNamespace.getParentInstance()
    assert.deepEqual(parent, childNamespace)
  })

  test('returns the data domain when the namespace has no parent', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    dataDomain.graph.removeParent(namespace.key)
    const parent = namespace.getParentInstance()
    assert.deepEqual(parent, dataDomain)
  })
})

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

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

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

  test('namespaces and models in the list are of type DomainNamespace and DomainModel', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    namespace.addNamespace()
    namespace.addModel()
    const fields = [...namespace.listFields()]
    assert.equal(fields[0].kind, DomainNamespaceKind)
    assert.equal(fields[1].kind, DomainModelKind)
  })

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

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

test.group('DomainNamespace.hasFields()', () => {
  test('returns true if there are child namespaces', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    namespace.addNamespace()
    assert.isTrue(namespace.hasFields())
  })

  test('returns true if there are child models', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    namespace.addModel()
    assert.isTrue(namespace.hasFields())
  })

  test('returns true if there are both namespaces and models', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    namespace.addNamespace()
    namespace.addModel()
    assert.isTrue(namespace.hasFields())
  })

  test('returns false if there are no child namespaces or models', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    assert.isFalse(namespace.hasFields())
  })

  test('returns false if the namespace has no fields', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    namespace.fields = []
    assert.isFalse(namespace.hasFields())
  })
})

test.group('DomainNamespace.isChildOf()', () => {
  test('returns false when called on the root namespace', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const result = namespace.isChildOf('some-key')
    assert.isFalse(result)
  })

  test('returns true when called on a direct child', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const childNamespace = parentNamespace.addNamespace()
    const result = childNamespace.isChildOf(parentNamespace.key)
    assert.isTrue(result)
  })

  test('returns true when called on a grandchild', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const childNamespace = parentNamespace.addNamespace()
    const grandchildNamespace = childNamespace.addNamespace()
    const result = grandchildNamespace.isChildOf(parentNamespace.key)
    assert.isTrue(result)
  })

  test('returns false when called on a sibling', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const childNamespace1 = parentNamespace.addNamespace()
    const childNamespace2 = parentNamespace.addNamespace()
    const result = childNamespace1.isChildOf(childNamespace2.key)
    assert.isFalse(result)
  })

  test('returns false when called on a parent', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const childNamespace = parentNamespace.addNamespace()
    const result = parentNamespace.isChildOf(childNamespace.key)
    assert.isFalse(result)
  })

  test('returns false when called on a non-existent namespace', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const childNamespace = parentNamespace.addNamespace()
    const result = childNamespace.isChildOf('non-existent-key')
    assert.isFalse(result)
  })

  test('returns false when called on self', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const childNamespace = parentNamespace.addNamespace()
    const result = childNamespace.isChildOf(childNamespace.key)
    assert.isFalse(result)
  })

  test('returns true when the namespace is a child of the given namespace', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const childNamespace = parentNamespace.addNamespace()
    const result = childNamespace.isChildOf(parentNamespace.key)
    assert.isTrue(result)
  })

  test('returns false when the namespace is not a child of the given namespace', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const childNamespace = parentNamespace.addNamespace()
    const otherNamespace = parentNamespace.addNamespace()
    const result = childNamespace.isChildOf(otherNamespace.key)
    assert.isFalse(result)
  })

  test('returns false when the namespace is the same as the given namespace', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const result = namespace.isChildOf(namespace.key)
    assert.isFalse(result)
  })

  test('returns false when the namespace is the root namespace', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace({ key: 'test-ns' })
    const result = namespace.isChildOf(namespace.key)
    assert.isFalse(result)
  })
})
