/*
 * Copyright (C) 2009 The Guava 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 com.google.common.collect;

import static com.google.common.collect.Iterators.emptyIterator;
import static java.lang.reflect.Modifier.STATIC;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;

import com.google.common.base.Function;
import com.google.common.collect.testing.MapTestSuiteBuilder;
import com.google.common.collect.testing.TestStringMapGenerator;
import com.google.common.collect.testing.features.CollectionFeature;
import com.google.common.collect.testing.features.CollectionSize;
import com.google.common.collect.testing.features.MapFeature;
import com.google.common.reflect.Parameter;
import com.google.common.reflect.TypeToken;
import com.google.common.testing.ArbitraryInstances;
import com.google.common.testing.EqualsTester;
import com.google.common.testing.ForwardingWrapperTester;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;

/**
 * Unit test for {@link ForwardingMap}.
 *
 * @author Hayward Chan
 * @author Louis Wasserman
 */
@NullUnmarked
public class ForwardingMapTest extends TestCase {
  static class StandardImplForwardingMap<K, V> extends ForwardingMap<K, V> {
    private final Map<K, V> backingMap;

    StandardImplForwardingMap(Map<K, V> backingMap) {
      this.backingMap = backingMap;
    }

    @Override
    protected Map<K, V> delegate() {
      return backingMap;
    }

    @Override
    public boolean containsKey(Object key) {
      return standardContainsKey(key);
    }

    @Override
    public boolean containsValue(Object value) {
      return standardContainsValue(value);
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> map) {
      standardPutAll(map);
    }

    @Override
    public @Nullable V remove(Object object) {
      return standardRemove(object);
    }

    @Override
    public boolean equals(@Nullable Object object) {
      return standardEquals(object);
    }

    @Override
    public int hashCode() {
      return standardHashCode();
    }

    @Override
    public Set<K> keySet() {
      return new StandardKeySet();
    }

    @Override
    public Collection<V> values() {
      return new StandardValues();
    }

    @Override
    public String toString() {
      return standardToString();
    }

    @Override
    public Set<Entry<K, V>> entrySet() {
      return new StandardEntrySet() {
        @Override
        public Iterator<Entry<K, V>> iterator() {
          return delegate().entrySet().iterator();
        }
      };
    }

    @Override
    public void clear() {
      standardClear();
    }

    @Override
    public boolean isEmpty() {
      return standardIsEmpty();
    }
  }

  @AndroidIncompatible // test-suite builders
  public static Test suite() {
    TestSuite suite = new TestSuite();

    suite.addTestSuite(ForwardingMapTest.class);
    suite.addTest(
        MapTestSuiteBuilder.using(
                new TestStringMapGenerator() {

                  @Override
                  protected Map<String, String> create(Entry<String, String>[] entries) {
                    Map<String, String> map = new LinkedHashMap<>();
                    for (Entry<String, String> entry : entries) {
                      map.put(entry.getKey(), entry.getValue());
                    }
                    return new StandardImplForwardingMap<>(map);
                  }
                })
            .named("ForwardingMap[LinkedHashMap] with standard implementations")
            .withFeatures(
                CollectionSize.ANY,
                MapFeature.ALLOWS_NULL_VALUES,
                MapFeature.ALLOWS_NULL_KEYS,
                MapFeature.ALLOWS_ANY_NULL_QUERIES,
                MapFeature.GENERAL_PURPOSE,
                CollectionFeature.SUPPORTS_ITERATOR_REMOVE,
                CollectionFeature.KNOWN_ORDER)
            .createTestSuite());
    suite.addTest(
        MapTestSuiteBuilder.using(
                new TestStringMapGenerator() {

                  @Override
                  protected Map<String, String> create(Entry<String, String>[] entries) {
                    ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
                    for (Entry<String, String> entry : entries) {
                      builder.put(entry.getKey(), entry.getValue());
                    }
                    return new StandardImplForwardingMap<>(builder.buildOrThrow());
                  }
                })
            .named("ForwardingMap[ImmutableMap] with standard implementations")
            .withFeatures(
                CollectionSize.ANY,
                MapFeature.REJECTS_DUPLICATES_AT_CREATION,
                MapFeature.ALLOWS_ANY_NULL_QUERIES,
                CollectionFeature.KNOWN_ORDER)
            .createTestSuite());

    return suite;
  }

  @SuppressWarnings({"rawtypes", "unchecked"})
  public void testForwarding() {
    new ForwardingWrapperTester()
        .testForwarding(
            Map.class,
            new Function<Map, Map>() {
              @Override
              public Map apply(Map delegate) {
                return wrap(delegate);
              }
            });
  }

  public void testEquals() {
    Map<Integer, String> map1 = ImmutableMap.of(1, "one");
    Map<Integer, String> map2 = ImmutableMap.of(2, "two");
    new EqualsTester()
        .addEqualityGroup(map1, wrap(map1), wrap(map1))
        .addEqualityGroup(map2, wrap(map2))
        .testEquals();
  }

  public void testStandardEntrySet() throws InvocationTargetException {
    @SuppressWarnings("unchecked")
    Map<String, Boolean> map = mock(Map.class);

    Map<String, Boolean> forward =
        new ForwardingMap<String, Boolean>() {
          @Override
          protected Map<String, Boolean> delegate() {
            return map;
          }

          @Override
          public Set<Entry<String, Boolean>> entrySet() {
            return new StandardEntrySet() {
              @Override
              public Iterator<Entry<String, Boolean>> iterator() {
                return emptyIterator();
              }
            };
          }
        };
    callAllPublicMethods(new TypeToken<Set<Entry<String, Boolean>>>() {}, forward.entrySet());

    // These are the methods specified by StandardEntrySet
    verify(map, atLeast(0)).clear();
    verify(map, atLeast(0)).containsKey(any());
    verify(map, atLeast(0)).get(any());
    verify(map, atLeast(0)).isEmpty();
    verify(map, atLeast(0)).remove(any());
    verify(map, atLeast(0)).size();
    verifyNoMoreInteractions(map);
  }

  public void testStandardKeySet() throws InvocationTargetException {
    @SuppressWarnings("unchecked")
    Map<String, Boolean> map = mock(Map.class);

    Map<String, Boolean> forward =
        new ForwardingMap<String, Boolean>() {
          @Override
          protected Map<String, Boolean> delegate() {
            return map;
          }

          @Override
          public Set<String> keySet() {
            return new StandardKeySet();
          }
        };
    callAllPublicMethods(new TypeToken<Set<String>>() {}, forward.keySet());

    // These are the methods specified by StandardKeySet
    verify(map, atLeast(0)).clear();
    verify(map, atLeast(0)).containsKey(any());
    verify(map, atLeast(0)).isEmpty();
    verify(map, atLeast(0)).remove(any());
    verify(map, atLeast(0)).size();
    verify(map, atLeast(0)).entrySet();
    verifyNoMoreInteractions(map);
  }

  public void testStandardValues() throws InvocationTargetException {
    @SuppressWarnings("unchecked")
    Map<String, Boolean> map = mock(Map.class);

    Map<String, Boolean> forward =
        new ForwardingMap<String, Boolean>() {
          @Override
          protected Map<String, Boolean> delegate() {
            return map;
          }

          @Override
          public Collection<Boolean> values() {
            return new StandardValues();
          }
        };
    callAllPublicMethods(new TypeToken<Collection<Boolean>>() {}, forward.values());

    // These are the methods specified by StandardValues
    verify(map, atLeast(0)).clear();
    verify(map, atLeast(0)).containsValue(any());
    verify(map, atLeast(0)).isEmpty();
    verify(map, atLeast(0)).size();
    verify(map, atLeast(0)).entrySet();
    verifyNoMoreInteractions(map);
  }

  public void testToStringWithNullKeys() throws Exception {
    Map<String, String> hashmap = new HashMap<>();
    hashmap.put("foo", "bar");
    hashmap.put(null, "baz");

    StandardImplForwardingMap<String, String> forwardingMap =
        new StandardImplForwardingMap<>(new HashMap<>());
    forwardingMap.put("foo", "bar");
    forwardingMap.put(null, "baz");

    assertEquals(hashmap.toString(), forwardingMap.toString());
  }

  public void testToStringWithNullValues() throws Exception {
    Map<String, String> hashmap = new HashMap<>();
    hashmap.put("foo", "bar");
    hashmap.put("baz", null);

    StandardImplForwardingMap<String, String> forwardingMap =
        new StandardImplForwardingMap<>(new HashMap<>());
    forwardingMap.put("foo", "bar");
    forwardingMap.put("baz", null);

    assertEquals(hashmap.toString(), forwardingMap.toString());
  }

  private static <K, V> Map<K, V> wrap(Map<K, V> delegate) {
    return new ForwardingMap<K, V>() {
      @Override
      protected Map<K, V> delegate() {
        return delegate;
      }
    };
  }

  private static @Nullable Object getDefaultValue(TypeToken<?> type) {
    Class<?> rawType = type.getRawType();
    Object defaultValue = ArbitraryInstances.get(rawType);
    if (defaultValue != null) {
      return defaultValue;
    }

    // TODO(cpovirk): Support these types in ArbitraryInstances itself?
    if (rawType.equals(Predicate.class)) {
      return (Predicate<Object>) v -> (boolean) getDefaultValue(TypeToken.of(boolean.class));
    } else if (rawType.equals(IntFunction.class)) {
      try {
        Method method = IntFunction.class.getMethod("apply", int.class);
        return (IntFunction<Object>) v -> getDefaultValue(type.method(method).getReturnType());
      } catch (NoSuchMethodException e) {
        throw newLinkageError(e);
      }
    } else if (rawType.equals(Consumer.class)) {
      return (Consumer<Object>) v -> {};
    } else {
      return null;
    }
  }

  private static <T> void callAllPublicMethods(TypeToken<T> type, T object)
      throws InvocationTargetException {
    for (Method method : type.getRawType().getMethods()) {
      if ((method.getModifiers() & STATIC) != 0) {
        continue;
      }
      ImmutableList<Parameter> parameters = type.method(method).getParameters();
      Object[] args = new Object[parameters.size()];
      for (int i = 0; i < parameters.size(); i++) {
        args[i] = getDefaultValue(parameters.get(i).getType());
      }
      try {
        try {
          method.invoke(object, args);
        } catch (InvocationTargetException ex) {
          try {
            throw ex.getCause();
          } catch (UnsupportedOperationException unsupported) {
            // this is a legit exception
          }
        }
      } catch (Throwable cause) {
        throw new InvocationTargetException(cause, method + " with args: " + Arrays.toString(args));
      }
    }
  }

  private static LinkageError newLinkageError(Throwable cause) {
    return new LinkageError(cause.toString(), cause);
  }
}
