/*
 * Copyright 2015 The 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.
 */

package io.grpc.internal;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.net.InetAddresses;
import com.google.common.testing.FakeTicker;
import io.grpc.Attributes;
import io.grpc.ChannelLogger;
import io.grpc.EquivalentAddressGroup;
import io.grpc.HttpConnectProxiedSocketAddress;
import io.grpc.NameResolver;
import io.grpc.NameResolver.ConfigOrError;
import io.grpc.NameResolver.ResolutionResult;
import io.grpc.NameResolver.ServiceConfigParser;
import io.grpc.ProxyDetector;
import io.grpc.StaticTestingClassLoader;
import io.grpc.Status;
import io.grpc.Status.Code;
import io.grpc.SynchronizationContext;
import io.grpc.internal.DnsNameResolver.AddressResolver;
import io.grpc.internal.DnsNameResolver.ResourceResolver;
import io.grpc.internal.DnsNameResolver.ResourceResolverFactory;
import io.grpc.internal.JndiResourceResolverFactory.JndiResourceResolver;
import io.grpc.internal.JndiResourceResolverFactory.RecordFetcher;
import io.grpc.internal.SharedResourceHolder.Resource;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.DisableOnDebug;
import org.junit.rules.ExpectedException;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

/** Unit tests for {@link DnsNameResolver}. */
@RunWith(JUnit4.class)
public class DnsNameResolverTest {

  @Rule public final TestRule globalTimeout = new DisableOnDebug(Timeout.seconds(10));
  @Rule public final MockitoRule mocks = MockitoJUnit.rule();
  @Rule public final ExpectedException thrown = ExpectedException.none();

  private final Map<String, ?> serviceConfig = new LinkedHashMap<>();

  private static final int DEFAULT_PORT = 887;
  private final SynchronizationContext syncContext = new SynchronizationContext(
      new Thread.UncaughtExceptionHandler() {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
          throw new AssertionError(e);
        }
      });
  private final NameResolver.Args args = NameResolver.Args.newBuilder()
      .setDefaultPort(DEFAULT_PORT)
      .setProxyDetector(GrpcUtil.DEFAULT_PROXY_DETECTOR)
      .setSynchronizationContext(syncContext)
      .setServiceConfigParser(mock(ServiceConfigParser.class))
      .setChannelLogger(mock(ChannelLogger.class))
      .build();

  private final DnsNameResolverProvider provider = new DnsNameResolverProvider();
  private final FakeClock fakeClock = new FakeClock();
  private final FakeClock fakeExecutor = new FakeClock();

  private final FakeExecutorResource fakeExecutorResource = new FakeExecutorResource();

  private final class FakeExecutorResource implements Resource<Executor> {
    private final AtomicInteger createCount = new AtomicInteger();

    @Override
    public Executor create() {
      createCount.incrementAndGet();
      return fakeExecutor.getScheduledExecutorService();
    }

    @Override
    public void close(Executor instance) {}
  }

  @Mock
  private NameResolver.Listener2 mockListener;
  @Captor
  private ArgumentCaptor<ResolutionResult> resultCaptor;
  @Captor
  private ArgumentCaptor<Status> errorCaptor;
  @Nullable
  private String networkaddressCacheTtlPropertyValue;
  @Mock
  private RecordFetcher recordFetcher;

  private DnsNameResolver newResolver(String name, int defaultPort) {
    return newResolver(
        name, defaultPort, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted());
  }

  private DnsNameResolver newResolver(String name, int defaultPort, boolean isAndroid) {
    return newResolver(
        name, defaultPort, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(),
        isAndroid);
  }

  private DnsNameResolver newResolver(
      String name,
      int defaultPort,
      ProxyDetector proxyDetector,
      Stopwatch stopwatch) {
    return newResolver(name, defaultPort, proxyDetector, stopwatch, false);
  }

  private DnsNameResolver newResolver(
      String name,
      final int defaultPort,
      final ProxyDetector proxyDetector,
      Stopwatch stopwatch,
      boolean isAndroid) {
    NameResolver.Args args =
        NameResolver.Args.newBuilder()
            .setDefaultPort(defaultPort)
            .setProxyDetector(proxyDetector)
            .setSynchronizationContext(syncContext)
            .setServiceConfigParser(mock(ServiceConfigParser.class))
            .setChannelLogger(mock(ChannelLogger.class))
            .build();
    return newResolver(name, stopwatch, isAndroid, args);
  }

  private DnsNameResolver newResolver(
      String name,
      Stopwatch stopwatch,
      boolean isAndroid,
      NameResolver.Args args) {
    DnsNameResolver dnsResolver =
        new DnsNameResolver(
            null, name, args, fakeExecutorResource, stopwatch, isAndroid);
    // By default, using the mocked ResourceResolver to avoid I/O
    dnsResolver.setResourceResolver(new JndiResourceResolver(recordFetcher));
    return dnsResolver;
  }

  @Before
  public void setUp() {
    DnsNameResolver.enableJndi = true;
    networkaddressCacheTtlPropertyValue =
        System.getProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY);
  }

  @After
  public void restoreSystemProperty() {
    if (networkaddressCacheTtlPropertyValue == null) {
      System.clearProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY);
    } else {
      System.setProperty(
          DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY,
          networkaddressCacheTtlPropertyValue);
    }
  }

  @After
  public void noMorePendingTasks() {
    assertEquals(0, fakeClock.numPendingTasks());
    assertEquals(0, fakeExecutor.numPendingTasks());
  }

  @Test
  public void invalidDnsName() throws Exception {
    testInvalidUri(new URI("dns", null, "/[invalid]", null));
  }

  @Test
  public void validIpv6() throws Exception {
    testValidUri(new URI("dns", null, "/[::1]", null), "[::1]", DEFAULT_PORT);
  }

  @Test
  public void validDnsNameWithoutPort() throws Exception {
    testValidUri(new URI("dns", null, "/foo.googleapis.com", null),
        "foo.googleapis.com", DEFAULT_PORT);
  }

  @Test
  public void validDnsNameWithPort() throws Exception {
    testValidUri(new URI("dns", null, "/foo.googleapis.com:456", null),
        "foo.googleapis.com:456", 456);
  }

  @Test
  public void nullDnsName() {
    try {
      newResolver(null, DEFAULT_PORT);
      fail("Expected NullPointerException");
    } catch (NullPointerException e) {
      // expected
    }
  }

  @Test
  public void invalidDnsName_containsUnderscore() {
    try {
      newResolver("host_1", DEFAULT_PORT);
      fail("Expected IllegalArgumentException");
    } catch (IllegalArgumentException e) {
      // expected
    }
  }

  @Test
  public void resolve_androidIgnoresPropertyValue() throws Exception {
    System.setProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, Long.toString(2));
    resolveNeverCache(true);
  }

  @Test
  public void resolve_androidIgnoresPropertyValueCacheForever() throws Exception {
    System.setProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, Long.toString(-1));
    resolveNeverCache(true);
  }

  @Test
  public void resolve_neverCache() throws Exception {
    System.setProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, "0");
    resolveNeverCache(false);
  }

  private void resolveNeverCache(boolean isAndroid) throws Exception {
    final List<InetAddress> answer1 = createAddressList(2);
    final List<InetAddress> answer2 = createAddressList(1);
    String name = "foo.googleapis.com";

    DnsNameResolver resolver = newResolver(name, 81, isAndroid);
    AddressResolver mockResolver = mock(AddressResolver.class);
    when(mockResolver.resolveAddress(anyString())).thenReturn(answer1).thenReturn(answer2);
    resolver.setAddressResolver(mockResolver);

    resolver.start(mockListener);
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener).onResult(resultCaptor.capture());
    assertAnswerMatches(answer1, 81, resultCaptor.getValue());
    assertEquals(0, fakeClock.numPendingTasks());

    resolver.refresh();
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener, times(2)).onResult(resultCaptor.capture());
    assertAnswerMatches(answer2, 81, resultCaptor.getValue());
    assertEquals(0, fakeClock.numPendingTasks());

    resolver.shutdown();

    verify(mockResolver, times(2)).resolveAddress(anyString());
  }

  @Test
  public void testExecutor_default() throws Exception {
    final List<InetAddress> answer = createAddressList(2);

    DnsNameResolver resolver = newResolver("foo.googleapis.com", 81);
    AddressResolver mockResolver = mock(AddressResolver.class);
    when(mockResolver.resolveAddress(anyString())).thenReturn(answer);
    resolver.setAddressResolver(mockResolver);

    resolver.start(mockListener);
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener).onResult(resultCaptor.capture());
    assertAnswerMatches(answer, 81, resultCaptor.getValue());
    assertEquals(0, fakeClock.numPendingTasks());

    resolver.shutdown();

    assertThat(fakeExecutorResource.createCount.get()).isEqualTo(1);
  }

  @Test
  public void testExecutor_custom() throws Exception {
    final List<InetAddress> answer = createAddressList(2);
    final AtomicInteger executions = new AtomicInteger();

    NameResolver.Args args =
        NameResolver.Args.newBuilder()
            .setDefaultPort(81)
            .setProxyDetector(GrpcUtil.NOOP_PROXY_DETECTOR)
            .setSynchronizationContext(syncContext)
            .setServiceConfigParser(mock(ServiceConfigParser.class))
            .setChannelLogger(mock(ChannelLogger.class))
            .setOffloadExecutor(
                new Executor() {
                  @Override
                  public void execute(Runnable command) {
                    executions.incrementAndGet();
                    command.run();
                  }
                })
            .build();

    DnsNameResolver resolver =
        newResolver("foo.googleapis.com", Stopwatch.createUnstarted(), false, args);
    AddressResolver mockResolver = mock(AddressResolver.class);
    when(mockResolver.resolveAddress(anyString())).thenReturn(answer);
    resolver.setAddressResolver(mockResolver);

    resolver.start(mockListener);
    assertEquals(0, fakeExecutor.runDueTasks());
    verify(mockListener).onResult(resultCaptor.capture());
    assertAnswerMatches(answer, 81, resultCaptor.getValue());
    assertEquals(0, fakeClock.numPendingTasks());

    resolver.shutdown();

    assertThat(fakeExecutorResource.createCount.get()).isEqualTo(0);
    assertThat(executions.get()).isEqualTo(1);
  }

  @Test
  public void resolve_cacheForever() throws Exception {
    System.setProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, "-1");
    final List<InetAddress> answer1 = createAddressList(2);
    String name = "foo.googleapis.com";
    FakeTicker fakeTicker = new FakeTicker();

    DnsNameResolver resolver =
        newResolver(name, 81, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(fakeTicker));
    AddressResolver mockResolver = mock(AddressResolver.class);
    when(mockResolver.resolveAddress(anyString()))
        .thenReturn(answer1)
        .thenThrow(new AssertionError("should not called twice"));
    resolver.setAddressResolver(mockResolver);

    resolver.start(mockListener);
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener).onResult(resultCaptor.capture());
    assertAnswerMatches(answer1, 81, resultCaptor.getValue());
    assertEquals(0, fakeClock.numPendingTasks());

    fakeTicker.advance(1, TimeUnit.DAYS);
    resolver.refresh();
    assertEquals(0, fakeExecutor.runDueTasks());
    assertEquals(0, fakeClock.numPendingTasks());
    verifyNoMoreInteractions(mockListener);

    resolver.shutdown();

    verify(mockResolver).resolveAddress(anyString());
  }

  @Test
  public void resolve_usingCache() throws Exception {
    long ttl = 60;
    System.setProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, Long.toString(ttl));
    final List<InetAddress> answer = createAddressList(2);
    String name = "foo.googleapis.com";
    FakeTicker fakeTicker = new FakeTicker();

    DnsNameResolver resolver =
        newResolver(name, 81, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(fakeTicker));
    AddressResolver mockResolver = mock(AddressResolver.class);
    when(mockResolver.resolveAddress(anyString()))
        .thenReturn(answer)
        .thenThrow(new AssertionError("should not reach here."));
    resolver.setAddressResolver(mockResolver);

    resolver.start(mockListener);
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener).onResult(resultCaptor.capture());
    assertAnswerMatches(answer, 81, resultCaptor.getValue());
    assertEquals(0, fakeClock.numPendingTasks());

    // this refresh should return cached result
    fakeTicker.advance(ttl - 1, TimeUnit.SECONDS);
    resolver.refresh();
    assertEquals(0, fakeExecutor.runDueTasks());
    assertEquals(0, fakeClock.numPendingTasks());
    verifyNoMoreInteractions(mockListener);

    resolver.shutdown();

    verify(mockResolver).resolveAddress(anyString());
  }

  @Test
  public void resolve_cacheExpired() throws Exception {
    long ttl = 60;
    System.setProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, Long.toString(ttl));
    final List<InetAddress> answer1 = createAddressList(2);
    final List<InetAddress> answer2 = createAddressList(1);
    String name = "foo.googleapis.com";
    FakeTicker fakeTicker = new FakeTicker();

    DnsNameResolver resolver =
        newResolver(name, 81, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(fakeTicker));
    AddressResolver mockResolver = mock(AddressResolver.class);
    when(mockResolver.resolveAddress(anyString())).thenReturn(answer1)
        .thenReturn(answer2);
    resolver.setAddressResolver(mockResolver);

    resolver.start(mockListener);
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener).onResult(resultCaptor.capture());
    assertAnswerMatches(answer1, 81, resultCaptor.getValue());
    assertEquals(0, fakeClock.numPendingTasks());

    fakeTicker.advance(ttl + 1, TimeUnit.SECONDS);
    resolver.refresh();
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener, times(2)).onResult(resultCaptor.capture());
    assertAnswerMatches(answer2, 81, resultCaptor.getValue());
    assertEquals(0, fakeClock.numPendingTasks());

    resolver.shutdown();

    verify(mockResolver, times(2)).resolveAddress(anyString());
  }

  @Test
  public void resolve_invalidTtlPropertyValue() throws Exception {
    System.setProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, "not_a_number");
    resolveDefaultValue();
  }

  @Test
  public void resolve_noPropertyValue() throws Exception {
    System.clearProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY);
    resolveDefaultValue();
  }

  private void resolveDefaultValue() throws Exception {
    final List<InetAddress> answer1 = createAddressList(2);
    final List<InetAddress> answer2 = createAddressList(1);
    String name = "foo.googleapis.com";
    FakeTicker fakeTicker = new FakeTicker();

    DnsNameResolver resolver =
        newResolver(name, 81, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(fakeTicker));
    AddressResolver mockResolver = mock(AddressResolver.class);
    when(mockResolver.resolveAddress(anyString())).thenReturn(answer1).thenReturn(answer2);
    resolver.setAddressResolver(mockResolver);

    resolver.start(mockListener);
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener).onResult(resultCaptor.capture());
    assertAnswerMatches(answer1, 81, resultCaptor.getValue());
    assertEquals(0, fakeClock.numPendingTasks());

    fakeTicker.advance(DnsNameResolver.DEFAULT_NETWORK_CACHE_TTL_SECONDS, TimeUnit.SECONDS);
    resolver.refresh();
    assertEquals(0, fakeExecutor.runDueTasks());
    assertEquals(0, fakeClock.numPendingTasks());
    verifyNoMoreInteractions(mockListener);

    fakeTicker.advance(1, TimeUnit.SECONDS);
    resolver.refresh();
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener, times(2)).onResult(resultCaptor.capture());
    assertAnswerMatches(answer2, 81, resultCaptor.getValue());
    assertEquals(0, fakeClock.numPendingTasks());

    resolver.shutdown();

    verify(mockResolver, times(2)).resolveAddress(anyString());
  }

  @Test
  public void resolve_emptyResult() throws Exception {
    DnsNameResolver.enableTxt = true;
    DnsNameResolver nr = newResolver("dns:///addr.fake:1234", 443);
    nr.setAddressResolver(new AddressResolver() {
      @Override
      public List<InetAddress> resolveAddress(String host) throws Exception {
        return Collections.emptyList();
      }
    });
    ResourceResolver mockResourceResolver = mock(ResourceResolver.class);
    when(mockResourceResolver.resolveTxt(anyString()))
        .thenReturn(Collections.<String>emptyList());

    nr.setResourceResolver(mockResourceResolver);

    nr.start(mockListener);
    assertThat(fakeExecutor.runDueTasks()).isEqualTo(1);

    ArgumentCaptor<ResolutionResult> ac = ArgumentCaptor.forClass(ResolutionResult.class);
    verify(mockListener).onResult(ac.capture());
    verifyNoMoreInteractions(mockListener);
    assertThat(ac.getValue().getAddresses()).isEmpty();
    assertThat(ac.getValue().getAttributes()).isEqualTo(Attributes.EMPTY);
    assertThat(ac.getValue().getServiceConfig()).isNull();
    verify(mockResourceResolver, never()).resolveSrv(anyString());
  }

  @Test
  public void resolve_nullResourceResolver() throws Exception {
    DnsNameResolver.enableTxt = true;
    InetAddress backendAddr = InetAddresses.fromInteger(0x7f000001);
    AddressResolver mockAddressResolver = mock(AddressResolver.class);
    when(mockAddressResolver.resolveAddress(anyString()))
        .thenReturn(Collections.singletonList(backendAddr));
    String name = "foo.googleapis.com";

    DnsNameResolver resolver = newResolver(name, 81);
    resolver.setAddressResolver(mockAddressResolver);
    resolver.setResourceResolver(null);
    resolver.start(mockListener);
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener).onResult(resultCaptor.capture());
    ResolutionResult result = resultCaptor.getValue();
    InetSocketAddress resolvedBackendAddr =
        (InetSocketAddress) Iterables.getOnlyElement(
            Iterables.getOnlyElement(result.getAddresses()).getAddresses());
    assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr);
    verify(mockAddressResolver).resolveAddress(name);
    assertThat(result.getAttributes()).isEqualTo(Attributes.EMPTY);
    assertThat(result.getServiceConfig()).isNull();
  }

  @Test
  public void resolve_nullResourceResolver_addressFailure() throws Exception {
    DnsNameResolver.enableTxt = true;
    AddressResolver mockAddressResolver = mock(AddressResolver.class);
    when(mockAddressResolver.resolveAddress(anyString()))
        .thenThrow(new IOException("no addr"));
    String name = "foo.googleapis.com";

    DnsNameResolver resolver = newResolver(name, 81);
    resolver.setAddressResolver(mockAddressResolver);
    resolver.setResourceResolver(null);
    resolver.start(mockListener);
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener).onError(errorCaptor.capture());
    Status errorStatus = errorCaptor.getValue();
    assertThat(errorStatus.getCode()).isEqualTo(Code.UNAVAILABLE);
    assertThat(errorStatus.getCause()).hasMessageThat().contains("no addr");
  }

  @Test
  public void resolve_presentResourceResolver() throws Exception {
    DnsNameResolver.enableTxt = true;
    InetAddress backendAddr = InetAddresses.fromInteger(0x7f000001);
    AddressResolver mockAddressResolver = mock(AddressResolver.class);
    when(mockAddressResolver.resolveAddress(anyString()))
        .thenReturn(Collections.singletonList(backendAddr));
    ResourceResolver mockResourceResolver = mock(ResourceResolver.class);
    when(mockResourceResolver.resolveTxt(anyString()))
        .thenReturn(
            Collections.singletonList(
                "grpc_config=[{\"clientLanguage\": [\"java\"], \"serviceConfig\": {}}]"));
    ServiceConfigParser serviceConfigParser = new ServiceConfigParser() {
      @Override
      public ConfigOrError parseServiceConfig(Map<String, ?> rawServiceConfig) {
        return ConfigOrError.fromConfig(rawServiceConfig);
      }
    };
    NameResolver.Args args =
        NameResolver.Args.newBuilder()
            .setDefaultPort(DEFAULT_PORT)
            .setProxyDetector(GrpcUtil.NOOP_PROXY_DETECTOR)
            .setSynchronizationContext(syncContext)
            .setServiceConfigParser(serviceConfigParser)
            .build();

    String name = "foo.googleapis.com";
    DnsNameResolver resolver = newResolver(name, Stopwatch.createUnstarted(), false, args);
    resolver.setAddressResolver(mockAddressResolver);
    resolver.setResourceResolver(mockResourceResolver);

    resolver.start(mockListener);
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener).onResult(resultCaptor.capture());
    ResolutionResult result = resultCaptor.getValue();
    InetSocketAddress resolvedBackendAddr =
        (InetSocketAddress) Iterables.getOnlyElement(
            Iterables.getOnlyElement(result.getAddresses()).getAddresses());
    assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr);
    assertThat(result.getServiceConfig().getConfig()).isNotNull();
    verify(mockAddressResolver).resolveAddress(name);
    verify(mockResourceResolver).resolveTxt("_grpc_config." + name);
  }

  @Test
  public void resolve_addressFailure_neverLookUpServiceConfig() throws Exception {
    DnsNameResolver.enableTxt = true;
    AddressResolver mockAddressResolver = mock(AddressResolver.class);
    when(mockAddressResolver.resolveAddress(anyString()))
        .thenThrow(new IOException("no addr"));
    String name = "foo.googleapis.com";

    ResourceResolver mockResourceResolver = mock(ResourceResolver.class);
    DnsNameResolver resolver = newResolver(name, 81);
    resolver.setAddressResolver(mockAddressResolver);
    resolver.setResourceResolver(mockResourceResolver);
    resolver.start(mockListener);
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener).onError(errorCaptor.capture());
    Status errorStatus = errorCaptor.getValue();
    assertThat(errorStatus.getCode()).isEqualTo(Code.UNAVAILABLE);
    assertThat(errorStatus.getCause()).hasMessageThat().contains("no addr");
    verify(mockResourceResolver, never()).resolveTxt(anyString());
  }

  @Test
  public void resolve_serviceConfigLookupFails_nullServiceConfig() throws Exception {
    DnsNameResolver.enableTxt = true;
    InetAddress backendAddr = InetAddresses.fromInteger(0x7f000001);
    AddressResolver mockAddressResolver = mock(AddressResolver.class);
    when(mockAddressResolver.resolveAddress(anyString()))
        .thenReturn(Collections.singletonList(backendAddr));
    String name = "foo.googleapis.com";
    ResourceResolver mockResourceResolver = mock(ResourceResolver.class);
    when(mockResourceResolver.resolveTxt(anyString()))
        .thenThrow(new Exception("something like javax.naming.NamingException"));

    DnsNameResolver resolver = newResolver(name, 81);
    resolver.setAddressResolver(mockAddressResolver);
    resolver.setResourceResolver(mockResourceResolver);
    resolver.start(mockListener);
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener).onResult(resultCaptor.capture());
    ResolutionResult result = resultCaptor.getValue();
    InetSocketAddress resolvedBackendAddr =
        (InetSocketAddress) Iterables.getOnlyElement(
            Iterables.getOnlyElement(result.getAddresses()).getAddresses());
    assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr);
    verify(mockAddressResolver).resolveAddress(name);
    assertThat(result.getAttributes()).isEqualTo(Attributes.EMPTY);
    assertThat(result.getServiceConfig()).isNull();
    verify(mockResourceResolver).resolveTxt(anyString());
  }

  @Test
  public void resolve_serviceConfigMalformed_serviceConfigError() throws Exception {
    DnsNameResolver.enableTxt = true;
    InetAddress backendAddr = InetAddresses.fromInteger(0x7f000001);
    AddressResolver mockAddressResolver = mock(AddressResolver.class);
    when(mockAddressResolver.resolveAddress(anyString()))
        .thenReturn(Collections.singletonList(backendAddr));
    String name = "foo.googleapis.com";
    ResourceResolver mockResourceResolver = mock(ResourceResolver.class);
    when(mockResourceResolver.resolveTxt(anyString()))
        .thenReturn(Collections.singletonList("grpc_config=something invalid"));

    DnsNameResolver resolver = newResolver(name, 81);
    resolver.setAddressResolver(mockAddressResolver);
    resolver.setResourceResolver(mockResourceResolver);
    resolver.start(mockListener);
    assertEquals(1, fakeExecutor.runDueTasks());
    verify(mockListener).onResult(resultCaptor.capture());
    ResolutionResult result = resultCaptor.getValue();
    InetSocketAddress resolvedBackendAddr =
        (InetSocketAddress) Iterables.getOnlyElement(
            Iterables.getOnlyElement(result.getAddresses()).getAddresses());
    assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr);
    verify(mockAddressResolver).resolveAddress(name);
    assertThat(result.getAttributes()).isEqualTo(Attributes.EMPTY);
    assertThat(result.getServiceConfig()).isNotNull();
    assertThat(result.getServiceConfig().getError()).isNotNull();
    verify(mockResourceResolver).resolveTxt(anyString());
  }

  @Test
  public void skipMissingJndiResolverResolver() throws Exception {
    ClassLoader cl = new ClassLoader() {
      @Override
      protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        if ("io.grpc.internal.JndiResourceResolverFactory".equals(name)) {
          throw new ClassNotFoundException();
        }
        return super.loadClass(name, resolve);
      }
    };

    ResourceResolverFactory factory = DnsNameResolver.getResourceResolverFactory(cl);

    assertThat(factory).isNull();
  }

  @Test
  public void skipWrongJndiResolverResolver() throws Exception {
    ClassLoader cl = new StaticTestingClassLoader(
        DnsNameResolverTest.class.getClassLoader(),
        Pattern.compile("io\\.grpc\\..+"));

    ResourceResolverFactory factory = DnsNameResolver.getResourceResolverFactory(cl);

    assertThat(factory).isNull();
  }

  @Test
  public void doNotResolveWhenProxyDetected() throws Exception {
    final String name = "foo.googleapis.com";
    final int port = 81;
    final InetSocketAddress proxyAddress =
        new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 1000);
    ProxyDetector alwaysDetectProxy = new ProxyDetector() {
        @Override
        public HttpConnectProxiedSocketAddress proxyFor(SocketAddress targetAddress) {
          return HttpConnectProxiedSocketAddress.newBuilder()
              .setTargetAddress((InetSocketAddress) targetAddress)
              .setProxyAddress(proxyAddress)
              .setUsername("username")
              .setPassword("password").build();
        }
      };
    DnsNameResolver resolver =
        newResolver(name, port, alwaysDetectProxy, Stopwatch.createUnstarted());
    AddressResolver mockAddressResolver = mock(AddressResolver.class);
    when(mockAddressResolver.resolveAddress(anyString())).thenThrow(new AssertionError());
    resolver.setAddressResolver(mockAddressResolver);
    resolver.start(mockListener);
    assertEquals(1, fakeExecutor.runDueTasks());

    verify(mockListener).onResult(resultCaptor.capture());
    List<EquivalentAddressGroup> result = resultCaptor.getValue().getAddresses();
    assertThat(result).hasSize(1);
    EquivalentAddressGroup eag = result.get(0);
    assertThat(eag.getAddresses()).hasSize(1);

    HttpConnectProxiedSocketAddress socketAddress =
        (HttpConnectProxiedSocketAddress) eag.getAddresses().get(0);
    assertSame(proxyAddress, socketAddress.getProxyAddress());
    assertEquals("username", socketAddress.getUsername());
    assertEquals("password", socketAddress.getPassword());
    assertTrue(socketAddress.getTargetAddress().isUnresolved());
  }

  @Test
  public void maybeChooseServiceConfig_failsOnMisspelling() {
    Map<String, Object> bad = new LinkedHashMap<>();
    bad.put("parcentage", 1.0);
    thrown.expectMessage("Bad key");

    DnsNameResolver.maybeChooseServiceConfig(bad, new Random(), "host");
  }

  @Test
  public void maybeChooseServiceConfig_clientLanguageMatchesJava() {
    Map<String, Object> choice = new LinkedHashMap<>();
    List<String> langs = new ArrayList<>();
    langs.add("java");
    choice.put("clientLanguage", langs);
    choice.put("serviceConfig", serviceConfig);

    assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
  }

  @Test
  public void maybeChooseServiceConfig_clientLanguageDoesntMatchGo() {
    Map<String, Object> choice = new LinkedHashMap<>();
    List<String> langs = new ArrayList<>();
    langs.add("go");
    choice.put("clientLanguage", langs);
    choice.put("serviceConfig", serviceConfig);

    assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
  }

  @Test
  public void maybeChooseServiceConfig_clientLanguageCaseInsensitive() {
    Map<String, Object> choice = new LinkedHashMap<>();
    List<String> langs = new ArrayList<>();
    langs.add("JAVA");
    choice.put("clientLanguage", langs);
    choice.put("serviceConfig", serviceConfig);

    assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
  }

  @Test
  public void maybeChooseServiceConfig_clientLanguageMatchesEmtpy() {
    Map<String, Object> choice = new LinkedHashMap<>();
    List<String> langs = new ArrayList<>();
    choice.put("clientLanguage", langs);
    choice.put("serviceConfig", serviceConfig);

    assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
  }

  @Test
  public void maybeChooseServiceConfig_clientLanguageMatchesMulti() {
    Map<String, Object> choice = new LinkedHashMap<>();
    List<String> langs = new ArrayList<>();
    langs.add("go");
    langs.add("java");
    choice.put("clientLanguage", langs);
    choice.put("serviceConfig", serviceConfig);

    assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
  }

  @Test
  public void maybeChooseServiceConfig_percentageZeroAlwaysFails() {
    Map<String, Object> choice = new LinkedHashMap<>();
    choice.put("percentage", 0D);
    choice.put("serviceConfig", serviceConfig);

    assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
  }

  @Test
  public void maybeChooseServiceConfig_percentageHundredAlwaysSucceeds() {
    Map<String, Object> choice = new LinkedHashMap<>();
    choice.put("percentage", 100D);
    choice.put("serviceConfig", serviceConfig);

    assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
  }

  @Test
  public void maybeChooseServiceConfig_percentageAboveMatches50() {
    Map<String, Object> choice = new LinkedHashMap<>();
    choice.put("percentage", 50D);
    choice.put("serviceConfig", serviceConfig);

    Random r = new Random() {
      @Override
      public int nextInt(int bound) {
        return 49;
      }
    };

    assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
  }

  @Test
  public void maybeChooseServiceConfig_percentageAtFails50() {
    Map<String, Object> choice = new LinkedHashMap<>();
    choice.put("percentage", 50D);
    choice.put("serviceConfig", serviceConfig);

    Random r = new Random() {
      @Override
      public int nextInt(int bound) {
        return 50;
      }
    };

    assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
  }

  @Test
  public void maybeChooseServiceConfig_percentageAboveMatches99() {
    Map<String, Object> choice = new LinkedHashMap<>();
    choice.put("percentage", 99D);
    choice.put("serviceConfig", serviceConfig);

    Random r = new Random() {
      @Override
      public int nextInt(int bound) {
        return 98;
      }
    };

    assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
  }

  @Test
  public void maybeChooseServiceConfig_percentageAtFails99() {
    Map<String, Object> choice = new LinkedHashMap<>();
    choice.put("percentage", 99D);
    choice.put("serviceConfig", serviceConfig);

    Random r = new Random() {
      @Override
      public int nextInt(int bound) {
        return 99;
      }
    };

    assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
  }

  @Test
  public void maybeChooseServiceConfig_percentageAboveMatches1() {
    Map<String, Object> choice = new LinkedHashMap<>();
    choice.put("percentage", 1D);
    choice.put("serviceConfig", serviceConfig);

    Random r = new Random() {
      @Override
      public int nextInt(int bound) {
        return 0;
      }
    };

    assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
  }

  @Test
  public void maybeChooseServiceConfig_percentageAtFails1() {
    Map<String, Object> choice = new LinkedHashMap<>();
    choice.put("percentage", 1D);
    choice.put("serviceConfig", serviceConfig);

    Random r = new Random() {
      @Override
      public int nextInt(int bound) {
        return 1;
      }
    };

    assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
  }

  @Test
  public void maybeChooseServiceConfig_hostnameMatches() {
    Map<String, Object> choice = new LinkedHashMap<>();
    List<String> hosts = new ArrayList<>();
    hosts.add("localhost");
    choice.put("clientHostname", hosts);
    choice.put("serviceConfig", serviceConfig);

    assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "localhost"));
  }

  @Test
  public void maybeChooseServiceConfig_hostnameDoesntMatch() {
    Map<String, Object> choice = new LinkedHashMap<>();
    List<String> hosts = new ArrayList<>();
    hosts.add("localhorse");
    choice.put("clientHostname", hosts);
    choice.put("serviceConfig", serviceConfig);

    assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "localhost"));
  }

  @Test
  public void maybeChooseServiceConfig_clientLanguageCaseSensitive() {
    Map<String, Object> choice = new LinkedHashMap<>();
    List<String> hosts = new ArrayList<>();
    hosts.add("LOCALHOST");
    choice.put("clientHostname", hosts);
    choice.put("serviceConfig", serviceConfig);

    assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "localhost"));
  }

  @Test
  public void maybeChooseServiceConfig_hostnameMatchesEmtpy() {
    Map<String, Object> choice = new LinkedHashMap<>();
    List<String> hosts = new ArrayList<>();
    choice.put("clientHostname", hosts);
    choice.put("serviceConfig", serviceConfig);

    assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
  }

  @Test
  public void maybeChooseServiceConfig_hostnameMatchesMulti() {
    Map<String, Object> choice = new LinkedHashMap<>();
    List<String> hosts = new ArrayList<>();
    hosts.add("localhorse");
    hosts.add("localhost");
    choice.put("clientHostname", hosts);
    choice.put("serviceConfig", serviceConfig);

    assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "localhost"));
  }

  @Test
  public void parseTxtResults_misspelledName() throws Exception {
    List<String> txtRecords = new ArrayList<>();
    txtRecords.add("some_record");
    txtRecords.add("_grpc_config=[]");

    List<? extends Map<String, ?>> results = DnsNameResolver.parseTxtResults(txtRecords);

    assertThat(results).isEmpty();
  }

  @Test
  public void parseTxtResults_badTypeFails() throws Exception {
    List<String> txtRecords = new ArrayList<>();
    txtRecords.add("some_record");
    txtRecords.add("grpc_config={}");

    thrown.expect(ClassCastException.class);
    thrown.expectMessage("wrong type");
    DnsNameResolver.parseTxtResults(txtRecords);
  }

  @Test
  public void parseTxtResults_badInnerTypeFails() throws Exception {
    List<String> txtRecords = new ArrayList<>();
    txtRecords.add("some_record");
    txtRecords.add("grpc_config=[\"bogus\"]");

    thrown.expect(ClassCastException.class);
    thrown.expectMessage("not object");
    DnsNameResolver.parseTxtResults(txtRecords);
  }

  @Test
  public void parseTxtResults_combineAll() throws Exception {
    Logger logger = Logger.getLogger(DnsNameResolver.class.getName());
    Level level = logger.getLevel();
    logger.setLevel(Level.SEVERE);
    try {
      List<String> txtRecords = new ArrayList<>();
      txtRecords.add("some_record");
      txtRecords.add("grpc_config=[{}, {}]"); // 2 records
      txtRecords.add("grpc_config=[{\"\":{}}]"); // 1 record

      List<? extends Map<String, ?>> results = DnsNameResolver.parseTxtResults(txtRecords);

      assertThat(results).hasSize(2 + 1);
    } finally {
      logger.setLevel(level);
    }
  }

  @Test
  public void shouldUseJndi_alwaysFalseIfDisabled() {
    boolean enableJndi = false;
    boolean enableJndiLocalhost = true;
    String host = "seemingly.valid.host";

    assertFalse(DnsNameResolver.shouldUseJndi(enableJndi, enableJndiLocalhost, host));
  }

  @Test
  public void shouldUseJndi_falseIfDisabledForLocalhost() {
    boolean enableJndi = true;
    boolean enableJndiLocalhost = false;

    assertFalse(DnsNameResolver.shouldUseJndi(enableJndi, enableJndiLocalhost, "localhost"));
    assertFalse(DnsNameResolver.shouldUseJndi(enableJndi, enableJndiLocalhost, "LOCALHOST"));
  }

  @Test
  public void shouldUseJndi_trueIfLocalhostOverriden() {
    boolean enableJndi = true;
    boolean enableJndiLocalhost = true;
    String host = "localhost";

    assertTrue(DnsNameResolver.shouldUseJndi(enableJndi, enableJndiLocalhost, host));
  }

  @Test
  public void shouldUseJndi_falseForIpv6() {
    boolean enableJndi = true;
    boolean enableJndiLocalhost = false;

    assertFalse(DnsNameResolver.shouldUseJndi(enableJndi, enableJndiLocalhost, "::"));
    assertFalse(DnsNameResolver.shouldUseJndi(enableJndi, enableJndiLocalhost, "::1"));
    assertFalse(DnsNameResolver.shouldUseJndi(enableJndi, enableJndiLocalhost, "2001:db8:1234::"));
    assertFalse(DnsNameResolver.shouldUseJndi(
        enableJndi, enableJndiLocalhost, "[2001:db8:1234::]"));
    assertFalse(DnsNameResolver.shouldUseJndi(
        enableJndi, enableJndiLocalhost, "2001:db8:1234::%3"));
  }

  @Test
  public void shouldUseJndi_falseForIpv4() {
    boolean enableJndi = true;
    boolean enableJndiLocalhost = false;

    assertFalse(DnsNameResolver.shouldUseJndi(enableJndi, enableJndiLocalhost, "127.0.0.1"));
    assertFalse(DnsNameResolver.shouldUseJndi(enableJndi, enableJndiLocalhost, "192.168.0.1"));
    assertFalse(DnsNameResolver.shouldUseJndi(enableJndi, enableJndiLocalhost, "134744072"));
  }

  @Test
  public void shouldUseJndi_falseForEmpty() {
    boolean enableJndi = true;
    boolean enableJndiLocalhost = false;

    assertFalse(DnsNameResolver.shouldUseJndi(enableJndi, enableJndiLocalhost, ""));
  }

  @Test
  public void shouldUseJndi_trueIfItMightPossiblyBeValid() {
    boolean enableJndi = true;
    boolean enableJndiLocalhost = false;

    assertTrue(DnsNameResolver.shouldUseJndi(enableJndi, enableJndiLocalhost, "remotehost"));
    assertTrue(DnsNameResolver.shouldUseJndi(enableJndi, enableJndiLocalhost, "remotehost.gov"));
    assertTrue(DnsNameResolver.shouldUseJndi(enableJndi, enableJndiLocalhost, "f.q.d.n."));
    assertTrue(DnsNameResolver.shouldUseJndi(
        enableJndi, enableJndiLocalhost, "8.8.8.8.in-addr.arpa."));
    assertTrue(DnsNameResolver.shouldUseJndi(
        enableJndi, enableJndiLocalhost, "2001-db8-1234--as3.ipv6-literal.net"));
  }

  @Test
  public void parseServiceConfig_capturesParseError() {
    ConfigOrError result = DnsNameResolver.parseServiceConfig(
        Arrays.asList("grpc_config=bogus"), new Random(), "localhost");

    assertThat(result).isNotNull();
    assertThat(result.getError().getCode()).isEqualTo(Status.Code.UNKNOWN);
    assertThat(result.getError().getDescription()).contains("failed to parse TXT records");
  }

  @Test
  public void parseServiceConfig_capturesChoiceError() {
    ConfigOrError result = DnsNameResolver.parseServiceConfig(
        Arrays.asList("grpc_config=[{\"hi\":{}}]"), new Random(), "localhost");

    assertThat(result).isNotNull();
    assertThat(result.getError().getCode()).isEqualTo(Status.Code.UNKNOWN);
    assertThat(result.getError().getDescription()).contains("failed to pick");
  }

  @Test
  public void parseServiceConfig_noChoiceIsNull() {
    ConfigOrError result = DnsNameResolver.parseServiceConfig(
        Arrays.asList("grpc_config=[]"), new Random(), "localhost");

    assertThat(result).isNull();
  }

  @Test
  public void parseServiceConfig_matches() {
    ConfigOrError result = DnsNameResolver.parseServiceConfig(
        Arrays.asList("grpc_config=[{\"serviceConfig\":{}}]"), new Random(), "localhost");

    assertThat(result).isNotNull();
    assertThat(result.getError()).isNull();
    assertThat(result.getConfig()).isEqualTo(ImmutableMap.of());
  }

  private void testInvalidUri(URI uri) {
    try {
      provider.newNameResolver(uri, args);
      fail("Should have failed");
    } catch (IllegalArgumentException e) {
      // expected
    }
  }

  private void testValidUri(URI uri, String exportedAuthority, int expectedPort) {
    DnsNameResolver resolver = provider.newNameResolver(uri, args);
    assertNotNull(resolver);
    assertEquals(expectedPort, resolver.getPort());
    assertEquals(exportedAuthority, resolver.getServiceAuthority());
  }

  private byte lastByte = 0;

  private List<InetAddress> createAddressList(int n) throws UnknownHostException {
    List<InetAddress> list = new ArrayList<>(n);
    for (int i = 0; i < n; i++) {
      list.add(InetAddress.getByAddress(new byte[] {127, 0, 0, ++lastByte}));
    }
    return list;
  }

  private static void assertAnswerMatches(
      List<InetAddress> addrs, int port, ResolutionResult resolutionResult) {
    assertThat(resolutionResult.getAddresses()).hasSize(addrs.size());
    for (int i = 0; i < addrs.size(); i++) {
      EquivalentAddressGroup addrGroup = resolutionResult.getAddresses().get(i);
      InetSocketAddress socketAddr =
          (InetSocketAddress) Iterables.getOnlyElement(addrGroup.getAddresses());
      assertEquals("Addr " + i, port, socketAddr.getPort());
      assertEquals("Addr " + i, addrs.get(i), socketAddr.getAddress());
    }
  }
}