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

test.group('ApiModel.createSchema()', () => {
  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.user)
    assert.isUndefined(schema.dependencyList)
    assert.isUndefined(schema.authentication)
    assert.isUndefined(schema.authorization)
    assert.isUndefined(schema.session)
    assert.isUndefined(schema.accessRule)
    assert.isUndefined(schema.rateLimiting)
    assert.isUndefined(schema.termsOfService)
    assert.isUndefined(schema.contact)
    assert.isUndefined(schema.license)
  }).tags(['@modeling', '@api', '@schema'])

  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: [],
          hasCollection: true,
          kind: ExposedEntityKind,
          resourcePath: '/',
          entity: { key: 'entity1' },
        },
      ],
      user: { key: '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: [] },
      termsOfService: 'https://example.com/terms',
      contact: { name: 'John Doe', email: 'john.doe@example.com' } as ApiContact,
      license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' } as ApiLicense,
    }
    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.lengthOf(schema.exposes, 1, 'should have one exposed entity')
    assert.equal(schema.exposes[0].key, 'entity1', 'exposed entity should have correct key')
    assert.deepEqual(schema.user, { key: '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: [] })
    assert.equal(schema.termsOfService, 'https://example.com/terms')
    assert.deepEqual(schema.contact, { name: 'John Doe', email: 'john.doe@example.com' })
    assert.deepEqual(schema.license, { name: 'MIT', url: 'https://opensource.org/licenses/MIT' })
  }).tags(['@modeling', '@api', '@schema'])

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

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

test.group('ApiModel.constructor()', () => {
  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.user)
    assert.isUndefined(model.authentication)
    assert.isUndefined(model.authorization)
    assert.isUndefined(model.session)
    assert.isUndefined(model.accessRule)
    assert.isUndefined(model.rateLimiting)
    assert.isUndefined(model.termsOfService)
    assert.isUndefined(model.contact)
    assert.isUndefined(model.license)
    assert.deepEqual(model.dependencyList, [])
  }).tags(['@modeling', '@api', '@creation'])

  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: [],
          resourcePath: '/',
          entity: { key: 'entity1' },
          hasCollection: true,
          kind: ExposedEntityKind,
        },
      ],
      user: { key: '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: [] },
      termsOfService: 'https://example.com/terms',
      contact: { name: 'John Doe', email: 'john.doe@example.com' },
      license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' },
    }
    const model = new ApiModel(schema)

    assert.equal(model.key, 'test-api')
    assert.equal(model.info.name, 'Test API')
    assert.lengthOf(model.exposes, 1, 'should have one exposed entity')
    assert.equal(model.exposes[0].key, 'entity1', 'exposed entity should have correct key')
    assert.deepEqual(model.user, { key: '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: [] })
    assert.equal(model.termsOfService, 'https://example.com/terms')
    assert.deepEqual(model.contact, { name: 'John Doe', email: 'john.doe@example.com' })
    assert.deepEqual(model.license, { name: 'MIT', url: 'https://opensource.org/licenses/MIT' })
  }).tags(['@modeling', '@api', '@creation'])

  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')
  }).tags(['@modeling', '@api', '@creation'])

  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)
  }).tags(['@modeling', '@api', '@creation'])
})

test.group('ApiModel.toJSON()', () => {
  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.user)
    assert.isUndefined(json.dependencyList)
    assert.isUndefined(json.authentication)
    assert.isUndefined(json.authorization)
    assert.isUndefined(json.session)
    assert.isUndefined(json.accessRule)
    assert.isUndefined(json.rateLimiting)
    assert.isUndefined(json.termsOfService)
    assert.isUndefined(json.contact)
    assert.isUndefined(json.license)
  }).tags(['@modeling', '@api', '@serialization'])

  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: [],
          resourcePath: '/',
          entity: { key: 'entity1' },
          hasCollection: true,
          kind: ExposedEntityKind,
        },
      ],
      user: { key: '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: [] },
      termsOfService: 'https://example.com/terms',
      contact: { name: 'John Doe', email: 'john.doe@example.com' },
      license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' },
    }
    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.lengthOf(json.exposes, 1, 'should have one exposed entity')
    assert.equal(json.exposes[0].key, 'entity1', 'exposed entity should have correct key')
    assert.deepEqual(json.user, { key: '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: [] })
    assert.equal(json.termsOfService, 'https://example.com/terms')
    assert.deepEqual(json.contact, { name: 'John Doe', email: 'john.doe@example.com' })
    assert.deepEqual(json.license, { name: 'MIT', url: 'https://opensource.org/licenses/MIT' })
  }).tags(['@modeling', '@api', '@serialization'])
})

test.group('ApiModel.getExposedEntity()', () => {
  test('returns an existing exposed entity', ({ assert }) => {
    const model = new ApiModel()
    const entityKey = 'get-entity'
    const exposed: ExposedEntitySchema = {
      key: entityKey,
      actions: [],
      hasCollection: true,
      kind: ExposedEntityKind,
      resourcePath: '/',
      entity: { key: entityKey },
    }
    model.exposes.push(new ExposedEntity(model, exposed))

    const retrievedEntity = model.getExposedEntity({ key: entityKey })
    assert.deepEqual(retrievedEntity?.toJSON(), exposed)
  }).tags(['@modeling', '@api'])

  test('returns undefined if entity is not exposed', ({ assert }) => {
    const model = new ApiModel()
    const retrievedEntity = model.getExposedEntity({ key: 'non-exposed-entity' })
    assert.isUndefined(retrievedEntity)
  }).tags(['@modeling', '@api'])
})
