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

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

  test('creates a schema with provided values', ({ assert }) => {
    const schema = DomainModel.createSchema({
      key: 'test-model',
      info: { name: 'Test Model', description: 'A test model' },
      deprecated: true,
    })
    assert.equal(schema.kind, DomainModelKind)
    assert.equal(schema.key, 'test-model')
    assert.deepInclude(schema.info, { name: 'Test Model', description: 'A test model' })
    assert.isTrue(schema.deprecated)
  })

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

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

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

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

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

  test('creates a schema with a custom key', ({ assert }) => {
    const customKey = 'custom-key'
    const schema = DomainModel.createSchema({ key: customKey })
    assert.equal(schema.key, customKey)
  })

  test('creates a schema with custom info', ({ assert }) => {
    const customInfo = { name: 'Custom Name', description: 'Custom Description' }
    const schema = DomainModel.createSchema({ info: customInfo })
    assert.deepInclude(schema.info, customInfo)
  })

  test('creates a schema with custom fields', ({ assert }) => {
    const customFields: ModelOrderedItem[] = [{ key: 'custom-entity', type: 'entity' }]
    const schema = DomainModel.createSchema({ fields: customFields })
    assert.deepEqual(schema.fields, customFields)
  })

  test('creates a schema with custom deprecated value', ({ assert }) => {
    const schemaTrue = DomainModel.createSchema({ deprecated: true })
    assert.isTrue(schemaTrue.deprecated)

    const schemaFalse = DomainModel.createSchema({ deprecated: false })
    assert.isFalse(schemaFalse.deprecated)
  })
})

test.group('DomainModel.constructor()', () => {
  test('creates a new DomainModel with default values', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = new DomainModel(dataDomain)
    assert.equal(model.kind, DomainModelKind)
    assert.typeOf(model.key, 'string')
    assert.isNotEmpty(model.key)
    assert.instanceOf(model.info, Thing)
    assert.equal(model.info.name, 'New model')
    assert.deepEqual(model.fields, [])
    assert.isUndefined(model.deprecated)
  })

  test('creates a new DomainModel with provided values', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = new DomainModel(dataDomain, {
      key: 'test-model',
      info: { name: 'Test Model', description: 'A test model' },
      deprecated: true,
      fields: [{ key: 'test-entity', type: 'entity' }],
    })
    assert.equal(model.kind, DomainModelKind)
    assert.equal(model.key, 'test-model')
    assert.instanceOf(model.info, Thing)
    assert.equal(model.info.name, 'Test Model')
    assert.equal(model.info.description, 'A test model')
    assert.isTrue(model.deprecated)
    assert.deepEqual(model.fields, [{ key: 'test-entity', type: 'entity' }])
  })

  test('creates a new DomainModel with partial info', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = new DomainModel(dataDomain, {
      info: { name: 'Test Model' },
    })
    assert.equal(model.kind, DomainModelKind)
    assert.typeOf(model.key, 'string')
    assert.isNotEmpty(model.key)
    assert.instanceOf(model.info, Thing)
    assert.equal(model.info.name, 'Test Model')
  })

  test('creates a new DomainModel with empty info', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = new DomainModel(dataDomain, {
      info: {},
    })
    assert.equal(model.kind, DomainModelKind)
    assert.typeOf(model.key, 'string')
    assert.isNotEmpty(model.key)
    assert.instanceOf(model.info, Thing)
    assert.equal(model.info.name, 'New model')
  })

  test('creates a new DomainModel with deprecated set to true', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = new DomainModel(dataDomain, {
      deprecated: true,
    })
    assert.equal(model.kind, DomainModelKind)
    assert.typeOf(model.key, 'string')
    assert.isNotEmpty(model.key)
    assert.instanceOf(model.info, Thing)
    assert.equal(model.info.name, 'New model')
    assert.isTrue(model.deprecated)
  })

  test('creates a new DomainModel with deprecated set to false', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = new DomainModel(dataDomain, {
      deprecated: false,
    })
    assert.equal(model.kind, DomainModelKind)
    assert.typeOf(model.key, 'string')
    assert.isNotEmpty(model.key)
    assert.instanceOf(model.info, Thing)
    assert.equal(model.info.name, 'New model')
    assert.isFalse(model.deprecated)
  })

  test('creates a new DomainModel with a custom key', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = new DomainModel(dataDomain, { key: 'custom-key' })
    assert.equal(model.key, 'custom-key')
  })

  test('creates a new DomainModel with custom info', ({ assert }) => {
    const dataDomain = new DataDomain()
    const customInfo = { name: 'Custom Name', description: 'Custom Description' }
    const model = new DomainModel(dataDomain, { info: customInfo })
    assert.deepInclude(model.info, customInfo)
  })

  test('creates a new DomainModel with custom fields', ({ assert }) => {
    const dataDomain = new DataDomain()
    const customFields: ModelOrderedItem[] = [{ key: 'custom-entity', type: 'entity' }]
    const model = new DomainModel(dataDomain, { fields: customFields })
    assert.deepEqual(model.fields, customFields)
  })

  test('creates a new DomainModel with custom deprecated value', ({ assert }) => {
    const dataDomain = new DataDomain()
    const modelTrue = new DomainModel(dataDomain, { deprecated: true })
    assert.isTrue(modelTrue.deprecated)

    const modelFalse = new DomainModel(dataDomain, { deprecated: false })
    assert.isFalse(modelFalse.deprecated)
  })

  test('sets deprecated to undefined when not provided', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = new DomainModel(dataDomain, {})
    assert.isUndefined(model.deprecated)
  })

  test('sets fields to empty array when not provided', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = new DomainModel(dataDomain, {})
    assert.deepEqual(model.fields, [])
  })

  test('sets fields to empty array when fields is undefined', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = new DomainModel(dataDomain, { fields: undefined })
    assert.deepEqual(model.fields, [])
  })
})

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

  test('returns a valid schema with provided values', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = new DomainModel(dataDomain, {
      key: 'test-model',
      info: { name: 'Test Model', description: 'A test model' },
      deprecated: true,
      fields: [{ key: 'test-entity', type: 'entity' }],
    })
    const json = model.toJSON()
    assert.equal(json.kind, DomainModelKind)
    assert.equal(json.key, 'test-model')
    assert.deepInclude(json.info, { name: 'Test Model', description: 'A test model' })
    assert.isTrue(json.deprecated)
    assert.deepEqual(json.fields, [{ key: 'test-entity', type: 'entity' }])
  })

  test('includes child entities in the schema', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const entity = model.addEntity({ key: 'test-entity' })
    const json = model.toJSON()
    assert.isArray(json.fields)
    assert.lengthOf(json.fields!, 1)
    assert.deepEqual(json.fields, [{ key: entity.key, type: 'entity' }])
  })

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

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

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

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

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

  test('returns the parent namespace when the model is a child', ({ assert }) => {
    const dataDomain = new DataDomain()
    const parentNamespace = dataDomain.addNamespace()
    const model = parentNamespace.addModel()
    const parent = model.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 model = parentNamespace.addModel()
    model.addEntity()
    const parent = model.getParentInstance()
    assert.deepEqual(parent, parentNamespace)
  })

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

test.group('DomainModel.breadcrumbs()', () => {
  test('creates breadcrumbs from model to data domain', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ info: { name: 'Test Model' } })
    const breadcrumbs = model.breadcrumbs()
    assert.lengthOf(breadcrumbs, 2)
    assert.deepEqual(breadcrumbs[0], {
      key: dataDomain.key,
      name: 'Unnamed domain',
      kind: DataDomainKind,
    })
    assert.deepEqual(breadcrumbs[1], {
      key: model.key,
      name: 'Test Model',
      kind: DomainModelKind,
    })
  })

  test('creates breadcrumbs from model to data domain with custom names', ({ assert }) => {
    const dataDomain = new DataDomain()
    dataDomain.info.name = 'Custom Data Domain'
    const model = dataDomain.addModel({ info: { name: 'Test Model' } })
    const breadcrumbs = model.breadcrumbs()
    assert.lengthOf(breadcrumbs, 2)
    assert.deepEqual(breadcrumbs[0], {
      key: dataDomain.key,
      name: 'Custom Data Domain',
      kind: DataDomainKind,
    })
    assert.deepEqual(breadcrumbs[1], {
      key: model.key,
      name: 'Test Model',
      kind: DomainModelKind,
    })
  })

  test('creates breadcrumbs from model to namespace to data domain', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace({ info: { name: 'Test Namespace' } })
    const model = namespace.addModel({ info: { name: 'Test Model' } })
    const breadcrumbs = model.breadcrumbs()
    assert.lengthOf(breadcrumbs, 3)
    assert.deepEqual(breadcrumbs[0], {
      key: dataDomain.key,
      name: 'Unnamed domain',
      kind: DataDomainKind,
    })
    assert.deepEqual(breadcrumbs[1], {
      key: namespace.key,
      name: 'Test Namespace',
      kind: DomainNamespaceKind,
    })
    assert.deepEqual(breadcrumbs[2], {
      key: model.key,
      name: 'Test Model',
      kind: DomainModelKind,
    })
  })

  test('creates breadcrumbs from model to nested namespaces to data domain', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace1 = dataDomain.addNamespace({ info: { name: 'Test Namespace 1' } })
    const namespace2 = namespace1.addNamespace({ info: { name: 'Test Namespace 2' } })
    const model = namespace2.addModel({ info: { name: 'Test Model' } })
    const breadcrumbs = model.breadcrumbs()
    assert.lengthOf(breadcrumbs, 4)
    assert.deepEqual(breadcrumbs[0], {
      key: dataDomain.key,
      name: 'Unnamed domain',
      kind: DataDomainKind,
    })
    assert.deepEqual(breadcrumbs[1], {
      key: namespace1.key,
      name: 'Test Namespace 1',
      kind: DomainNamespaceKind,
    })
    assert.deepEqual(breadcrumbs[2], {
      key: namespace2.key,
      name: 'Test Namespace 2',
      kind: DomainNamespaceKind,
    })
    assert.deepEqual(breadcrumbs[3], {
      key: model.key,
      name: 'Test Model',
      kind: DomainModelKind,
    })
  })

  test('creates breadcrumbs with unnamed data domain, namespaces, and model', ({ assert }) => {
    const root = new DataDomain()
    root.info.name = undefined
    const n1 = root.addNamespace()
    n1.info.name = undefined
    const n2 = n1.addNamespace()
    n2.info.name = undefined
    const m1 = n2.addModel()
    m1.info.name = undefined
    const breadcrumbs = m1.breadcrumbs()
    assert.lengthOf(breadcrumbs, 4)
    const [b1, b2, b3, b4] = breadcrumbs
    assert.deepEqual(b1, {
      key: root.key,
      name: 'Unnamed data domain',
      kind: DataDomainKind,
    })
    assert.deepEqual(b2, {
      key: n1.key,
      name: 'Unnamed namespace',
      kind: DomainNamespaceKind,
    })
    assert.deepEqual(b3, {
      key: n2.key,
      name: 'Unnamed namespace',
      kind: DomainNamespaceKind,
    })
    assert.deepEqual(b4, {
      key: m1.key,
      name: 'Unnamed model',
      kind: DomainModelKind,
    })
  })
})

test.group('DomainModel.hasFields()', () => {
  test('returns true if there are child entities', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    model.addEntity({ key: 'test-entity' })
    assert.isTrue(model.hasFields())
  })

  test('returns false if there are no child entities', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    assert.isFalse(model.hasFields())
  })

  test('returns false if there are only other domain elements', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    dataDomain.addNamespace({ key: 'test-namespace' })
    assert.isFalse(model.hasFields())
  })

  test('returns true if there are both entities and other domain elements', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    model.addEntity({ key: 'test-entity' })
    dataDomain.addNamespace({ key: 'test-namespace' })
    assert.isTrue(model.hasFields())
  })

  test('returns false if the model has no fields', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel({ key: 'test-model' })
    model.fields = []
    assert.isFalse(model.hasFields())
  })
})

test.group('DomainModel.isChildOf()', () => {
  test('returns false when called on non existing key', ({ assert }) => {
    const dataDomain = new DataDomain()
    const model = dataDomain.addModel()
    const result = model.isChildOf('some-key')
    assert.isFalse(result)
  })

  test('returns true when called on a direct parent', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model = namespace.addModel()
    const result = model.isChildOf(namespace.key)
    assert.isTrue(result)
  })

  test('returns true when called on a grandparent', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace1 = dataDomain.addNamespace()
    const namespace2 = namespace1.addNamespace()
    const model = namespace2.addModel()
    const result = model.isChildOf(dataDomain.key)
    assert.isTrue(result)
  })

  test('returns false when called on a sibling', ({ assert }) => {
    const dataDomain = new DataDomain()
    const namespace = dataDomain.addNamespace()
    const model1 = namespace.addModel()
    const model2 = namespace.addModel()
    const result = model1.isChildOf(model2.key)
    assert.isFalse(result)
  })

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

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

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