package net.zortrium.util;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.security.Permission;

import net.zortrium.util.Options.FailedParseException;
import net.zortrium.util.Options.Parser;
import net.zortrium.util.Options.Validator;

import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

/**
 * Tests of the {@code net.zortrium.util.Options} class. This code may be used,
 * modified, and redistributed provided that the author tag below remains
 * intact.
 * @author Sean Barker (zortrium@gmail.com)
 */
public class OptionsTest {

  private static SecurityManager prevSecurityManager;

  private static class ErrorMessageException extends RuntimeException {
    private static final long serialVersionUID = 1L;
  }

  @BeforeClass
  public static void disallowExit() {
    prevSecurityManager = System.getSecurityManager();
    System.setSecurityManager(new SecurityManager() {

      @Override
      public void checkExit(int status) {
        super.checkExit(status);
        throw new ErrorMessageException();
      }

      @Override
      public void checkPermission(Permission perm) {
        // always allow
      }

    });
  }

  @AfterClass
  public static void allowExit() {
    System.setSecurityManager(prevSecurityManager);
  }

  @Before
  public void resetOptions() {
    Options.reset();
  }

  @Test
  public void testHelp() {
    assertFalse(Options.get(Boolean.class, "help"));
    try {
      Options.readArguments("--help");
    } catch (ErrorMessageException e) {
      // good
    }
    assertTrue(Options.get(Boolean.class, "help"));
    try {
      Options.readArguments("asdf");
    } catch (ErrorMessageException e) {
      // good
    }
  }

  @Test
  public void testBasic() {
    try {
      Options.get(Boolean.class, "foo");
      fail();
    } catch (IllegalArgumentException e) {
      assertTrue(e.getMessage().contains("no such option"));
    }
    Options.add(Boolean.class, "foo");
    assertNull(Options.get(Boolean.class, "foo"));
    Options.readArguments("--foo true");
    assertTrue(Options.get(Boolean.class, "foo"));
    Options.add(String.class, "foo2");
    try {
      Options.add(String.class, "foo");
      fail();
    } catch (IllegalStateException e) {
      assertTrue(e.getMessage().contains("cannot redefine"));
    }
  }

  @Test
  public void testShortFlag() {
    Options.add(Boolean.class, "foo").setShortFlag('f');
    Options.readArguments("-f");
    try {
      Options.add(Boolean.class, "bar").setShortFlag('f');
      fail();
    } catch (IllegalStateException e) {
      assertTrue(e.getMessage().contains("cannot redefine"));
    }
  }

  @Test
  public void testReadArgs() {
    Options.add(Integer.class, "foo");
    Options.add(Integer.class, "bar");
    Options.readArguments("--foo", "1", "--bar", "2");
    assertEquals(1, Options.get(Integer.class, "foo"));
    assertEquals(2, Options.get(Integer.class, "bar"));
    Options.readArguments("--foo 1", "--bar 2");
    assertEquals(1, Options.get(Integer.class, "foo"));
    assertEquals(2, Options.get(Integer.class, "bar"));
  }

  @Test
  public void testDefaults() {
    Options.add(Double.class, "foo");
    assertNull(Options.get(Double.class, "foo"));
    Options.add(Double.class, "bar").setRawDefaultValue("3.14");
    assertNull(Options.get(Double.class, "bar"));
    Options.readArguments();
    assertEquals(3.14, Options.get(Double.class, "bar"));
    Options.add(Double.class, "quux").setDefaultValue(3.14);
    assertEquals(3.14, Options.get(Double.class, "quux"));
    Options.add(Double.class, "baz").setRawDefaultValue("abc");
    try {
      Options.readArguments();
      fail();
    } catch (ErrorMessageException e) {
      // good
    }
  }

  @Test
  public void testTypes() {
    Options.add(Float.class, "f");
    Options.add(Double.class, "d");
    Options.readArguments("--f 2", "--d 2");
    Options.get(Float.class, "f");
    Options.get(Double.class, "d");
    try {
      Options.get(Float.class, "d");
      fail();
    } catch (IllegalArgumentException e) {
      assertTrue(e.getMessage().contains("invalid option class type"));
    }
    try {
      Options.get(Double.class, "f");
      fail();
    } catch (IllegalArgumentException e) {
      assertTrue(e.getMessage().contains("invalid option class type"));
    }
  }

  @Test
  public void testInputFormats() {
    Options.add(Boolean.class, "bool").setShortFlag('b');
    Options.readArguments("--bool", "-b", "--bool true", "-b true");
    Options.add(Double.class, "dec").setShortFlag('d');
    Options.readArguments("--dec 1.2");
    assertEquals(1.2, Options.get(Double.class, "dec"));
    Options.readArguments("-d 2.1");
    assertEquals(2.1, Options.get(Double.class, "dec"));
    try {
      Options.readArguments("--dec");
    } catch (ErrorMessageException e) {
      // good
    }
  }

  private void resetRequiredTest() {
    Options.reset();
    Options.add(Integer.class, "a").setShortFlag('a').setRequired(false);
    Options.add(Integer.class, "b").setShortFlag('b').setRequired(true).setDefaultValue(5);
    Options.add(Integer.class, "c").setShortFlag('c').setRequired(true);
  }

  @Test
  public void testRequiredOptions() {
    resetRequiredTest();
    Options.readArguments("-a 1", "-b 1", "-c 1");
    resetRequiredTest();
    Options.readArguments("-b 1", "-c 1");
    resetRequiredTest();
    Options.readArguments("-c 1");
    resetRequiredTest();
    try {
      Options.readArguments("-a 1", "-b 1");
      fail();
    } catch (ErrorMessageException e) {
      // good
    }
  }

  @Test
  public void testConditions() {
    Options.add(Integer.class, "a");
    Validator<Integer> greaterThanFive = new Validator<Integer>() {
      @Override
      public String check(Integer parsedValue) {
        return (parsedValue > 5) ? null : "error";
      }
    };
    Options.add(Integer.class, "b").setValidator(greaterThanFive);
    Options.readArguments("--a 10", "--b 10");
    Options.readArguments("--a 3");
    try {
      Options.readArguments("--b 3");
      fail();
    } catch (ErrorMessageException e) {
      // good
    }
    Options.add(Integer.class, "c").setValidator(greaterThanFive).setDefaultValue(4);
  }

  @Test
  public void testConditionMessages() {
    Options.add(Integer.class, "a").setValidator(new Validator<Integer>() {

      @Override
      public String check(Integer val) {
        return (val == 1) ? "alpha" : ((val == 2) ? "beta" : null);
      }

    });
    Options.readArguments("--a 0");
    try {
      Options.readArguments("--a 1");
      fail();
    } catch (ErrorMessageException e) {
      // TODO: check this
      // String msg = e.getMessage();
      // assertTrue(msg.contains("alpha") && !msg.contains("beta"));
    }
    try {
      Options.readArguments("--a 2");
      fail();
    } catch (ErrorMessageException e) {
      // TODO: check this
      // String msg = e.getMessage();
      // assertTrue(msg.contains("beta") && !msg.contains("alpha"));
    }
  }

  public class CustomObject {

    private final String str;

    private final String str2;

    public CustomObject(String str, String str2) {
      this.str = str;
      this.str2 = str2;
    }

    @Override
    public boolean equals(Object obj) {
      return str.equals(((CustomObject) obj).str) && str2.equals(((CustomObject) obj).str2);
    }
  }

  @Test
  public void testCustomObjectOption() {
    Options.add(CustomObject.class, "z");
    try {
      Options.readArguments("--z abc");
      fail();
    } catch (RuntimeException e) {
      assertTrue(e.getMessage().contains("no String constructor"));
    }
    Parser<CustomObject> parser = new Parser<CustomObject>() {

      @Override
      public CustomObject readInput(String inputString) throws FailedParseException {
        return new CustomObject(inputString, inputString);
      }

    };
    Options.add(CustomObject.class, "a").setParser(parser);
    Options.add(CustomObject.class, "b").setParser(parser);
    Options.add(CustomObject.class, "c").setParser(parser);
    Options.readArguments("--a abc", "--b abc", "--c def");
    CustomObject a = Options.get(CustomObject.class, "a");
    CustomObject b = Options.get(CustomObject.class, "b");
    CustomObject c = Options.get(CustomObject.class, "c");
    assertTrue(a.equals(b));
    assertFalse(a.equals(c));
    assertFalse(b.equals(c));
  }

  public class LazyA {
  }

  public class LazyB {
  }

  private void resetLazyTest() {
    Options.reset();
    Options.add(LazyA.class, "a").setParser(new Parser<LazyA>() {

      @Override
      public LazyA readInput(String inputString) throws FailedParseException {
        return new LazyA();
      }
    }).setValidator(new Validator<LazyA>() {

      @Override
      public String check(LazyA parsedValue) {
        return (Options.get(LazyB.class, "b") == null) ? null : "fail";
      }
    });
    Options.add(LazyB.class, "b").setParser(new Parser<LazyB>() {

      @Override
      public LazyB readInput(String inputString) throws FailedParseException {
        return new LazyB();
      }
    });
  }

  @Test
  public void testLazyInitialization() {
    resetLazyTest();
    Options.readArguments("--a");
    resetLazyTest();
    Options.readArguments("--b");
    resetLazyTest();
    try {
      Options.readArguments("--a", "--b");
      fail();
    } catch (ErrorMessageException e) {
      // good
    }
    resetLazyTest();
    try {
      Options.readArguments("--b", "--a");
      fail();
    } catch (ErrorMessageException e) {
      // good
    }
  }

  @Test
  public void testSubclassing() {
    Options.add(Object.class, "a").setParser(new Parser<Object>() {

      @Override
      public Object readInput(String inputString) throws FailedParseException {
        return new Integer(1);
      }
    });
    Options.readArguments("--a 5");
    Options.get(Object.class, "a");
    Options.get(Integer.class, "a");
    try {
      Options.get(String.class, "a");
    } catch (IllegalArgumentException e) {
      assertTrue(e.getMessage().contains("invalid option class type"));
    }
  }

}

