import { test } from '@japa/runner'
import {
  DataNamespace,
  DataModel,
  DataAssociation,
  IDataAssociation,
  DataAssociationKind,
  DataEntity,
  Thing,
  ValidationError,
} from '../../../src/index.js'

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

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

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

  test('sets the default "info"', ({ assert }) => {
    const assoc = new DataAssociation(root)
    assert.typeOf(assoc.info, 'object')
    assert.equal(assoc.info.name, 'Unnamed association')
  })

  test('sets the default schema', ({ assert }) => {
    const assoc = new DataAssociation(root)
    assert.isUndefined(assoc.schema)
  })

  test('does not set multiple', ({ assert }) => {
    const assoc = new DataAssociation(root)
    assert.isUndefined(assoc.multiple)
  })

  test('does not set required', ({ assert }) => {
    const assoc = new DataAssociation(root)
    assert.isUndefined(assoc.required)
  })

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

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

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

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

  test('sets the multiple', ({ assert }) => {
    const orig = new DataAssociation(root).toJSON()
    orig.multiple = true
    const instance = new DataAssociation(root, orig)
    assert.isTrue(instance.multiple)
  })

  test('sets the required', ({ assert }) => {
    const orig = new DataAssociation(root).toJSON()
    orig.required = true
    const instance = new DataAssociation(root, orig)
    assert.isTrue(instance.required)
  })

  test('sets the targets', ({ assert }) => {
    const orig = new DataAssociation(root).toJSON()
    orig.targets = [{ key: 'test' }]
    const instance = new DataAssociation(root, orig)
    assert.deepEqual(instance.targets, [{ key: 'test' }])
  })

  test('sets the schema', ({ assert }) => {
    const orig = new DataAssociation(root).toJSON()
    orig.schema = { linked: true }
    const instance = new DataAssociation(root, orig)
    assert.deepEqual(instance.schema, { linked: true })
  })

  test('initializes from JSON schema ', ({ assert }) => {
    const orig = new DataAssociation(root).toJSON()
    orig.schema = { linked: true }
    const instance = new DataAssociation(root, JSON.stringify(orig))
    assert.deepEqual(instance.schema, { linked: true })
  })
})

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

  test('sets the target', ({ assert }) => {
    const assoc = DataAssociation.fromTarget(root, 'test')
    assert.deepEqual(assoc.targets, [{ key: 'test' }])
  })
})

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

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

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

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

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

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

  test('sets the schema', ({ assert }) => {
    const assoc = new DataAssociation(root)
    assoc.new({ ...base, schema: { linked: true } })
    assert.deepEqual(assoc.schema, { linked: true })
  })

  test('sets default schema', ({ assert }) => {
    const assoc = new DataAssociation(root)
    assoc.new(base)
    assert.isUndefined(assoc.schema)
  })

  test('sets the multiple', ({ assert }) => {
    const assoc = new DataAssociation(root)
    assoc.new({ ...base, multiple: true })
    assert.isTrue(assoc.multiple)
  })

  test('does not set multiple when not in the input', ({ assert }) => {
    const assoc = new DataAssociation(root)
    assoc.new(base)
    assert.isUndefined(assoc.multiple)
  })

  test('sets the required', ({ assert }) => {
    const assoc = new DataAssociation(root)
    assoc.new({ ...base, required: true })
    assert.isTrue(assoc.required)
  })

  test('does not set required when not in the input', ({ assert }) => {
    const assoc = new DataAssociation(root)
    assoc.new(base)
    assert.isUndefined(assoc.required)
  })

  test('sets the targets', ({ assert }) => {
    const assoc = new DataAssociation(root)
    assoc.new({ ...base, targets: [{ key: 'target-id' }] })
    assert.deepEqual(assoc.targets, [{ key: 'target-id' }])
  })

  test('re-sets targets when not in the input', ({ assert }) => {
    const assoc = new DataAssociation(root)
    assoc.targets = [{ key: 'target-id' }]
    assoc.new(base)
    assert.deepEqual(assoc.targets, [])
  })

  test('throws when unknown input', ({ assert }) => {
    const assoc = new DataAssociation(root)
    assert.throws(() => {
      // @ts-expect-error Used in testing
      assoc.new({})
    }, 'Validation failure')
  })
})

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

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

  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('does not serialize the schema by default', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.schema)
  })

  test('does not serialize the multiple by default', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.multiple)
  })

  test('does not serialize the required by default', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.required)
  })

  test('does not serialize targets when empty', ({ assert }) => {
    const result = base.toJSON()
    assert.isUndefined(result.targets)
  })

  test('serializes the schema', ({ assert }) => {
    base.schema = { linked: true }
    const result = base.toJSON()
    assert.deepEqual(result.schema, { linked: true })
  })

  test('serialize the set multiple', ({ assert }) => {
    base.multiple = false
    const result = base.toJSON()
    assert.isFalse(result.multiple)
  })

  test('serialize the set required', ({ assert }) => {
    base.required = false
    const result = base.toJSON()
    assert.isFalse(result.required)
  })

  test('serializes targets as a copy', ({ assert }) => {
    base.targets = [{ key: 'abc' }]
    const result = base.toJSON()
    assert.deepEqual(result.targets, [{ key: 'abc' }])
    base.targets.push({ key: 'def' })
    assert.deepEqual(result.targets, [{ key: 'abc' }])
  })
})

test.group('#isSingle', (g) => {
  let root: DataNamespace
  let e1: DataEntity
  let e2: DataEntity

  g.each.setup(() => {
    root = new DataNamespace()
    const model = root.addDataModel('m1')
    e1 = model.addEntity('e1')
    e2 = model.addEntity('e2')
  })

  test('returns true when no targets', ({ assert }) => {
    const assoc = e1.addNamedAssociation('a1')
    assert.isTrue(assoc.isSingle)
  })

  test('returns true when a single target', ({ assert }) => {
    const assoc = e1.addTargetAssociation(e2.key)
    assert.isTrue(assoc.isSingle)
  })

  test('returns false when multiple targets', ({ assert }) => {
    const assoc = e1.addTargetAssociation(e2.key)
    assoc.targets.push({ key: 'other' })
    assert.isFalse(assoc.isSingle)
  })
})

test.group('getTargets()', (g) => {
  let root: DataNamespace
  let e1: DataEntity
  let e2: DataEntity

  g.each.setup(() => {
    root = new DataNamespace()
    const model = root.addDataModel('m1')
    e1 = model.addEntity('e1')
    e2 = model.addEntity('e2')
  })

  test('returns the entity', ({ assert }) => {
    const assoc = e1.addTargetAssociation(e2.key)
    const result = assoc.getTargets()
    assert.deepEqual(result, [e2])
  })

  test('returns empty array when no target', ({ assert }) => {
    const assoc = e1.addNamedAssociation('test')
    const result = assoc.getTargets()
    assert.deepEqual(result, [])
  })

  test('returns undefined when target has no definition', ({ assert }) => {
    const assoc = e1.addNamedAssociation('test')
    assoc.targets = [{ key: 'nothing' }]
    const result = assoc.getTargets()
    assert.deepEqual(result, [])
  })
})

test.group('remove()', (g) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let e2: DataEntity
  let a1: DataAssociation

  g.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    e2 = m1.addEntity('e2')
    a1 = e1.addTargetAssociation(e2.key)
  })

  test('removes self from the parent entity', ({ assert }) => {
    a1.remove()
    assert.deepEqual(e1.associations, [])
  })

  test('removes self from the root definitions', ({ assert }) => {
    a1.remove()
    assert.deepEqual(root.definitions.associations, [])
  })

  test('does not serialize associations', ({ assert }) => {
    a1.remove()
    const schema = root.toJSON()
    assert.isUndefined(schema.definitions.associations)
    assert.isUndefined(schema.definitions.entities![0].associations)
    assert.isUndefined(schema.definitions.entities![1].associations)
  })

  test('removes self only', ({ assert }) => {
    const e3 = m1.addEntity('e2')
    const a2 = e1.addTargetAssociation(e3.key)
    a1.remove()
    assert.deepEqual(e1.associations, [a2])
    assert.deepEqual(root.definitions.associations, [a2])
  })
})

test.group('addTarget()', (g) => {
  let root: DataNamespace
  let e1: DataEntity
  let e2: DataEntity
  let a1: DataAssociation
  let foreign: DataNamespace

  g.each.setup(() => {
    root = new DataNamespace()
    foreign = new DataNamespace()
    root.addForeign(foreign)
    const model = root.addDataModel('m1')
    const model2 = foreign.addDataModel('m2')
    e1 = model.addEntity('e1')
    e2 = model2.addEntity('e2')
    a1 = e1.addNamedAssociation('a1')
  })

  test('adds the target from the key', ({ assert }) => {
    a1.addTarget(e1.key)
    assert.deepEqual(a1.targets, [{ key: e1.key }])
  })

  test('adds the target from an entity', ({ assert }) => {
    a1.addTarget(e1)
    assert.deepEqual(a1.targets, [{ key: e1.key }])
  })

  test('adds the target from an entity schema', ({ assert }) => {
    a1.addTarget(e1.toJSON())
    assert.deepEqual(a1.targets, [{ key: e1.key }])
  })

  test('adds the target with foreign namespace', ({ assert }) => {
    a1.addTarget(e2)
    assert.deepEqual(a1.targets, [{ key: e2.key, namespace: foreign.key }])
  })

  test('adds the target from an entity with foreign namespace', ({ assert }) => {
    a1.addTarget(e2)
    assert.deepEqual(a1.targets, [{ key: e2.key, namespace: foreign.key }])
  })

  test('throws an error when namespace does not exist', ({ assert }) => {
    assert.throws(() => a1.addTarget('some-key', 'not-exist'), 'Foreign namespace "not-exist" does not exist.')
  })
})

test.group('removeTarget()', (g) => {
  let root: DataNamespace
  let e1: DataEntity
  let e2: DataEntity
  let a1: DataAssociation

  g.each.setup(() => {
    root = new DataNamespace()
    const model = root.addDataModel('m1')
    e1 = model.addEntity('e1')
    e2 = model.addEntity('e2')
    a1 = e1.addTargetAssociation(e2.key)
  })

  test('removes the target from the key', ({ assert }) => {
    a1.removeTarget(e2.key)
    assert.deepEqual(a1.targets, [])
  })

  test('removes the target from an entity', ({ assert }) => {
    a1.removeTarget(e2)
    assert.deepEqual(a1.targets, [])
  })

  test('removes the target from an entity schema', ({ assert }) => {
    a1.removeTarget(e2.toJSON())
    assert.deepEqual(a1.targets, [])
  })

  test('does nothing when the target does not exist', ({ assert }) => {
    a1.removeTarget('other')
    assert.deepEqual(a1.targets, [{ key: e2.key }])
  })
})

test.group('toApiShape()', (g) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let e2: DataEntity
  let a1: DataAssociation

  g.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    e2 = m1.addEntity('e2')
    a1 = e1.addTargetAssociation(e2.key)
  })

  // these tests only check whether the AmfShapeGenerator is called.
  // specific tests are performed elsewhere

  test('returns an object', ({ assert }) => {
    const result = a1.toApiShape()

    assert.typeOf(result, 'object')
    assert.typeOf(result.range, 'object')
  })
})

test.group('getParent()', (g) => {
  let root: DataNamespace
  let m1: DataModel
  let e1: DataEntity
  let e2: DataEntity

  g.each.setup(() => {
    root = new DataNamespace()
    m1 = root.addDataModel('m1')
    e1 = m1.addEntity('e1')
    e2 = m1.addEntity('e2')
  })

  test('returns the reference to the parent entity', ({ assert }) => {
    const a1 = e1.addTargetAssociation(e2.key)
    const result = a1.getParent()
    assert.deepEqual(result, e1)
  })
})

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

  test('throws when kind is missing', ({ assert }) => {
    const orig = new DataAssociation(root).toJSON()
    // @ts-expect-error Testing invalid data
    delete orig.kind
    assert.throws(
      () => new DataAssociation(root, orig),
      ValidationError as unknown as ErrorConstructor,
      'Validation failure'
    )
  })

  test('throws when kind is wrong', ({ assert }) => {
    const orig = new DataAssociation(root).toJSON()
    // @ts-expect-error Testing invalid data
    orig.kind = 'wrong kind'
    assert.throws(
      () => new DataAssociation(root, orig),
      ValidationError as unknown as ErrorConstructor,
      'Validation failure'
    )
  })

  test('throws when input is not an object', ({ assert }) => {
    assert.throws(() => new DataAssociation(root, 'not an object'), ValidationError as unknown as ErrorConstructor)
  })

  test('throws when info is invalid', ({ assert }) => {
    const orig = new DataAssociation(root).toJSON()
    // @ts-expect-error Testing invalid data
    orig.info = 'not an info'
    assert.throws(() => new DataAssociation(root, orig), ValidationError as unknown as ErrorConstructor)
  })
})

test.group('Foreign Namespace Handling', (g) => {
  let root: DataNamespace
  let foreign: DataNamespace
  let e1: DataEntity
  let e2: DataEntity
  g.each.setup(() => {
    root = new DataNamespace()
    foreign = new DataNamespace()
    root.addForeign(foreign)
    const model1 = root.addDataModel('m1')
    const model2 = foreign.addDataModel('m2')
    e1 = model1.addEntity('e1')
    e2 = model2.addEntity('e2')
  })

  test('fromTarget() with foreign namespace', ({ assert }) => {
    const assoc = DataAssociation.fromTarget(root, e2.key, foreign.key)
    assert.deepEqual(assoc.targets, [{ key: e2.key, namespace: foreign.key }])
  })

  test('addTarget() with foreign namespace', ({ assert }) => {
    const assoc = e1.addNamedAssociation('assoc1')
    assoc.addTarget(e2)
    assert.deepEqual(assoc.targets, [{ key: e2.key, namespace: foreign.key }])
  })
  test('addTarget() with foreign namespace - namespace does not exist', ({ assert }) => {
    assert.throws(() => {
      const assoc = e1.addNamedAssociation('assoc1')
      assoc.addTarget(e2.toJSON(), 'not-exist')
    }, 'Foreign namespace "not-exist" does not exist.')
  })

  test('getTargets() with foreign namespace', ({ assert }) => {
    const assoc = e1.addForeignAssociation(e2.key, foreign.key)
    const result = assoc.getTargets()
    assert.deepEqual(result, [e2])
  })

  test('getTargets() with foreign namespace - no target entity', ({ assert }) => {
    const assoc = e1.addNamedAssociation('assoc')
    assoc.targets = [{ key: 'no-target', namespace: foreign.key }]
    const result = assoc.getTargets()
    assert.deepEqual(result, [])
  })
  test('getTargets() with foreign namespace - wrong namespace', ({ assert }) => {
    const assoc = e1.addNamedAssociation('assoc')
    assoc.targets = [{ key: e2.key, namespace: 'no-namespace' }]
    const result = assoc.getTargets()
    assert.deepEqual(result, [])
  })
})

test.group('DataAssociation Multiple targets', (g) => {
  let root: DataNamespace
  let e1: DataEntity
  let e2: DataEntity
  let e3: DataEntity

  g.each.setup(() => {
    root = new DataNamespace()
    const model = root.addDataModel('m1')
    e1 = model.addEntity('e1')
    e2 = model.addEntity('e2')
    e3 = model.addEntity('e3')
  })

  test('addTarget() with multiple targets', ({ assert }) => {
    const a1 = e1.addNamedAssociation('assoc1')
    a1.addTarget(e2.key)
    a1.addTarget(e3.key)
    assert.deepEqual(a1.targets, [{ key: e2.key }, { key: e3.key }])
  })
})
