/*
 * 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.
 *
 */

// Allow `any` data type for testing runtime type checking.
// tslint:disable no-any
import * as assert from 'assert';
import * as resolverManager from '../src/resolver';
import * as resolver_dns from '../src/resolver-dns';
import * as resolver_uds from '../src/resolver-uds';
import * as resolver_ip from '../src/resolver-ip';
import { ServiceConfig } from '../src/service-config';
import { StatusObject } from '../src/call-interface';
import {
  Endpoint,
  SubchannelAddress,
  endpointToString,
  subchannelAddressEqual,
} from '../src/subchannel-address';
import { parseUri, GrpcUri } from '../src/uri-parser';
import { GRPC_NODE_USE_ALTERNATIVE_RESOLVER } from '../src/environment';

function hasMatchingAddress(
  endpointList: Endpoint[],
  expectedAddress: SubchannelAddress
): boolean {
  for (const endpoint of endpointList) {
    for (const address of endpoint.addresses) {
      if (subchannelAddressEqual(address, expectedAddress)) {
        return true;
      }
    }
  }
  return false;
}

describe('Name Resolver', () => {
  before(() => {
    resolver_dns.setup();
    resolver_uds.setup();
    resolver_ip.setup();
  });
  describe('DNS Names', function () {
    // For some reason DNS queries sometimes take a long time on Windows
    this.timeout(4000);
    it('Should resolve localhost properly', function (done) {
      if (GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
        this.skip();
      }
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('localhost:50051')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(
            hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 50051 })
          );
          assert(
            hasMatchingAddress(endpointList, { host: '::1', port: 50051 })
          );
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    it('Should default to port 443', function (done) {
      if (GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
        this.skip();
      }
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('localhost')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(
            hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 })
          );
          assert(hasMatchingAddress(endpointList, { host: '::1', port: 443 }));
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    it('Should correctly represent an ipv4 address', done => {
      const target = resolverManager.mapUriDefaultScheme(parseUri('1.2.3.4')!)!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(
            hasMatchingAddress(endpointList, { host: '1.2.3.4', port: 443 })
          );
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    it('Should correctly represent an ipv6 address', done => {
      const target = resolverManager.mapUriDefaultScheme(parseUri('::1')!)!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(hasMatchingAddress(endpointList, { host: '::1', port: 443 }));
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    it('Should correctly represent a bracketed ipv6 address', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('[::1]:50051')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(
            hasMatchingAddress(endpointList, { host: '::1', port: 50051 })
          );
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    it('Should resolve a public address', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('example.com')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(endpointList.length > 0);
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    // Created DNS TXT record using TXT sample from https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
    // "grpc_config=[{\"serviceConfig\":{\"loadBalancingPolicy\":\"round_robin\",\"methodConfig\":[{\"name\":[{\"service\":\"MyService\",\"method\":\"Foo\"}],\"waitForReady\":true}]}}]"
    it.skip('Should resolve a name with TXT service config', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('grpctest.kleinsch.com')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          if (serviceConfig !== null) {
            assert(
              serviceConfig.loadBalancingPolicy === 'round_robin',
              'Should have found round robin LB policy'
            );
            done();
          }
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    it.skip('Should not resolve TXT service config if we disabled service config', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('grpctest.kleinsch.com')!
      )!;
      let count = 0;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          assert(
            serviceConfig === null,
            'Should not have found service config'
          );
          count++;
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {
        'grpc.service_config_disable_resolution': 1,
      });
      resolver.updateResolution();
      setTimeout(() => {
        assert(count === 1, 'Should have only resolved once');
        done();
      }, 2_000);
    });
    /* The DNS entry for loopback4.unittest.grpc.io only has a single A record
     * with the address 127.0.0.1, but the Mac DNS resolver appears to use
     * NAT64 to create an IPv6 address in that case, so it instead returns
     * 64:ff9b::7f00:1. Handling that kind of translation is outside of the
     * scope of this test, so we are skipping it. The test primarily exists
     * as a regression test for https://github.com/grpc/grpc-node/issues/1044,
     * and the test 'Should resolve gRPC interop servers' tests the same thing.
     */
    it.skip('Should resolve a name with multiple dots', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('loopback4.unittest.grpc.io')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(
            hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 }),
            `None of [${endpointList.map(addr =>
              endpointToString(addr)
            )}] matched '127.0.0.1:443'`
          );
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    /* TODO(murgatroid99): re-enable this test, once we can get the IPv6 result
     * consistently */
    it.skip('Should resolve a DNS name to an IPv6 address', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('loopback6.unittest.grpc.io')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(hasMatchingAddress(endpointList, { host: '::1', port: 443 }));
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    /* This DNS name resolves to only the IPv4 address on Windows, and only the
     * IPv6 address on Mac. There is no result that we can consistently test
     * for here. */
    it.skip('Should resolve a DNS name to IPv4 and IPv6 addresses', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('loopback46.unittest.grpc.io')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(
            hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 }),
            `None of [${endpointList.map(addr =>
              endpointToString(addr)
            )}] matched '127.0.0.1:443'`
          );
          /* TODO(murgatroid99): check for IPv6 result, once we can get that
           * consistently */
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    it('Should resolve a name with a hyphen', done => {
      /* TODO(murgatroid99): Find or create a better domain name to test this with.
       * This is just the first one I found with a hyphen. */
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('network-tools.com')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(endpointList.length > 0);
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    /* This test also serves as a regression test for
     * https://github.com/grpc/grpc-node/issues/1044, specifically handling
     * hyphens and multiple periods in a DNS name. It should not be skipped
     * unless there is another test for the same issue. */
    it('Should resolve gRPC interop servers', done => {
      let completeCount = 0;
      const target1 = resolverManager.mapUriDefaultScheme(
        parseUri('grpc-test.sandbox.googleapis.com')!
      )!;
      const target2 = resolverManager.mapUriDefaultScheme(
        parseUri('grpc-test4.sandbox.googleapis.com')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          assert(endpointList.length > 0);
          completeCount += 1;
          if (completeCount === 2) {
            // Only handle the first resolution result
            listener.onSuccessfulResolution = () => {};
            done();
          }
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver1 = resolverManager.createResolver(target1, listener, {});
      resolver1.updateResolution();
      const resolver2 = resolverManager.createResolver(target2, listener, {});
      resolver2.updateResolution();
    });
    it('should not keep repeating successful resolutions', function (done) {
      if (GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
        this.skip();
      }
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('localhost')!
      )!;
      let resultCount = 0;
      const resolver = resolverManager.createResolver(
        target,
        {
          onSuccessfulResolution: (
            endpointList: Endpoint[],
            serviceConfig: ServiceConfig | null,
            serviceConfigError: StatusObject | null
          ) => {
            assert(
              hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 })
            );
            assert(
              hasMatchingAddress(endpointList, { host: '::1', port: 443 })
            );
            resultCount += 1;
            if (resultCount === 1) {
              process.nextTick(() => resolver.updateResolution());
            }
          },
          onError: (error: StatusObject) => {
            assert.ifError(error);
          },
        },
        { 'grpc.dns_min_time_between_resolutions_ms': 2000 }
      );
      resolver.updateResolution();
      setTimeout(() => {
        assert.strictEqual(resultCount, 2, `resultCount ${resultCount} !== 2`);
        done();
      }, 10_000);
    }).timeout(15_000);
    it('should not keep repeating failed resolutions', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('host.invalid')!
      )!;
      let resultCount = 0;
      const resolver = resolverManager.createResolver(
        target,
        {
          onSuccessfulResolution: (
            endpointList: Endpoint[],
            serviceConfig: ServiceConfig | null,
            serviceConfigError: StatusObject | null
          ) => {
            assert.fail('Resolution succeeded unexpectedly');
          },
          onError: (error: StatusObject) => {
            resultCount += 1;
            if (resultCount === 1) {
              process.nextTick(() => resolver.updateResolution());
            }
          },
        },
        {}
      );
      resolver.updateResolution();
      setTimeout(() => {
        assert.strictEqual(resultCount, 2, `resultCount ${resultCount} !== 2`);
        done();
      }, 10_000);
    }).timeout(15_000);
  });
  describe('UDS Names', () => {
    it('Should handle a relative Unix Domain Socket name', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('unix:socket')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(hasMatchingAddress(endpointList, { path: 'socket' }));
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    it('Should handle an absolute Unix Domain Socket name', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('unix:///tmp/socket')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(hasMatchingAddress(endpointList, { path: '/tmp/socket' }));
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
  });
  describe('IP Addresses', () => {
    it('should handle one IPv4 address with no port', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('ipv4:127.0.0.1')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(
            hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 })
          );
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    it('should handle one IPv4 address with a port', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('ipv4:127.0.0.1:50051')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(
            hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 50051 })
          );
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    it('should handle multiple IPv4 addresses with different ports', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('ipv4:127.0.0.1:50051,127.0.0.1:50052')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(
            hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 50051 })
          );
          assert(
            hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 50052 })
          );
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    it('should handle one IPv6 address with no port', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('ipv6:::1')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(hasMatchingAddress(endpointList, { host: '::1', port: 443 }));
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    it('should handle one IPv6 address with a port', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('ipv6:[::1]:50051')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(
            hasMatchingAddress(endpointList, { host: '::1', port: 50051 })
          );
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
    it('should handle multiple IPv6 addresses with different ports', done => {
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('ipv6:[::1]:50051,[::1]:50052')!
      )!;
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          endpointList: Endpoint[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          // Only handle the first resolution result
          listener.onSuccessfulResolution = () => {};
          assert(
            hasMatchingAddress(endpointList, { host: '::1', port: 50051 })
          );
          assert(
            hasMatchingAddress(endpointList, { host: '::1', port: 50052 })
          );
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener, {});
      resolver.updateResolution();
    });
  });
  describe('getDefaultAuthority', () => {
    class OtherResolver implements resolverManager.Resolver {
      updateResolution() {
        return [];
      }

      destroy() {}

      static getDefaultAuthority(target: GrpcUri): string {
        return 'other';
      }
    }

    it('Should return the correct authority if a different resolver has been registered', () => {
      resolverManager.registerResolver('other', OtherResolver);
      const target = resolverManager.mapUriDefaultScheme(
        parseUri('other:name')!
      )!;
      console.log(target);

      const authority = resolverManager.getDefaultAuthority(target);
      assert.equal(authority, 'other');
    });
  });
});
