import { test } from '@japa/runner'
import { DomainImpactAnalysis } from '../../../src/modeling/DomainImpactAnalysis.js'
import {
  DomainNamespaceKind,
  DomainEntityKind,
  DomainModelKind,
  DomainPropertyKind,
  DomainAssociationKind,
  DataDomain,
  DomainNamespace,
  DomainModel,
  DomainEntity,
  DomainProperty,
  DomainAssociation,
} from '../../../src/index.js'

test.group('DomainImpactAnalysis', (group) => {
  let analyzer: DomainImpactAnalysis
  let root: DataDomain
  let n1: DomainNamespace
  let n2: DomainNamespace
  let m1: DomainModel
  let m2: DomainModel
  let m3: DomainModel
  let e1: DomainEntity
  let e2: DomainEntity
  let e3: DomainEntity
  let e4: DomainEntity
  let p1: DomainProperty
  let a1: DomainAssociation

  group.each.setup(() => {
    root = new DataDomain()
    analyzer = new DomainImpactAnalysis(root)
    n1 = root.addNamespace({ key: 'n1', info: { name: 'n1' } })
    m1 = root.addModel({ key: 'm1', info: { name: 'm1' } })
    n2 = n1.addNamespace({ key: 'n2', info: { name: 'n2' } })
    m2 = n1.addModel({ key: 'm2', info: { name: 'm2' } })
    e1 = m1.addEntity({ key: 'e1', info: { name: 'e1' } })
    e2 = m2.addEntity({ key: 'e2', info: { name: 'e2' } })
    m3 = n2.addModel({ key: 'm3', info: { name: 'm3' } })
    e3 = m3.addEntity({ key: 'e3', info: { name: 'e3' } })
    p1 = e1.addProperty({ key: 'p1', info: { name: 'p1' } })
    a1 = e1.addAssociation()
    a1.info.name = 'a1'
    a1.addTarget(e2)
    e2.addParent(e3.key)
    e3.addParent(e1.key)
    e4 = root.addModel({ key: 'm4', info: { name: 'm4' } }).addEntity({ key: 'e4', info: { name: 'e4' } })
    e4.addParent(e3.key)
    // DataDomain (root)
    // ├── DomainNamespace (n1)
    // │   ├── DomainModel (m2)
    // │   │   └── DomainEntity (e2)
    // │   │       └── parent: DomainEntity (e3)
    // │   └── DomainNamespace (n2)
    // │       └── DomainModel (m3)
    // │           └── DomainEntity (e3)
    // │               └── parent: DomainEntity (e1)
    // ├── DomainModel (m1)
    // │   └── DomainEntity (e1)
    // │       ├── DomainProperty (p1)
    // │       └── DomainAssociation (a1)
    // │           └── target: DomainEntity (e2)
    // └── DomainModel (m4)
    //     └── DomainEntity (e4)
    //         └── parent: DomainEntity (e3)
  })

  test('returns a report for deleting a namespace', ({ assert }) => {
    const report = analyzer.deleteAnalysis(n1.key, DomainNamespaceKind)
    assert.equal(report.key, n1.key)
    assert.equal(report.kind, DomainNamespaceKind)
    assert.isFalse(report.canProceed, 'operation should not be able to proceed')
    // Affected items:
    // - n1 (self)
    // - n2 (child)
    // - m3 (child)
    // - e3 (child)
    // - e4 (parent of e3)
    // - m2 (child)
    // - e2 (child), note, e3 parent is not included because it is included in the deletion.
    // - a1 (target of e2)
    assert.lengthOf(report.impact, 8, 'has all items')
    const [n1r, n2r, m3r, e3r, e4r, m2r, e2r, a1r] = report.impact

    assert.ok(n1r)
    assert.equal(n1r.key, n1.key)
    assert.equal(n1r.kind, DomainNamespaceKind)
    assert.equal(n1r.type, 'delete')
    assert.equal(n1r.impact, `The n1 namespace will be deleted.`)
    assert.equal(n1r.severity, 'info')

    assert.ok(n2r)
    assert.equal(n2r.key, n2.key)
    assert.equal(n2r.kind, DomainNamespaceKind)
    assert.equal(n2r.type, 'delete')
    assert.equal(n2r.impact, `The n2 namespace will be deleted.`)
    assert.equal(n2r.severity, 'info')

    assert.ok(m3r)
    assert.equal(m3r.key, m3.key)
    assert.equal(m3r.kind, DomainModelKind)
    assert.equal(m3r.type, 'delete')
    assert.equal(m3r.impact, `The m3 data model will be deleted.`)
    assert.equal(m3r.severity, 'info')

    assert.ok(e3r)
    assert.equal(e3r.key, e3.key)
    assert.equal(e3r.kind, DomainEntityKind)
    assert.equal(e3r.type, 'delete')
    assert.equal(e3r.impact, `The e3 entity will be deleted.`)
    assert.equal(e3r.severity, 'info')

    assert.ok(e4r)
    assert.equal(e4r.key, e4.key)
    assert.equal(e4r.kind, DomainEntityKind)
    assert.equal(e4r.type, 'delete')
    assert.equal(e4r.impact, `The "e4" entity will become an orphan because it is a child of the "e3" entity.`)
    assert.equal(e4r.resolution, `The "e3" entity will be removed as the parent of the "e4" entity.`)
    assert.equal(e4r.severity, 'error')
    assert.equal(e4r.relationship, 'child')

    assert.ok(m2r)
    assert.equal(m2r.key, m2.key)
    assert.equal(m2r.kind, DomainModelKind)
    assert.equal(m2r.type, 'delete')
    assert.equal(m2r.impact, `The m2 data model will be deleted.`)
    assert.equal(m2r.severity, 'info')

    assert.ok(e2r)
    assert.equal(e2r.key, e2.key)
    assert.equal(e2r.kind, DomainEntityKind)
    assert.equal(e2r.type, 'delete')
    assert.equal(e2r.impact, `The e2 entity will be deleted.`)
    assert.equal(e2r.severity, 'info')

    assert.ok(a1r)
    assert.equal(a1r.key, a1.key)
    assert.equal(a1r.kind, DomainAssociationKind)
    assert.equal(a1r.type, 'delete')
    assert.equal(a1r.impact, `The a1 association will be broken because it has a target to e2.`)
    assert.equal(a1r.resolution, `The a1 association will be removed from e2.`)
    assert.equal(a1r.severity, 'error')
  })

  test('returns a report for deleting a data model', ({ assert }) => {
    const report = analyzer.deleteAnalysis(m1.key, DomainModelKind)
    assert.equal(report.key, m1.key)
    assert.equal(report.kind, DomainModelKind)
    assert.isFalse(report.canProceed, 'operation should not be able to proceed')

    // Affected items:
    // - m1 (self)
    // - e1 (child)
    // - p1 (child of e1)
    // - a1 (child of e1)
    // - e3 (parent of e4)
    assert.lengthOf(report.impact, 5, 'has all items')
    const [m1r, e1r, e3r, p1r, a1r] = report.impact

    assert.ok(m1r)
    assert.equal(m1r.key, m1.key)
    assert.equal(m1r.kind, DomainModelKind)
    assert.equal(m1r.type, 'delete')
    assert.equal(m1r.impact, `The m1 data model will be deleted.`)
    assert.equal(m1r.severity, 'info')

    assert.ok(e1r)
    assert.equal(e1r.key, e1.key)
    assert.equal(e1r.kind, DomainEntityKind)
    assert.equal(e1r.type, 'delete')
    assert.equal(e1r.impact, `The e1 entity will be deleted.`)
    assert.equal(e1r.severity, 'info')

    assert.ok(p1r)
    assert.equal(p1r.key, p1.key)
    assert.equal(p1r.kind, DomainPropertyKind)
    assert.equal(p1r.type, 'delete')
    assert.equal(p1r.impact, `The p1 property will be deleted.`)
    assert.equal(p1r.severity, 'info')

    assert.ok(a1r)
    assert.equal(a1r.key, a1.key)
    assert.equal(a1r.kind, DomainAssociationKind)
    assert.equal(a1r.type, 'delete')
    assert.equal(a1r.impact, `The a1 association will be deleted.`)
    assert.equal(a1r.severity, 'info')

    assert.ok(e3r)
    assert.equal(e3r.key, e3.key)
    assert.equal(e3r.kind, DomainEntityKind)
    assert.equal(e3r.type, 'delete')
    assert.equal(e3r.impact, `The "e3" entity will become an orphan because it is a child of the "e1" entity.`)
    assert.equal(e3r.resolution, `The "e1" entity will be removed as the parent of the "e3" entity.`)
    assert.equal(e3r.severity, 'error')
    assert.equal(e3r.relationship, 'child')
  })

  test('returns a report for deleting an entity', ({ assert }) => {
    const report = analyzer.deleteAnalysis(e1.key, DomainEntityKind)
    assert.equal(report.key, e1.key)
    assert.equal(report.kind, DomainEntityKind)
    assert.isFalse(report.canProceed, 'operation should not be able to proceed')

    // Affected items:
    // - e1 (self)
    // - e3 (parent)
    // - p1 (child)
    // - a1 (child)
    assert.lengthOf(report.impact, 4, 'has all items')
    const [e1r, e3r, p1r, a1r] = report.impact

    assert.ok(e1r)
    assert.equal(e1r.key, e1.key)
    assert.equal(e1r.kind, DomainEntityKind)
    assert.equal(e1r.type, 'delete')
    assert.equal(e1r.impact, `The e1 entity will be deleted.`)
    assert.equal(e1r.severity, 'info')

    assert.ok(p1r)
    assert.equal(p1r.key, p1.key)
    assert.equal(p1r.kind, DomainPropertyKind)
    assert.equal(p1r.type, 'delete')
    assert.equal(p1r.impact, `The p1 property will be deleted.`)
    assert.equal(p1r.severity, 'info')

    assert.ok(a1r)
    assert.equal(a1r.key, a1.key)
    assert.equal(a1r.kind, DomainAssociationKind)
    assert.equal(a1r.type, 'delete')
    assert.equal(a1r.impact, `The a1 association will be deleted.`)
    assert.equal(a1r.severity, 'info')

    assert.ok(e3r)
    assert.equal(e3r.key, e3.key)
    assert.equal(e3r.kind, DomainEntityKind)
    assert.equal(e3r.type, 'delete')
    assert.equal(e3r.impact, `The "e3" entity will become an orphan because it is a child of the "e1" entity.`)
    assert.equal(e3r.resolution, `The "e1" entity will be removed as the parent of the "e3" entity.`)
    assert.equal(e3r.severity, 'error')
    assert.equal(e3r.relationship, 'child')
  })

  test('returns a report for deleting a property', ({ assert }) => {
    p1.getWebBinding().hidden = true
    const report = analyzer.deleteAnalysis(p1.key, DomainPropertyKind)
    assert.equal(report.key, p1.key)
    assert.equal(report.kind, DomainPropertyKind)
    assert.isTrue(report.canProceed, 'operation should be able to proceed')

    // Affected items:
    // - p1 (self)
    assert.lengthOf(report.impact, 1, 'has all items')
    const [p1r] = report.impact

    assert.ok(p1r)
    assert.equal(p1r.key, p1.key)
    assert.equal(p1r.kind, DomainPropertyKind)
    assert.equal(p1r.type, 'delete')
    assert.equal(p1r.impact, `The p1 property will be deleted.`)
    assert.equal(p1r.severity, 'info')
  })

  test('returns a report for deleting an association', ({ assert }) => {
    a1.getWebBinding().hidden = true
    const report = analyzer.deleteAnalysis(a1.key, DomainAssociationKind)
    assert.equal(report.key, a1.key)
    assert.equal(report.kind, DomainAssociationKind)
    assert.isTrue(report.canProceed, 'operation should be able to proceed')

    // Affected items:
    // - a1 (self)
    assert.lengthOf(report.impact, 1, 'has all items')
    const [a1r] = report.impact

    assert.ok(a1r)
    assert.equal(a1r.key, a1.key)
    assert.equal(a1r.kind, DomainAssociationKind)
    assert.equal(a1r.type, 'delete')
    assert.equal(a1r.impact, `The a1 association will be deleted.`)
    assert.equal(a1r.severity, 'info')
  })

  test('handles circular dependencies', ({ assert }) => {
    const report = analyzer.deleteAnalysis(e3.key, DomainEntityKind)
    assert.equal(report.key, e3.key)
    assert.equal(report.kind, DomainEntityKind)
    assert.isFalse(report.canProceed, 'operation should not be able to proceed')

    // Affected items:
    // - e3 (self)
    // - e2 (target of a1)
    // - e4 (child)
    assert.lengthOf(report.impact, 3, 'has all items')
    const [e3r, e2r, e4r] = report.impact

    assert.ok(e3r)
    assert.equal(e3r.key, e3.key)
    assert.equal(e3r.kind, DomainEntityKind)
    assert.equal(e3r.type, 'delete')
    assert.equal(e3r.impact, `The e3 entity will be deleted.`)
    assert.equal(e3r.severity, 'info')

    assert.ok(e2r)
    assert.equal(e2r.key, e2.key)
    assert.equal(e2r.kind, DomainEntityKind)
    assert.equal(e2r.type, 'delete')
    assert.equal(e2r.impact, `The "e2" entity will become an orphan because it is a child of the "e3" entity.`)
    assert.equal(e2r.resolution, `The "e3" entity will be removed as the parent of the "e2" entity.`)
    assert.equal(e2r.severity, 'error')
    assert.equal(e2r.relationship, 'child')

    assert.ok(e4r)
    assert.equal(e4r.key, e4.key)
    assert.equal(e4r.kind, DomainEntityKind)
    assert.equal(e4r.type, 'delete')
    assert.equal(e4r.impact, `The "e4" entity will become an orphan because it is a child of the "e3" entity.`)
    assert.equal(e4r.resolution, `The "e3" entity will be removed as the parent of the "e4" entity.`)
    assert.equal(e4r.severity, 'error')
    assert.equal(e4r.relationship, 'child')
  })

  test('deleteAnalysis() handles foreign namespaces', ({ assert }) => {
    const f1 = new DataDomain()
    const fm1 = f1.addModel({ info: { name: 'fm1' } })
    const fe1 = fm1.addEntity({ info: { name: 'fe1' } })
    f1.info.version = '1.0.0'
    root.registerForeignDomain(f1)
    a1.addTarget(fe1)

    const report = analyzer.deleteAnalysis(e1.key, DomainEntityKind)
    assert.equal(report.key, e1.key)
    assert.equal(report.kind, DomainEntityKind)
    assert.isFalse(report.canProceed, 'operation should not be able to proceed')

    // Affected items:
    // - e1 (self)
    // - e3 (parent)
    // - p1 (child)
    // - a1 (child)
    assert.lengthOf(report.impact, 4, 'has all items')
    const [e1r, e3r, p1r, a1r] = report.impact

    assert.ok(e1r)
    assert.equal(e1r.key, e1.key)
    assert.equal(e1r.kind, DomainEntityKind)
    assert.equal(e1r.type, 'delete')
    assert.equal(e1r.impact, `The e1 entity will be deleted.`)
    assert.equal(e1r.severity, 'info')

    assert.ok(p1r)
    assert.equal(p1r.key, p1.key)
    assert.equal(p1r.kind, DomainPropertyKind)
    assert.equal(p1r.type, 'delete')
    assert.equal(p1r.impact, `The p1 property will be deleted.`)
    assert.equal(p1r.severity, 'info')

    assert.ok(a1r)
    assert.equal(a1r.key, a1.key)
    assert.equal(a1r.kind, DomainAssociationKind)
    assert.equal(a1r.type, 'delete')
    assert.equal(a1r.impact, `The a1 association will be deleted.`)
    assert.equal(a1r.severity, 'info')

    assert.ok(e3r)
    assert.equal(e3r.key, e3.key)
    assert.equal(e3r.kind, DomainEntityKind)
    assert.equal(e3r.type, 'delete')
    assert.equal(e3r.impact, `The "e3" entity will become an orphan because it is a child of the "e1" entity.`)
    assert.equal(e3r.resolution, `The "e1" entity will be removed as the parent of the "e3" entity.`)
    assert.equal(e3r.severity, 'error')
    assert.equal(e3r.relationship, 'child')
  })

  test('removeForeignNamespaceAnalysis() handles parent relationships to foreign entities', ({ assert }) => {
    const f1 = new DataDomain()
    f1.key = 'f1'
    f1.info.name = 'f1'
    const fm1 = f1.addModel({ key: 'fm1', info: { name: 'fm1' } })
    const fe1 = fm1.addEntity({ key: 'fe1', info: { name: 'fe1' } })
    f1.info.version = '1.0.0'
    root.registerForeignDomain(f1)
    e1.addParent(fe1.key, f1.key)

    const report = analyzer.removeForeignNamespaceAnalysis(f1.key)
    assert.equal(report.key, f1.key)
    assert.equal(report.kind, DomainNamespaceKind)
    assert.isFalse(report.canProceed, 'operation should not be able to proceed')

    assert.lengthOf(report.impact, 1, 'has all items')
    const [e1r] = report.impact

    assert.ok(e1r)
    assert.equal(e1r.key, e1.key)
    assert.equal(e1r.kind, DomainEntityKind)
    assert.equal(e1r.type, 'delete')
    assert.equal(
      e1r.impact,
      `The "e1" entity will become an orphan because its parent "fe1" is in the foreign namespace "${f1.key}".`
    )
    assert.equal(e1r.resolution, `The "fe1" entity will be removed as the parent of the "e1" entity.`)
    assert.equal(e1r.severity, 'error')
    assert.equal(e1r.relationship, 'child')
  })

  test('removeForeignNamespaceAnalysis() handles associations to foreign entities', ({ assert }) => {
    const f1 = new DataDomain()
    f1.key = 'f1'
    f1.info.name = 'f1'
    const fm1 = f1.addModel({ key: 'fm1', info: { name: 'fm1' } })
    const fe1 = fm1.addEntity({ key: 'fe1', info: { name: 'fe1' } })
    f1.info.version = '1.0.0'
    root.registerForeignDomain(f1)
    a1.addTarget(fe1)

    const report = analyzer.removeForeignNamespaceAnalysis(f1.key)
    assert.equal(report.key, f1.key)
    assert.equal(report.kind, DomainNamespaceKind)
    assert.isFalse(report.canProceed, 'operation should not be able to proceed')

    assert.lengthOf(report.impact, 1, 'has all items')
    const [a1r] = report.impact

    assert.ok(a1r)
    assert.equal(a1r.key, a1.key)
    assert.equal(a1r.kind, DomainAssociationKind)
    assert.equal(a1r.type, 'delete')
    assert.equal(
      a1r.impact,
      `The "a1" association from "e1" will be broken because it targets "fe1" in the foreign namespace "${f1.key}".`
    )
    assert.equal(a1r.resolution, `The "a1" association will be removed from "e1".`)
    assert.equal(a1r.severity, 'error')
  })

  test('removeForeignNamespaceAnalysis() handles no foreign entities', ({ assert }) => {
    const f1 = new DataDomain()
    const fm1 = f1.addModel({ key: 'fm1', info: { name: 'fm1' } })
    const fe1 = fm1.addEntity({ key: 'fe1', info: { name: 'fe1' } })
    fe1.addProperty()
    f1.info.version = '1.0.0'
    root.registerForeignDomain(f1)

    const report = analyzer.removeForeignNamespaceAnalysis(f1.key)
    assert.equal(report.key, f1.key)
    assert.equal(report.kind, DomainNamespaceKind)
    assert.isTrue(report.canProceed, 'operation should be able to proceed')
    assert.lengthOf(report.impact, 0, 'has all items')
  })
})
