import { test } from '@japa/runner'
import {
  ApiModel,
  ApiModelKind,
  DataDomain,
  type RolesBasedAccessControl,
  type ApiModelSchema,
  type ExposedEntity,
} from '../../../src/index.js'

test.group('ApiModel.createSchema()', (g) => {
  g.tests.forEach((test) => {
    test.tags(['@modeling', '@api', '@schema'])
  })
  test('creates a schema with default values', ({ assert }) => {
    const schema = ApiModel.createSchema()
    assert.equal(schema.kind, ApiModelKind)
    assert.typeOf(schema.key, 'string')
    assert.isNotEmpty(schema.key)
    assert.deepInclude(schema.info, { name: 'Unnamed API' })
    assert.deepEqual(schema.exposes, [])
    assert.isUndefined(schema.userKey)
    assert.isUndefined(schema.dependencyList)
    assert.isUndefined(schema.authentication)
    assert.isUndefined(schema.authorization)
    assert.isUndefined(schema.session)
    assert.isUndefined(schema.accessRule)
    assert.isUndefined(schema.rateLimiting)
  })

  test('creates a schema with provided values', ({ assert }) => {
    const input: Partial<ApiModelSchema> = {
      key: 'test-api',
      info: { name: 'Test API', description: 'A test API' },
      exposes: [{ key: 'entity1', actions: [] }],
      userKey: 'user-entity',
      dependencyList: [{ key: 'domain1', version: '1.0.0' }],
      authentication: { strategy: 'UsernamePassword' },
      authorization: { strategy: 'RBAC', roleKey: 'role' } as RolesBasedAccessControl,
      session: { secret: 'secret', properties: ['email'] },
      accessRule: [{ type: 'public' }],
      rateLimiting: { rules: [] },
    }
    const schema = ApiModel.createSchema(input)

    assert.equal(schema.kind, ApiModelKind)
    assert.equal(schema.key, 'test-api')
    assert.deepInclude(schema.info, { name: 'Test API', description: 'A test API' })
    assert.deepEqual(schema.exposes, [{ key: 'entity1', actions: [] }])
    assert.equal(schema.userKey, 'user-entity')
    assert.deepEqual(schema.dependencyList, [{ key: 'domain1', version: '1.0.0' }])
    assert.deepEqual(schema.authentication, { strategy: 'UsernamePassword' })
    assert.deepEqual(schema.authorization, { strategy: 'RBAC', roleKey: 'role' })
    assert.deepEqual(schema.session, { secret: 'secret', properties: ['email'] })
    assert.deepEqual(schema.accessRule, [{ type: 'public' }])
    assert.deepEqual(schema.rateLimiting, { rules: [] })
  })

  test('creates a schema with partial info', ({ assert }) => {
    const schema = ApiModel.createSchema({ info: { name: 'Partial API' } })
    assert.deepInclude(schema.info, { name: 'Partial API' })
  })

  test('creates a schema with empty info', ({ assert }) => {
    const schema = ApiModel.createSchema({ info: {} })
    assert.deepInclude(schema.info, { name: 'Unnamed API' })
  })
})

test.group('ApiModel.constructor()', (g) => {
  g.tests.forEach((test) => {
    test.tags(['@modeling', '@api', '@creation'])
  })

  test('creates an instance with default values', ({ assert }) => {
    const model = new ApiModel()
    assert.equal(model.kind, ApiModelKind)
    assert.typeOf(model.key, 'string')
    assert.isNotEmpty(model.key)
    assert.equal(model.info.name, 'Unnamed API')
    assert.deepEqual(model.exposes, [])
    assert.isUndefined(model.userKey)
    assert.isUndefined(model.authentication)
    assert.isUndefined(model.authorization)
    assert.isUndefined(model.session)
    assert.isUndefined(model.accessRule)
    assert.isUndefined(model.rateLimiting)
    assert.deepEqual(model.dependencyList, [])
  })

  test('creates an instance with provided schema values', ({ assert }) => {
    const schema: ApiModelSchema = {
      kind: ApiModelKind,
      key: 'test-api',
      info: { name: 'Test API', description: 'A test API' },
      exposes: [{ key: 'entity1', actions: [] }],
      userKey: 'user-entity',
      dependencyList: [{ key: 'domain1', version: '1.0.0' }],
      authentication: { strategy: 'UsernamePassword' },
      authorization: { strategy: 'RBAC', roleKey: 'role' } as RolesBasedAccessControl,
      session: { secret: 'secret', properties: ['email'] },
      accessRule: [{ type: 'public' }],
      rateLimiting: { rules: [] },
    }
    const model = new ApiModel(schema)

    assert.equal(model.key, 'test-api')
    assert.equal(model.info.name, 'Test API')
    assert.deepEqual(model.exposes, [{ key: 'entity1', actions: [] }])
    assert.equal(model.userKey, 'user-entity')
    assert.deepEqual(model.dependencyList, [{ key: 'domain1', version: '1.0.0' }])
    assert.deepEqual(model.authentication, { strategy: 'UsernamePassword' })
    assert.deepEqual(model.authorization, { strategy: 'RBAC', roleKey: 'role' })
    assert.deepEqual(model.session, { secret: 'secret', properties: ['email'] })
    assert.deepEqual(model.accessRule, [{ type: 'public' }])
    assert.deepEqual(model.rateLimiting, { rules: [] })
  })

  test('creates an instance with a DataDomain', ({ assert }) => {
    const domainSchema = DataDomain.createSchema({ key: 'my-domain' })
    domainSchema.info.version = '1.0.0'
    const model = new ApiModel(
      { dependencyList: [{ version: domainSchema.info.version, key: domainSchema.key }] },
      domainSchema
    )
    assert.isDefined(model.domain)
    assert.instanceOf(model.domain, DataDomain)
    assert.equal(model.domain!.key, 'my-domain')
  })

  test('notifies change when info is modified', async ({ assert }) => {
    const model = new ApiModel()
    let notified = false
    model.addEventListener('change', () => {
      notified = true
    })
    model.info.name = 'New Name'
    await Promise.resolve() // Allow microtask to run
    assert.isTrue(notified)
  })
})

test.group('ApiModel.toJSON()', (g) => {
  g.tests.forEach((test) => {
    test.tags(['@modeling', '@api', '@serialization'])
  })
  test('serializes default values', ({ assert }) => {
    const model = new ApiModel()
    const json = model.toJSON()

    assert.equal(json.kind, ApiModelKind)
    assert.equal(json.key, model.key)
    assert.deepInclude(json.info, { name: 'Unnamed API' })
    assert.deepEqual(json.exposes, [])
    assert.isUndefined(json.userKey)
    assert.isUndefined(json.dependencyList)
    assert.isUndefined(json.authentication)
    assert.isUndefined(json.authorization)
    assert.isUndefined(json.session)
    assert.isUndefined(json.accessRule)
    assert.isUndefined(json.rateLimiting)
  })

  test('serializes all provided values', ({ assert }) => {
    const schema: ApiModelSchema = {
      kind: ApiModelKind,
      key: 'test-api',
      info: { name: 'Test API', description: 'A test API' },
      exposes: [{ key: 'entity1', actions: [] }],
      userKey: 'user-entity',
      dependencyList: [{ key: 'domain1', version: '1.0.0' }],
      authentication: { strategy: 'UsernamePassword' },
      authorization: { strategy: 'RBAC', roleKey: 'role' } as RolesBasedAccessControl,
      session: { secret: 'secret', properties: ['email'] },
      accessRule: [{ type: 'public' }],
      rateLimiting: { rules: [] },
    }
    const model = new ApiModel(schema)
    const json = model.toJSON()

    assert.equal(json.key, 'test-api')
    assert.deepInclude(json.info, { name: 'Test API', description: 'A test API' })
    assert.deepEqual(json.exposes, [{ key: 'entity1', actions: [] }])
    assert.equal(json.userKey, 'user-entity')
    assert.deepEqual(json.dependencyList, [{ key: 'domain1', version: '1.0.0' }])
    assert.deepEqual(json.authentication, { strategy: 'UsernamePassword' })
    assert.deepEqual(json.authorization, { strategy: 'RBAC', roleKey: 'role' })
    assert.deepEqual(json.session, { secret: 'secret', properties: ['email'] })
    assert.deepEqual(json.accessRule, [{ type: 'public' }])
    assert.deepEqual(json.rateLimiting, { rules: [] })
  })
})

test.group('ApiModel.exposeEntity()', (g) => {
  g.tests.forEach((test) => {
    test.tags(['@modeling', '@api'])
  })
  test('exposes a new entity', ({ assert }) => {
    const model = new ApiModel()
    const entityKey = 'new-entity'
    const exposedEntity = model.exposeEntity(entityKey)

    assert.isDefined(exposedEntity)
    assert.equal(exposedEntity.key, entityKey)
    assert.deepEqual(exposedEntity.actions, [])
    assert.includeDeepMembers(model.exposes, [exposedEntity])
  })

  test('returns an existing entity if already exposed', ({ assert }) => {
    const model = new ApiModel()
    const entityKey = 'existing-entity'
    const initialExposedEntity = model.exposeEntity(entityKey)
    const retrievedExposedEntity = model.exposeEntity(entityKey)

    assert.strictEqual(retrievedExposedEntity, initialExposedEntity)
    assert.lengthOf(model.exposes, 1)
  })

  test('notifies change when a new entity is exposed', async ({ assert }) => {
    const model = new ApiModel()
    let notified = false
    model.addEventListener('change', () => {
      notified = true
    })
    model.exposeEntity('notify-entity')
    await Promise.resolve() // Allow microtask to run
    assert.isTrue(notified)
  })

  test('does not notify change if entity already exposed', async ({ assert }) => {
    const model = new ApiModel()
    model.exposeEntity('no-notify-entity') // First exposure
    await Promise.resolve() // Allow microtask to run
    let notified = false
    model.addEventListener('change', () => {
      notified = true
    })
    model.exposeEntity('no-notify-entity') // Second exposure
    await Promise.resolve() // Allow microtask to run
    assert.isFalse(notified)
  })
})

test.group('ApiModel.removeEntity()', (g) => {
  g.tests.forEach((test) => {
    test.tags(['@modeling', '@api'])
  })
  test('removes an existing entity', ({ assert }) => {
    const model = new ApiModel()
    const entityKey = 'entity-to-remove'
    model.exposeEntity(entityKey)
    assert.lengthOf(model.exposes, 1)

    model.removeEntity(entityKey)
    assert.lengthOf(model.exposes, 0)
  })

  test('does nothing if entity does not exist', ({ assert }) => {
    const model = new ApiModel()
    model.exposeEntity('existing-entity')
    const initialExposes = [...model.exposes]

    model.removeEntity('non-existing-entity')
    assert.deepEqual(model.exposes, initialExposes)
  })

  test('notifies change when an entity is removed', async ({ assert }) => {
    const model = new ApiModel()
    const entityKey = 'notify-remove-entity'
    model.exposeEntity(entityKey)

    let notified = false
    model.addEventListener('change', () => {
      notified = true
    })
    model.removeEntity(entityKey)
    await Promise.resolve() // Allow microtask to run
    assert.isTrue(notified)
  })

  test('does not notify change if entity to remove does not exist', async ({ assert }) => {
    const model = new ApiModel()
    let notified = false
    model.addEventListener('change', () => {
      notified = true
    })
    model.removeEntity('no-notify-remove-entity')
    await Promise.resolve() // Allow microtask to run
    assert.isFalse(notified)
  })
})

test.group('ApiModel.getExposedEntity()', (g) => {
  g.tests.forEach((test) => {
    test.tags(['@modeling', '@api'])
  })
  test('returns an existing exposed entity', ({ assert }) => {
    const model = new ApiModel()
    const entityKey = 'get-entity'
    const exposed: ExposedEntity = { key: entityKey, actions: [] }
    model.exposes.push(exposed)

    const retrievedEntity = model.getExposedEntity(entityKey)
    assert.deepEqual(retrievedEntity, exposed)
  })

  test('returns undefined if entity is not exposed', ({ assert }) => {
    const model = new ApiModel()
    const retrievedEntity = model.getExposedEntity('non-exposed-entity')
    assert.isUndefined(retrievedEntity)
  })
})
