import { test } from '@japa/runner'
import { DataNamespace, DataModel, IDataModel, DataEntity } from '../../../../src/legacy.js'
import { DataModelKind, Thing } from '../../../../src/index.js'

test.group('constructor() with defaults', (group) => {
  let root: DataNamespace
  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('sets the kind', ({ assert }) => {
    const assoc = new DataModel(root)
    assert.equal(assoc.kind, DataModelKind)
  })

  test('sets the key', ({ assert }) => {
    const assoc = new DataModel(root)
    assert.typeOf(assoc.key, 'string')
    assert.isNotEmpty(assoc.key)
  })

  test('sets the default "info"', ({ assert }) => {
    const assoc = new DataModel(root)
    assert.typeOf(assoc.info, 'EventTarget')
    assert.equal(assoc.info.name, '')
  })

  test('sets the default "entities"', ({ assert }) => {
    const assoc = new DataModel(root)
    assert.deepEqual(assoc.entities, [])
  })
})

test.group('constructor() from schema initialization', (group) => {
  let root: DataNamespace
  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('sets the key', ({ assert }) => {
    const orig = new DataModel(root).toJSON()
    orig.key = 'test'
    const instance = new DataModel(root, orig)
    assert.equal(instance.key, 'test')
  })

  test('sets the info', ({ assert }) => {
    const orig = new DataModel(root).toJSON()
    orig.info.name = 'test'
    const instance = new DataModel(root, orig)
    assert.equal(instance.info.name, 'test')
  })

  test('initializes from JSON schema ', ({ assert }) => {
    const orig = new DataModel(root).toJSON()
    orig.key = 'test'
    const instance = new DataModel(root, JSON.stringify(orig))
    assert.equal(instance.key, 'test')
  })
})

test.group('fromName()', (group) => {
  let root: DataNamespace
  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('sets the name', ({ assert }) => {
    const assoc = DataModel.fromName(root, 'test')
    assert.equal(assoc.info.name, 'test')
  })
})

test.group('new()', (group) => {
  let root: DataNamespace
  let base: IDataModel
  group.each.setup(() => {
    root = new DataNamespace()
    base = {
      info: Thing.fromName('test name').toJSON(),
      key: 'test123',
      kind: DataModelKind,
    }
  })

  test('sets the key', ({ assert }) => {
    const assoc = new DataModel(root)
    assoc.new(base)
    assert.equal(assoc.key, 'test123')
  })

  test('sets the info', ({ assert }) => {
    const assoc = new DataModel(root)
    assoc.new(base)
    assert.equal(assoc.info.name, 'test name')
  })

  test('sets default info', ({ assert }) => {
    const assoc = new DataModel(root)
    // @ts-expect-error Used in testing
    delete base.info
    assoc.new(base)
    assert.equal(assoc.info.name, '')
  })

  test('sets the entities as objects', ({ assert }) => {
    const e1 = DataEntity.fromName(root, 'e1')
    root.definitions.entities.push(e1)
    const model = new DataModel(root)
    base.entities = [e1.key]
    model.new(base)
    assert.deepEqual(model.entities, [e1])
  })

  test('re-sets entities when missing', ({ assert }) => {
    const model = new DataModel(root)
    model.addEntity('test')
    base.entities = []
    model.new(base)
    assert.deepEqual(model.entities, [])
  })

  test('throws when unknown input', ({ assert }) => {
    const assoc = new DataModel(root)
    assert.throws(() => {
      // @ts-expect-error Used in testing
      assoc.new({})
    }, 'Not a data model.')
  })
})

test.group('toJSON()', (group) => {
  let root: DataNamespace
  let base: DataModel
  group.each.setup(() => {
    root = new DataNamespace()
    base = new DataModel(root, {
      info: Thing.fromName('test name').toJSON(),
      key: 'test123',
      kind: DataModelKind,
    })
  })

  test('serializes the kind', ({ assert }) => {
    const result = base.toJSON()
    assert.equal(result.kind, DataModelKind)
  })

  test('serializes the key', ({ assert }) => {
    const result = base.toJSON()
    assert.equal(result.key, base.key)
  })

  test('serializes the info', ({ assert }) => {
    const result = base.toJSON()
    assert.equal(result.info.name, 'test name')
  })

  test('serializes the entities', ({ assert }) => {
    const e1 = base.addEntity('e1')
    const result = base.toJSON()
    assert.deepEqual(result.entities, [e1.key])
  })

  test('ignores entities when missing', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.entities)
  })
})

test.group('remove()', (group) => {
  let root: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('removes self from the root definitions', ({ assert }) => {
    const m1 = root.addDataModel('m1')
    m1.remove()
    assert.deepEqual(root.definitions.models, [])
  })

  test('removes self only', ({ assert }) => {
    const m1 = root.addDataModel('m1')
    const m2 = root.addDataModel('m2')
    m1.remove()
    assert.deepEqual(root.definitions.models, [m2])
  })

  test('removes self a sub-namespace as parent', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    const m1 = n1.addDataModel('d1')
    m1.remove()
    assert.deepEqual(n1.items, [])
    assert.deepEqual(root.definitions.models, [])
  })

  test('removes model entities', ({ assert }) => {
    const m1 = root.addDataModel('d1')
    m1.addEntity('e1')
    m1.remove()
    assert.deepEqual(root.definitions.entities, [])
  })
})

test.group('addEntity()', (group) => {
  let root: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('adds an entity when the parent is root', ({ assert }) => {
    const m1 = root.addDataModel('m1')
    const e1 = m1.addEntity('e1')
    assert.deepEqual(root.definitions.entities, [e1])
    assert.deepEqual(m1.entities, [e1])
  })

  test('adds an entity when the parent is as sub-namespace', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    const m1 = n1.addDataModel('m1')
    const e1 = m1.addEntity('e1')
    assert.deepEqual(root.definitions.entities, [e1])
    assert.deepEqual(m1.entities, [e1])
  })
})

test.group('getParentInstance()', (group) => {
  let root: DataNamespace

  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('returns the parent as root', ({ assert }) => {
    const m1 = root.addDataModel('m1')
    const result = m1.getParentInstance()
    assert.isTrue(result === root)
  })

  test('returns the parent is as sub-namespace', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    const m1 = n1.addDataModel('m1')
    const result = m1.getParentInstance()
    assert.isTrue(result === n1)
  })
})

test.group('breadcrumbs()', (group) => {
  let root: DataNamespace
  group.each.setup(() => {
    root = new DataNamespace()
  })

  test('adds root and self', ({ assert }) => {
    const m1 = root.addDataModel('m1')
    const result = m1.breadcrumbs()

    assert.lengthOf(result, 2, 'has the entire path')
    assert.equal(result[0].key, root.key, 'has the root as first')
    assert.equal(result[1].key, m1.key, 'has the self last')
  })

  test('adds root, sub-ns, and self', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    const m1 = n1.addDataModel('m1')
    const result = m1.breadcrumbs()
    assert.lengthOf(result, 3, 'has the entire path')
    assert.equal(result[0].key, root.key, 'has the root as first')
    assert.equal(result[1].key, n1.key, 'has the child-namespace')
    assert.equal(result[2].key, m1.key, 'has self last')
  })

  test('adds root, sub-ns, sub-ns, and self', ({ assert }) => {
    const n1 = root.addNamespace('n1')
    const n2 = n1.addNamespace('n2')
    const m1 = n2.addDataModel('m1')
    const result = m1.breadcrumbs()
    assert.lengthOf(result, 4, 'has the entire path')
    assert.equal(result[0].key, root.key, 'has the root as first')
    assert.equal(result[1].key, n1.key, 'has the child-namespace')
    assert.equal(result[2].key, n2.key, 'has the child-namespace')
    assert.equal(result[3].key, m1.key, 'has self last')
  })
})

test.group('adaptEntity()', (group) => {
  let root: DataNamespace
  let dm1: DataModel
  let dm2: DataModel
  let e1: DataEntity
  let e2: DataEntity

  group.each.setup(() => {
    root = new DataNamespace()
    dm1 = root.addNamespace('dm1').addDataModel('Data Model 1')
    dm2 = root.addNamespace('dm2').addDataModel('Data Model 2')
    e1 = dm1.addEntity('Entity 1')
    e2 = dm1.addEntity('Entity 2')
  })

  test('moves an entity to a new data model', ({ assert }) => {
    dm2.adaptEntity(e1)
    assert.deepEqual(dm1.entities, [e2], 'removes from the old data model')
    assert.deepEqual(dm2.entities, [e1], 'adds to the new data model')
    assert.deepEqual(root.definitions.entities, [e1, e2], 'keeps the entity in the root definitions')
  })

  test('moves an entity to a new index', ({ assert }) => {
    const e3 = dm2.addEntity('Entity 3')
    dm2.adaptEntity(e1, { index: 0 })
    assert.deepEqual(dm1.entities, [e2], 'removes from the old data model')
    assert.deepEqual(dm2.entities, [e1, e3], 'adds to the new data model')
    assert.deepEqual(root.definitions.entities, [e1, e2, e3], 'keeps the entity in the root definitions')
  })

  test('throws when adapting an entity from another root', ({ assert }) => {
    const otherRoot = new DataNamespace()
    const otherDm = otherRoot.addNamespace('other').addDataModel('Other Data Model')
    const otherEntity = otherDm.addEntity('Other Entity')
    assert.throws(
      () => dm1.adaptEntity(otherEntity),
      `The entity ${otherEntity.key} is not in the same namespace as this data model.`
    )
  })

  test('throws when adapting an entity that is already adapted', ({ assert }) => {
    dm2.adaptEntity(e1)
    assert.throws(() => dm2.adaptEntity(e1), `The entity ${e1.key} is already adapted by this data model.`)
  })

  test('throws when index is out of range (minimum)', ({ assert }) => {
    assert.throws(() => dm2.adaptEntity(e1, { index: -1 }), `The index -1 cannot be below 0.`)
  })

  test('throws when index is out of range (maximum)', ({ assert }) => {
    assert.throws(() => dm2.adaptEntity(e1, { index: 1 }), `The index 1 is not valid.`)
  })
})

test.group('isChildOf()', (group) => {
  let root: DataNamespace
  let n1: DataNamespace
  let n2: DataNamespace
  let m1: DataModel
  let m2: DataModel
  let m3: DataModel

  group.each.setup(() => {
    root = new DataNamespace()
    n1 = root.addNamespace('n1')
    n2 = n1.addNamespace('n2')
    m1 = root.addDataModel('m1')
    m2 = n1.addDataModel('m2')
    m3 = n2.addDataModel('m3')
  })

  test('returns false when called on the root data model', ({ assert }) => {
    const result = m1.isChildOf('some-key')
    assert.isFalse(result)
  })

  test('returns true when called on a direct child', ({ assert }) => {
    const result = m2.isChildOf(n1.key)
    assert.isTrue(result)
  })

  test('returns true when called on a grandchild', ({ assert }) => {
    const result = m3.isChildOf(root.key)
    assert.isTrue(result)
  })

  test('returns false when called on a sibling', ({ assert }) => {
    const m4 = root.addDataModel('m4')
    const result = m1.isChildOf(m4.key)
    assert.isFalse(result)
  })

  test('returns false when called on a parent', ({ assert }) => {
    const result = n1.isChildOf(m2.key)
    assert.isFalse(result)
  })

  test('returns false when called on a non-existent namespace', ({ assert }) => {
    const result = m2.isChildOf('non-existent-key')
    assert.isFalse(result)
  })

  test('returns false when called on self', ({ assert }) => {
    const result = m2.isChildOf(m2.key)
    assert.isFalse(result)
  })
})
