/*
 * Copyright 2019 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

import * as assert from 'assert';

import {
  CallCredentials,
  CallMetadataGenerator,
} from '../src/call-credentials';
import { Metadata } from '../src/metadata';

// Metadata generators

function makeAfterMsElapsedGenerator(ms: number): CallMetadataGenerator {
  return (options, cb) => {
    const metadata = new Metadata();
    metadata.add('msElapsed', `${ms}`);
    setTimeout(() => cb(null, metadata), ms);
  };
}

const generateFromServiceURL: CallMetadataGenerator = (options, cb) => {
  const metadata: Metadata = new Metadata();
  metadata.add('service_url', options.service_url);
  cb(null, metadata);
};
const generateWithError: CallMetadataGenerator = (options, cb) =>
  cb(new Error());

// Tests

describe('CallCredentials', () => {
  describe('createFromMetadataGenerator', () => {
    it('should accept a metadata generator', () => {
      assert.doesNotThrow(() =>
        CallCredentials.createFromMetadataGenerator(generateFromServiceURL)
      );
    });
  });

  describe('compose', () => {
    it('should accept a CallCredentials object and return a new object', () => {
      const callCredentials1 = CallCredentials.createFromMetadataGenerator(
        generateFromServiceURL
      );
      const callCredentials2 = CallCredentials.createFromMetadataGenerator(
        generateFromServiceURL
      );
      const combinedCredentials = callCredentials1.compose(callCredentials2);
      assert.notStrictEqual(combinedCredentials, callCredentials1);
      assert.notStrictEqual(combinedCredentials, callCredentials2);
    });

    it('should be chainable', () => {
      const callCredentials1 = CallCredentials.createFromMetadataGenerator(
        generateFromServiceURL
      );
      const callCredentials2 = CallCredentials.createFromMetadataGenerator(
        generateFromServiceURL
      );
      assert.doesNotThrow(() => {
        callCredentials1
          .compose(callCredentials2)
          .compose(callCredentials2)
          .compose(callCredentials2);
      });
    });
  });

  describe('generateMetadata', () => {
    it('should call the function passed to createFromMetadataGenerator', async () => {
      const callCredentials = CallCredentials.createFromMetadataGenerator(
        generateFromServiceURL
      );
      let metadata: Metadata;
      try {
        metadata = await callCredentials.generateMetadata({
          service_url: 'foo',
        });
      } catch (err) {
        throw err;
      }
      assert.deepStrictEqual(metadata.get('service_url'), ['foo']);
    });

    it('should emit an error if the associated metadataGenerator does', async () => {
      const callCredentials = CallCredentials.createFromMetadataGenerator(
        generateWithError
      );
      let metadata: Metadata | null = null;
      try {
        metadata = await callCredentials.generateMetadata({ service_url: '' });
      } catch (err) {
        assert.ok(err instanceof Error);
      }
      assert.strictEqual(metadata, null);
    });

    it('should combine metadata from multiple generators', async () => {
      const [callCreds1, callCreds2, callCreds3, callCreds4] = [
        50,
        100,
        150,
        200,
      ].map(ms => {
        const generator: CallMetadataGenerator = makeAfterMsElapsedGenerator(
          ms
        );
        return CallCredentials.createFromMetadataGenerator(generator);
      });
      const testCases = [
        {
          credentials: callCreds1
            .compose(callCreds2)
            .compose(callCreds3)
            .compose(callCreds4),
          expected: ['50', '100', '150', '200'],
        },
        {
          credentials: callCreds4.compose(
            callCreds3.compose(callCreds2.compose(callCreds1))
          ),
          expected: ['200', '150', '100', '50'],
        },
        {
          credentials: callCreds3.compose(
            callCreds4.compose(callCreds1).compose(callCreds2)
          ),
          expected: ['150', '200', '50', '100'],
        },
      ];
      // Try each test case and make sure the msElapsed field is as expected
      await Promise.all(
        testCases.map(async testCase => {
          const { credentials, expected } = testCase;
          let metadata: Metadata;
          try {
            metadata = await credentials.generateMetadata({ service_url: '' });
          } catch (err) {
            throw err;
          }
          assert.deepStrictEqual(metadata.get('msElapsed'), expected);
        })
      );
    });
  });
});
