/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.reef.tang.implementation.java;

import org.apache.reef.tang.ExternalConstructor;
import org.apache.reef.tang.JavaConfigurationBuilder;
import org.apache.reef.tang.Tang;
import org.apache.reef.tang.annotations.Name;
import org.apache.reef.tang.annotations.NamedParameter;
import org.apache.reef.tang.annotations.Parameter;
import org.apache.reef.tang.exceptions.BindException;
import org.apache.reef.tang.exceptions.InjectionException;
import org.apache.reef.tang.formats.ParameterParser;
import org.apache.reef.tang.util.ReflectionUtilities;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import javax.inject.Inject;

public class TestParameterParser {

  @Rule
  public ExpectedException thrown = ExpectedException.none();

  @Test
  public void testParameterParser() throws BindException {
    final ParameterParser p = new ParameterParser();
    p.addParser(FooParser.class);
    final Foo f = p.parse(Foo.class, "woot");
    Assert.assertEquals(f.s, "woot");
  }

  @Test
  public void testUnregisteredParameterParser() throws BindException {
    thrown.expect(UnsupportedOperationException.class);
    thrown.expectMessage("Don't know how to parse a org.apache.reef.tang.implementation.java.TestParameterParser$Foo");
    final ParameterParser p = new ParameterParser();
    //p.addParser(FooParser.class);
    final Foo f = p.parse(Foo.class, "woot");
    Assert.assertEquals(f.s, "woot");
  }

  @Test
  public void testReturnSubclass() throws BindException {
    final ParameterParser p = new ParameterParser();
    p.addParser(BarParser.class);
    final Bar f = (Bar) p.parse(Foo.class, "woot");
    Assert.assertEquals(f.getS(), "woot");

  }

  @Test
  public void testGoodMerge() throws BindException {
    final ParameterParser old = new ParameterParser();
    old.addParser(BarParser.class);
    final ParameterParser nw = new ParameterParser();
    nw.mergeIn(old);
    nw.parse(Foo.class, "woot");
  }

  @Test
  public void testGoodMerge2() throws BindException {
    final ParameterParser old = new ParameterParser();
    old.addParser(BarParser.class);
    final ParameterParser nw = new ParameterParser();
    nw.addParser(BarParser.class);
    nw.mergeIn(old);
    nw.parse(Foo.class, "woot");
  }

  @Test
  public void testBadMerge() throws BindException {
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage("Conflict detected when merging parameter parsers! To parse " +
        "org.apache.reef.tang.implementation.java.TestParameterParser$Foo I have a: " +
        "org.apache.reef.tang.implementation.java.TestParameterParser$FooParser the other instance has a: " +
        "org.apache.reef.tang.implementation.java.TestParameterParser$BarParser");
    final ParameterParser old = new ParameterParser();
    old.addParser(BarParser.class);
    final ParameterParser nw = new ParameterParser();
    nw.addParser(FooParser.class);
    nw.mergeIn(old);
  }

  @SuppressWarnings("unchecked")
  @Test
  public void testEndToEnd() throws BindException, InjectionException {
    final Tang tang = Tang.Factory.getTang();
    final JavaConfigurationBuilder cb = tang.newConfigurationBuilder(BarParser.class);
    cb.bindNamedParameter(SomeNamedFoo.class, "hdfs://woot");
    final ILikeBars ilb = tang.newInjector(cb.build()).getInstance(ILikeBars.class);
    Assert.assertNotNull(ilb);
  }

  @SuppressWarnings("unchecked")
  @Test
  public void testDelegatingParser() throws BindException, InjectionException, ClassNotFoundException {
    final Tang tang = Tang.Factory.getTang();
    final JavaConfigurationBuilder cb = tang.newConfigurationBuilder(TypeParser.class);

    JavaConfigurationBuilder cb2 = tang.newConfigurationBuilder(cb.build());

    cb2.bind(ReflectionUtilities.getFullName(ParseName.class), "a");

    final ParseableType t = tang.newInjector(cb2.build()).getNamedInstance(ParseName.class);
    Assert.assertTrue(t instanceof ParseTypeA);

    cb2 = tang.newConfigurationBuilder(cb.build());

    cb2.bind(ReflectionUtilities.getFullName(ParseNameB.class), "b");
    cb2.bindNamedParameter(ParseNameA.class, "a");
    tang.newInjector(cb2.build()).getInstance(NeedsA.class);
    tang.newInjector(cb2.build()).getInstance(NeedsB.class);

  }

  private static class FooParser implements ExternalConstructor<Foo> {
    private final Foo foo;

    @Inject
    FooParser(final String s) {
      this.foo = new Foo(s);
    }

    @Override
    public Foo newInstance() {
      return foo;
    }
  }

  private static class BarParser implements ExternalConstructor<Foo> {
    private final Bar bar;

    @Inject
    BarParser(final String s) {
      this.bar = new Bar(s);
    }

    @Override
    public Bar newInstance() {
      return bar;
    }
  }

  private static class Foo {
    private final String s;
    String getS() {
      return s;
    }

    Foo(final String s) {
      this.s = s;
    }
  }

  private static class Bar extends Foo {
    Bar(final String s) {
      super(s);
    }
  }

  @NamedParameter
  private static class SomeNamedFoo implements Name<Foo> {
  }

  private static class ILikeBars {
    @Inject
    ILikeBars(@Parameter(SomeNamedFoo.class) final Foo bar) {
      final Bar b = (Bar) bar;
      Assert.assertEquals(b.getS(), "hdfs://woot");
    }
  }

  private static class ParseableType {
  }

  private static class ParseTypeA extends ParseableType {

  }

  private static class ParseTypeB extends ParseableType {

  }

  private static class TypeParser implements ExternalConstructor<ParseableType> {
    private ParseableType instance;

    @Inject
    TypeParser(final String s) {
      if (s.equals("a")) {
        instance = new ParseTypeA();
      }
      if (s.equals("b")) {
        instance = new ParseTypeB();
      }

    }

    @Override
    public ParseableType newInstance() {
      return instance;
    }
  }

  @NamedParameter()
  private static class ParseName implements Name<ParseableType> {

  }

  @NamedParameter()
  private static class ParseNameA implements Name<ParseableType> {

  }

  @NamedParameter()
  private static class ParseNameB implements Name<ParseTypeB> {

  }

  private static class NeedsA {
    @Inject
    NeedsA(@Parameter(ParseNameA.class) final ParseableType a) {
      Assert.assertTrue(a instanceof ParseTypeA);
    }
  }

  private static class NeedsB {
    @Inject
    NeedsB(@Parameter(ParseNameB.class) final ParseTypeB b) {
      Assert.assertTrue(b instanceof ParseTypeB);
    }
  }
}