import { test } from '@japa/runner'
import { ApiModel, DataDomain } from '../../../src/index.js'

test.group('ApiModel.exposeEntity()', () => {
  test('exposes a new entity', ({ assert }) => {
    const domain = new DataDomain()
    domain.info.version = '1.0.0'
    const dm = domain.addModel()
    const e1 = dm.addEntity()
    const model = new ApiModel()
    model.attachDataDomain(domain)
    const exposedEntity = model.exposeEntity({ key: e1.key })

    assert.isDefined(exposedEntity)
    assert.typeOf(exposedEntity.key, 'string')
    assert.deepEqual(exposedEntity.entity, { key: e1.key })
    assert.deepEqual(exposedEntity.actions, [])
  }).tags(['@modeling', '@api'])

  test('returns an existing entity if already exposed', ({ assert }) => {
    const domain = new DataDomain()
    domain.info.version = '1.0.0'
    const dm = domain.addModel()
    const e1 = dm.addEntity()
    const model = new ApiModel()
    model.attachDataDomain(domain)
    const initialExposedEntity = model.exposeEntity({ key: e1.key })
    const retrievedExposedEntity = model.exposeEntity({ key: e1.key })

    assert.deepEqual(retrievedExposedEntity.toJSON(), initialExposedEntity.toJSON())
    assert.lengthOf(model.exposes, 1)
  }).tags(['@modeling', '@api'])

  test('notifies change when a new entity is exposed', async ({ assert }) => {
    const domain = new DataDomain()
    domain.info.version = '1.0.0'
    const dm = domain.addModel()
    const e1 = dm.addEntity()
    const model = new ApiModel()
    model.attachDataDomain(domain)
    let notified = false
    model.addEventListener('change', () => {
      notified = true
    })
    model.exposeEntity({ key: e1.key })
    await Promise.resolve() // Allow microtask to run
    assert.isTrue(notified)
  }).tags(['@modeling', '@api'])

  test('does not notify change if entity already exposed', async ({ assert }) => {
    const domain = new DataDomain()
    domain.info.version = '1.0.0'
    const dm = domain.addModel()
    const e1 = dm.addEntity()
    const model = new ApiModel()
    model.attachDataDomain(domain)
    model.exposeEntity({ key: e1.key }) // First exposure
    await Promise.resolve() // Allow microtask to run
    let notified = false
    model.addEventListener('change', () => {
      notified = true
    })
    model.exposeEntity({ key: e1.key }) // Second exposure
    await Promise.resolve() // Allow microtask to run
    assert.isFalse(notified)
  }).tags(['@modeling', '@api'])

  test('exposes nested entities through associations', ({ assert }) => {
    const domain = new DataDomain()
    domain.info.version = '1.0.0'
    const dm = domain.addModel()
    const eA = dm.addEntity({ info: { name: 'A' } })
    const eB = dm.addEntity({ info: { name: 'B' } })
    // Add association from A to B
    eA.addAssociation({ info: { name: 'entityB' }, targets: [{ key: eB.key }] })
    const model = new ApiModel()
    model.attachDataDomain(domain)
    const exposedA = model.exposeEntity({ key: eA.key }, { followAssociations: true })
    // Find nested exposure for B
    const nestedB = model.exposes.find((e) => !e.isRoot && e.entity.key === eB.key)
    assert.isDefined(nestedB)
    assert.deepEqual(nestedB?.parent?.key, exposedA.key)
    assert.strictEqual(nestedB?.collectionPath, '/entitybs')
  })

  test('does not infinitely expose circular associations', ({ assert }) => {
    const domain = new DataDomain()
    domain.info.version = '1.0.0'
    const dm = domain.addModel()
    const eA = dm.addEntity({ info: { name: 'A' } })
    const eB = dm.addEntity({ info: { name: 'B' } })
    // A -> B, B -> A
    eA.addAssociation({ info: { name: 'assocAB' }, targets: [{ key: eB.key }] })
    eB.addAssociation({ info: { name: 'assocBA' }, targets: [{ key: eA.key }] })
    const model = new ApiModel()
    model.attachDataDomain(domain)
    model.exposeEntity({ key: eA.key }, { followAssociations: true, maxDepth: 4 })

    assert.lengthOf(model.exposes, 2, 'has only 2 exposures')
    // Should expose A (root), B (nested under A), but not infinitely nest
    const exposedA = model.exposes.find((e) => e.isRoot && e.entity.key === eA.key)
    const nestedB = model.exposes.find((e) => !e.isRoot && e.entity.key === eB.key)
    // There should be only one nested exposure for B under A
    assert.isDefined(exposedA)
    assert.isDefined(nestedB)
    // There should NOT be a nested exposure for A under B
    const circularA = model.exposes.find((e) => !e.isRoot && e.entity.key === eA.key && e.parent?.key === nestedB?.key)
    assert.isUndefined(circularA)
  })

  test('does not expose self-association as nested entity', ({ assert }) => {
    const domain = new DataDomain()
    domain.info.version = '1.0.0'
    const dm = domain.addModel()
    const eA = dm.addEntity({ info: { name: 'A' } })
    // A -> A (self-association)
    eA.addAssociation({ info: { name: 'assocAA' }, targets: [{ key: eA.key }] })
    const model = new ApiModel()
    model.attachDataDomain(domain)
    model.exposeEntity({ key: eA.key }, { followAssociations: true })
    // Should only expose A as root, not as nested
    const exposedA = model.exposes.find((e) => e.isRoot && e.entity.key === eA.key)
    const nestedA = model.exposes.find((e) => !e.isRoot && e.entity.key === eA.key)
    assert.isDefined(exposedA)
    assert.isUndefined(nestedA)
  })

  test('exposes multi-level associations (A -> B -> C)', ({ assert }) => {
    const domain = new DataDomain()
    domain.info.version = '1.0.0'
    const dm = domain.addModel()
    const eA = dm.addEntity({ info: { name: 'A' } })
    const eB = dm.addEntity({ info: { name: 'B' } })
    const eC = dm.addEntity({ info: { name: 'C' } })
    eA.addAssociation({ info: { name: 'toB' }, targets: [{ key: eB.key }] })
    eB.addAssociation({ info: { name: 'toC' }, targets: [{ key: eC.key }] })
    const model = new ApiModel()
    model.attachDataDomain(domain)
    model.exposeEntity({ key: eA.key }, { followAssociations: true })
    const nestedB = model.exposes.find((e) => !e.isRoot && e.entity.key === eB.key)
    const nestedC = model.exposes.find((e) => !e.isRoot && e.entity.key === eC.key)
    assert.isDefined(nestedB)
    assert.isDefined(nestedC)
    assert.deepEqual(nestedB?.parent?.key, model.exposes.find((e) => e.isRoot && e.entity.key === eA.key)?.key)
    assert.deepEqual(nestedC?.parent?.key, nestedB?.key)
  })

  test('exposes multiple associations from one entity (A -> B, A -> C)', ({ assert }) => {
    const domain = new DataDomain()
    domain.info.version = '1.0.0'
    const dm = domain.addModel()
    const eA = dm.addEntity({ info: { name: 'A' } })
    const eB = dm.addEntity({ info: { name: 'B' } })
    const eC = dm.addEntity({ info: { name: 'C' } })
    eA.addAssociation({ info: { name: 'toB' }, targets: [{ key: eB.key }] })
    eA.addAssociation({ info: { name: 'toC' }, targets: [{ key: eC.key }] })
    const model = new ApiModel()
    model.attachDataDomain(domain)
    model.exposeEntity({ key: eA.key }, { followAssociations: true })
    const nestedB = model.exposes.find((e) => !e.isRoot && e.entity.key === eB.key)
    const nestedC = model.exposes.find((e) => !e.isRoot && e.entity.key === eC.key)
    assert.isDefined(nestedB)
    assert.isDefined(nestedC)
    assert.deepEqual(nestedB?.parent?.key, model.exposes.find((e) => e.isRoot && e.entity.key === eA.key)?.key)
    assert.deepEqual(nestedC?.parent?.key, model.exposes.find((e) => e.isRoot && e.entity.key === eA.key)?.key)
  })

  test('respects maxDepth option (A -> B -> C -> D, maxDepth=2)', ({ assert }) => {
    const domain = new DataDomain()
    domain.info.version = '1.0.0'
    const dm = domain.addModel()
    const eA = dm.addEntity({ info: { name: 'A' } })
    const eB = dm.addEntity({ info: { name: 'B' } })
    const eC = dm.addEntity({ info: { name: 'C' } })
    const eD = dm.addEntity({ info: { name: 'D' } })
    eA.addAssociation({ info: { name: 'toB' }, targets: [{ key: eB.key }] })
    eB.addAssociation({ info: { name: 'toC' }, targets: [{ key: eC.key }] })
    eC.addAssociation({ info: { name: 'toD' }, targets: [{ key: eD.key }] })
    const model = new ApiModel()
    model.attachDataDomain(domain)
    model.exposeEntity({ key: eA.key }, { followAssociations: true, maxDepth: 2 })
    const nestedB = model.exposes.find((e) => !e.isRoot && e.entity.key === eB.key)
    const nestedC = model.exposes.find((e) => !e.isRoot && e.entity.key === eC.key)
    const nestedD = model.exposes.find((e) => !e.isRoot && e.entity.key === eD.key)
    assert.isDefined(nestedB)
    assert.isDefined(nestedC)
    assert.isUndefined(nestedD)
  })
  test('resolves root path collision by appending a number', ({ assert }) => {
    const domain = new DataDomain()
    domain.info.version = '1.0.0'
    const dm = domain.addModel()
    // Two entities that will generate the same plural path "/items"
    const e1 = dm.addEntity({ info: { name: 'Item' } })
    const e2 = dm.addEntity({ info: { name: 'Item' } }) // Same name

    const model = new ApiModel()
    model.attachDataDomain(domain)

    // Expose first entity -> /items
    const exp1 = model.exposeEntity({ key: e1.key })
    assert.equal(exp1.collectionPath, '/items')

    // Expose second entity -> should resolve collision to /items-1
    const exp2 = model.exposeEntity({ key: e2.key })
    assert.equal(exp2.collectionPath, '/items-1')
    assert.equal(exp2.resourcePath, '/items-1/{id}')

    // Expose third entity -> /items-2
    const e3 = dm.addEntity({ info: { name: 'Item' } })
    const exp3 = model.exposeEntity({ key: e3.key })
    assert.equal(exp3.collectionPath, '/items-2')
  }).tags(['@modeling', '@api'])
})
