@Test
public void testRuleDefinitionReadingFromFile() throws Exception {
    File adultRuleDescriptor = new File("src/test/resources/adult-rule.json");
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(adultRuleDescriptor));
    assertThat(ruleDefinitions).hasSize(1);
    RuleDefinition adultRuleDefinition = ruleDefinitions.get(0);
    assertThat(adultRuleDefinition).isNotNull();
    assertThat(adultRuleDefinition.getName()).isEqualTo("adult rule");
    assertThat(adultRuleDefinition.getDescription()).isEqualTo("when age is greater than 18, then mark as adult");
    assertThat(adultRuleDefinition.getPriority()).isEqualTo(1);
    assertThat(adultRuleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(adultRuleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
}

public List<RuleDefinition> read(Reader reader) throws Exception {
    List<RuleDefinition> ruleDefinitions = new ArrayList<>();
    Iterable<Map<String, Object>> rules = loadRules(reader);
    for (Map<String, Object> rule : rules) {
        ruleDefinitions.add(createRuleDefinition(rule));
    }
    return ruleDefinitions;
}

@Test
public void testRuleDefinitionReadingFromString() throws Exception {
    String adultRuleDescriptor = new String(Files.readAllBytes(Paths.get("src/test/resources/adult-rule.json")));
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new StringReader(adultRuleDescriptor));
    assertThat(ruleDefinitions).hasSize(1);
    RuleDefinition adultRuleDefinition = ruleDefinitions.get(0);
    assertThat(adultRuleDefinition).isNotNull();
    assertThat(adultRuleDefinition.getName()).isEqualTo("adult rule");
    assertThat(adultRuleDefinition.getDescription()).isEqualTo("when age is greater than 18, then mark as adult");
    assertThat(adultRuleDefinition.getPriority()).isEqualTo(1);
    assertThat(adultRuleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(adultRuleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
}

@Test
public void testEmptyRulesDefinitionReading() throws Exception {
    File rulesDescriptor = new File("src/test/resources/rules-empty.json");
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(rulesDescriptor));
    assertThat(ruleDefinitions).hasSize(0);
}

@Test(expected = IllegalArgumentException.class)
public void testInvalidRuleDefinitionReading_whenNoCondition() throws Exception {
    File adultRuleDescriptor = new File("src/test/resources/adult-rule-without-condition.json");
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(adultRuleDescriptor));
}

@Test
public void testRuleDefinitionReading_withCompositeAndBasicRules() throws Exception {
    File compositeRuleDescriptor = new File("src/test/resources/composite-rules.json");
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(compositeRuleDescriptor));
    assertThat(ruleDefinitions).hasSize(2);
    RuleDefinition ruleDefinition = ruleDefinitions.get(0);
    assertThat(ruleDefinition).isNotNull();
    assertThat(ruleDefinition.getName()).isEqualTo("Movie id rule");
    assertThat(ruleDefinition.getDescription()).isEqualTo("description");
    assertThat(ruleDefinition.getPriority()).isEqualTo(1);
    assertThat(ruleDefinition.getCompositeRuleType()).isEqualTo("UnitRuleGroup");
    assertThat(ruleDefinition.getComposingRules()).isNotEmpty();
    List<RuleDefinition> subrules = ruleDefinition.getComposingRules();
    assertThat(subrules).hasSize(2);
    RuleDefinition subrule = subrules.get(0);
    assertThat(subrule.getName()).isEqualTo("Time is evening");
    assertThat(subrule.getDescription()).isEqualTo("If it's later than 7pm");
    assertThat(subrule.getPriority()).isEqualTo(1);
    subrule = subrules.get(1);
    assertThat(subrule.getName()).isEqualTo("Movie is rated R");
    assertThat(subrule.getDescription()).isEqualTo("If the movie is rated R");
    assertThat(subrule.getPriority()).isEqualTo(1);
    ruleDefinition = ruleDefinitions.get(1);
    assertThat(ruleDefinition).isNotNull();
    assertThat(ruleDefinition.getName()).isEqualTo("weather rule");
    assertThat(ruleDefinition.getDescription()).isEqualTo("when it rains, then take an umbrella");
    assertThat(ruleDefinition.getComposingRules()).isEmpty();
    assertThat(ruleDefinition.getCondition()).isEqualTo("rain == True");
    assertThat(ruleDefinition.getActions()).isEqualTo(Collections.singletonList("System.out.println(\"It rains, take an umbrella!\");"));
}

@Test
public void testRulesDefinitionReading() throws Exception {
    File rulesDescriptor = new File("src/test/resources/rules.json");
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(rulesDescriptor));
    assertThat(ruleDefinitions).hasSize(2);
    RuleDefinition ruleDefinition = ruleDefinitions.get(0);
    assertThat(ruleDefinition).isNotNull();
    assertThat(ruleDefinition.getName()).isEqualTo("adult rule");
    assertThat(ruleDefinition.getDescription()).isEqualTo("when age is greater than 18, then mark as adult");
    assertThat(ruleDefinition.getPriority()).isEqualTo(1);
    assertThat(ruleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(ruleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
    ruleDefinition = ruleDefinitions.get(1);
    assertThat(ruleDefinition).isNotNull();
    assertThat(ruleDefinition.getName()).isEqualTo("weather rule");
    assertThat(ruleDefinition.getDescription()).isEqualTo("when it rains, then take an umbrella");
    assertThat(ruleDefinition.getPriority()).isEqualTo(2);
    assertThat(ruleDefinition.getCondition()).isEqualTo("rain == true");
    assertThat(ruleDefinition.getActions()).isEqualTo(Collections.singletonList("System.out.println(\"It rains, take an umbrella!\");"));
}

@Test(expected = IllegalArgumentException.class)
public void testInvalidRuleDefinitionReading_whenNoActions() throws Exception {
    File adultRuleDescriptor = new File("src/test/resources/adult-rule-without-actions.json");
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(adultRuleDescriptor));
}

@Test
public void testRuleDefinitionReading_withDefaultValues() throws Exception {
    File adultRuleDescriptor = new File("src/test/resources/adult-rule-with-default-values.json");
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(adultRuleDescriptor));
    assertThat(ruleDefinitions).hasSize(1);
    RuleDefinition adultRuleDefinition = ruleDefinitions.get(0);
    assertThat(adultRuleDefinition).isNotNull();
    assertThat(adultRuleDefinition.getName()).isEqualTo(Rule.DEFAULT_NAME);
    assertThat(adultRuleDefinition.getDescription()).isEqualTo(Rule.DEFAULT_DESCRIPTION);
    assertThat(adultRuleDefinition.getPriority()).isEqualTo(Rule.DEFAULT_PRIORITY);
    assertThat(adultRuleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(adultRuleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
}


@Test
public void testRuleDefinitionReadingFromString() throws Exception {
    String adultRuleDescriptor = new String(Files.readAllBytes(Paths.get("src/test/resources/adult-rule.yml")));
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new StringReader(adultRuleDescriptor));
    assertThat(ruleDefinitions).hasSize(1);
    RuleDefinition adultRuleDefinition = ruleDefinitions.get(0);
    assertThat(adultRuleDefinition).isNotNull();
    assertThat(adultRuleDefinition.getName()).isEqualTo("adult rule");
    assertThat(adultRuleDefinition.getDescription()).isEqualTo("when age is greater than 18, then mark as adult");
    assertThat(adultRuleDefinition.getPriority()).isEqualTo(1);
    assertThat(adultRuleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(adultRuleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
}

@Test(expected = IllegalArgumentException.class)
public void testInvalidRuleDefinitionReading_whenNoActions() throws Exception {
    File adultRuleDescriptor = new File("src/test/resources/adult-rule-without-actions.yml");
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(adultRuleDescriptor));
}

@Test
public void testEmptyRulesDefinitionReading() throws Exception {
    File rulesDescriptor = new File("src/test/resources/rules-empty.yml");
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(rulesDescriptor));
    assertThat(ruleDefinitions).hasSize(0);
}

@Test(expected = IllegalArgumentException.class)
public void testInvalidRuleDefinitionReading_whenNoCondition() throws Exception {
    File adultRuleDescriptor = new File("src/test/resources/adult-rule-without-condition.yml");
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(adultRuleDescriptor));
}

@Test
public void testRuleDefinitionReading_withDefaultValues() throws Exception {
    File adultRuleDescriptor = new File("src/test/resources/adult-rule-with-default-values.yml");
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(adultRuleDescriptor));
    assertThat(ruleDefinitions).hasSize(1);
    RuleDefinition adultRuleDefinition = ruleDefinitions.get(0);
    assertThat(adultRuleDefinition).isNotNull();
    assertThat(adultRuleDefinition.getName()).isEqualTo(Rule.DEFAULT_NAME);
    assertThat(adultRuleDefinition.getDescription()).isEqualTo(Rule.DEFAULT_DESCRIPTION);
    assertThat(adultRuleDefinition.getPriority()).isEqualTo(Rule.DEFAULT_PRIORITY);
    assertThat(adultRuleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(adultRuleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
}

@Test
public void testRuleDefinitionReading_withCompositeAndBasicRules() throws Exception {
    File compositeRuleDescriptor = new File("src/test/resources/composite-rules.yml");
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(compositeRuleDescriptor));
    assertThat(ruleDefinitions).hasSize(2);
    RuleDefinition ruleDefinition = ruleDefinitions.get(0);
    assertThat(ruleDefinition).isNotNull();
    assertThat(ruleDefinition.getName()).isEqualTo("Movie id rule");
    assertThat(ruleDefinition.getDescription()).isEqualTo("description");
    assertThat(ruleDefinition.getPriority()).isEqualTo(1);
    assertThat(ruleDefinition.getCompositeRuleType()).isEqualTo("UnitRuleGroup");
    assertThat(ruleDefinition.getComposingRules()).isNotEmpty();
    List<RuleDefinition> subrules = ruleDefinition.getComposingRules();
    assertThat(subrules).hasSize(2);
    RuleDefinition subrule = subrules.get(0);
    assertThat(subrule.getName()).isEqualTo("Time is evening");
    assertThat(subrule.getDescription()).isEqualTo("If it's later than 7pm");
    assertThat(subrule.getPriority()).isEqualTo(1);
    subrule = subrules.get(1);
    assertThat(subrule.getName()).isEqualTo("Movie is rated R");
    assertThat(subrule.getDescription()).isEqualTo("If the movie is rated R");
    assertThat(subrule.getPriority()).isEqualTo(1);
    ruleDefinition = ruleDefinitions.get(1);
    assertThat(ruleDefinition).isNotNull();
    assertThat(ruleDefinition.getName()).isEqualTo("weather rule");
    assertThat(ruleDefinition.getDescription()).isEqualTo("when it rains, then take an umbrella");
    assertThat(ruleDefinition.getComposingRules()).isEmpty();
    assertThat(ruleDefinition.getCondition()).isEqualTo("rain == True");
    assertThat(ruleDefinition.getActions()).isEqualTo(Collections.singletonList("System.out.println(\"It rains, take an umbrella!\");"));
}

@Test
public void testRuleDefinitionReadingFromFile() throws Exception {
    File adultRuleDescriptor = new File("src/test/resources/adult-rule.yml");
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(adultRuleDescriptor));
    assertThat(ruleDefinitions).hasSize(1);
    RuleDefinition adultRuleDefinition = ruleDefinitions.get(0);
    assertThat(adultRuleDefinition).isNotNull();
    assertThat(adultRuleDefinition.getName()).isEqualTo("adult rule");
    assertThat(adultRuleDefinition.getDescription()).isEqualTo("when age is greater than 18, then mark as adult");
    assertThat(adultRuleDefinition.getPriority()).isEqualTo(1);
    assertThat(adultRuleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(adultRuleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
}

@Test
public void testRulesDefinitionReading() throws Exception {
    File rulesDescriptor = new File("src/test/resources/rules.yml");
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(rulesDescriptor));
    assertThat(ruleDefinitions).hasSize(2);
    RuleDefinition ruleDefinition = ruleDefinitions.get(0);
    assertThat(ruleDefinition).isNotNull();
    assertThat(ruleDefinition.getName()).isEqualTo("adult rule");
    assertThat(ruleDefinition.getDescription()).isEqualTo("when age is greater than 18, then mark as adult");
    assertThat(ruleDefinition.getPriority()).isEqualTo(1);
    assertThat(ruleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(ruleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
    ruleDefinition = ruleDefinitions.get(1);
    assertThat(ruleDefinition).isNotNull();
    assertThat(ruleDefinition.getName()).isEqualTo("weather rule");
    assertThat(ruleDefinition.getDescription()).isEqualTo("when it rains, then take an umbrella");
    assertThat(ruleDefinition.getPriority()).isEqualTo(2);
    assertThat(ruleDefinition.getCondition()).isEqualTo("rain == true");
    assertThat(ruleDefinition.getActions()).isEqualTo(Collections.singletonList("System.out.println(\"It rains, take an umbrella!\");"));
}


@Test(expected = IllegalArgumentException.class)
public void testInvalidRuleDefinitionReading_whenNoActions() throws Exception {
    File adultRuleDescriptor = new File("src/test/resources/adult-rule-without-actions." + fileExtension);
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(adultRuleDescriptor));
}

@Test(expected = IllegalArgumentException.class)
public void testInvalidRuleDefinitionReading_whenNoCondition() throws Exception {
    File adultRuleDescriptor = new File("src/test/resources/adult-rule-without-condition." + fileExtension);
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(adultRuleDescriptor));
}

@Test
public void testRuleDefinitionReading_withDefaultValues() throws Exception {
    File adultRuleDescriptor = new File("src/test/resources/adult-rule-with-default-values." + fileExtension);
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(adultRuleDescriptor));
    assertThat(ruleDefinitions).hasSize(1);
    RuleDefinition adultRuleDefinition = ruleDefinitions.get(0);
    assertThat(adultRuleDefinition).isNotNull();
    assertThat(adultRuleDefinition.getName()).isEqualTo(Rule.DEFAULT_NAME);
    assertThat(adultRuleDefinition.getDescription()).isEqualTo(Rule.DEFAULT_DESCRIPTION);
    assertThat(adultRuleDefinition.getPriority()).isEqualTo(Rule.DEFAULT_PRIORITY);
    assertThat(adultRuleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(adultRuleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
}

@Test
public void testRuleDefinitionReadingFromString() throws Exception {
    Path ruleDescriptor = Paths.get("src/test/resources/adult-rule." + fileExtension);
    String adultRuleDescriptor = new String(Files.readAllBytes(ruleDescriptor));
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new StringReader(adultRuleDescriptor));
    assertThat(ruleDefinitions).hasSize(1);
    RuleDefinition adultRuleDefinition = ruleDefinitions.get(0);
    assertThat(adultRuleDefinition).isNotNull();
    assertThat(adultRuleDefinition.getName()).isEqualTo("adult rule");
    assertThat(adultRuleDefinition.getDescription()).isEqualTo("when age is greater than 18, then mark as adult");
    assertThat(adultRuleDefinition.getPriority()).isEqualTo(1);
    assertThat(adultRuleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(adultRuleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
}

@Test
public void testRulesDefinitionReading() throws Exception {
    File rulesDescriptor = new File("src/test/resources/rules." + fileExtension);
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(rulesDescriptor));
    assertThat(ruleDefinitions).hasSize(2);
    RuleDefinition ruleDefinition = ruleDefinitions.get(0);
    assertThat(ruleDefinition).isNotNull();
    assertThat(ruleDefinition.getName()).isEqualTo("adult rule");
    assertThat(ruleDefinition.getDescription()).isEqualTo("when age is greater than 18, then mark as adult");
    assertThat(ruleDefinition.getPriority()).isEqualTo(1);
    assertThat(ruleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(ruleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
    ruleDefinition = ruleDefinitions.get(1);
    assertThat(ruleDefinition).isNotNull();
    assertThat(ruleDefinition.getName()).isEqualTo("weather rule");
    assertThat(ruleDefinition.getDescription()).isEqualTo("when it rains, then take an umbrella");
    assertThat(ruleDefinition.getPriority()).isEqualTo(2);
    assertThat(ruleDefinition.getCondition()).isEqualTo("rain == true");
    assertThat(ruleDefinition.getActions()).isEqualTo(Collections.singletonList("System.out.println(\"It rains, take an umbrella!\");"));
}

@Test
public void testRuleDefinitionReading_withCompositeAndBasicRules() throws Exception {
    File compositeRuleDescriptor = new File("src/test/resources/composite-rules." + fileExtension);
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(compositeRuleDescriptor));
    assertThat(ruleDefinitions).hasSize(2);
    RuleDefinition ruleDefinition = ruleDefinitions.get(0);
    assertThat(ruleDefinition).isNotNull();
    assertThat(ruleDefinition.getName()).isEqualTo("Movie id rule");
    assertThat(ruleDefinition.getDescription()).isEqualTo("description");
    assertThat(ruleDefinition.getPriority()).isEqualTo(1);
    assertThat(ruleDefinition.getCompositeRuleType()).isEqualTo("UnitRuleGroup");
    assertThat(ruleDefinition.getComposingRules()).isNotEmpty();
    List<RuleDefinition> subrules = ruleDefinition.getComposingRules();
    assertThat(subrules).hasSize(2);
    RuleDefinition subrule = subrules.get(0);
    assertThat(subrule.getName()).isEqualTo("Time is evening");
    assertThat(subrule.getDescription()).isEqualTo("If it's later than 7pm");
    assertThat(subrule.getPriority()).isEqualTo(1);
    subrule = subrules.get(1);
    assertThat(subrule.getName()).isEqualTo("Movie is rated R");
    assertThat(subrule.getDescription()).isEqualTo("If the movie is rated R");
    assertThat(subrule.getPriority()).isEqualTo(1);
    ruleDefinition = ruleDefinitions.get(1);
    assertThat(ruleDefinition).isNotNull();
    assertThat(ruleDefinition.getName()).isEqualTo("weather rule");
    assertThat(ruleDefinition.getDescription()).isEqualTo("when it rains, then take an umbrella");
    assertThat(ruleDefinition.getComposingRules()).isEmpty();
    assertThat(ruleDefinition.getCondition()).isEqualTo("rain == True");
    assertThat(ruleDefinition.getActions()).isEqualTo(Collections.singletonList("System.out.println(\"It rains, take an umbrella!\");"));
}

@Test
public void testEmptyRulesDefinitionReading() throws Exception {
    File rulesDescriptor = new File("src/test/resources/rules-empty." + fileExtension);
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(rulesDescriptor));
    assertThat(ruleDefinitions).hasSize(0);
}

@Test
public void testRuleDefinitionReadingFromFile() throws Exception {
    File adultRuleDescriptor = new File("src/test/resources/adult-rule." + fileExtension);
    List<RuleDefinition> ruleDefinitions = ruleDefinitionReader.read(new FileReader(adultRuleDescriptor));
    assertThat(ruleDefinitions).hasSize(1);
    RuleDefinition adultRuleDefinition = ruleDefinitions.get(0);
    assertThat(adultRuleDefinition).isNotNull();
    assertThat(adultRuleDefinition.getName()).isEqualTo("adult rule");
    assertThat(adultRuleDefinition.getDescription()).isEqualTo("when age is greater than 18, then mark as adult");
    assertThat(adultRuleDefinition.getPriority()).isEqualTo(1);
    assertThat(adultRuleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(adultRuleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
}


@Test(expected = IllegalArgumentException.class)
public void twoRulesWithSameHighestPriorityIsNotAllowed() {
    conditionalRuleGroup.addRule(new MyOtherRule(1));
    conditionalRuleGroup.addRule(new MyOtherRule(2));
    conditionalRuleGroup.addRule(new MyRule());
    rules.register(conditionalRuleGroup);
    rulesEngine.fire(rules, facts);
}

public void addRule(final Object rule) {
    Rule proxy = RuleProxy.asRule(rule);
    rules.add(proxy);
    proxyRules.put(rule, proxy);
}

@Test
public void rulesMustNotBeExecutedIfConditionalRuleEvaluatesToFalse() throws Exception {
    when(conditionalRule.evaluate(facts)).thenReturn(false);
    conditionalRuleGroup.addRule(rule1);
    conditionalRuleGroup.addRule(rule2);
    conditionalRuleGroup.addRule(conditionalRule);
    rules.register(conditionalRuleGroup);
    rulesEngine.fire(rules, facts);
    verify(conditionalRule, never()).execute(facts);
    verify(rule1, never()).execute(facts);
    verify(rule2, never()).execute(facts);
}

@Test
public void rulesMustBeExecutedForThoseThatEvaluateToTrueIfConditionalRuleEvaluatesToTrue() throws Exception {
    when(conditionalRule.evaluate(facts)).thenReturn(true);
    conditionalRuleGroup.addRule(rule1);
    conditionalRuleGroup.addRule(rule2);
    conditionalRuleGroup.addRule(conditionalRule);
    rules.register(conditionalRuleGroup);
    rulesEngine.fire(rules, facts);
    verify(conditionalRule, times(1)).execute(facts);
    verify(rule1, never()).execute(facts);
    verify(rule2, times(1)).execute(facts);
}

@Test
public void whenARuleIsRemoved_thenItShouldNotBeEvaluated() throws Exception {
    when(conditionalRule.evaluate(facts)).thenReturn(true);
    conditionalRuleGroup.addRule(rule1);
    conditionalRuleGroup.addRule(rule2);
    conditionalRuleGroup.addRule(conditionalRule);
    conditionalRuleGroup.removeRule(rule2);
    rules.register(conditionalRuleGroup);
    rulesEngine.fire(rules, facts);
    verify(conditionalRule, times(1)).execute(facts);
    verify(rule1, times(1)).evaluate(facts);
    verify(rule1, never()).execute(facts);
    verify(rule2, never()).evaluate(facts);
    verify(rule2, never()).execute(facts);
}

public void removeRule(final Object rule) {
    Rule proxy = proxyRules.get(rule);
    if (proxy != null) {
        rules.remove(proxy);
    }
}


@Test
public void compositeRuleAndComposingRulesMustBeExecuted() throws Exception {
    unitRuleGroup = new UnitRuleGroup();
    unitRuleGroup.addRule(rule1);
    unitRuleGroup.addRule(rule2);
    rules.register(unitRuleGroup);
    rulesEngine.fire(rules, facts);
    verify(rule1).execute(facts);
    verify(rule2).execute(facts);
}

@Test
public void whenARuleIsRemoved_thenItShouldNotBeEvaluated() throws Exception {
    unitRuleGroup = new UnitRuleGroup();
    unitRuleGroup.addRule(rule1);
    unitRuleGroup.addRule(rule2);
    unitRuleGroup.removeRule(rule2);
    rules.register(unitRuleGroup);
    rulesEngine.fire(rules, facts);
    verify(rule1).execute(facts);
    verify(rule2, never()).evaluate(facts);
    verify(rule2, never()).execute(facts);
}

@Test
public void whenNoComposingRulesAreRegistered_thenUnitRuleGroupShouldEvaluateToFalse() {
    unitRuleGroup = new UnitRuleGroup();
    boolean evaluationResult = unitRuleGroup.evaluate(facts);
    assertThat(evaluationResult).isFalse();
}

@Override
public boolean evaluate(Facts facts) {
    if (!rules.isEmpty()) {
        for (Rule rule : rules) {
            if (!rule.evaluate(facts)) {
                return false;
            }
        }
        return true;
    }
    return false;
}


@SuppressWarnings("unchecked")
@Test
public void aRuleWithoutPriorityHasAHighPriororty() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    MyOtherRule rule1 = new MyOtherRule(3);
    conditionalRuleGroup.addRule(rule1);
    conditionalRuleGroup.addRule(new UnprioritizedRule());
    Method m = conditionalRuleGroup.getClass().getDeclaredMethod("sortRules");
    m.setAccessible(true);
    List<Rule> sorted = (List<Rule>) m.invoke(conditionalRuleGroup);
    assertThat(sorted.get(0).getPriority()).isEqualTo(Integer.MAX_VALUE - 1);
    assertThat(sorted.get(1).getPriority()).isEqualTo(3);
}

private List<Rule> sortRules() {
    List<Rule> copy = new ArrayList<>(rules);
    Collections.sort(copy, new Comparator<Rule>() {

        @Override
        public int compare(Rule o1, Rule o2) {
            Integer i2 = o2.getPriority();
            return i2.compareTo(o1.getPriority());
        }
    });
    return copy;
}


@Test
public void testRuleDefinitionReading() throws Exception {
    File adultRuleDescriptor = new File("src/test/resources/adult-rule.yml");
    MVELRuleDefinition adultRuleDefinition = ruleDefinitionReader.read(adultRuleDescriptor);
    assertThat(adultRuleDefinition).isNotNull();
    assertThat(adultRuleDefinition.getName()).isEqualTo("adult rule");
    assertThat(adultRuleDefinition.getDescription()).isEqualTo("when age is greater then 18, then mark as adult");
    assertThat(adultRuleDefinition.getPriority()).isEqualTo(1);
    assertThat(adultRuleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(adultRuleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
}

MVELRuleDefinition read(File descriptor) throws FileNotFoundException {
    Object object = yaml.load(new FileReader(descriptor));
    Map<String, Object> map = (Map<String, Object>) object;
    return createRuleDefinitionFrom(map);
}


@Test
public void whenTheConditionIsTrue_thenActionsShouldBeExecuted() throws Exception {
    Person foo = new Person("foo", 20);
    facts.put("person", foo);
    mvelRule.execute(facts);
    assertThat(foo.isAdult()).isTrue();
}

@Override
public void execute(Facts facts) throws Exception {
    for (Action action : actions) {
        action.execute(facts);
    }
}

@Test
public void whenTheRuleIsTriggered_thenConditionShouldBeEvaluated() throws Exception {
    facts.put("person", new Person("foo", 20));
    boolean evaluationResult = mvelRule.evaluate(facts);
    assertThat(evaluationResult).isTrue();
}

@Override
public boolean evaluate(Facts facts) {
    return condition.evaluate(facts);
}


@Test
public void size() {
    assertThat(rules.size()).isEqualTo(0);
    rules.register(new DummyRule());
    assertThat(rules.size()).isEqualTo(1);
    rules.unregister(new DummyRule());
    assertThat(rules.size()).isEqualTo(0);
}

public int size() {
    return rules.size();
}


@Test
public void testRulesDefinitionReading() throws Exception {
    File rulesDescriptor = new File("src/test/resources/rules.yml");
    List<MVELRuleDefinition> ruleDefinitions = ruleDefinitionReader.readAll(new FileReader(rulesDescriptor));
    assertThat(ruleDefinitions).hasSize(2);
    MVELRuleDefinition ruleDefinition = ruleDefinitions.get(0);
    assertThat(ruleDefinition).isNotNull();
    assertThat(ruleDefinition.getName()).isEqualTo("adult rule");
    assertThat(ruleDefinition.getDescription()).isEqualTo("when age is greater then 18, then mark as adult");
    assertThat(ruleDefinition.getPriority()).isEqualTo(1);
    assertThat(ruleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(ruleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
    ruleDefinition = ruleDefinitions.get(1);
    assertThat(ruleDefinition).isNotNull();
    assertThat(ruleDefinition.getName()).isEqualTo("weather rule");
    assertThat(ruleDefinition.getDescription()).isEqualTo("when it rains, then take an umbrella");
    assertThat(ruleDefinition.getPriority()).isEqualTo(2);
    assertThat(ruleDefinition.getCondition()).isEqualTo("rain == true");
    assertThat(ruleDefinition.getActions()).isEqualTo(Collections.singletonList("System.out.println(\"It rains, take an umbrella!\");"));
}

List<MVELRuleDefinition> readAll(Reader reader) {
    List<MVELRuleDefinition> ruleDefinitions = new ArrayList<>();
    Iterable<Object> rules = yaml.loadAll(reader);
    for (Object rule : rules) {
        Map<String, Object> map = (Map<String, Object>) rule;
        ruleDefinitions.add(createRuleDefinitionFrom(map));
    }
    return ruleDefinitions;
}

@Test
public void testEmptyRulesDefinitionReading() throws Exception {
    File rulesDescriptor = new File("src/test/resources/rules-empty.yml");
    List<MVELRuleDefinition> ruleDefinitions = ruleDefinitionReader.readAll(new FileReader(rulesDescriptor));
    assertThat(ruleDefinitions).hasSize(0);
}


@Test
public void testRulesCreation() throws Exception {
    File rulesDescriptor = new File("src/test/resources/rules.yml");
    Rules rules = MVELRuleFactory.createRulesFrom(new FileReader(rulesDescriptor));
    assertThat(rules).hasSize(2);
    Iterator<Rule> iterator = rules.iterator();
    Rule rule = iterator.next();
    assertThat(rule).isNotNull();
    assertThat(rule.getName()).isEqualTo("adult rule");
    assertThat(rule.getDescription()).isEqualTo("when age is greater then 18, then mark as adult");
    assertThat(rule.getPriority()).isEqualTo(1);
    rule = iterator.next();
    assertThat(rule).isNotNull();
    assertThat(rule.getName()).isEqualTo("weather rule");
    assertThat(rule.getDescription()).isEqualTo("when it rains, then take an umbrella");
    assertThat(rule.getPriority()).isEqualTo(2);
}

public static Rules createRulesFrom(Reader rulesDescriptorReader) throws IOException {
    Rules rules = new Rules();
    List<MVELRuleDefinition> ruleDefinition = reader.readAll(rulesDescriptorReader);
    for (MVELRuleDefinition mvelRuleDefinition : ruleDefinition) {
        rules.register(mvelRuleDefinition.create());
    }
    return rules;
}


@Test
public void testRemove() {
    Fact<Integer> foo = new Fact<>("foo", 1);
    facts.add(foo);
    facts.remove(foo);
    assertThat(facts).isEmpty();
}

public <T> void remove(Fact<T> fact) {
    Objects.requireNonNull(fact, "fact must not be null");
    facts.remove(fact);
}

@Test
public void testAsMap() {
    Fact<Integer> fact1 = new Fact<>("foo", 1);
    Fact<Integer> fact2 = new Fact<>("bar", 2);
    facts.add(fact1);
    facts.add(fact2);
    Map<String, Object> map = facts.asMap();
    assertThat(map).containsKeys("foo", "bar");
    assertThat(map).containsValues(1, 2);
}

public Map<String, Object> asMap() {
    return new HashMap<>(facts);
}

public Map<String, Object> asMap() {
    Map<String, Object> map = new HashMap<>();
    for (Fact<?> fact : facts) {
        map.put(fact.getName(), fact.getValue());
    }
    return map;
}

@Test
public void testGet() {
    Fact<Integer> fact = new Fact<>("foo", 1);
    facts.add(fact);
    Integer value = facts.get("foo");
    assertThat(value).isEqualTo(1);
}

@SuppressWarnings("unchecked")
public <T> T get(String name) {
    Objects.requireNonNull(name);
    return (T) facts.get(name);
}

@SuppressWarnings("unchecked")
public <T> T get(String factName) {
    Objects.requireNonNull(factName, "fact name must not be null");
    Fact<?> fact = getFact(factName);
    if (fact != null) {
        return (T) fact.getValue();
    }
    return null;
}

@Test
public void testPut() {
    facts.put("foo", 1);
    facts.put("bar", 2);
    assertThat(facts).contains(new Fact<>("foo", 1));
    assertThat(facts).contains(new Fact<>("bar", 2));
}

public Object put(String name, Object fact) {
    Objects.requireNonNull(name);
    return facts.put(name, fact);
}

public <T> void put(String name, T value) {
    Objects.requireNonNull(name, "fact name must not be null");
    Objects.requireNonNull(value, "fact value must not be null");
    Fact<?> retrievedFact = getFact(name);
    if (retrievedFact != null) {
        remove(retrievedFact);
    }
    add(new Fact<>(name, value));
}

@Test
public void testGetFact() {
    Fact<Integer> fact = new Fact<>("foo", 1);
    facts.add(fact);
    Fact<?> retrievedFact = facts.getFact("foo");
    assertThat(retrievedFact).isEqualTo(fact);
}

public Fact<?> getFact(String factName) {
    Objects.requireNonNull(factName, "fact name must not be null");
    return facts.stream().filter(fact -> fact.getName().equals(factName)).findFirst().orElse(null);
}

@Test
public void testRemoveByName() {
    Fact<Integer> foo = new Fact<>("foo", 1);
    facts.add(foo);
    facts.remove("foo");
    assertThat(facts).isEmpty();
}

public Object remove(String name) {
    Objects.requireNonNull(name);
    return facts.remove(name);
}

public void remove(String factName) {
    Objects.requireNonNull(factName, "fact name must not be null");
    Fact<?> fact = getFact(factName);
    if (fact != null) {
        remove(fact);
    }
}

@Test
public void testAdd() {
    Fact<Integer> fact1 = new Fact<>("foo", 1);
    Fact<Integer> fact2 = new Fact<>("bar", 2);
    facts.add(fact1);
    facts.add(fact2);
    assertThat(facts).contains(fact1);
    assertThat(facts).contains(fact2);
}

public <T> void add(Fact<T> fact) {
    Objects.requireNonNull(fact, "fact must not be null");
    Fact<?> retrievedFact = getFact(fact.getName());
    if (retrievedFact != null) {
        remove(retrievedFact);
    }
    facts.add(fact);
}


@Test
public void testMissingFact() {
    Rules rules = new Rules();
    rules.register(new AnnotatedParametersRule());
    Facts facts = new Facts();
    facts.put("fact1", new Object());
    Map<org.jeasy.rules.api.Rule, Boolean> results = rulesEngine.check(rules, facts);
    for (boolean b : results.values()) {
        Assert.assertFalse(b);
    }
}


@Test
public void test() throws Exception {
    Facts facts = new Facts();
    facts.add("rain", true);
    facts.add("age", 18);
    Rules rules = new Rules(new WeatherRule(), new AgeRule());
    RulesEngine rulesEngine = new DefaultRulesEngine();
    rulesEngine.fire(rules, facts);
}

@Override
public void fire(Rules rules, Facts facts) {
    if (rules.isEmpty()) {
        LOGGER.warning("No rules registered! Nothing to apply");
        return;
    }
    sort(rules);
    logEngineParameters();
    log(rules, facts);
    apply(rules, facts);
}


@Test
public void testSkipOnFirstNonTriggeredRule() throws Exception {
    rulesEngine.fire(rules, facts);
    verify(rule1, never()).execute(facts);
    verify(rule2, never()).execute(facts);
}


@Test
public void testRuleCreationFromFileReader() throws Exception {
    Reader adultRuleDescriptorAsReader = new FileReader("src/test/resources/adult-rule.yml");
    MVELRule adultRule = MVELRuleFactory.createRuleFrom(adultRuleDescriptorAsReader);
    assertThat(adultRule.getName()).isEqualTo("adult rule");
    assertThat(adultRule.getDescription()).isEqualTo("when age is greater then 18, then mark as adult");
    assertThat(adultRule.getPriority()).isEqualTo(1);
}

public static MVELRule createRuleFrom(Reader ruleDescriptorReader) throws IOException {
    StringBuilder ruleDescriptor = new StringBuilder();
    int charValue;
    while ((charValue = ruleDescriptorReader.read()) != -1) {
        ruleDescriptor.append((char) charValue);
    }
    ruleDescriptorReader.close();
    MVELRuleDefinition ruleDefinition = reader.read(ruleDescriptor.toString());
    return ruleDefinition.create();
}


@Test
public void rulesEngineListenersShouldBeCalledInOrderWhenFiringRules() throws Exception {
    when(rule1.evaluate(facts)).thenReturn(true);
    rules.register(rule1);
    rulesEngine.fire(rules, facts);
    InOrder inOrder = inOrder(rule1, fact1, fact2, rulesEngineListener1, rulesEngineListener2);
    inOrder.verify(rulesEngineListener1).beforeEvaluate(rules, facts);
    inOrder.verify(rulesEngineListener2).beforeEvaluate(rules, facts);
    inOrder.verify(rule1).evaluate(facts);
    inOrder.verify(rule1).execute(facts);
    inOrder.verify(rulesEngineListener1).afterExecute(rules, facts);
    inOrder.verify(rulesEngineListener2).afterExecute(rules, facts);
}

@Override
public void fire(Rules rules, Facts facts) {
    if (rules.isEmpty()) {
        LOGGER.warn("No rules registered! Nothing to apply");
        return;
    }
    logEngineParameters();
    log(rules);
    log(facts);
    apply(rules, facts);
}

@Override
public void fire(Rules rules, Facts facts) {
    triggerListenersBeforeRules(rules, facts);
    if (rules.isEmpty()) {
        LOGGER.warn("No rules registered! Nothing to apply");
        return;
    }
    logEngineParameters();
    log(rules);
    log(facts);
    apply(rules, facts);
    triggerListenersAfterRules(rules, facts);
}


@Test
public void testGetRulesEngineListeners() throws Exception {
    rulesEngine = RulesEngineBuilder.aNewRulesEngine().withRulesEngineListener(rulesEngineListener).build();
    List<RulesEngineListener> rulesEngineListeners = rulesEngine.getRulesEngineListeners();
    assertThat(rulesEngineListeners).contains(rulesEngineListener);
}

@Override
public List<RulesEngineListener> getRulesEngineListeners() {
    return rulesEngineListeners;
}


@Test
public void rulesEngineListenersShouldBeCalledInOrder() throws Exception {
    when(rule1.evaluate(facts)).thenReturn(true);
    rules.register(rule1);
    rulesEngine.fire(rules, facts);
    InOrder inOrder = inOrder(rule1, fact1, fact2, rulesEngineListener1, rulesEngineListener2);
    inOrder.verify(rulesEngineListener1).beforeFiringRules(rules, facts);
    inOrder.verify(rulesEngineListener2).beforeFiringRules(rules, facts);
    inOrder.verify(rule1).evaluate(facts);
    inOrder.verify(rule1).execute(facts);
    inOrder.verify(rulesEngineListener1).afterFiringRules(rules, facts);
    inOrder.verify(rulesEngineListener2).afterFiringRules(rules, facts);
}


@Test
public void testGeneratorIssue41UserNormalizer() throws DiffException {
    DiffRowGenerator generator = DiffRowGenerator.create().lineNormalizer(str -> str.replace("\t", "    ")).build();
    List<DiffRow> rows = generator.generateDiffRows(Arrays.asList("<"), Arrays.asList("<"));
    assertEquals("[[EQUAL,<,<]]", rows.toString());
    rows = generator.generateDiffRows(Arrays.asList("\t<"), Arrays.asList("<"));
    assertEquals("[[CHANGE,    <,<]]", rows.toString());
}

public Builder lineNormalizer(Function<String, String> lineNormalizer) {
    this.lineNormalizer = lineNormalizer;
    return this;
}

@Test
public void testNormalize_List() {
    DiffRowGenerator generator = DiffRowGenerator.create().build();
    assertEquals(Collections.singletonList("    test"), generator.normalizeLines(Collections.singletonList("\ttest")));
}

List<String> normalizeLines(List<String> list) {
    return list.stream().map(lineNormalizer::apply).collect(toList());
}


@Test
public void testParseIssue193Copy() throws IOException {
    UnifiedDiff diff = UnifiedDiffReader.parseUnifiedDiff(UnifiedDiffReaderTest.class.getResourceAsStream("problem_diff_parsing_issue193.diff"));
    UnifiedDiffFile file1 = diff.getFiles().get(0);
    assertThat(file1.getCopyFrom()).isEqualTo("modules/configuration/config/web/pcf/account/AccountContactCV.pcf");
    assertThat(file1.getCopyTo()).isEqualTo("modules/configuration/config/web/pcf/account/AccountContactCV.default.pcf");
}

public String getCopyTo() {
    return copyTo;
}


@Test
public void testParseIssue98() throws IOException {
    UnifiedDiff diff = UnifiedDiffReader.parseUnifiedDiff(UnifiedDiffReaderTest.class.getResourceAsStream("problem_diff_issue98.diff"));
    assertThat(diff.getFiles().size()).isEqualTo(1);
    assertEquals(1, diff.getFiles().size());
    final UnifiedDiffFile file1 = diff.getFiles().get(0);
    assertEquals("100644", file1.getDeletedFileMode());
    assertEquals("src/test/java/se/bjurr/violations/lib/model/ViolationTest.java", file1.getFromFile());
    assertThat(diff.getTail()).isEqualTo("2.25.1");
}

public String getDeletedFileMode() {
    return deletedFileMode;
}


@Test
public void testHandleWriteFailure() {
    final Meter writeFailures = (Meter) this.listener.getMetrics().get(DropwizardApnsClientMetricsListener.WRITE_FAILURES_METER_NAME);
    assertEquals(0, writeFailures.getCount());
    this.listener.handleWriteFailure(null, 1);
    assertEquals(1, writeFailures.getCount());
}

@Override
public void handleWriteFailure(final ApnsClient apnsClient, final long notificationId) {
    this.stopTimerForNotification(notificationId);
    this.writeFailures.mark();
}

@Test
public void testHandleNotificationAccepted() {
    final Meter acceptedNotifications = (Meter) this.listener.getMetrics().get(DropwizardApnsClientMetricsListener.ACCEPTED_NOTIFICATIONS_METER_NAME);
    assertEquals(0, acceptedNotifications.getCount());
    this.listener.handleNotificationAccepted(null, 1);
    assertEquals(1, acceptedNotifications.getCount());
}

@Override
public void handleNotificationAccepted(final ApnsClient apnsClient, final long notificationId) {
    this.stopTimerForNotification(notificationId);
    this.acceptedNotifications.mark();
}

@Test
public void testHandleNotificationRejected() {
    final Meter rejectedNotifications = (Meter) this.listener.getMetrics().get(DropwizardApnsClientMetricsListener.REJECTED_NOTIFICATIONS_METER_NAME);
    assertEquals(0, rejectedNotifications.getCount());
    this.listener.handleNotificationRejected(null, 1);
    assertEquals(1, rejectedNotifications.getCount());
}

@Override
public void handleNotificationRejected(final ApnsClient apnsClient, final long notificationId) {
    this.stopTimerForNotification(notificationId);
    this.rejectedNotifications.mark();
}

@Test
public void testHandleConnectionAddedAndRemoved() {
    @SuppressWarnings("unchecked")
    final Gauge<Integer> openConnectionGauge = (Gauge<Integer>) this.listener.getMetrics().get(DropwizardApnsClientMetricsListener.OPEN_CONNECTIONS_GAUGE_NAME);
    this.listener.handleConnectionAdded(null);
    assertEquals(1, (long) openConnectionGauge.getValue());
    this.listener.handleConnectionRemoved(null);
    assertEquals(0, (long) openConnectionGauge.getValue());
}

@Override
public void handleConnectionRemoved(final ApnsClient apnsClient) {
    this.openConnections.decrementAndGet();
}

@Test
public void testGetMetrics() {
    final Map<String, Metric> metrics = this.listener.getMetrics();
    assertTrue(metrics.get(DropwizardApnsClientMetricsListener.NOTIFICATION_TIMER_NAME) instanceof Timer);
    assertTrue(metrics.get(DropwizardApnsClientMetricsListener.WRITE_FAILURES_METER_NAME) instanceof Meter);
    assertTrue(metrics.get(DropwizardApnsClientMetricsListener.SENT_NOTIFICATIONS_METER_NAME) instanceof Meter);
    assertTrue(metrics.get(DropwizardApnsClientMetricsListener.ACCEPTED_NOTIFICATIONS_METER_NAME) instanceof Meter);
    assertTrue(metrics.get(DropwizardApnsClientMetricsListener.REJECTED_NOTIFICATIONS_METER_NAME) instanceof Meter);
    assertTrue(metrics.get(DropwizardApnsClientMetricsListener.OPEN_CONNECTIONS_GAUGE_NAME) instanceof Gauge);
    assertTrue(metrics.get(DropwizardApnsClientMetricsListener.CONNECTION_FAILURES_METER_NAME) instanceof Meter);
}

@Override
public Map<String, Metric> getMetrics() {
    return this.metrics.getMetrics();
}

@Test
public void testHandleConnectionCreationFailed() {
    final Meter connectionFailures = (Meter) this.listener.getMetrics().get(DropwizardApnsClientMetricsListener.CONNECTION_FAILURES_METER_NAME);
    assertEquals(0, connectionFailures.getCount());
    this.listener.handleConnectionCreationFailed(null);
    assertEquals(1, connectionFailures.getCount());
}

@Override
public void handleConnectionCreationFailed(final ApnsClient apnsClient) {
    this.connectionFailures.mark();
}

@Test
public void testHandleNotificationSent() {
    final Meter sentNotifications = (Meter) this.listener.getMetrics().get(DropwizardApnsClientMetricsListener.SENT_NOTIFICATIONS_METER_NAME);
    assertEquals(0, sentNotifications.getCount());
    this.listener.handleNotificationSent(null, 1);
    assertEquals(1, sentNotifications.getCount());
}

@Override
public void handleNotificationSent(final ApnsClient apnsClient, final long notificationId) {
    this.sentNotifications.mark();
    this.notificationTimerContexts.put(notificationId, this.notificationTimer.time());
}


@Test
public void testHandleNotificationRejected() {
    final Counter rejectedNotifications = this.meterRegistry.get(MicrometerApnsClientMetricsListener.REJECTED_NOTIFICATIONS_COUNTER_NAME).counter();
    assertEquals(0, (int) rejectedNotifications.count());
    this.listener.handleNotificationRejected(null, 1);
    assertEquals(1, (int) rejectedNotifications.count());
}

@Override
public void handleNotificationRejected(final ApnsClient apnsClient, final long notificationId) {
    this.recordEndTimeForNotification(notificationId);
    this.rejectedNotifications.increment();
}

@Test
public void testHandleConnectionCreationFailed() {
    final Counter connectionFailures = this.meterRegistry.get(MicrometerApnsClientMetricsListener.CONNECTION_FAILURES_COUNTER_NAME).counter();
    assertEquals(0, (int) connectionFailures.count());
    this.listener.handleConnectionCreationFailed(null);
    assertEquals(1, (int) connectionFailures.count());
}

@Override
public void handleConnectionCreationFailed(final ApnsClient apnsClient) {
    this.connectionFailures.increment();
}

@Test
public void testHandleConnectionAddedAndRemoved() {
    final Gauge openConnectionGauge = this.meterRegistry.get(MicrometerApnsClientMetricsListener.OPEN_CONNECTIONS_GAUGE_NAME).gauge();
    this.listener.handleConnectionAdded(null);
    assertEquals(1, (int) openConnectionGauge.value());
    this.listener.handleConnectionRemoved(null);
    assertEquals(0, (int) openConnectionGauge.value());
}

@Test
public void testHandleWriteFailure() {
    final Counter writeFailures = this.meterRegistry.get(MicrometerApnsClientMetricsListener.WRITE_FAILURES_COUNTER_NAME).counter();
    assertEquals(0, (int) writeFailures.count());
    this.listener.handleWriteFailure(null, 1);
    assertEquals(1, (int) writeFailures.count());
}

@Override
public void handleWriteFailure(final ApnsClient apnsClient, final long notificationId) {
    this.notificationStartTimes.remove(notificationId);
    this.writeFailures.increment();
}

@Test
public void testHandleNotificationSent() {
    final Counter sentNotifications = this.meterRegistry.get(MicrometerApnsClientMetricsListener.SENT_NOTIFICATIONS_COUNTER_NAME).counter();
    assertEquals(0, (int) sentNotifications.count());
    this.listener.handleNotificationSent(null, 1);
    assertEquals(1, (int) sentNotifications.count());
}

@Override
public void handleNotificationSent(final ApnsClient apnsClient, final long notificationId) {
    this.notificationStartTimes.put(notificationId, System.nanoTime());
    this.sentNotifications.increment();
}

@Test
public void testHandleNotificationAccepted() {
    final Counter acceptedNotifications = this.meterRegistry.get(MicrometerApnsClientMetricsListener.ACCEPTED_NOTIFICATIONS_COUNTER_NAME).counter();
    assertEquals(0, (int) acceptedNotifications.count());
    this.listener.handleNotificationAccepted(null, 1);
    assertEquals(1, (int) acceptedNotifications.count());
}

@Override
public void handleNotificationAccepted(final ApnsClient apnsClient, final long notificationId) {
    this.recordEndTimeForNotification(notificationId);
    this.acceptedNotifications.increment();
}


@Test
@Parameters({ "Zg==,     f", "Zm8=,     fo", "Zm9v,     foo", "Zm9vYg==, foob", "Zm9vYmE=, fooba", "Zm9vYmFy, foobar" })
public void testDecodeBase64EncodedString(final String base64EncodedString, final String decodedAsciiString) {
    assertEquals(decodedAsciiString, new String(ApnsKey.decodeBase64EncodedString(base64EncodedString), StandardCharsets.US_ASCII));
}

protected static byte[] decodeBase64EncodedString(final String base64EncodedString) {
    final ByteBuf base64EncodedByteBuf = Unpooled.wrappedBuffer(base64EncodedString.getBytes(StandardCharsets.US_ASCII));
    final ByteBuf decodedByteBuf = Base64.decode(base64EncodedByteBuf);
    final byte[] decodedBytes = new byte[decodedByteBuf.readableBytes()];
    decodedByteBuf.readBytes(decodedBytes);
    base64EncodedByteBuf.release();
    decodedByteBuf.release();
    return decodedBytes;
}


@Test
public void testEncodeDecodeBase64() {
    final byte[] originalBytes = "We expect to get these bytes back after encoding, then decoding as Base64.".getBytes();
    final String encodedString = AuthenticationToken.encodeUnpaddedBase64UrlString(originalBytes);
    assertArrayEquals(originalBytes, AuthenticationToken.decodeBase64UrlEncodedString(encodedString));
}

static String encodeUnpaddedBase64UrlString(final byte[] data) {
    final ByteBuf wrappedString = Unpooled.wrappedBuffer(data);
    final ByteBuf encodedString = Base64.encode(wrappedString, Base64Dialect.URL_SAFE);
    final String encodedUnpaddedString = encodedString.toString(StandardCharsets.US_ASCII).replace("=", "");
    wrappedString.release();
    encodedString.release();
    return encodedUnpaddedString;
}


@Test
public void testHandlePushNotificationAccepted() {
    final TestParsingMockApnsServerListener listener = new TestParsingMockApnsServerListener();
    {
        final String token = "test-token";
        final Http2Headers headers = new DefaultHttp2Headers().path(APNS_PATH_PREFIX + token);
        listener.handlePushNotificationAccepted(headers, null);
        assertEquals(token, listener.mostRecentPushNotification.getToken());
        assertNull(listener.mostRecentPushNotification.getTopic());
        assertNull(listener.mostRecentPushNotification.getPriority());
        assertNull(listener.mostRecentPushNotification.getExpiration());
        assertNull(listener.mostRecentPushNotification.getCollapseId());
        assertNull(listener.mostRecentPushNotification.getPayload());
    }
    {
        final String topic = "test-topic";
        final Http2Headers headers = new DefaultHttp2Headers().add(APNS_TOPIC_HEADER, topic);
        listener.handlePushNotificationAccepted(headers, null);
        assertNull(listener.mostRecentPushNotification.getToken());
        assertEquals(topic, listener.mostRecentPushNotification.getTopic());
        assertNull(listener.mostRecentPushNotification.getPriority());
        assertNull(listener.mostRecentPushNotification.getExpiration());
        assertNull(listener.mostRecentPushNotification.getCollapseId());
        assertNull(listener.mostRecentPushNotification.getPayload());
    }
    {
        final DeliveryPriority priority = DeliveryPriority.CONSERVE_POWER;
        final Http2Headers headers = new DefaultHttp2Headers().addInt(APNS_PRIORITY_HEADER, priority.getCode());
        listener.handlePushNotificationAccepted(headers, null);
        assertNull(listener.mostRecentPushNotification.getToken());
        assertNull(listener.mostRecentPushNotification.getTopic());
        assertEquals(priority, listener.mostRecentPushNotification.getPriority());
        assertNull(listener.mostRecentPushNotification.getExpiration());
        assertNull(listener.mostRecentPushNotification.getCollapseId());
        assertNull(listener.mostRecentPushNotification.getPayload());
    }
    {
        final Date expiration = new Date(1_000_000_000);
        final Http2Headers headers = new DefaultHttp2Headers().addInt(APNS_EXPIRATION_HEADER, (int) (expiration.getTime() / 1000));
        listener.handlePushNotificationAccepted(headers, null);
        assertNull(listener.mostRecentPushNotification.getToken());
        assertNull(listener.mostRecentPushNotification.getTopic());
        assertNull(listener.mostRecentPushNotification.getPriority());
        assertEquals(expiration, listener.mostRecentPushNotification.getExpiration());
        assertNull(listener.mostRecentPushNotification.getCollapseId());
        assertNull(listener.mostRecentPushNotification.getPayload());
    }
    {
        final String collapseId = "collapse-id";
        final Http2Headers headers = new DefaultHttp2Headers().add(APNS_COLLAPSE_ID_HEADER, collapseId);
        listener.handlePushNotificationAccepted(headers, null);
        assertNull(listener.mostRecentPushNotification.getToken());
        assertNull(listener.mostRecentPushNotification.getTopic());
        assertNull(listener.mostRecentPushNotification.getPriority());
        assertNull(listener.mostRecentPushNotification.getExpiration());
        assertEquals(collapseId, listener.mostRecentPushNotification.getCollapseId());
        assertNull(listener.mostRecentPushNotification.getPayload());
    }
    {
        final String payload = "A test payload!";
        final Http2Headers headers = new DefaultHttp2Headers();
        final ByteBuf payloadBuffer = ByteBufUtil.writeUtf8(UnpooledByteBufAllocator.DEFAULT, payload);
        try {
            listener.handlePushNotificationAccepted(headers, payloadBuffer);
            assertNull(listener.mostRecentPushNotification.getToken());
            assertNull(listener.mostRecentPushNotification.getTopic());
            assertNull(listener.mostRecentPushNotification.getPriority());
            assertNull(listener.mostRecentPushNotification.getExpiration());
            assertNull(listener.mostRecentPushNotification.getCollapseId());
            assertEquals(payload, listener.mostRecentPushNotification.getPayload());
        } finally {
            payloadBuffer.release();
        }
    }
}

public void handlePushNotificationAccepted(final Http2Headers headers, final ByteBuf payload) {
    this.handlePushNotificationAccepted(parsePushNotification(headers, payload));
}

@Test
public void testHandlePushNotificationRejected() {
    final TestParsingMockApnsServerListener listener = new TestParsingMockApnsServerListener();
    {
        final RejectionReason rejectionReason = RejectionReason.BAD_DEVICE_TOKEN;
        listener.handlePushNotificationRejected(new DefaultHttp2Headers(), null, rejectionReason, null);
        assertEquals(rejectionReason, listener.mostRecentRejectionReason);
        assertNull(listener.mostRecentDeviceTokenExpiration);
    }
    {
        final RejectionReason rejectionReason = RejectionReason.BAD_PRIORITY;
        final Date deviceTokenExpiration = new Date();
        listener.handlePushNotificationRejected(new DefaultHttp2Headers(), null, rejectionReason, deviceTokenExpiration);
        assertEquals(rejectionReason, listener.mostRecentRejectionReason);
        assertEquals(deviceTokenExpiration, listener.mostRecentDeviceTokenExpiration);
    }
}

public void handlePushNotificationRejected(final Http2Headers headers, final ByteBuf payload, final RejectionReason rejectionReason, final Date deviceTokenExpirationTimestamp) {
    this.handlePushNotificationRejected(parsePushNotification(headers, payload), rejectionReason, deviceTokenExpirationTimestamp);
}


@Test
public void testApnsIdForRejectedNotification() throws Exception {
    final MockApnsServer server = this.buildServer(new PushNotificationHandlerFactory() {

        @Override
        public PushNotificationHandler buildHandler(final SSLSession sslSession) {
            return new PushNotificationHandler() {

                @Override
                public void handlePushNotification(final Http2Headers headers, final ByteBuf payload) throws RejectedNotificationException {
                    throw new RejectedNotificationException(RejectionReason.MISSING_TOPIC);
                }
            };
        }
    });
    final ApnsClient client = this.buildTokenAuthenticationClient();
    try {
        server.start(PORT).await();
        final SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(DEVICE_TOKEN, TOPIC, PAYLOAD);
        final PushNotificationResponse<SimpleApnsPushNotification> response = client.sendNotification(pushNotification).get();
        assertFalse(response.isAccepted());
        assertNotNull(response.getApnsId());
    } finally {
        client.close().await();
        server.shutdown().await();
    }
}

@Override
public UUID getApnsId() {
    return this.apnsId;
}

@Test
public void testApnsIdForAcceptedNotification() throws Exception {
    final MockApnsServer server = this.buildServer(new AcceptAllPushNotificationHandlerFactory());
    final ApnsClient client = this.buildTokenAuthenticationClient();
    try {
        server.start(PORT).await();
        final SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(DEVICE_TOKEN, TOPIC, PAYLOAD);
        final PushNotificationResponse<SimpleApnsPushNotification> response = client.sendNotification(pushNotification).get();
        assertTrue(response.isAccepted());
        assertNotNull(response.getApnsId());
    } finally {
        client.close().await();
        server.shutdown().await();
    }
}


@Test
public void testSimpleApnsPushNotificationTokenTopicPayloadExpirationPriorityCollapseIdApnsId() {
    final String token = "test-token";
    final String topic = "test-topic";
    final String payload = "{\"test\": true}";
    final Date expiration = new Date();
    final DeliveryPriority priority = DeliveryPriority.CONSERVE_POWER;
    final String collapseId = "test-collapse-id";
    final UUID apnsId = UUID.randomUUID();
    final SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(token, topic, payload, expiration, priority, collapseId, apnsId);
    assertEquals(token, pushNotification.getToken());
    assertEquals(topic, pushNotification.getTopic());
    assertEquals(payload, pushNotification.getPayload());
    assertEquals(expiration, pushNotification.getExpiration());
    assertEquals(priority, pushNotification.getPriority());
    assertEquals(collapseId, pushNotification.getCollapseId());
    assertEquals(apnsId, pushNotification.getApnsId());
}


@Test
public void testAdd() throws Exception {
    Ticker4Test t = new Ticker4Test();
    LocalCacheHandler cache = new LocalCacheHandler(t);
    int seconds = 100;
    String key = "key";
    String value = "value";
    int seconds2 = 200;
    String key2 = "key2";
    String value2 = "value2";
    cache.set(key, value, seconds);
    cache.add(key, value2, seconds);
    cache.add(key2, value2, seconds2);
    assertThat((String) cache.get(key), equalTo(value));
    assertThat((String) cache.get(key2), equalTo(value2));
}

@Override
public void add(String key, Object value, int exptimeSeconds) {
    long now = ticker.read();
    Entry entry = new Entry(value, now + TimeUnit.SECONDS.toNanos(exptimeSeconds));
    cache.putIfAbsent(key, entry);
}

@Test
public void testDelete() throws Exception {
    Ticker4Test t = new Ticker4Test();
    LocalCacheHandler cache = new LocalCacheHandler(t);
    int seconds = 100;
    String key = "key";
    String value = "value";
    cache.set(key, value, seconds);
    assertThat((String) cache.get(key), equalTo(value));
    cache.delete(key);
    assertThat(cache.get(key), nullValue());
}

@Override
public void delete(String key) {
    cache.remove(key);
}


@Test
public void testMonitor() throws Exception {
    final Method m = StatCollectorTest.class.getDeclaredMethod("testMonitor");
    StatCollector sc = new StatCollector();
    final CountDownLatch cdl1 = new CountDownLatch(1);
    final CountDownLatch cdl2 = new CountDownLatch(1);
    final AtomicInteger t = new AtomicInteger();
    sc.initStatMonitor(new StatMonitor() {

        @Override
        public void check(long statStartTime, long statEndTime, List<OperatorStat> stats) throws Exception {
            int round = t.incrementAndGet();
            if (round == 1) {
                assertThat(stats.size(), equalTo(1));
                assertThat(stats.get(0).getDatabaseExecuteSuccessCount(), equalTo(2L));
                assertThat(stats.get(0).getTotalDatabaseExecuteTime(), equalTo(300L));
                cdl1.countDown();
            } else if (round == 2) {
                assertThat(stats.size(), equalTo(1));
                assertThat(stats.get(0).getDatabaseExecuteSuccessCount(), equalTo(1L));
                assertThat(stats.get(0).getTotalDatabaseExecuteTime(), equalTo(100L));
                cdl2.countDown();
            } else {
                throw new Exception();
            }
        }

        @Override
        public int getCheckPeriodSecond() {
            return 1;
        }
    });
    OneExecuteStat stat = OneExecuteStat.create();
    stat.recordDatabaseExecuteSuccess(100);
    stat.recordDatabaseExecuteSuccess(200);
    sc.getCombinedStat(m).getExecuteStat().accumulate(stat);
    cdl1.await();
    stat = OneExecuteStat.create();
    stat.recordDatabaseExecuteSuccess(100);
    sc.getCombinedStat(m).getExecuteStat().accumulate(stat);
    cdl2.await();
}

public synchronized void initStatMonitor(final StatMonitor statMonitor) {
    if (scheduler != null) {
        throw new IllegalStateException("StatMonitor is initialized many times");
    }
    scheduler = Executors.newSingleThreadScheduledExecutor();
    long period = statMonitor.getCheckPeriodSecond();
    scheduler.scheduleAtFixedRate(new Runnable() {

        @Override
        public void run() {
            try {
                StatInfo statInfo = resetAndGetStatInfo();
                statMonitor.check(statInfo.getStatStartTime(), statInfo.getStatEndTime(), statInfo.getStats());
            } catch (Exception e) {
                logger.error("StatMonitor check error", e);
            }
        }
    }, period, period, TimeUnit.SECONDS);
}


@Test
public void test() throws Exception {
    CommonFindAllBuilderFactory factory = new CommonFindAllBuilderFactory();
    String name = "findAll";
    Class<?> entityClass = Order.class;
    Class<Integer> idClass = Integer.class;
    Type returnType = DynamicTokens.iterableToken(TypeToken.of(entityClass)).getType();
    List<Type> parameterTypes = Lists.newArrayList();
    Builder b = factory.doTryGetBuilder(name, returnType, parameterTypes, entityClass, idClass);
    assertThat(b, notNullValue());
    assertThat(b.buildSql(), equalTo("select id, userid, user_age from #table"));
}

public static <E> TypeToken<Iterable<E>> iterableToken(TypeToken<E> entityToken) {
    return new TypeToken<Iterable<E>>() {
    }.where(new TypeParameter<E>() {
    }, entityToken);
}


@Test
public void test() throws Exception {
    CommonFindByIdBuilderFactory factory = new CommonFindByIdBuilderFactory();
    String name = "findById";
    Class<?> entityClass = Order.class;
    Type returnType = DynamicTokens.optionalToken(TypeToken.of(entityClass)).getType();
    Class<Integer> idClass = Integer.class;
    List<Type> types = Lists.newArrayList((Type) idClass);
    Builder b = factory.doTryGetBuilder(name, returnType, types, entityClass, idClass);
    assertThat(b, notNullValue());
    assertThat(b.buildSql(), equalTo("select id, userid, user_age from #table where id = :1"));
}

public static <E> TypeToken<Optional<E>> optionalToken(TypeToken<E> entityToken) {
    return new TypeToken<Optional<E>>() {
    }.where(new TypeParameter<E>() {
    }, entityToken);
}


@Test
public void test() throws Exception {
    CommonFindByIdsBuilderFactory factory = new CommonFindByIdsBuilderFactory();
    String name = "findByIds";
    Class<?> entityClass = Order.class;
    Class<Integer> idClass = Integer.class;
    Type returnType = DynamicTokens.iterableToken(TypeToken.of(entityClass)).getType();
    List<Type> parameterTypes = Lists.newArrayList(DynamicTokens.iterableToken(TypeToken.of(idClass)).getType());
    Builder b = factory.doTryGetBuilder(name, returnType, parameterTypes, entityClass, idClass);
    assertThat(b, notNullValue());
    assertThat(b.buildSql(), equalTo("select id, userid, user_age from #table where id in (:1)"));
}


@Test
public void testIterableToken() throws Exception {
    TypeToken<Iterable<String>> token = DynamicTokens.iterableToken(TypeToken.of(String.class));
    Type expectedType = DynamicTokensTest.class.getMethod("func").getGenericReturnType();
    assertThat(token.getType(), equalTo(expectedType));
}

@Test
public void testOptionalToken() throws Exception {
    TypeToken<Optional<String>> token = DynamicTokens.optionalToken(TypeToken.of(String.class));
    Type expectedType = DynamicTokensTest.class.getMethod("func2").getGenericReturnType();
    assertThat(token.getType(), equalTo(expectedType));
}


@Test
public void testNotReadableParameterException() throws Exception {
    thrown.expect(BindingException.class);
    thrown.expectMessage("Parameter '3' not found, available root parameters are [1, 2]");
    List<Annotation> empty = Collections.emptyList();
    TypeToken<String> t = new TypeToken<String>() {
    };
    ParameterDescriptor p = ParameterDescriptor.create(0, t.getType(), empty, "1");
    ParameterDescriptor p2 = ParameterDescriptor.create(1, t.getType(), empty, "2");
    List<ParameterDescriptor> pds = Arrays.asList(p, p2);
    ParameterContext ctx = DefaultParameterContext.create(pds);
    ctx.getBindingParameterInvoker(BindingParameter.create("3", ""));
}

public static ParameterDescriptor create(int position, Type type, List<Annotation> annotations, String name) {
    return new ParameterDescriptor(position, type, annotations, name);
}

@Test
public void testSelectAndUpdate() throws Exception {
    List<Annotation> empty = Collections.emptyList();
    TypeToken<String> t0 = new TypeToken<String>() {
    };
    ParameterDescriptor p0 = ParameterDescriptor.create(0, t0.getType(), empty, "1");
    TypeToken<Integer> t1 = new TypeToken<Integer>() {
    };
    ParameterDescriptor p1 = ParameterDescriptor.create(1, t1.getType(), empty, "2");
    List<ParameterDescriptor> pds = Arrays.asList(p0, p1);
    ParameterContext ctx = DefaultParameterContext.create(pds);
    assertThat(ctx.getBindingParameterInvoker(BindingParameter.create("1", "")).getTargetType(), equalTo(t0.getType()));
    assertThat(ctx.getBindingParameterInvoker(BindingParameter.create("2", "")).getTargetType(), equalTo(t1.getType()));
}

@Test
public void testNotReadablePropertyException() throws Exception {
    thrown.expect(BindingException.class);
    thrown.expectMessage("Parameter ':1.id' can't be readable; caused by: There is no getter for property named 'id' in 'class java.lang.String'");
    List<Annotation> empty = Collections.emptyList();
    TypeToken<String> t = new TypeToken<String>() {
    };
    ParameterDescriptor p = ParameterDescriptor.create(0, t.getType(), empty, "1");
    List<ParameterDescriptor> pds = Arrays.asList(p);
    ParameterContext ctx = DefaultParameterContext.create(pds);
    ctx.getBindingParameterInvoker(BindingParameter.create("1", "id"));
}


@Test
public void testGetParameterNameByPosition() throws Exception {
    List<Annotation> empty = Collections.emptyList();
    TypeToken<String> t0 = TypeToken.of(String.class);
    ParameterDescriptor p0 = ParameterDescriptor.create(0, t0.getType(), empty, "1");
    TypeToken<Integer> t1 = new TypeToken<Integer>() {
    };
    ParameterDescriptor p1 = ParameterDescriptor.create(1, t1.getType(), empty, "2");
    List<ParameterDescriptor> pds = Arrays.asList(p0, p1);
    ParameterContext ctx = DefaultParameterContext.create(pds);
    assertThat(ctx.getBindingParameterInvoker(BindingParameter.create("1", "")).getTargetType(), equalTo(t0.getType()));
    assertThat(ctx.getBindingParameterInvoker(BindingParameter.create("2", "")).getTargetType(), equalTo(t1.getType()));
}

@Override
public BindingParameterInvoker getBindingParameterInvoker(BindingParameter bindingParameter) {
    String parameterName = bindingParameter.getParameterName();
    Type type = nameToTypeMap.get(parameterName);
    if (type == null) {
        throw new BindingException("Parameter '" + parameterName + "' not found, " + "available root parameters are " + nameToTypeMap.keySet());
    }
    return FunctionalBindingParameterInvoker.create(type, bindingParameter);
}


@Test
public void test() throws Exception {
    CommonFindAllPageBuilderFactory factory = new CommonFindAllPageBuilderFactory();
    String name = "findAll";
    Class<?> entityClass = Order.class;
    Class<Integer> idClass = Integer.class;
    Type returnType = DynamicTokens.pageResultToken(TypeToken.of(entityClass)).getType();
    List<Type> parameterTypes = Lists.newArrayList(Page.class);
    Builder b = factory.doTryGetBuilder(name, returnType, parameterTypes, entityClass, idClass);
    assertThat(b, notNullValue());
    assertThat(b.buildSql(), equalTo("select id, userid, user_age from #table"));
}

public static <E> TypeToken<PageResult<E>> pageResultToken(TypeToken<E> entityToken) {
    return new TypeToken<PageResult<E>>() {
    }.where(new TypeParameter<E>() {
    }, entityToken);
}


@Test
public void test() throws Exception {
    CommonFindAllSortBuilderFactory factory = new CommonFindAllSortBuilderFactory();
    String name = "findAll";
    Class<?> entityClass = Order.class;
    Class<Integer> idClass = Integer.class;
    Type returnType = DynamicTokens.listToken(TypeToken.of(entityClass)).getType();
    List<Type> parameterTypes = Lists.newArrayList(Sort.class);
    Builder b = factory.doTryGetBuilder(name, returnType, parameterTypes, entityClass, idClass);
    assertThat(b, notNullValue());
    assertThat(b.buildSql(), equalTo("select id, userid, user_age from #table"));
}

public static <E> TypeToken<List<E>> listToken(TypeToken<E> entityToken) {
    return new TypeToken<List<E>>() {
    }.where(new TypeParameter<E>() {
    }, entityToken);
}


@Test
public void test() throws Exception {
    PropertyTokenizer prop = new PropertyTokenizer("a.b.c");
    assertThat(prop.getName(), equalTo("a"));
    assertThat(prop.getChildren(), equalTo("b.c"));
    PropertyTokenizer prop2 = new PropertyTokenizer("a");
    assertThat(prop2.getName(), equalTo("a"));
    assertThat(prop2.getChildren(), nullValue());
}

@Nullable
public String getName() {
    return name;
}


@Test
public void testMsg() throws Exception {
    ConsoleLogger logger = new ConsoleLogger("org");
    logger.trace("ok");
}

@Override
public void trace(String msg) {
    println(msg);
}


@Test
public void testCollectionToken() throws Exception {
    TypeToken<Collection<String>> token = DynamicTokens.collectionToken(TypeToken.of(String.class));
    Type expectedType = DynamicTokensTest.class.getMethod("func").getGenericReturnType();
    assertThat(token.getType(), equalTo(expectedType));
}

public static <E> TypeToken<Collection<E>> collectionToken(TypeToken<E> entityToken) {
    return new TypeToken<Collection<E>>() {
    }.where(new TypeParameter<E>() {
    }, entityToken);
}


@Test
void expectNotNull() {
    String foo = "Foo";
    String foo2 = Validate.expectNotNull(foo);
    assertSame(foo, foo2);
    String bar = null;
    boolean threw = false;
    try {
        Validate.expectNotNull(bar);
    } catch (ValidationException e) {
        threw = true;
        assertEquals("Object must not be null", e.getMessage());
    }
    assertTrue(threw);
}

public static <T> T expectNotNull(@Nullable T obj) {
    if (obj == null)
        throw new ValidationException("Object must not be null");
    else
        return obj;
}


@Test
void expectFirstThrows() {
    Document doc = Jsoup.parse("One Two");
    boolean threw = false;
    try {
        doc.expectFirstNode("::comment", Comment.class);
    } catch (IllegalArgumentException e) {
        threw = true;
        assertEquals("No nodes matched the query '::comment' in the document.", e.getMessage());
    }
    assertTrue(threw);
}

public <T extends Node> T expectFirstNode(String cssQuery, Class<T> type) {
    return Validate.expectNotNull(selectFirstNode(cssQuery, type), parent() != null ? "No nodes matched the query '%s' on element '%s'." : "No nodes matched the query '%s' in the document.", cssQuery, this.tagName());
}

@Test
void expectFirstNode() {
    Document doc = Jsoup.parse("One Two");
    TextNode text = doc.expectFirstNode("::text", TextNode.class);
    assertEquals("1", text.parent().id());
    TextNode text2 = doc.selectFirstNode("::text", TextNode.class);
    assertSame(text, text2);
    assertNull(doc.selectFirstNode("::comment", Comment.class));
}


@Test
void firstLastSibling() {
    Document doc = Jsoup.parse("
One Two Three");
    Elements spans = doc.select("span");
    TextNode text = (TextNode) spans.first().nextSibling();
    assertSame(spans.get(0), text.firstSibling());
    assertSame(spans.get(1), text.lastSibling());
}

public Node firstSibling() {
    if (parentNode != null) {
        return parentNode.firstChild();
    } else
        return this;
}


@Test
void parentElement() {
    Document doc = Jsoup.parse("
Text
");
    TextNode text = doc.selectNodes("::text", TextNode.class).first();
    assertNotNull(text);
    Element p = text.parent();
    assertNotNull(p);
    p = ((Node) text).parentElement();
    assertNotNull(p);
}

@Nullable
public Element parentElement() {
    return parentNode;
}


@Test
void wrap() {
    Document doc = Jsoup.parse("One Two Three");
    Nodes<TextNode> nodes = doc.selectNodes("::text:contains(o)", TextNode.class);
    nodes.wrap("");
    assertEquals("One Two Three", doc.body().html());
}

public Nodes<T> wrap(String html) {
    Validate.notEmpty(html);
    for (T node : this) {
        node.wrap(html);
    }
    return this;
}

@Test
void before() {
    Document doc = Jsoup.parse("One Two Three");
    Nodes<TextNode> nodes = doc.selectNodes("::text:contains(o)", TextNode.class);
    nodes.before("<wbr>");
    assertEquals("<wbr>One <wbr>Two Three", doc.body().html());
}

public Nodes<T> before(String html) {
    for (T node : this) {
        node.before(html);
    }
    return this;
}

@Test
void after() {
    Document doc = Jsoup.parse("One Two Three");
    Nodes<TextNode> nodes = doc.selectNodes("::text:contains(o)", TextNode.class);
    nodes.after("<wbr>");
    assertEquals("One<wbr> Two<wbr> Three", doc.body().html());
}

public Nodes<T> after(String html) {
    for (T node : this) {
        node.after(html);
    }
    return this;
}


@Test
void elementsViaNodeInterface() {
    Document doc = Jsoup.parse("
One

Two
");
    Nodes<Element> ps = doc.selectNodes("p", Element.class);
    assertEquals(2, ps.size());
    assertEquals("One", ps.get(0).text());
    assertEquals("Two", ps.get(1).text());
}

public <T extends Node> Nodes<T> selectNodes(String cssQuery, Class<T> type) {
    Validate.notEmpty(cssQuery);
    return selectNodes(evaluatorOf(cssQuery), type);
}

@Test
void nonBlankText() {
    Document doc = Jsoup.parse("

One

Two
");
    Elements els = doc.select(":not(:blank)");
    assertSelectedIds(els, "2", "4");
    Nodes<TextNode> text = doc.selectNodes("::text:not(:blank)", TextNode.class);
    assertEquals(2, text.size());
    assertEquals("One", text.get(0).getWholeText());
    assertEquals("Two", text.get(1).getWholeText());
}

@Test
void nodeContains() {
    Document doc = Jsoup.parse("

One

Two
");
    String q = "div ::node:contains(One)";
    Nodes<Node> nodes = doc.selectNodes(Selector.evaluatorOf(q));
    assertEquals(2, nodes.size());
    assertEquals("One", nodes.get(0).nodeValue());
    assertEquals("p", nodes.get(0).nodeName());
    assertEquals("One", nodes.get(1).nodeValue());
    assertEquals("#text", nodes.get(1).nodeName());
}

@Override
public String nodeValue() {
    return wholeOwnText();
}

@Test
void selectTextNodes() {
    Document doc = Jsoup.parse("
One

Two
");
    Nodes<TextNode> text = doc.selectNodes("p ::text", TextNode.class);
    assertEquals(2, text.size());
    assertEquals("One", text.get(0).getWholeText());
    assertEquals("Two", text.get(1).getWholeText());
}


@Test
void stableHashcode() {
    HashSet<Tag> tags = new HashSet<>();
    Tag img = Tag.valueOf("img");
    Tag IMG = Tag.valueOf("IMG");
    Tag imgS = Tag.valueOf("img", NamespaceSvg, ParseSettings.htmlDefault);
    assertEquals(-2074969810, img.hashCode());
    assertEquals(-2075954866, IMG.hashCode());
    assertEquals(-292873947, imgS.hashCode());
    tags.add(img);
    tags.add(IMG);
    tags.add(imgS);
    imgS.set(Tag.Block);
    assertEquals(-292873947, imgS.hashCode());
    assertTrue(tags.contains(img));
    assertTrue(tags.contains(IMG));
    assertTrue(tags.contains(imgS));
}

@Override
public int hashCode() {
    return Objects.hash(tagName, namespace, normalName, options);
}

@Override
public int hashCode() {
    return Objects.hash(tagName, namespace);
}


@Test
public void asList() {
    Document doc = Jsoup.parse("
One

Two

Three
");
    Elements els = doc.select("p");
    ArrayList<Element> list = els.asList();
    assertEquals(els.size(), list.size());
    list.remove(0);
    assertEquals(3, els.size());
    assertEquals(2, list.size());
    Element el = doc.expectFirst("#1");
    assertSame(doc, el.ownerDocument());
}

public ArrayList<Element> asList() {
    return new ArrayList<>(this);
}

@Test
public void deselect() {
    Document doc = Jsoup.parse("

One

Two

Three

");
    Elements els = doc.select("p");
    Element parent = doc.expectFirst("div");
    Element removedByIndex = els.deselect(1);
    assertEquals("Two", removedByIndex.text());
    assertEquals(2, els.size());
    assertEquals(3, parent.childrenSize());
    Element toRemove = doc.expectFirst("p:contains(Three)");
    boolean removedByObject = els.deselect(toRemove);
    assertTrue(removedByObject);
    assertEquals(1, els.size());
    assertEquals(3, parent.childrenSize());
}

public Element deselect(int index) {
    return super.remove(index);
}


@Test
void testSetTag() {
    Document doc = Jsoup.parse("
Hello
");
    Element el = doc.expectFirst("em");
    el.tag(new Tag("I", NamespaceHtml));
    assertEquals("Hello", el.outerHtml());
}

public Element tag(Tag tag) {
    Validate.notNull(tag);
    this.tag = tag;
    return this;
}


@Test
void customRcdataTag() {
    String inner = "Blah\nblah\n<foo>Foo</foo>\n"";
    String innerText = "Blah\nblah\n<foo>Foo</foo>\n\"";
    String html = "
<x>" + inner + "</x>

<x id=2></x>
";
    TagSet custom = TagSet.Html();
    Tag x = custom.valueOf("x", NamespaceHtml);
    x.set(Tag.RcData);
    Document doc = Jsoup.parse(html, Parser.htmlParser().tagSet(custom));
    Element xEl = doc.expectFirst("x");
    assertEquals(x, xEl.tag());
    assertEquals(innerText, xEl.wholeText());
    Element x2 = doc.expectFirst("#2");
    x2.html(inner);
    assertEquals(innerText, x2.wholeText());
}

public static TagSet Html() {
    return new TagSet(HtmlTagSet);
}

@Test
void customDataTag() {
    String inner = "Blah\nblah\n<foo>Foo</foo>\n"";
    String html = "
<x>" + inner + "</x>

<x id=2></x>
";
    TagSet custom = TagSet.Html();
    Tag x = custom.valueOf("x", NamespaceHtml);
    x.set(Tag.Data);
    Document doc = Jsoup.parse(html, Parser.htmlParser().tagSet(custom));
    Element xEl = doc.expectFirst("x");
    assertEquals(x, xEl.tag());
    assertEquals(inner, xEl.data());
    Element x2 = doc.expectFirst("#2");
    x2.html(inner);
    assertEquals(inner, xEl.data());
}

public Tag set(int option) {
    options |= option;
    options |= Tag.Known;
    return this;
}


@Test
void canRetrieveNewTagsInsensitive() {
    Document doc = Jsoup.parse("

One

");
    TagSet tags = doc.parser().tagSet();
    Tag meta = tags.get("meta", NamespaceHtml);
    assertNotNull(meta);
    assertTrue(meta.isKnownTag());
    Element p = doc.expectFirst("p");
    assertTrue(p.tag().isKnownTag());
    assertNull(tags.get("FOO", NamespaceHtml));
    p.tagName("FOO");
    Tag foo = p.tag();
    assertEquals("foo", foo.name());
    assertEquals("foo", foo.normalName());
    assertEquals(NamespaceHtml, foo.namespace());
    assertFalse(foo.isKnownTag());
    assertSame(foo, tags.get("foo", NamespaceHtml));
    assertSame(foo, tags.valueOf("FOO", NamespaceHtml, doc.parser().settings()));
    assertNull(tags.get("foo", "SomeOtherNamespace"));
}

@Nullable
public Tag get(String tagName, String namespace) {
    Validate.notNull(tagName);
    Validate.notNull(namespace);
    HashMap<String, Tag> nsTags = tags.get(namespace);
    if (nsTags == null)
        return null;
    return nsTags.get(tagName);
}

@Test
void supplyCustomTagSet() {
    TagSet tags = TagSet.Html();
    tags.valueOf("custom", NamespaceHtml).set(Tag.PreserveWhitespace).set(Tag.Block);
    Parser parser = Parser.htmlParser().tagSet(tags);
    Document doc = Jsoup.parse("<body><custom>\n\nFoo\n Bar</custom></body>", parser);
    Element custom = doc.expectFirst("custom");
    assertTrue(custom.tag().preserveWhitespace());
    assertTrue(custom.tag().isBlock());
    assertEquals("<custom>\n" + "\n" + "Foo\n" + " Bar" + "</custom>", custom.outerHtml());
}

public Tag valueOf(String tagName, String namespace) {
    return valueOf(tagName, namespace, ParseSettings.preserveCase);
}

@Test
void canRetrieveNewTagsSensitive() {
    Document doc = Jsoup.parse("

One

", "", Parser.htmlParser().settings(ParseSettings.preserveCase));
    TagSet tags = doc.parser().tagSet();
    Tag meta = tags.get("meta", NamespaceHtml);
    assertNotNull(meta);
    assertTrue(meta.isKnownTag());
    Element p = doc.expectFirst("p");
    assertTrue(p.tag().isKnownTag());
    assertNull(tags.get("FOO", NamespaceHtml));
    p.tagName("FOO");
    Tag foo = p.tag();
    assertEquals("FOO", foo.name());
    assertEquals("foo", foo.normalName());
    assertEquals(NamespaceHtml, foo.namespace());
    assertFalse(foo.isKnownTag());
    assertSame(foo, tags.get("FOO", NamespaceHtml));
    assertSame(foo, tags.valueOf("FOO", NamespaceHtml));
    assertNull(tags.get("FOO", "SomeOtherNamespace"));
}

@Test
void knownTags() {
    TagSet tags = TagSet.Html();
    Tag custom = new Tag("custom");
    assertEquals("custom", custom.name());
    assertEquals(NamespaceHtml, custom.namespace());
    assertFalse(custom.isKnownTag());
    Tag br = tags.get("br", NamespaceHtml);
    assertNotNull(br);
    assertTrue(br.isKnownTag());
    assertSame(br, tags.valueOf("br", NamespaceHtml));
    Tag foo = tags.valueOf("foo", NamespaceHtml);
    assertFalse(foo.isKnownTag());
    tags.add(custom);
    assertTrue(custom.isKnownTag());
    assertSame(custom, tags.get("custom", NamespaceHtml));
    assertSame(custom, tags.valueOf("custom", NamespaceHtml));
    Tag capCustom = tags.valueOf("Custom", NamespaceHtml);
    assertTrue(capCustom.isKnownTag());
    Tag c1 = new Tag("bar");
    assertFalse(c1.isKnownTag());
    c1.set(Tag.Block);
    assertTrue(c1.isKnownTag());
    c1.clear(Tag.Block);
    assertTrue(c1.isKnownTag());
    c1.clear(Tag.Known);
    assertFalse(c1.isKnownTag());
}

public TagSet add(Tag tag) {
    tag.set(Tag.Known);
    doAdd(tag);
    return this;
}


@Test
void updateNameAndNamespace() {
    Tag tag = new Tag("foo", NamespaceHtml);
    tag.name("bar").namespace(NamespaceSvg);
    tag.set(Tag.Block);
    assertEquals("bar", tag.name());
    assertEquals(NamespaceSvg, tag.namespace());
    assertTrue(tag.isBlock());
    Document doc = Jsoup.parse("<foo>One</foo><foo>Two</foo>");
    Tag foo = doc.expectFirst("foo").tag();
    foo.name("BAR");
    assertEquals("<BAR>One</BAR><BAR>Two</BAR>", doc.body().html());
}

public Tag namespace(String namespace) {
    this.namespace = namespace;
    return this;
}

@Test
void canSetOptions() {
    Tag tag = new Tag("foo", NamespaceHtml);
    assertFalse(tag.isKnownTag());
    assertFalse(tag.isEmpty());
    tag.set(Tag.Void);
    assertTrue(tag.isEmpty());
    assertTrue(tag.isKnownTag());
}


@Test
void canSetCustomDataTag() {
    String inner = "Blah\nblah\n<foo></foo>"";
    String xml = "<x><y><z>" + inner + "</z></y></x><x><z id=2></z>";
    TagSet custom = new TagSet();
    Tag z = custom.valueOf("z", NamespaceXml, ParseSettings.preserveCase);
    z.set(Tag.Data);
    Document doc = Jsoup.parse(xml, Parser.xmlParser().tagSet(custom));
    Element zEl = doc.expectFirst("z");
    assertNotSame(z, zEl.tag());
    assertEquals(z, zEl.tag());
    assertEquals(1, zEl.childNodeSize());
    Node child = zEl.childNode(0);
    assertTrue(child instanceof DataNode);
    assertEquals(inner, ((DataNode) child).getWholeData());
    assertEquals(inner, zEl.data());
    Element z2 = doc.expectFirst("#2");
    z2.html(inner);
    assertEquals(inner, ((DataNode) child).getWholeData());
    assertEquals(inner, zEl.data());
}

@Test
void canSetCustomRcdataTag() {
    String inner = "Blah\nblah\n<foo></foo>"";
    String innerText = "Blah\nblah\n<foo></foo>\"";
    String xml = "<x><y><z>" + inner + "</z></y></x><x><z id=2></z>";
    TagSet custom = new TagSet();
    Tag z = custom.valueOf("z", NamespaceXml, ParseSettings.preserveCase);
    z.set(Tag.RcData);
    Document doc = Jsoup.parse(xml, Parser.xmlParser().tagSet(custom));
    Element zEl = doc.expectFirst("z");
    assertNotSame(z, zEl.tag());
    assertEquals(z, zEl.tag());
    assertEquals(1, zEl.childNodeSize());
    Node child = zEl.childNode(0);
    assertTrue(child instanceof TextNode);
    assertEquals(innerText, ((TextNode) child).getWholeText());
    Element z2 = doc.expectFirst("#2");
    z2.html(inner);
    assertEquals(innerText, z2.wholeText());
}

@Test
void prettyFormatsTextInline() {
    String xml = "<package><metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n" + "<dc:identifier id=\"pub-id\">id</dc:identifier>\n" + "<dc:title>title</dc:title>\n" + "<dc:language>ja</dc:language>\n" + "<dc:description>desc</dc:description>\n" + "</metadata></package>";
    Document doc = Jsoup.parse(xml, Parser.xmlParser());
    doc.outputSettings().prettyPrint(true);
    assertEquals("<package>\n" + " <metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n" + "  <dc:identifier id=\"pub-id\">id</dc:identifier><dc:title>title</dc:title> <dc:language>ja</dc:language> <dc:description>desc</dc:description>\n" + " </metadata>\n" + "</package>", doc.html());
    Element meta = doc.expectFirst("metadata");
    Tag metaTag = meta.tag();
    metaTag.set(Tag.Block);
    for (Element inner : meta) inner.tag().set(Tag.Block);
    assertEquals("<package>\n" + " <metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n" + "  <dc:identifier id=\"pub-id\">id</dc:identifier>\n" + "  <dc:title>title</dc:title>\n" + "  <dc:language>ja</dc:language>\n" + "  <dc:description>desc</dc:description>\n" + " </metadata>\n" + "</package>", doc.html());
}


@Test
void nofollowOnlyOnExternalLinks() {
    String html = "One Two Three Four";
    Safelist basic = Safelist.basic().preserveRelativeLinks(true);
    String clean = Jsoup.clean(html, "http://example.com/", basic);
    assertEquals("One Two Three Four", clean);
    String clean2 = Jsoup.clean(html, basic);
    assertEquals("One Two Three Four", clean2);
    assertTrue(Jsoup.isValid(html, basic));
    Safelist relaxed = Safelist.relaxed().preserveRelativeLinks(true).addEnforcedAttribute("a", "rel", "nofollow");
    String clean3 = Jsoup.clean(html, "http://example.com/", relaxed);
    assertEquals("One Two Three Four", clean3);
    assertTrue(Jsoup.isValid(html, relaxed));
    String clean4 = Jsoup.clean(html, relaxed);
    assertEquals("One Two Three Four", clean4);
}

public static String clean(String bodyHtml, String baseUri, Safelist safelist) {
    Document dirty = parseBodyFragment(bodyHtml, baseUri);
    Cleaner cleaner = new Cleaner(safelist);
    Document clean = cleaner.clean(dirty);
    return clean.body().html();
}

public static String clean(String bodyHtml, String baseUri, Safelist safelist) {
    if (baseUri.isEmpty() && safelist.preserveRelativeLinks()) {
        baseUri = DummyUri;
    }
    Document dirty = parseBodyFragment(bodyHtml, baseUri);
    Cleaner cleaner = new Cleaner(safelist);
    Document clean = cleaner.clean(dirty);
    return clean.body().html();
}


@Test
public void deselectAll() {
    Document doc = Jsoup.parse("

One

Two

Three

");
    Elements els = doc.select("p");
    Element parent = doc.expectFirst("div");
    els.deselectAll();
    assertEquals(0, els.size());
    assertEquals(3, parent.childrenSize());
}

public void deselectAll() {
    super.clear();
}


@Test
void sizeDoesNotIncludeInternal() {
    Element el = new Element("el");
    Attributes attrs = el.attributes();
    assertEquals(0, attrs.size());
    assertTrue(attrs.isEmpty());
    attrs.userData("foo", "bar");
    attrs.put(Attributes.internalKey("qux"), "bar");
    assertEquals(0, attrs.size());
    assertEquals(2, attrs.size);
    assertTrue(attrs.isEmpty());
    attrs.put("foo", "bar");
    attrs.put("qux", "bar");
    assertEquals(2, attrs.size());
    assertEquals(4, attrs.size);
    el.clearAttributes();
    assertEquals(0, attrs.size());
    assertEquals(2, attrs.size);
    assertTrue(attrs.isEmpty());
}

public int size() {
    if (size == 0)
        return 0;
    int count = 0;
    for (int i = 0; i < size; i++) {
        if (!isInternalKey(keys[i]))
            count++;
    }
    return count;
}


@Test
public void xmlParserRespectsConfiguredMaxDepth() {
    Parser parser = Parser.xmlParser().setMaxDepth(5);
    Document doc = Jsoup.parse(deepXml(100), "", parser);
    Element target = doc.selectFirst("target");
    assertNotNull(target);
    assertEquals(parser.getMaxDepth(), depth(target));
}

public static Parser xmlParser() {
    return new Parser(new XmlTreeBuilder());
}

public static Parser xmlParser() {
    return new Parser(new XmlTreeBuilder()).setMaxDepth(Integer.MAX_VALUE);
}

@Test
public void xmlParserHasUnlimitedDepthByDefault() {
    Parser parser = Parser.xmlParser();
    Document doc = Jsoup.parse(deepXml(600), "", parser);
    Element target = doc.selectFirst("target");
    assertNotNull(target);
    assertTrue(depth(target) > 512);
}


@Test
void updateNameAndNamespace() {
    Tag tag = new Tag("foo", NamespaceHtml);
    tag.name("bar").namespace(NamespaceSvg);
    assertEquals("bar", tag.name());
    assertEquals(NamespaceSvg, tag.namespace());
    Document doc = Jsoup.parse("<foo>One</foo><foo>Two</foo>");
    Tag foo = doc.expectFirst("foo").tag();
    foo.name("BAR");
    assertEquals("<BAR>One</BAR><BAR>Two</BAR>", doc.body().html());
}


@Test
void isAsciiLetter() {
    assertTrue(StringUtil.isAsciiLetter('a'));
    assertTrue(StringUtil.isAsciiLetter('n'));
    assertTrue(StringUtil.isAsciiLetter('z'));
    assertTrue(StringUtil.isAsciiLetter('A'));
    assertTrue(StringUtil.isAsciiLetter('N'));
    assertTrue(StringUtil.isAsciiLetter('Z'));
    assertFalse(StringUtil.isAsciiLetter(' '));
    assertFalse(StringUtil.isAsciiLetter('-'));
    assertFalse(StringUtil.isAsciiLetter('0'));
    assertFalse(StringUtil.isAsciiLetter('ß'));
    assertFalse(StringUtil.isAsciiLetter('Ě'));
}

public static boolean isAsciiLetter(char c) {
    return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z';
}

@Test
void isDigit() {
    assertTrue(StringUtil.isDigit('0'));
    assertTrue(StringUtil.isDigit('1'));
    assertTrue(StringUtil.isDigit('2'));
    assertTrue(StringUtil.isDigit('3'));
    assertTrue(StringUtil.isDigit('4'));
    assertTrue(StringUtil.isDigit('5'));
    assertTrue(StringUtil.isDigit('6'));
    assertTrue(StringUtil.isDigit('7'));
    assertTrue(StringUtil.isDigit('8'));
    assertTrue(StringUtil.isDigit('9'));
    assertFalse(StringUtil.isDigit('a'));
    assertFalse(StringUtil.isDigit('A'));
    assertFalse(StringUtil.isDigit('ä'));
    assertFalse(StringUtil.isDigit('Ä'));
    assertFalse(StringUtil.isDigit('١'));
    assertFalse(StringUtil.isDigit('୳'));
}

public static boolean isDigit(char c) {
    return c >= '0' && c <= '9';
}

@Test
void isHexDigit() {
    assertTrue(StringUtil.isHexDigit('0'));
    assertTrue(StringUtil.isHexDigit('1'));
    assertTrue(StringUtil.isHexDigit('2'));
    assertTrue(StringUtil.isHexDigit('3'));
    assertTrue(StringUtil.isHexDigit('4'));
    assertTrue(StringUtil.isHexDigit('5'));
    assertTrue(StringUtil.isHexDigit('6'));
    assertTrue(StringUtil.isHexDigit('7'));
    assertTrue(StringUtil.isHexDigit('8'));
    assertTrue(StringUtil.isHexDigit('9'));
    assertTrue(StringUtil.isHexDigit('a'));
    assertTrue(StringUtil.isHexDigit('b'));
    assertTrue(StringUtil.isHexDigit('c'));
    assertTrue(StringUtil.isHexDigit('d'));
    assertTrue(StringUtil.isHexDigit('e'));
    assertTrue(StringUtil.isHexDigit('f'));
    assertTrue(StringUtil.isHexDigit('A'));
    assertTrue(StringUtil.isHexDigit('B'));
    assertTrue(StringUtil.isHexDigit('C'));
    assertTrue(StringUtil.isHexDigit('D'));
    assertTrue(StringUtil.isHexDigit('E'));
    assertTrue(StringUtil.isHexDigit('F'));
    assertFalse(StringUtil.isHexDigit('g'));
    assertFalse(StringUtil.isHexDigit('G'));
    assertFalse(StringUtil.isHexDigit('ä'));
    assertFalse(StringUtil.isHexDigit('Ä'));
    assertFalse(StringUtil.isHexDigit('١'));
    assertFalse(StringUtil.isHexDigit('୳'));
}

public static boolean isHexDigit(char c) {
    return isDigit(c) || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F';
}


@Test
public void testIsNthLastOfTypeToString() {
    Evaluator.IsNthLastOfType evaluator = new Evaluator.IsNthLastOfType(2, 3);
    assertEquals(":nth-last-of-type(2n+3)", evaluator.toString());
}

@Override
public String toString() {
    if (a == 0)
        return String.format(":%s(%d)", getPseudoClass(), b);
    if (b == 0)
        return String.format(":%s(%dn)", getPseudoClass(), a);
    return String.format(":%s(%dn%+d)", getPseudoClass(), a, b);
}

@Override
public String toString() {
    if (step == 0)
        return String.format(":%s(%d)", getPseudoClass(), offset);
    if (offset == 0)
        return String.format(":%s(%dn)", getPseudoClass(), step);
    return String.format(":%s(%dn%+d)", getPseudoClass(), step, offset);
}

@Test
public void testIsNthChildToStringVariants() {
    Evaluator.IsNthChild evaluator1 = new Evaluator.IsNthChild(0, 3);
    assertEquals(":nth-child(3)", evaluator1.toString());
    Evaluator.IsNthChild evaluator2 = new Evaluator.IsNthChild(2, 0);
    assertEquals(":nth-child(2n)", evaluator2.toString());
    Evaluator.IsNthChild evaluator3 = new Evaluator.IsNthChild(2, 3);
    assertEquals(":nth-child(2n+3)", evaluator3.toString());
}

@Test
public void testIsNthOfTypeToString() {
    Evaluator.IsNthOfType evaluator = new Evaluator.IsNthOfType(2, 3);
    assertEquals(":nth-of-type(2n+3)", evaluator.toString());
}

@Test
public void testTagStartsWithToString() {
    Evaluator.TagStartsWith evaluator = new Evaluator.TagStartsWith("ns");
    assertEquals("ns|*", evaluator.toString());
}

@Override
public String toString() {
    return String.format("%s", tagName);
}

@Override
public String toString() {
    return String.format("%s|*", tagName);
}

@Test
public void testIsNthLastChildToString() {
    Evaluator.IsNthLastChild evaluator = new Evaluator.IsNthLastChild(2, 3);
    assertEquals(":nth-last-child(2n+3)", evaluator.toString());
}

@Test
public void testIsNthChildToString() {
    Evaluator.IsNthChild evaluator = new Evaluator.IsNthChild(2, 3);
    assertEquals(":nth-child(2n+3)", evaluator.toString());
}

@Test
public void testTagEndsWithToString() {
    Evaluator.TagEndsWith evaluator = new Evaluator.TagEndsWith("div");
    assertEquals("*|div", evaluator.toString());
}

@Override
public String toString() {
    return String.format("*|%s", tagName);
}


@Test
void sendsRequestBodyStream() throws IOException {
    final String body = "{key:value}";
    InputStream stream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
    Document doc = Jsoup.connect(echoUrl).requestBodyStream(stream).header("Content-Type", "application/json").data("foo", "true").post();
    assertEquals("POST", ihVal("Method", doc));
    assertEquals("application/json", ihVal("Content-Type", doc));
    assertEquals("foo=true", ihVal("Query String", doc));
    assertEquals(body, ihVal("Post Data", doc));
}

@Override
public Connection requestBodyStream(InputStream stream) {
    req.requestBodyStream(stream);
    return this;
}


@Test
void expectFirst() {
    Document doc = Jsoup.parse("
One

Two Jsoup

Three
");
    Element span = doc.children().expectFirst("span");
    assertNotNull(span);
    assertEquals("Jsoup", span.text());
}

public Element expectFirst(String cssQuery) {
    return (Element) Validate.ensureNotNull(Selector.selectFirst(cssQuery, this), "No elements matched the query '%s' in the elements.", cssQuery);
}

@Test
void selectFirstFromPreviousSelect() {
    Document doc = Jsoup.parse("

One

Two

Three

");
    Elements divs = doc.select("div");
    assertEquals(3, divs.size());
    Element span = divs.selectFirst("p span");
    assertNotNull(span);
    assertEquals("Two", span.text());
    assertNotNull(span.selectFirst("span"));
    assertNull(span.selectFirst(">span"));
    assertNotNull(divs.selectFirst("div"));
    assertNull(divs.selectFirst(">div"));
}

@Nullable
public Element selectFirst(String cssQuery) {
    return Selector.selectFirst(cssQuery, this);
}

@Test
void selectFirst() {
    Document doc = Jsoup.parse("
One

Two Jsoup

Three
");
    Element span = doc.children().selectFirst("span");
    assertNotNull(span);
    assertEquals("Jsoup", span.text());
}

@Test
void expectFirstThrowsOnNoMatch() {
    Document doc = Jsoup.parse("
One

Two

Three
");
    boolean threw = false;
    try {
        Element span = doc.children().expectFirst("span");
    } catch (IllegalArgumentException e) {
        threw = true;
        assertEquals("No elements matched the query 'span' in the elements.", e.getMessage());
    }
    assertTrue(threw);
}

@Test
void selectFirstNullOnNoMatch() {
    Document doc = Jsoup.parse("
One

Two

Three
");
    Element span = doc.children().selectFirst("span");
    assertNull(span);
}


@Test
public void authorize() {
    AuthRequest request = new AuthWeChatEnterpriseWebRequest(AuthConfig.builder().clientId("a").clientSecret("a").redirectUri("https://www.justauth.cn").build());
    System.out.println(request.authorize(AuthStateUtils.createState()));
}

@Override
public String authorize(String state) {
    return UrlBuilder.fromBaseUrl(source.authorize()).queryParam("appid", config.getClientId()).queryParam("agentid", config.getAgentId()).queryParam("redirect_uri", GlobalAuthUtils.urlEncode(config.getRedirectUri())).queryParam("response_type", "code").queryParam("scope", this.getScopes(",", false, AuthScopeUtils.getDefaultScopes(AuthWeChatEnterpriseWebScope.values()))).queryParam("state", getRealState(state).concat("#wechat_redirect")).build();
}

@Override
public String authorize(String state) {
    return UrlBuilder.fromBaseUrl(source.authorize()).queryParam("appid", config.getClientId()).queryParam("redirect_uri", GlobalAuthUtils.urlEncode(config.getRedirectUri())).queryParam("response_type", "code").queryParam("scope", this.getScopes(",", false, AuthScopeUtils.getDefaultScopes(AuthWeChatEnterpriseWebScope.values()))).queryParam("state", getRealState(state).concat("#wechat_redirect")).build();
}