'use strict'

const { expect } = require('chai')
const log = require('log').get('serverless:test')
const awsRequest = require('@serverless/test/aws-request')
const CloudFormationService = require('aws-sdk').CloudFormation
const CognitoIdentityServiceProviderService =
  require('aws-sdk').CognitoIdentityServiceProvider
const ApiGatewayV2Service = require('aws-sdk').ApiGatewayV2
const fixtures = require('../../fixtures/programmatic')
const { confirmCloudWatchLogs } = require('../../utils/misc')

const {
  deployService,
  removeService,
  fetch,
} = require('../../utils/integration')

describe('HTTP API Integration Test', function () {
  this.timeout(1000 * 60 * 20) // Involves time-taking deploys
  let endpoint
  let stackName
  let serviceDir
  const stage = 'dev'

  const resolveEndpoint = async () => {
    const result = await awsRequest(CloudFormationService, 'describeStacks', {
      StackName: stackName,
    })
    const endpointOutput = result.Stacks[0].Outputs.find(
      (output) => output.OutputKey === 'HttpApiUrl',
    ).OutputValue
    endpoint = endpointOutput.match(
      /https:\/\/.+\.execute-api\..+\.amazonaws\.com/,
    )[0]
  }

  describe('Specific endpoints', () => {
    let poolId
    let clientId
    const userName = 'test-http-api'
    const userPassword = 'razDwa3!'

    before(async () => {
      poolId = (
        await awsRequest(
          CognitoIdentityServiceProviderService,
          'createUserPool',
          {
            PoolName: `test-http-api-${process.hrtime()[1]}`,
          },
        )
      ).UserPool.Id
      ;[clientId] = await Promise.all([
        awsRequest(
          CognitoIdentityServiceProviderService,
          'createUserPoolClient',
          {
            ClientName: 'test-http-api',
            UserPoolId: poolId,
            ExplicitAuthFlows: [
              'ALLOW_USER_PASSWORD_AUTH',
              'ALLOW_REFRESH_TOKEN_AUTH',
            ],
            PreventUserExistenceErrors: 'ENABLED',
          },
        ).then((result) => result.UserPoolClient.ClientId),
        awsRequest(CognitoIdentityServiceProviderService, 'adminCreateUser', {
          UserPoolId: poolId,
          Username: userName,
        }).then(() =>
          awsRequest(
            CognitoIdentityServiceProviderService,
            'adminSetUserPassword',
            {
              UserPoolId: poolId,
              Username: userName,
              Password: userPassword,
              Permanent: true,
            },
          ),
        ),
      ])

      const serviceData = await fixtures.setup('http-api', {
        configExt: {
          provider: {
            httpApi: {
              cors: { exposedResponseHeaders: 'X-foo' },
              authorizers: {
                someAuthorizer: {
                  identitySource: '$request.header.Authorization',
                  issuerUrl: `https://cognito-idp.us-east-1.amazonaws.com/${poolId}`,
                  audience: clientId,
                },
                simpleCustomLambdaAuthorizer: {
                  type: 'request',
                  functionName: 'simpleCustomAuthorizer',
                  enableSimpleResponses: true,
                },
                standardCustomLambdaAuthorizer: {
                  type: 'request',
                  functionName: 'standardCustomAuthorizer',
                },
              },
            },
            logs: { httpApi: true },
          },
          functions: {
            foo: {
              events: [
                {
                  httpApi: {
                    authorizer: 'someAuthorizer',
                  },
                },
              ],
            },
            other: {
              timeout: 1,
            },
            behindSimpleCustomAuthorizer: {
              handler: 'index.handler',
              events: [
                {
                  httpApi: {
                    method: 'GET',
                    path: '/behind-simple-authorizer',
                    authorizer: 'simpleCustomLambdaAuthorizer',
                  },
                },
              ],
            },
            behindStandardCustomAuthorizer: {
              handler: 'index.handler',
              events: [
                {
                  httpApi: {
                    method: 'GET',
                    path: '/behind-standard-authorizer',
                    authorizer: 'standardCustomLambdaAuthorizer',
                  },
                },
              ],
            },
          },
        },
      })
      ;({ servicePath: serviceDir } = serviceData)
      const serviceName = serviceData.serviceConfig.service
      stackName = `${serviceName}-${stage}`
      await deployService(serviceDir)
      return resolveEndpoint()
    })

    after(async () => {
      await awsRequest(
        CognitoIdentityServiceProviderService,
        'deleteUserPool',
        {
          UserPoolId: poolId,
        },
      )
      if (!serviceDir) return
      await removeService(serviceDir)
    })

    it('should expose an accessible POST HTTP endpoint', async () => {
      const testEndpoint = `${endpoint}/some-post`

      const response = await fetch(testEndpoint, { method: 'POST' })
      const json = await response.json()
      expect(json).to.deep.equal({ method: 'POST', path: '/some-post' })
    })

    it('should expose an accessible paramed GET HTTP endpoint', async () => {
      const testEndpoint = `${endpoint}/bar/whatever`

      const response = await fetch(testEndpoint, { method: 'GET' })
      const json = await response.json()
      expect(json).to.deep.equal({ method: 'GET', path: '/bar/whatever' })
    })

    it('should return 404 on not supported method', async () => {
      const testEndpoint = `${endpoint}/foo`

      const response = await fetch(testEndpoint, { method: 'POST' })
      expect(response.status).to.equal(404)
    })

    it('should return 404 on not configured path', async () => {
      const testEndpoint = `${endpoint}/not-configured`

      const response = await fetch(testEndpoint, { method: 'GET' })
      expect(response.status).to.equal(404)
    })

    it('should respect timeout settings', async () => {
      const testEndpoint = `${endpoint}/bar/timeout`

      const response = await fetch(testEndpoint, { method: 'GET' })
      expect(response.status).to.equal(500)
    })

    it('should support CORS when indicated', async () => {
      const testEndpoint = `${endpoint}/bar/whatever`

      const response = await fetch(testEndpoint, {
        method: 'GET',
        headers: { Origin: 'https://serverless.com' },
      })
      expect(response.headers.get('access-control-allow-origin')).to.equal('*')
      expect(response.headers.get('access-control-expose-headers')).to.equal(
        'x-foo',
      )
    })

    it('should expose a GET HTTP endpoint backed by JWT authorization', async () => {
      const testEndpoint = `${endpoint}/foo`

      const responseUnauthorized = await fetch(testEndpoint, {
        method: 'GET',
      })
      expect(responseUnauthorized.status).to.equal(401)

      const token = (
        await awsRequest(
          CognitoIdentityServiceProviderService,
          'initiateAuth',
          {
            AuthFlow: 'USER_PASSWORD_AUTH',
            AuthParameters: { USERNAME: userName, PASSWORD: userPassword },
            ClientId: clientId,
          },
        )
      ).AuthenticationResult.IdToken
      const responseAuthorized = await fetch(testEndpoint, {
        method: 'GET',
        headers: { Authorization: token },
      })
      const json = await responseAuthorized.json()
      expect(json).to.deep.equal({ method: 'GET', path: '/foo' })
    })

    it('should expose a GET HTTP endpoint backed by simple custom request authorization', async () => {
      const testEndpoint = `${endpoint}/behind-simple-authorizer`

      const responseUnauthorized = await fetch(testEndpoint, {
        method: 'GET',
      })
      expect(responseUnauthorized.status).to.equal(403)

      const responseAuthorized = await fetch(testEndpoint, {
        method: 'GET',
        headers: { Authorization: 'secretToken' },
      })
      const json = await responseAuthorized.json()
      expect(json).to.deep.equal({
        method: 'GET',
        path: '/behind-simple-authorizer',
      })
    })

    it('should expose a GET HTTP endpoint backed by standard custom request authorization', async () => {
      const testEndpoint = `${endpoint}/behind-standard-authorizer`

      const responseUnauthorized = await fetch(testEndpoint, {
        method: 'GET',
      })
      expect(responseUnauthorized.status).to.equal(403)

      const responseAuthorized = await fetch(testEndpoint, {
        method: 'GET',
        headers: { Authorization: 'secretToken' },
      })
      const json = await responseAuthorized.json()
      expect(json).to.deep.equal({
        method: 'GET',
        path: '/behind-standard-authorizer',
      })
    })

    it('should expose access logs when configured to', () =>
      confirmCloudWatchLogs(`/aws/http-api/${stackName}`, async () => {
        const response = await fetch(`${endpoint}/some-post`, {
          method: 'POST',
        })
        await response.json()
      }).then((events) => {
        expect(events.length > 0).to.equal(true)
      }))
  })

  describe('Catch-all endpoints', () => {
    before(async () => {
      const serviceData = await fixtures.setup('http-api-catch-all')
      ;({ servicePath: serviceDir } = serviceData)
      const serviceName = serviceData.serviceConfig.service
      stackName = `${serviceName}-${stage}`
      await deployService(serviceDir)
      return resolveEndpoint()
    })

    after(async function () {
      if (this.test.parent.tests.some((test) => test.state === 'failed')) return
      log.notice('Removing service...')
      await removeService(serviceDir)
    })

    it('should catch all root endpoint', async () => {
      const testEndpoint = `${endpoint}`

      const response = await fetch(testEndpoint, { method: 'GET' })
      const json = await response.json()
      expect(json).to.deep.equal({ method: 'GET', path: '/' })
    })

    it('should catch all whatever endpoint', async () => {
      const testEndpoint = `${endpoint}/whatever`

      const response = await fetch(testEndpoint, { method: 'PATCH' })
      const json = await response.json()
      expect(json).to.deep.equal({ method: 'PATCH', path: '/whatever' })
    })

    it('should catch all methods on method catch all endpoint', async () => {
      const testEndpoint = `${endpoint}/foo`

      const response = await fetch(testEndpoint, { method: 'PATCH' })
      const json = await response.json()
      expect(json).to.deep.equal({ method: 'PATCH', path: '/foo' })
    })
  })

  describe('Shared API', () => {
    let exportServicePath
    let serviceName

    before(async () => {
      const exportServiceData = await fixtures.setup('http-api-export')
      ;({ servicePath: exportServicePath } = exportServiceData)
      const exportServiceName = exportServiceData.serviceConfig.service
      await deployService(exportServicePath)
      const httpApiId = (
        await awsRequest(CloudFormationService, 'describeStacks', {
          StackName: `${exportServiceName}-${stage}`,
        })
      ).Stacks[0].Outputs[0].OutputValue
      endpoint = (
        await awsRequest(ApiGatewayV2Service, 'getApi', { ApiId: httpApiId })
      ).ApiEndpoint

      const serviceData = await fixtures.setup('http-api', {
        configExt: {
          provider: { httpApi: { id: httpApiId } },
        },
      })
      ;({ servicePath: serviceDir } = serviceData)
      serviceName = serviceData.serviceConfig.service
      stackName = `${serviceName}-${stage}`
      await deployService(serviceDir)
    })

    after(async () => {
      if (serviceName) {
        await removeService(serviceDir)
      }
      await removeService(exportServicePath)
    })

    it('should expose an accessible POST HTTP endpoint', async () => {
      const testEndpoint = `${endpoint}/some-post`

      const response = await fetch(testEndpoint, { method: 'POST' })
      const json = await response.json()
      expect(json).to.deep.equal({ method: 'POST', path: '/some-post' })
    })

    it('should expose an accessible paramed GET HTTP endpoint', async () => {
      const testEndpoint = `${endpoint}/bar/whatever`

      const response = await fetch(testEndpoint, { method: 'GET' })
      const json = await response.json()
      expect(json).to.deep.equal({ method: 'GET', path: '/bar/whatever' })
    })

    it('should return 404 on not supported method', async () => {
      const testEndpoint = `${endpoint}/foo`

      const response = await fetch(testEndpoint, { method: 'POST' })
      expect(response.status).to.equal(404)
    })

    it('should return 404 on not configured path', async () => {
      const testEndpoint = `${endpoint}/not-configured`

      const response = await fetch(testEndpoint, { method: 'GET' })
      expect(response.status).to.equal(404)
    })
  })
})
