@Test
public void testParseColumnSimpleDateTimestampPattern() {
    String pattern = "timestamp";
    DateParser parser = new DateParser(pattern);
    ByteBuffer bb = SimpleDateType.instance.fromString("2015-10-10");
    Column<Integer> column = Column.builder("date").buildWithDecomposed(bb, SimpleDateType.instance);
    Date actualDate = parser.parse(column);
    Date expectedDate = parser.parse(1444435200000L);
    assertEquals("Date parser fails with SimpleDateType and timestamp pattern", expectedDate, actualDate);
}

public <K> Date parse(Column<K> column) {
    if (column.getType() instanceof SimpleDateType) {
        long timestamp = SimpleDateType.instance.toTimeInMillis(column.getDecomposedValue());
        if (concurrentDateFormat == null) {
            return new Date(timestamp);
        } else {
            int offset = concurrentDateFormat.get().getTimeZone().getOffset(timestamp);
            return new Date(timestamp - offset);
        }
    } else {
        return parse(column.getComposedValue());
    }
}

@Test
public void testParseColumnSimpleDateDifferentPattern() {
    String pattern = "yyyy/MM/dd";
    DateParser parser = new DateParser(pattern);
    ByteBuffer bb = SimpleDateType.instance.fromString("2015-10-10");
    Column<Integer> column = Column.builder("date").buildWithDecomposed(bb, SimpleDateType.instance);
    Date actualDate = parser.parse(column);
    Date expectedDate = parser.parse("2015/10/10");
    assertEquals("Date parser fails with SimpleDateType and different pattern", expectedDate, actualDate);
}

@Test
public void testParseColumnSimpleDateSamePattern() {
    String pattern = "yyyy-MM-dd";
    DateParser parser = new DateParser(pattern);
    ByteBuffer bb = SimpleDateType.instance.fromString("2015-10-10");
    Column<Integer> column = Column.builder("date").buildWithDecomposed(bb, SimpleDateType.instance);
    Date actualDate = parser.parse(column);
    Date expectedDate = parser.parse("2015-10-10");
    assertEquals("Date parser fails with SimpleDateType and default pattern", expectedDate, actualDate);
}


@Test
public void testConvexHullTransformationToString() {
    GeoTransformation transformation = new ConvexHull();
    assertEquals("ConvexHull{}", transformation.toString());
}

@Override
public String toString() {
    return MoreObjects.toStringHelper(this).toString();
}


@Test
public void testComposeDecomposeEmpty() {
    ByteBuffer[] bbs = ByteBufferUtils.decompose(ByteBufferUtils.compose());
    assertEquals("Compose-decompose with empty components is wrong", 0, bbs.length);
}

public static ByteBuffer[] decompose(ByteBuffer bb) {
    int countComponents = ByteBufferUtil.readShortLength(bb);
    ByteBuffer[] components = new ByteBuffer[countComponents];
    for (int i = 0; i < countComponents; i++) {
        int length = ByteBufferUtil.readShortLength(bb);
        components[i] = ByteBufferUtil.readBytes(bb, length);
    }
    return components;
}

@Test
public void testComposeDecompose() {
    ByteBuffer[] bbs = ByteBufferUtils.decompose(ByteBufferUtils.compose(UTF8Type.instance.decompose("test"), Int32Type.instance.decompose(999), BooleanType.instance.decompose(true)));
    assertEquals("Compose-decompose is wrong", 3, bbs.length);
    assertEquals("Compose-decompose is wrong", "test", UTF8Type.instance.compose(bbs[0]));
    assertEquals("Compose-decompose is wrong", 999, Int32Type.instance.compose(bbs[1]), 0);
    assertEquals("Compose-decompose is wrong", true, BooleanType.instance.compose(bbs[2]));
}


@Test
public void testParseWKTPolygon() throws Exception {
    String wkt = "LINESTRING (30 10, 10 30, 40 40)";
    JtsGeometry geometry = geometryFromWKT(CONTEXT, wkt);
    assertEquals("Unable to parse WKT polygon", wkt, geometry.toString());
}

public static JtsGeometry geometryFromWKT(JtsSpatialContext context, String string) {
    if (StringUtils.isBlank(string)) {
        throw new IndexException("Shape shouldn't be blank");
    }
    try {
        GeometryFactory geometryFactory = context.getGeometryFactory();
        WKTReader reader = new WKTReader(geometryFactory);
        Geometry geometry = reader.read(string);
        return context.makeShape(geometry);
    } catch (ParseException | IllegalArgumentException e) {
        throw new IndexException(e, "Shape '%s' is not parseable", string);
    }
}

public static JtsGeometry geometryFromWKT(JtsSpatialContext context, String string) {
    if (StringUtils.isBlank(string)) {
        throw new IndexException("Shape shouldn't be blank");
    }
    try {
        GeometryFactory geometryFactory = context.getGeometryFactory();
        WKTReader reader = new WKTReader(geometryFactory);
        Geometry geometry = reader.read(string);
        if (!geometry.isValid()) {
            geometry = geometry.buffer(0);
        }
        return context.makeShape(geometry);
    } catch (ParseException | IllegalArgumentException e) {
        throw new IndexException(e, "Shape '%s' is not parseable", string);
    }
}

@Test
public void testParseWKTPoint() throws Exception {
    String wkt = "POINT (30.01 -10.7)";
    JtsGeometry geometry = geometryFromWKT(CONTEXT, wkt);
    assertEquals("Unable to parse WKT point", wkt, geometry.toString());
}

@Test
public void testParseWKTSelfIntersectingPolygon() throws Exception {
    String wkt = "POLYGON((5 0, 10 0, 10 10, 0 10, 0 0, 5 0, 3 3, 5 6, 7 3, 5 0))";
    JtsGeometry geometry = geometryFromWKT(CONTEXT, wkt);
    assertEquals("Unable to parse invalid WKT shape", "POLYGON ((5 0, 0 0, 0 10, 10 10, 10 0, 5 0), (5 0, 7 3, 5 6, 3 3, 5 0))", geometry.toString());
}

@Test
public void testParseWKTMultipolygon() throws Exception {
    String wkt = "MULTIPOLYGON (((30 20, 45 40, 10 40, 30 20)), ((15 5, 40 10, 10 20, 5 10, 15 5)))";
    JtsGeometry geometry = geometryFromWKT(CONTEXT, wkt);
    assertEquals("Unable to parse WKT multipolygon", wkt, geometry.toString());
}

@Test
public void testParseWKTLine() throws Exception {
    String wkt = "LINESTRING (30 10, 10 30, 40 40)";
    JtsGeometry geometry = geometryFromWKT(CONTEXT, wkt);
    assertEquals("Unable to parse WKT line", wkt, geometry.toString());
}

@Test
public void testParseWKTMultiline() throws Exception {
    String wkt = "MULTILINESTRING ((10 10, 20 20, 10 40), (40 40, 30 30, 40 20, 30 10))";
    JtsGeometry geometry = geometryFromWKT(CONTEXT, wkt);
    assertEquals("Unable to parse WKT multiline", wkt, geometry.toString());
}

@Test
public void testParseWKTMultipoint() throws Exception {
    String wkt = "MULTIPOINT ((10 40), (40 30), (20 20), (30 10))";
    JtsGeometry geometry = geometryFromWKT(CONTEXT, wkt);
    assertEquals("Unable to parse WKT multipoint", wkt, geometry.toString());
}


@Test
public void testInvolvedFields() {
    assertEquals("Involved fields is wrong", Sets.newHashSet("f0", "f1", "f2"), lucene("f1:3 \t AND f2:").defaultField("f0").build().involvedFields());
    assertEquals("Involved fields is wrong with default field", Sets.newHashSet(LuceneCondition.DEFAULT_FIELD), lucene("f1 f2").build().involvedFields());
    assertEquals("Involved fields is wrong with complex expressions", Sets.newHashSet(LuceneCondition.DEFAULT_FIELD, "date"), lucene("\"jakarta apache\"^4 date:[20020101 TO 20030101]").build().involvedFields());
}

public Set<String> involvedFields() {
    Set<String> fields = new LinkedHashSet<>();
    if (!StringUtils.isBlank(defaultField)) {
        fields.add(defaultField);
    }
    for (String term : query.split("[ ]")) {
        if (term.contains(":")) {
            fields.add(term.split(":")[0]);
        }
    }
    return fields;
}


@Test
public void testRemoveSchema14() {
    String sql = "select `t_order`.`brand`,DATE_FORMAT(order_date, '%H:%i') as `time`,count(1) as `_$COUNT$_rpda_0`," + "DATE_FORMAT(t_order.order_date, '%H:%i') as `rpda_1` from  `t_order` where  ( `t_order`.`brand` = 'hjh' " + "AND `t_order`.`order_date` > '2022-03-26 11:30:00' AND `t_order`.`order_date` < '2022-03-26:11:40:00') " + "GROUP BY `t_order`.`brand`,DATE_FORMAT(order_date, '%H:%i') ORDER BY `t_order`.`brand` ASC,rpda_1 ASC";
    String afterAql = RouterUtil.removeSchema(sql, "order", true);
    Assert.assertEquals(sql, afterAql);
}

public static String removeSchema(String stmt, String schema, boolean isLowerCase) {
    final String forCmpStmt = isLowerCase ? stmt.toLowerCase() : stmt;
    final String maySchema1 = schema + ".";
    final String maySchema2 = "`" + schema + "`.";
    int index1 = forCmpStmt.indexOf(maySchema1);
    int index2 = forCmpStmt.indexOf(maySchema2);
    if (index1 < 0 && index2 < 0) {
        return stmt;
    }
    int startPos = 0;
    boolean flag;
    int firstE = forCmpStmt.indexOf("'");
    int endE = forCmpStmt.lastIndexOf("'");
    StringBuilder result = new StringBuilder();
    while (index1 >= 0 || index2 >= 0) {
        if (index1 < 0) {
            flag = true;
        } else if (index2 < 0) {
            flag = false;
        } else {
            flag = index2 < index1;
        }
        if (flag) {
            result.append(stmt, startPos, index2);
            startPos = index2 + maySchema2.length();
            if (index2 > firstE && index2 < endE && countChar(stmt, index2) % 2 != 0) {
                result.append(stmt, index2, startPos);
            }
            index2 = forCmpStmt.indexOf(maySchema2, startPos);
        } else {
            result.append(stmt, startPos, index1);
            startPos = index1 + maySchema1.length();
            if (index1 > firstE && index1 < endE && countChar(stmt, index1) % 2 != 0) {
                result.append(stmt, index1, startPos);
            }
            index1 = forCmpStmt.indexOf(maySchema1, startPos);
        }
    }
    result.append(stmt.substring(startPos));
    return result.toString();
}

public static String removeSchema(String stmt, String schema, boolean isLowerCase) {
    final String forCmpStmt = isLowerCase ? stmt.toLowerCase() : stmt;
    final String maySchema1 = schema + ".";
    final String maySchema2 = "`" + schema + "`.";
    int index1 = forCmpStmt.indexOf(maySchema1);
    int index2 = forCmpStmt.indexOf(maySchema2);
    if (index1 < 0 && index2 < 0) {
        return stmt;
    }
    if (!checkPrefix(forCmpStmt, index1)) {
        index1 = -1;
    }
    if (!checkPrefix(forCmpStmt, index2)) {
        index2 = -1;
    }
    int startPos = 0;
    boolean flag;
    int firstE = forCmpStmt.indexOf("'");
    int endE = forCmpStmt.lastIndexOf("'");
    StringBuilder result = new StringBuilder();
    while (index1 >= 0 || index2 >= 0) {
        if (index1 < 0) {
            flag = true;
        } else if (index2 < 0) {
            flag = false;
        } else {
            flag = index2 < index1;
        }
        if (flag) {
            result.append(stmt, startPos, index2);
            startPos = index2 + maySchema2.length();
            if (index2 > firstE && index2 < endE && countChar(stmt, index2) % 2 != 0) {
                result.append(stmt, index2, startPos);
            }
            index2 = recursionIndexOf(forCmpStmt, maySchema2, startPos);
        } else {
            result.append(stmt, startPos, index1);
            startPos = index1 + maySchema1.length();
            if (index1 > firstE && index1 < endE && countChar(stmt, index1) % 2 != 0) {
                result.append(stmt, index1, startPos);
            }
            index1 = recursionIndexOf(forCmpStmt, maySchema1, startPos);
        }
    }
    result.append(stmt.substring(startPos));
    return result.toString();
}


@Test
public void testDigitsVersionInfo2() throws Exception {
    GitFlowVersionInfo info = new GitFlowVersionInfo("0.9-RC3-feature-SNAPSHOT");
    Assert.assertNotNull(info);
    info = info.digitsVersionInfo();
    Assert.assertNotNull(info);
    Assert.assertEquals(new GitFlowVersionInfo("0.9"), info);
}

public GitFlowVersionInfo digitsVersionInfo() throws VersionParseException {
    return new GitFlowVersionInfo(joinDigitString(getDigits()));
}

@Test
public void testDigitsVersionInfo() throws Exception {
    GitFlowVersionInfo info = new GitFlowVersionInfo("0.9");
    Assert.assertNotNull(info);
    info = info.digitsVersionInfo();
    Assert.assertNotNull(info);
    Assert.assertEquals(new GitFlowVersionInfo("0.9"), info);
}


@Test
public void testWithoutVersionPolicy() throws Exception {
    GitFlowVersionInfo info1 = new GitFlowVersionInfo("1.0.0-SNAPSHOT", null);
    Assert.assertEquals("1.0.0", info1.getReleaseVersionString());
    Assert.assertEquals("1.0.1-SNAPSHOT", info1.nextSnapshotVersion());
    GitFlowVersionInfo info2 = new GitFlowVersionInfo("1.0.1-SNAPSHOT", null);
    Assert.assertEquals("1.0.1", info2.getReleaseVersionString());
    Assert.assertEquals("1.0.2-SNAPSHOT", info2.nextSnapshotVersion());
}

@Override
public String getReleaseVersionString() {
    if (versionPolicy != null) {
        try {
            VersionPolicyRequest request = new VersionPolicyRequest().setVersion(this.toString());
            return versionPolicy.getReleaseVersion(request).getVersion();
        } catch (PolicyException | VersionParseException ex) {
            throw new RuntimeException("Unable to get release version from policy.", ex);
        }
    }
    return super.getReleaseVersionString();
}

@Test
public void testWithVersionPolicy() throws Exception {
    GitFlowVersionInfo info1 = new GitFlowVersionInfo("1.0.0-SNAPSHOT", new OddEvenVersionPolicy());
    Assert.assertEquals("1.0.0", info1.getReleaseVersionString());
    Assert.assertEquals("1.0.1-SNAPSHOT", info1.nextSnapshotVersion());
    GitFlowVersionInfo info2 = new GitFlowVersionInfo("1.0.1-SNAPSHOT", new OddEvenVersionPolicy());
    Assert.assertEquals("1.0.2", info2.getReleaseVersionString());
    Assert.assertEquals("1.0.3-SNAPSHOT", info2.nextSnapshotVersion());
}


@Test
public void handleRetry() {
    DynamodbEvent normal_event = new DynamodbEvent();
    List<DynamodbEvent.DynamodbStreamRecord> normalRecords = new ArrayList<>();
    normalRecords.add(new DynamodbEvent.DynamodbStreamRecord());
    normal_event.setRecords(normalRecords);
    List<PutEventsResultEntry> putEventsResultEntryList = new ArrayList<>();
    putEventsResultEntryList.add(PutEventsResultEntry.builder().errorCode("formatError").errorMessage("request format error").build());
    PutEventsResponse putEventsResponse = PutEventsResponse.builder().failedEntryCount(1).entries(putEventsResultEntryList).build();
    when(awsEventClient.putEvents(any(PutEventsRequest.class))).thenReturn(putEventsResponse);
    eventBridgePublisher.publish(normal_event);
    verify(awsEventClient, times(4)).putEvents(any(PutEventsRequest.class));
    verify(awsSqsClient).sendMessage(any(SendMessageRequest.class));
}

@Override
@SneakyThrows(JsonProcessingException.class)
public void publish(DynamodbEvent event) {
    final List<DynamodbEvent.DynamodbStreamRecord> ddbStreamRecords = event.getRecords();
    List<PutEventsRequestEntry> putEventsRequestEntryList = new ArrayList<>();
    String eventBusName = System.getenv("EventBusName");
    for (DynamodbEvent.DynamodbStreamRecord ddbStreamRecord : ddbStreamRecords) {
        String strRecordJSON = objectMapper.writeValueAsString(ddbStreamRecord);
        putEventsRequestEntryList.add(PutEventsRequestEntry.builder().eventBusName(eventBusName).time(Instant.now()).source("com.amazonaws.dynamodb.stream.fanout").detailType("ddb_stream").detail(strRecordJSON).build());
    }
    PutEventsRequest putEventsRequest = PutEventsRequest.builder().entries(putEventsRequestEntryList).build();
    PutEventsResponse putEventsResult = awsEventsClient.putEvents(putEventsRequest);
    int EBRetryLimit = Integer.parseInt(System.getenv("EBRetryLimit"));
    int retryCount = 0;
    while (putEventsResult.failedEntryCount() > 0) {
        List<PutEventsRequestEntry> failedEntriesList = new ArrayList<>();
        List<PutEventsResultEntry> PutEventsResultEntryList = putEventsResult.entries();
        for (int i = 0; i < PutEventsResultEntryList.size(); i++) {
            PutEventsRequestEntry putEventsRequestEntry = putEventsRequestEntryList.get(i);
            PutEventsResultEntry putEventsResultEntry = PutEventsResultEntryList.get(i);
            if (putEventsResultEntry.errorCode() != null) {
                log.info(putEventsResultEntry.errorCode() + ": " + putEventsResultEntry.errorMessage());
                failedEntriesList.add(putEventsRequestEntry);
            }
        }
        if (retryCount < EBRetryLimit) {
            log.info(putEventsResult.failedEntryCount() + " failed entry. " + "retry count: " + retryCount);
            putEventsRequestEntryList = failedEntriesList;
            putEventsRequest.toBuilder().entries(putEventsRequestEntryList).build();
            putEventsResult = awsEventsClient.putEvents(putEventsRequest);
            retryCount++;
        } else {
            log.info("send failed records to SQS DLQ after " + EBRetryLimit + "retry.");
            for (PutEventsRequestEntry failedEntry : failedEntriesList) {
                String queueUrl = System.getenv("SqsQueueURL");
                awsSQSClient.sendMessage(SendMessageRequest.builder().queueUrl(queueUrl).messageBody(failedEntry.detail()).build());
            }
            return;
        }
    }
}

@Test
public void handleNormalStream() {
    DynamodbEvent normal_event = new DynamodbEvent();
    List<DynamodbEvent.DynamodbStreamRecord> normalRecords = new ArrayList<>();
    normalRecords.add(new DynamodbEvent.DynamodbStreamRecord());
    normal_event.setRecords(normalRecords);
    PutEventsResponse putEventsResponse = PutEventsResponse.builder().failedEntryCount(0).build();
    when(awsEventClient.putEvents(any(PutEventsRequest.class))).thenReturn(putEventsResponse);
    eventBridgePublisher.publish(normal_event);
    verify(awsEventClient).putEvents(any(PutEventsRequest.class));
}


@Test
public void toResponse_apiException() {
    NotFoundException exception = new NotFoundException();
    Response response = exceptionMapper.toResponse(new NotFoundApiException(exception));
    assertThat(response.getStatus()).isEqualTo(404);
    assertThat(response.getMediaType()).isEqualTo(MediaType.APPLICATION_JSON_TYPE);
    assertThat(response.getEntity()).isEqualTo(exception);
}

@Override
public Response toResponse(final Throwable throwable) {
    if (throwable instanceof ApiException) {
        log.info("4xx exception is thrown", throwable);
        ApiException apiException = (ApiException) throwable;
        return apiException.getResponse();
    } else {
        log.error("5xx exception is thrown", throwable);
        return new InternalServerApiException().getResponse();
    }
}

@Test
public void toResponse_otherException() {
    InternalServerErrorException exception = new InternalServerErrorException().errorCode("InternalError").message("Internal Server Error.");
    Response response = exceptionMapper.toResponse(new IllegalArgumentException());
    assertThat(response.getStatus()).isEqualTo(500);
    assertThat(response.getMediaType()).isEqualTo(MediaType.APPLICATION_JSON_TYPE);
    assertThat(response.getEntity()).isEqualTo(exception);
}


@Test
public void testStringList() throws Exception {
    Map<String, AttributeValue> startKey = new HashMap<>();
    startKey.put("StringList", AttributeValue.builder().ss("test").build());
    String startKeyString = serializer.serialize(startKey);
    assertThat(serializer.deserialize(startKeyString)).isEqualTo(startKey);
}

@Override
@SneakyThrows(JsonProcessingException.class)
public String serialize(final Map<String, AttributeValue> startKey) {
    return OBJECT_MAPPER.writeValueAsString(startKey);
}

@Test
public void testMap() throws Exception {
    Map<String, AttributeValue> startKey = new HashMap<>();
    Map<String, AttributeValue> nested = new HashMap<>();
    nested.put("Map", AttributeValue.builder().s("test").build());
    startKey.put("AttributeList", AttributeValue.builder().m(nested).build());
    String startKeyString = serializer.serialize(startKey);
    assertThat(serializer.deserialize(startKeyString)).isEqualTo(startKey);
}

@Test
public void testNumberList() throws Exception {
    Map<String, AttributeValue> startKey = new HashMap<>();
    startKey.put("NumberList", AttributeValue.builder().ns("100").build());
    String startKeyString = serializer.serialize(startKey);
    assertThat(serializer.deserialize(startKeyString)).isEqualTo(startKey);
}

@Test
public void testByteList() throws Exception {
    Map<String, AttributeValue> startKey = new HashMap<>();
    startKey.put("ByteList", AttributeValue.builder().bs(SdkBytes.fromUtf8String("test")).build());
    String startKeyString = serializer.serialize(startKey);
    assertThat(serializer.deserialize(startKeyString)).isEqualTo(startKey);
}

@Test
public void testNul() throws Exception {
    Map<String, AttributeValue> startKey = new HashMap<>();
    startKey.put("AttributeList", AttributeValue.builder().nul(false).build());
    String startKeyString = serializer.serialize(startKey);
    assertThat(serializer.deserialize(startKeyString)).isEqualTo(startKey);
}

@Test
public void testNumber() throws Exception {
    Map<String, AttributeValue> startKey = new HashMap<>();
    startKey.put("Number", AttributeValue.builder().n("100").build());
    String startKeyString = serializer.serialize(startKey);
    assertThat(serializer.deserialize(startKeyString)).isEqualTo(startKey);
}

@Test
public void testString() throws Exception {
    Map<String, AttributeValue> startKey = new HashMap<>();
    startKey.put("String", AttributeValue.builder().s("1.0.0").build());
    String startKeyString = serializer.serialize(startKey);
    assertThat(serializer.deserialize(startKeyString)).isEqualTo(startKey);
}

@Test
public void testBool() throws Exception {
    Map<String, AttributeValue> startKey = new HashMap<>();
    startKey.put("AttributeList", AttributeValue.builder().bool(true).build());
    String startKeyString = serializer.serialize(startKey);
    assertThat(serializer.deserialize(startKeyString)).isEqualTo(startKey);
}

@Test
public void testAttributeList() throws Exception {
    Map<String, AttributeValue> startKey = new HashMap<>();
    startKey.put("AttributeList", AttributeValue.builder().l(AttributeValue.builder().s("test").build()).build());
    String startKeyString = serializer.serialize(startKey);
    assertThat(serializer.deserialize(startKeyString)).isEqualTo(startKey);
}

@Test
public void testByte() throws Exception {
    Map<String, AttributeValue> startKey = new HashMap<>();
    startKey.put("Byte", AttributeValue.builder().b(SdkBytes.fromUtf8String("test")).build());
    String startKeyString = serializer.serialize(startKey);
    assertThat(serializer.deserialize(startKeyString)).isEqualTo(startKey);
}


@Test
public void get_staleCache_ssmSuccess() throws Exception {
    Clock clock = mock(Clock.class);
    client = new SsmParameterCachingClient(ssm, TTL, PATH_PREFIX, true, true, clock);
    Instant now = Instant.now();
    when(clock.instant()).thenReturn(now);
    assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
    when(clock.instant()).thenReturn(now.plus(Duration.ofSeconds(TTL.getSeconds() + 1)));
    assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
    GetParametersByPathRequest expected = GetParametersByPathRequest.builder().path(PATH_PREFIX).recursive(true).withDecryption(true).build();
    verify(ssm, times(2)).getParametersByPathPaginator(expected);
    verifyNoMoreInteractions(ssm);
}

public Parameter get(final String name) {
    CachedParameter cached = getCachedParameter(name);
    if (isStale(cached)) {
        log.debug("SSM Parameter cache miss for pathPrefix={}, name={}. Attempting to load value from SSM.", pathPrefix, name);
        try {
            cached = load(name);
        } catch (Exception e) {
            if (e instanceof ParameterNotFoundException) {
                throw e;
            }
            if (allowStaleValues && cached != null) {
                log.warn(String.format("Failed to update cache from SSM for pathPrefix=%s, name=%s. Returning stale value instead.", pathPrefix, name), e);
                return cached.getParameter();
            }
            throw e;
        }
    } else {
        log.debug("SSM Parameter cache hit for pathPrefix={}, name={}. Returning cached value.", pathPrefix, name);
    }
    return cached.parameter;
}

@Test
public void get_staleCache_ssmOtherException_noAllowStaleValues() throws Exception {
    Clock clock = mock(Clock.class);
    client = new SsmParameterCachingClient(ssm, TTL, PATH_PREFIX, false, false, clock);
    Instant now = Instant.now();
    when(clock.instant()).thenReturn(now);
    assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
    when(clock.instant()).thenReturn(now.plus(Duration.ofSeconds(TTL.getSeconds() + 1)));
    when(ssm.getParameter(GetParameterRequest.builder().name(STRING_PARAMETER_KEY).withDecryption(true).build())).thenThrow(new RuntimeException("boom!"));
    assertThatThrownBy(() -> client.get(STRING_PARAMETER_KEY_SUFFIX)).isInstanceOf(RuntimeException.class).hasMessage("boom!");
}

@Test
public void get_staleCache_ssmParameterNotFound() throws Exception {
    Clock clock = mock(Clock.class);
    client = new SsmParameterCachingClient(ssm, TTL, PATH_PREFIX, true, false, clock);
    Instant now = Instant.now();
    when(clock.instant()).thenReturn(now);
    assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
    when(clock.instant()).thenReturn(now.plus(Duration.ofSeconds(TTL.getSeconds() + 1)));
    when(ssm.getParameter(GetParameterRequest.builder().name(STRING_PARAMETER_KEY).withDecryption(true).build())).thenThrow(ParameterNotFoundException.class);
    assertThatThrownBy(() -> client.get(STRING_PARAMETER_KEY_SUFFIX)).isInstanceOf(ParameterNotFoundException.class);
}

@Test
public void getAsStringList() throws Exception {
    assertThat(client.getAsStringList(STRING_LIST_PARAMETER_KEY_SUFFIX)).isEqualTo(Arrays.asList("a", "b", "c"));
}

public List<String> getAsStringList(final String name) {
    Parameter parameter = get(name);
    if (parameter.type() != ParameterType.STRING_LIST) {
        throw new IllegalArgumentException(String.format("Parameter %s is of type %s, expected StringList", parameter.name(), parameter.type()));
    }
    return Arrays.asList(parameter.value().split(","));
}

@Test
public void get_bulkLoad() throws Exception {
    assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
    assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
    assertThat(client.get(STRING_LIST_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_LIST_PARAMETER);
    assertThat(client.get(STRING_LIST_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_LIST_PARAMETER);
    GetParametersByPathRequest expected = GetParametersByPathRequest.builder().path(PATH_PREFIX).recursive(true).withDecryption(true).build();
    verify(ssm, times(1)).getParametersByPathPaginator(expected);
    verifyNoMoreInteractions(ssm);
}

@Test
public void get_bulkLoad_noPathPrefix() throws Exception {
    client = new SsmParameterCachingClient(ssm, TTL);
    assertThat(client.get(STRING_PARAMETER_KEY)).isEqualTo(STRING_PARAMETER);
    assertThat(client.get(STRING_PARAMETER_KEY)).isEqualTo(STRING_PARAMETER);
    assertThat(client.get(STRING_LIST_PARAMETER_KEY)).isEqualTo(STRING_LIST_PARAMETER);
    assertThat(client.get(STRING_LIST_PARAMETER_KEY)).isEqualTo(STRING_LIST_PARAMETER);
    GetParameterRequest expectedStringParameterRequest = GetParameterRequest.builder().name(STRING_PARAMETER_KEY).withDecryption(true).build();
    verify(ssm, times(1)).getParameter(expectedStringParameterRequest);
    GetParameterRequest expectedStringListParameterRequest = GetParameterRequest.builder().name(STRING_LIST_PARAMETER_KEY).withDecryption(true).build();
    verify(ssm, times(1)).getParameter(expectedStringListParameterRequest);
    verifyNoMoreInteractions(ssm);
}

@Test
public void get_staleCache_ssmOtherException_allowStaleValues() throws Exception {
    Clock clock = mock(Clock.class);
    client = new SsmParameterCachingClient(ssm, TTL, PATH_PREFIX, true, false, clock);
    Instant now = Instant.now();
    when(clock.instant()).thenReturn(now);
    assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
    when(clock.instant()).thenReturn(now.plus(Duration.ofSeconds(TTL.getSeconds() + 1)));
    when(ssm.getParameter(GetParameterRequest.builder().name(STRING_PARAMETER_KEY).withDecryption(true).build())).thenThrow(AwsServiceException.class);
    assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
}

@Test
public void getAsString() throws Exception {
    assertThat(client.getAsString(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER_VALUE);
    assertThat(client.getAsString(STRING_LIST_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_LIST_PARAMETER_VALUE);
}

public String getAsString(final String name) {
    return get(name).value();
}

@Test
public void get_noBulkLoad() throws Exception {
    client = new SsmParameterCachingClient(ssm, TTL, PATH_PREFIX, true, false);
    assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
    assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
    assertThat(client.get(STRING_LIST_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_LIST_PARAMETER);
    assertThat(client.get(STRING_LIST_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_LIST_PARAMETER);
    GetParameterRequest expectedStringParameterRequest = GetParameterRequest.builder().name(STRING_PARAMETER_KEY).withDecryption(true).build();
    verify(ssm, times(1)).getParameter(expectedStringParameterRequest);
    GetParameterRequest expectedStringListParameterRequest = GetParameterRequest.builder().name(STRING_LIST_PARAMETER_KEY).withDecryption(true).build();
    verify(ssm, times(1)).getParameter(expectedStringListParameterRequest);
    verifyNoMoreInteractions(ssm);
}


@Test
public void less() throws IOException {
    assertEquals(true, Expressions.read("[\"<\", 1, 2]").evaluate(null));
    assertEquals(false, Expressions.read("[\"<\", 1, 1]").evaluate(null));
    assertEquals(false, Expressions.read("[\"<\", 1, 0]").evaluate(null));
}

static Expression read(String json) throws IOException {
    var mapper = new ObjectMapper();
    var simpleModule = new SimpleModule("SimpleModule", new Version(1, 0, 0, null));
    simpleModule.addDeserializer(Expression.class, new ExpressionDeserializer());
    mapper.registerModule(simpleModule);
    return mapper.readValue(new StringReader(json), Expression.class);
}

@Test
public void not() throws IOException {
    assertEquals(true, Expressions.read("[\"!\", false]").evaluate(null));
    assertEquals(false, Expressions.read("[\"!\", true]").evaluate(null));
}

@Test
public void greater() throws IOException {
    assertEquals(true, Expressions.read("[\">\", 1, 0]").evaluate(null));
    assertEquals(false, Expressions.read("[\">\", 1, 1]").evaluate(null));
    assertEquals(false, Expressions.read("[\">\", 1, 2]").evaluate(null));
}

@Test
public void greaterOrEqual() throws IOException {
    assertEquals(true, Expressions.read("[\">=\", 1, 0]").evaluate(null));
    assertEquals(true, Expressions.read("[\">=\", 1, 1]").evaluate(null));
    assertEquals(false, Expressions.read("[\">=\", 1, 2]").evaluate(null));
}

@Test
public void match() throws IOException {
    assertEquals("foo", Expressions.read("[\"match\", \"foo\", \"foo\", \"foo\", \"bar\", \"bar\", \"baz\"]").evaluate(null));
    assertEquals("bar", Expressions.read("[\"match\", \"bar\", \"foo\", \"foo\", \"bar\", \"bar\", \"baz\"]").evaluate(null));
    assertEquals("baz", Expressions.read("[\"match\", \"baz\", \"foo\", \"foo\", \"bar\", \"bar\", \"baz\"]").evaluate(null));
}

@Test
public void lessOrEqual() throws IOException {
    assertEquals(true, Expressions.read("[\"<=\", 1, 2]").evaluate(null));
    assertEquals(true, Expressions.read("[\"<=\", 1, 1]").evaluate(null));
    assertEquals(false, Expressions.read("[\"<=\", 1, 0]").evaluate(null));
}

@Test
public void equal() throws IOException {
    assertEquals(true, Expressions.read("[\"==\", 1, 1]").evaluate(null));
    assertEquals(false, Expressions.read("[\"==\", 1, 2]").evaluate(null));
}

@Test
public void notEqual() throws IOException {
    assertEquals(true, Expressions.read("[\"!=\", 1, 2]").evaluate(null));
    assertEquals(false, Expressions.read("[\"!=\", 1, 1]").evaluate(null));
}


@Test
void testFiltering() {
    final int streamSize = 85;
    final List<TileCoord> tileCoords = new ArrayList<>();
    for (int z = 0; z <= 3; z++) {
        for (int x = 0; x < IntMath.pow(2, z); x++) {
            for (int y = 0; y < IntMath.pow(2, z); y++) {
                tileCoords.add(new TileCoord(x, y, z));
            }
        }
    }
    assertEquals(streamSize, tileCoords.size());
    final int batchArraySize = 5;
    for (int batchArrayIndex = 0; batchArrayIndex < batchArraySize; batchArrayIndex++) {
        List<TileCoord> batch = tileCoords.stream().filter(new TileBatchPredicate(batchArraySize, batchArrayIndex)).sorted(Comparator.comparingLong(TileCoord::index)).toList();
        assertEquals(streamSize / batchArraySize, batch.size());
        int tileIndex = batchArrayIndex;
        for (TileCoord tileCoord : batch) {
            assertEquals(tileIndex, tileCoord.index());
            tileIndex += batchArraySize;
        }
    }
}

public long index() {
    long x = this.x;
    long y = this.y;
    long offset = offsets[z];
    long position = x + y * sides[z];
    return offset + position;
}


@Test
void partial() {
    Envelope e0 = new TileCoord(0, 0, 1).envelope();
    TileCoordIterator i0 = new TileCoordIterator(e0, 2, 2);
    List<TileCoord> l0 = ImmutableList.copyOf(i0);
    assertEquals(l0.size(), 4);
    Envelope e1 = new TileCoord(1, 1, 1).envelope();
    TileCoordIterator i1 = new TileCoordIterator(e1, 2, 2);
    List<TileCoord> l1 = ImmutableList.copyOf(i1);
    assertEquals(l1.size(), 4);
}

@Override
public boolean hasNext() {
    TileCoord max = max(envelope, this.z);
    return x <= max.x() && y <= max.y() && z <= maxZoom;
}

@Test
void iterator() {
    Envelope e = new TileCoord(0, 0, 0).envelope();
    TileCoordIterator i0 = new TileCoordIterator(e, 0, 0);
    List<TileCoord> l0 = ImmutableList.copyOf(i0);
    assertEquals(l0.size(), 1);
    TileCoordIterator i1 = new TileCoordIterator(e, 1, 1);
    List<TileCoord> l1 = ImmutableList.copyOf(i1);
    assertEquals(l1.size(), 4);
    TileCoordIterator i2 = new TileCoordIterator(e, 2, 2);
    List<TileCoord> l2 = ImmutableList.copyOf(i2);
    assertEquals(l2.size(), 16);
}


@Test
void count() {
    Envelope envelope = new TileCoord(0, 0, 0).envelope();
    assertEquals(TileCoord.count(envelope, 0, 2), TileCoord.list(envelope, 0, 2).size());
}

public static long count(Envelope envelope, int minzoom, int maxzoom) {
    int count = 0;
    for (int zoom = minzoom; zoom <= maxzoom; zoom++) {
        TileCoord min = min(envelope, zoom);
        TileCoord max = max(envelope, zoom);
        count += (max.x() - min.x() + 1) * (max.y() - min.y() + 1);
    }
    return count;
}

@Test
void countLiechtenstein() {
    double minLon = 9.471078;
    double maxLon = 9.636217;
    double minLat = 47.04774;
    double maxLat = 47.27128;
    int minZoom = 12;
    int maxZoom = 14;
    Envelope envelope = new Envelope(minLon, maxLon, minLat, maxLat);
    assertEquals(TileCoord.count(envelope, minZoom, maxZoom), TileCoord.list(envelope, minZoom, maxZoom).size());
}

public static List<TileCoord> list(Envelope envelope, int minzoom, int maxzoom) {
    return ImmutableList.copyOf(iterator(envelope, minzoom, maxzoom));
}

@Test
void getTile() {
    double lon = 0, lat = 0;
    for (int z = 0; z <= 14; z += 1) {
        TileCoord tileCoord = TileCoord.fromLonLat(lon, lat, z);
        int value = (int) Math.pow(2, z - 1);
        assertEquals(value, tileCoord.x());
        assertEquals(value, tileCoord.y());
    }
}

public static TileCoord fromLonLat(double lon, double lat, int z) {
    int x = (int) ((lon + 180.0) / 360.0 * (1 << z));
    int y = (int) ((1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI) / 2.0 * (1 << z));
    return new TileCoord(x, y, z);
}


@Test
@Tag("integration")
void readWriteDeleteTile() throws Exception {
    TileStore tileStore = createTileStore();
    TileCoord tileCoord = new TileCoord(1, 2, 3);
    ByteBuffer blob = ByteBuffer.wrap("tile_content".getBytes());
    tileStore.write(tileCoord, blob);
    ByteBuffer inputStream = tileStore.read(tileCoord);
    assertArrayEquals(blob.array(), inputStream.array());
    tileStore.delete(tileCoord);
    assertThrows(TileStoreException.class, () -> tileStore.read(tileCoord));
}

@Override
public void write(TileCoord tileCoord, ByteBuffer blob) throws TileStoreException {
    try (Connection connection = dataSource.getConnection();
        PreparedStatement statement = connection.prepareStatement(INSERT_TILE)) {
        statement.setInt(1, tileCoord.z());
        statement.setInt(2, tileCoord.x());
        statement.setInt(3, reverseY(tileCoord.y(), tileCoord.z()));
        statement.setBytes(4, blob.array());
        statement.executeUpdate();
    } catch (SQLException e) {
        throw new TileStoreException(e);
    }
}


@Test
void sameQueries() {
    List<PostgresQuery> queries = Arrays.asList(new PostgresQuery("a", 0, 20, "SELECT id, tags, geom FROM table"), new PostgresQuery("b", 0, 20, "SELECT id, tags, geom FROM table"));
    PostgresTileStore tileStore = new PostgresTileStore(null, queries);
    String query = tileStore.withQuery(new TileCoord(0, 0, 10));
    assertEquals("with ha14cb45b as (select * from table where ((true) OR (true)) and st_intersects(geom, st_tileenvelope(10, 0, 0))) select st_asmvt(target, 'a', 4096, 'geom', 'id') from (select id as id, (tags ||  jsonb_build_object('geometry', lower(replace(st_geometrytype(geom), 'ST_', '')))) as tags, st_asmvtgeom(geom, st_tileenvelope(10, 0, 0), 4096, 256, true) as geom from ha14cb45b ) as target union all select st_asmvt(target, 'b', 4096, 'geom', 'id') from (select id as id, (tags ||  jsonb_build_object('geometry', lower(replace(st_geometrytype(geom), 'ST_', '')))) as tags, st_asmvtgeom(geom, st_tileenvelope(10, 0, 0), 4096, 256, true) as geom from ha14cb45b ) as target", query);
}

protected String withQuery(TileCoord tileCoord) {
    int zoom = tileCoord.z();
    String sourceQueries = ctes(queries, zoom);
    String targetQueries = statements(queries, zoom);
    String withQuery = String.format(WITH_QUERY, sourceQueries, targetQueries);
    Map<String, String> variables = Map.of("envelope", tileEnvelope(tileCoord), "zoom", String.valueOf(zoom));
    return VariableUtils.interpolate(variables, withQuery);
}

@Test
void differentConditions2() {
    List<PostgresQuery> queries = Arrays.asList(new PostgresQuery("a", 0, 20, "SELECT id, tags, geom FROM table WHERE condition = 1"), new PostgresQuery("b", 0, 20, "SELECT id, tags, geom FROM table WHERE condition = 2"));
    PostgresTileStore tileStore = new PostgresTileStore(null, queries);
    String query = tileStore.withQuery(new TileCoord(0, 0, 10));
    assertEquals("with ha14cb45b as (select * from table where ((condition = 1) OR (condition = 2)) and st_intersects(geom, st_tileenvelope(10, 0, 0))) select st_asmvt(target, 'a', 4096, 'geom', 'id') from (select id as id, (tags ||  jsonb_build_object('geometry', lower(replace(st_geometrytype(geom), 'ST_', '')))) as tags, st_asmvtgeom(geom, st_tileenvelope(10, 0, 0), 4096, 256, true) as geom from ha14cb45b where condition = 1) as target union all select st_asmvt(target, 'b', 4096, 'geom', 'id') from (select id as id, (tags ||  jsonb_build_object('geometry', lower(replace(st_geometrytype(geom), 'ST_', '')))) as tags, st_asmvtgeom(geom, st_tileenvelope(10, 0, 0), 4096, 256, true) as geom from ha14cb45b where condition = 2) as target", query);
}

@Test
void differentConditions1() {
    List<PostgresQuery> queries = Arrays.asList(new PostgresQuery("a", 0, 20, "SELECT id, tags, geom FROM table"), new PostgresQuery("b", 0, 20, "SELECT id, tags, geom FROM table WHERE condition = 1"));
    PostgresTileStore tileStore = new PostgresTileStore(null, queries);
    String query = tileStore.withQuery(new TileCoord(0, 0, 10));
    assertEquals("with ha14cb45b as (select * from table where ((true) OR (condition = 1)) and st_intersects(geom, st_tileenvelope(10, 0, 0))) select st_asmvt(target, 'a', 4096, 'geom', 'id') from (select id as id, (tags ||  jsonb_build_object('geometry', lower(replace(st_geometrytype(geom), 'ST_', '')))) as tags, st_asmvtgeom(geom, st_tileenvelope(10, 0, 0), 4096, 256, true) as geom from ha14cb45b ) as target union all select st_asmvt(target, 'b', 4096, 'geom', 'id') from (select id as id, (tags ||  jsonb_build_object('geometry', lower(replace(st_geometrytype(geom), 'ST_', '')))) as tags, st_asmvtgeom(geom, st_tileenvelope(10, 0, 0), 4096, 256, true) as geom from ha14cb45b where condition = 1) as target", query);
}


@Test
public void testPoint() {
    var inputGeom = new GeometryFactory(new PrecisionModel(), 4326).createPoint(new Coordinate(1, 1));
    var outputGeom = (Point) new ProjectionTransformer(4326, 3857).transform(inputGeom);
    assertEquals(3857, outputGeom.getSRID());
    assertEquals(111319.49079327357, outputGeom.getX());
    assertEquals(111325.14286638486, outputGeom.getY());
}

protected Geometry transformPoint(Point geom, Geometry parent) {
    try {
        var geometry = super.transformPoint(geom, parent);
        return withTargetSRID(geometry);
    } catch (Exception e) {
        logger.error("Point cannot be reprojected", e);
        return parent.getFactory().createEmpty(0);
    }
}


@Test
void all() {
    NicObject nicObject = nicObjects.get(0);
    assertEquals(2, nicObject.all("mnt-by").size());
    assertEquals(7, nicObject.all("remarks").size());
}

public List<String> all(String name) {
    return attributes.stream().filter(attribute -> attribute.name().equals(name)).map(NicAttribute::value).toList();
}

@Test
void attributes() {
    assertEquals(21, nicObjects.get(0).attributes().size());
    assertEquals(15, nicObjects.get(1).attributes().size());
    assertEquals(18, nicObjects.get(2).attributes().size());
    assertEquals(23, nicObjects.get(3).attributes().size());
}

public List<NicAttribute> attributes() {
    return Collections.unmodifiableList(attributes);
}

@Test
void type() {
    assertEquals("organisation", nicObjects.get(0).type());
    assertEquals("mntner", nicObjects.get(1).type());
    assertEquals("organisation", nicObjects.get(2).type());
    assertEquals("inetnum", nicObjects.get(3).type());
}

public String type() {
    return attributes.get(0).name();
}

@Test
void first() {
    NicObject nicObject = nicObjects.get(0);
    assertEquals("RIPE-NCC-HM-MNT", nicObject.first("mnt-by").get());
    assertEquals("****************************", nicObject.first("remarks").get());
}

public Optional<String> first(String name) {
    return attributes.stream().filter(attribute -> attribute.name().equals(name)).map(NicAttribute::value).findFirst();
}

@Test
void id() {
    assertEquals("ORG-HEDI1-RIPE", nicObjects.get(0).id());
    assertEquals("ch-heig-vd-1-mnt", nicObjects.get(1).id());
    assertEquals("ORG-VA29373-RIPE", nicObjects.get(2).id());
    assertEquals("193.135.138.0 - 193.135.138.255", nicObjects.get(3).id());
}

public String id() {
    return attributes.get(0).value();
}


@Test
public void decodeMultiPoint() {
    var coordinates = new Coordinate[] { new Coordinate(5, 7), new Coordinate(3, 2) };
    var multiPoint = geometryFactory.createMultiPoint(coordinates);
    var decoder = new VectorTileDecoder();
    var encoding = List.of(17, 10, 14, 3, 9);
    assertEquals(multiPoint, decoder.decodePoint(encoding));
}

protected Geometry decodePoint(List<Integer> encoding) {
    List<Coordinate> coordinates = new ArrayList<>();
    int i = 0;
    while (i < encoding.size()) {
        int value = encoding.get(i);
        int command = command(value);
        int count = count(value);
        i++;
        int length = count * 2;
        for (int j = 0; j < length; j += 2) {
            cx += parameter(encoding.get(i + j));
            cy += parameter(encoding.get(i + j + 1));
            if (command == MOVE_TO) {
                coordinates.add(new Coordinate(cx, cy));
            }
        }
        i += length;
    }
    if (coordinates.size() == 1) {
        return GEOMETRY_FACTORY.createPoint(coordinates.get(0));
    } else if (coordinates.size() > 1) {
        return GEOMETRY_FACTORY.createMultiPointFromCoords(coordinates.toArray(new Coordinate[0]));
    } else {
        throw new IllegalStateException("No coordinates found.");
    }
}

@Test
public void decodePolygon() {
    var polygon = geometryFactory.createPolygon(new Coordinate[] { new Coordinate(3, 6), new Coordinate(8, 12), new Coordinate(20, 34), new Coordinate(3, 6) });
    var decoder = new VectorTileDecoder();
    var encoding = List.of(9, 6, 12, 18, 10, 12, 24, 44, 15);
    assertEquals(polygon, decoder.decodePolygon(encoding));
}

protected Geometry decodePolygon(List<Integer> encoding) {
    List<Polygon> polygons = new ArrayList<>();
    Optional<LinearRing> shell = Optional.empty();
    List<LinearRing> holes = new ArrayList<>();
    List<Coordinate> coordinates = new ArrayList<>();
    int i = 0;
    while (i < encoding.size()) {
        int value = encoding.get(i);
        int command = command(value);
        int count = count(value);
        if (command == MOVE_TO || command == LINE_TO) {
            i++;
            int length = count * 2;
            for (int j = 0; j < length; j += 2) {
                cx += parameter(encoding.get(i + j));
                cy += parameter(encoding.get(i + j + 1));
                if (command == MOVE_TO) {
                    coordinates.clear();
                    coordinates.add(new Coordinate(cx, cy));
                } else if (command == LINE_TO) {
                    coordinates.add(new Coordinate(cx, cy));
                }
            }
            i += length;
        }
        if (command == CLOSE_PATH) {
            coordinates.add(coordinates.get(0));
            LinearRing linearRing = GEOMETRY_FACTORY.createLinearRing(coordinates.toArray(new Coordinate[0]));
            boolean isShell = isClockWise(linearRing);
            if (isShell && shell.isPresent()) {
                polygons.add(GEOMETRY_FACTORY.createPolygon(shell.get(), holes.toArray(new LinearRing[0])));
                holes.clear();
            }
            if (isShell) {
                shell = Optional.of(linearRing);
            } else {
                holes.add(linearRing);
            }
            coordinates.clear();
            i++;
        }
    }
    if (shell.isPresent()) {
        polygons.add(GEOMETRY_FACTORY.createPolygon(shell.get(), holes.toArray(new LinearRing[0])));
        holes.clear();
    }
    if (polygons.size() == 1) {
        return polygons.get(0);
    } else if (polygons.size() > 1) {
        return GEOMETRY_FACTORY.createMultiPolygon(polygons.toArray(new Polygon[0]));
    } else {
        throw new IllegalStateException("No polygons found.");
    }
}

@Test
public void decodeMultiPolygon() {
    var multiPolygon = geometryFactory.createMultiPolygon(new Polygon[] { geometryFactory.createPolygon(new Coordinate[] { new Coordinate(0, 0), new Coordinate(10, 0), new Coordinate(10, 10), new Coordinate(0, 10), new Coordinate(0, 0) }), geometryFactory.createPolygon(geometryFactory.createLinearRing(new Coordinate[] { new Coordinate(11, 11), new Coordinate(20, 11), new Coordinate(20, 20), new Coordinate(11, 20), new Coordinate(11, 11) }), new LinearRing[] { geometryFactory.createLinearRing(new Coordinate[] { new Coordinate(13, 13), new Coordinate(13, 17), new Coordinate(17, 17), new Coordinate(17, 13), new Coordinate(13, 13) }) }) });
    var decoder = new VectorTileDecoder();
    var encoding = List.of(9, 0, 0, 26, 20, 0, 0, 20, 19, 0, 15, 9, 22, 2, 26, 18, 0, 0, 18, 17, 0, 15, 9, 4, 13, 26, 0, 8, 8, 0, 0, 7, 15);
    assertEquals(multiPolygon, decoder.decodePolygon(encoding));
}

@Test
public void decodePoint() {
    var coordinate = new Coordinate(25, 17);
    var point = geometryFactory.createPoint(coordinate);
    var decoder = new VectorTileDecoder();
    var encoding = List.of(9, 50, 34);
    assertEquals(point, decoder.decodePoint(encoding));
}

@Test
public void decodeLineString() {
    var lineString = geometryFactory.createLineString(new Coordinate[] { new Coordinate(2, 2), new Coordinate(2, 10), new Coordinate(10, 10) });
    var decoder = new VectorTileDecoder();
    assertEquals(lineString, decoder.decodeLineString(List.of(9, 4, 4, 18, 0, 16, 16, 0)));
}

protected Geometry decodeLineString(List<Integer> encoding) {
    List<LineString> lineStrings = new ArrayList<>();
    List<Coordinate> coordinates = new ArrayList<>();
    int i = 0;
    while (i < encoding.size()) {
        int value = encoding.get(i);
        int command = command(value);
        int count = count(value);
        i++;
        int length = count * 2;
        for (int j = 0; j < length; j += 2) {
            cx += parameter(encoding.get(i + j));
            cy += parameter(encoding.get(i + j + 1));
            if (command == MOVE_TO) {
                coordinates.clear();
                coordinates.add(new Coordinate(cx, cy));
            } else if (command == LINE_TO) {
                coordinates.add(new Coordinate(cx, cy));
            }
        }
        if (coordinates.size() > 1) {
            lineStrings.add(GEOMETRY_FACTORY.createLineString(coordinates.toArray(new Coordinate[0])));
        }
        i += length;
    }
    if (lineStrings.size() == 1) {
        return lineStrings.get(0);
    } else if (lineStrings.size() > 1) {
        return GEOMETRY_FACTORY.createMultiLineString(lineStrings.toArray(new LineString[0]));
    } else {
        throw new IllegalStateException("No linestrings found.");
    }
}

@Test
public void decodeMultiLineString() {
    var lineString1 = geometryFactory.createLineString(new Coordinate[] { new Coordinate(2, 2), new Coordinate(2, 10), new Coordinate(10, 10) });
    ;
    var lineString2 = geometryFactory.createLineString(new Coordinate[] { new Coordinate(1, 1), new Coordinate(3, 5) });
    var multiLineString = geometryFactory.createMultiLineString(new LineString[] { lineString1, lineString2 });
    var decoder = new VectorTileDecoder();
    var encoding = List.of(9, 4, 4, 18, 0, 16, 16, 0, 9, 17, 17, 10, 4, 8);
    assertEquals(multiLineString, decoder.decodeLineString(encoding));
}


@Test
public void encodeMultiPoint() {
    var coordinates = new Coordinate[] { new Coordinate(5, 7), new Coordinate(3, 2) };
    var multiPoint = geometryFactory.createMultiPoint(coordinates);
    var encoder = new VectorTileEncoder();
    var encoding = new ArrayList<Integer>();
    encoder.encodeMultiPoint(multiPoint, encoding::add);
    assertEquals(List.of(17, 10, 14, 3, 9), encoding);
}

protected void encodeMultiPoint(MultiPoint multiPoint, Consumer<Integer> encoding) {
    List<Coordinate> coordinates = List.of(multiPoint.getCoordinates());
    encoding.accept(command(MOVE_TO, coordinates.size()));
    encodeCoordinates(coordinates, encoding);
}

@Test
public void encodeLineString() {
    var lineString = geometryFactory.createLineString(new Coordinate[] { new Coordinate(2, 2), new Coordinate(2, 10), new Coordinate(10, 10) });
    var encoder = new VectorTileEncoder();
    var encoding = new ArrayList<Integer>();
    encoder.encodeLineString(lineString, encoding::add);
    assertEquals(List.of(9, 4, 4, 18, 0, 16, 16, 0), encoding);
}

protected void encodeLineString(LineString lineString, Consumer<Integer> encoding) {
    List<Coordinate> coordinates = List.of(lineString.getCoordinates());
    encoding.accept(command(MOVE_TO, 1));
    encodeCoordinates(coordinates.subList(0, 1), encoding);
    encoding.accept(command(LINE_TO, coordinates.size() - 1));
    encodeCoordinates(coordinates.subList(1, coordinates.size()), encoding);
}

@Test
public void encodePolygon() {
    var polygon = geometryFactory.createPolygon(new Coordinate[] { new Coordinate(3, 6), new Coordinate(8, 12), new Coordinate(20, 34), new Coordinate(3, 6) });
    var encoder = new VectorTileEncoder();
    var encoding = new ArrayList<Integer>();
    encoder.encodePolygon(polygon, encoding::add);
    assertEquals(List.of(9, 6, 12, 18, 10, 12, 24, 44, 15), encoding);
}

protected void encodePolygon(Polygon polygon, Consumer<Integer> encoding) {
    LinearRing exteriorRing = polygon.getExteriorRing();
    List<Coordinate> exteriorRingCoordinates = List.of(exteriorRing.getCoordinates());
    if (!isClockWise(exteriorRing)) {
        exteriorRingCoordinates = Lists.reverse(exteriorRingCoordinates);
    }
    encodeRing(exteriorRingCoordinates, encoding);
    for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
        LinearRing interiorRing = polygon.getInteriorRingN(i);
        List<Coordinate> interiorRingCoordinates = List.of(interiorRing.getCoordinates());
        if (isClockWise(interiorRing)) {
            interiorRingCoordinates = Lists.reverse(exteriorRingCoordinates);
        }
        encodeRing(interiorRingCoordinates, encoding);
    }
}

@Test
public void encodePoint() {
    var coordinate = new Coordinate(25, 17);
    var point = geometryFactory.createPoint(coordinate);
    var encoder = new VectorTileEncoder();
    var encoding = new ArrayList<Integer>();
    encoder.encodePoint(point, encoding::add);
    assertEquals(List.of(9, 50, 34), encoding);
}

protected void encodePoint(Point point, Consumer<Integer> encoding) {
    encoding.accept(command(MOVE_TO, 1));
    Coordinate coordinate = point.getCoordinate();
    int dx = (int) Math.round(coordinate.getX()) - cx;
    int dy = (int) Math.round(coordinate.getY()) - cy;
    encoding.accept(parameter(dx));
    encoding.accept(parameter(dy));
    cx += dx;
    cy += dy;
}

@Test
public void encodeMultiPolygon() {
    var multiPolygon = geometryFactory.createMultiPolygon(new Polygon[] { geometryFactory.createPolygon(new Coordinate[] { new Coordinate(0, 0), new Coordinate(10, 0), new Coordinate(10, 10), new Coordinate(0, 10), new Coordinate(0, 0) }), geometryFactory.createPolygon(geometryFactory.createLinearRing(new Coordinate[] { new Coordinate(11, 11), new Coordinate(20, 11), new Coordinate(20, 20), new Coordinate(11, 20), new Coordinate(11, 11) }), new LinearRing[] { geometryFactory.createLinearRing(new Coordinate[] { new Coordinate(13, 13), new Coordinate(13, 17), new Coordinate(17, 17), new Coordinate(17, 13), new Coordinate(13, 13) }) }) });
    var encoder = new VectorTileEncoder();
    var encoding = new ArrayList<Integer>();
    encoder.encodeMultiPolygon(multiPolygon, encoding::add);
    assertEquals(List.of(9, 0, 0, 26, 20, 0, 0, 20, 19, 0, 15, 9, 22, 2, 26, 18, 0, 0, 18, 17, 0, 15, 9, 4, 13, 26, 0, 8, 8, 0, 0, 7, 15), encoding);
}

protected void encodeMultiPolygon(MultiPolygon multiPolygon, Consumer<Integer> encoding) {
    for (int i = 0; i < multiPolygon.getNumGeometries(); i++) {
        Geometry geometry = multiPolygon.getGeometryN(i);
        if (geometry instanceof Polygon polygon) {
            encodePolygon(polygon, encoding);
        }
    }
}

@Test
public void encodeMultiLineString() {
    var lineString1 = geometryFactory.createLineString(new Coordinate[] { new Coordinate(2, 2), new Coordinate(2, 10), new Coordinate(10, 10) });
    ;
    var lineString2 = geometryFactory.createLineString(new Coordinate[] { new Coordinate(1, 1), new Coordinate(3, 5) });
    var multiLineString = geometryFactory.createMultiLineString(new LineString[] { lineString1, lineString2 });
    var encoder = new VectorTileEncoder();
    var encoding = new ArrayList<Integer>();
    encoder.encodeMultiLineString(multiLineString, encoding::add);
    assertEquals(List.of(9, 4, 4, 18, 0, 16, 16, 0, 9, 17, 17, 10, 4, 8), encoding);
}

protected void encodeMultiLineString(MultiLineString multiLineString, Consumer<Integer> encoding) {
    for (int i = 0; i < multiLineString.getNumGeometries(); i++) {
        Geometry geometry = multiLineString.getGeometryN(i);
        if (geometry instanceof LineString lineString) {
            encodeLineString(lineString, encoding);
        }
    }
}


@Test
public void file() throws IOException {
    var path = Path.of("src/test/resources/vectortile/14-8493-5795.mvt");
    try (var input = new GZIPInputStream(Files.newInputStream(path))) {
        var buffer = ByteBuffer.wrap(input.readAllBytes());
        var parsed = org.apache.baremaps.mvt.binary.VectorTile.Tile.parseFrom(buffer);
        var tile = new VectorTileDecoder().decodeTile(parsed);
        assertNotNull(tile);
        assertEquals(13, tile.getLayers().size());
    }
}

public Tile decodeTile(VectorTile.Tile tile) {
    List<Layer> layers = tile.getLayersList().stream().map(this::decodeLayer).collect(Collectors.toList());
    return new Tile(layers);
}

@Test
public void endToEnd() {
    var tile = new Tile(List.of(new Layer("layer", 256, List.of(new Feature(1, Map.of("a", 1.0, "b", "2"), GEOMETRY_FACTORY.createPoint(new Coordinate(1, 2))), new Feature(2, Map.of("c", 3.0, "d", "4"), GEOMETRY_FACTORY.createPoint(new Coordinate(2, 3)))))));
    var encoded = new VectorTileEncoder().encodeTile(tile);
    var decoded = new VectorTileDecoder().decodeTile(encoded);
    assertEquals(tile, decoded);
}

public VectorTile.Tile encodeTile(Tile tile) {
    VectorTile.Tile.Builder builder = VectorTile.Tile.newBuilder();
    tile.getLayers().forEach(layer -> builder.addLayers(encodeLayer(layer)));
    return builder.build();
}


@Test
void vectorTileGeom() {
    var coordinates = new Coordinate[] { new Coordinate(1, 1), new Coordinate(5, 9), new Coordinate(9, 1), new Coordinate(1, 1) };
    var geometryFactory = new GeometryFactory();
    var inputGeom = geometryFactory.createPolygon(coordinates);
    var envelope = new Envelope(0, 10, 0, 10);
    var extent = 100;
    var buffer = 10;
    var clipGeom = true;
    var outputGeom = VectorTileUtils.asVectorTileGeom(inputGeom, envelope, extent, buffer, clipGeom);
    assertNotNull(outputGeom);
    assertTrue(outputGeom.isValid());
    Coordinate[] expectedCoordinates = new Coordinate[] { new Coordinate(10, 90), new Coordinate(90, 90), new Coordinate(50, 10), new Coordinate(10, 90) };
    LinearRing expectedShell = geometryFactory.createLinearRing(expectedCoordinates);
    Polygon expectedGeom = geometryFactory.createPolygon(expectedShell);
    assertTrue(outputGeom.equalsTopo(expectedGeom));
    var backToInputGeom = VectorTileUtils.fromVectorTileGeom(outputGeom, envelope, extent);
    assertTrue(backToInputGeom.equalsTopo(inputGeom));
}

public static Geometry asVectorTileGeom(Geometry geometry, Envelope envelope, int extent, int buffer, boolean clipGeom) {
    double scaleX = extent / envelope.getWidth();
    double scaleY = extent / envelope.getHeight();
    AffineTransformation affineTransformation = new AffineTransformation();
    affineTransformation.translate(-envelope.getMinX(), -envelope.getMinY());
    affineTransformation.scale(scaleX, -scaleY);
    affineTransformation.translate(0, extent);
    Geometry scaledGeometry = affineTransformation.transform(geometry);
    if (clipGeom) {
        return clipToTile(scaledGeometry, extent, buffer);
    } else {
        return scaledGeometry;
    }
}


@Test
void testQueryNoHits() throws Exception {
    var geonamesQuery = new DataTableQueryBuilder().column("continent", 1.0f).query("test").build();
    var topDocs = searcher.search(geonamesQuery, 1);
    assertEquals(0, topDocs.totalHits.value);
}

public DataTableQueryBuilder query(String query) {
    this.query = query;
    return this;
}


@Test
void testOrQuery() throws Exception {
    var geonamesQuery = new GeonamesQueryBuilder().queryText("bains cheseaux").countryCode("CH").build();
    var topDocs = searcher.search(geonamesQuery, 2);
    assertEquals(2, topDocs.totalHits.value);
    var doc0 = searcher.doc(topDocs.scoreDocs[0].doc);
    assertEquals("Yverdon-les-bains", doc0.getField("name").stringValue());
    var doc1 = searcher.doc(topDocs.scoreDocs[1].doc);
    assertEquals("Route de Cheseaux 1", doc1.getField("name").stringValue());
}

public GeonamesQueryBuilder queryText(String queryText) {
    this.queryText = queryText;
    return this;
}

@Test
void testCreateIndex() throws Exception {
    var geonamesQuery = new GeonamesQueryBuilder().queryText("yverdon").countryCode("CH").build();
    var topDocs = searcher.search(geonamesQuery, 1);
    var doc = searcher.doc(Arrays.stream(topDocs.scoreDocs).findFirst().get().doc);
    assertEquals("Yverdon-les-bains", doc.getField("name").stringValue());
}

public Query build() throws ParseException {
    var builder = new BooleanQuery.Builder();
    if (queryText != null) {
        var queryTextEsc = QueryParserBase.escape(queryText);
        if (!queryTextEsc.isBlank()) {
            var fieldWeights = Map.of("name", 1f, "asciiname", 1f, "country", 1f, "countryCode", 1f);
            var parser = new SimpleQueryParser(analyzer, fieldWeights);
            if (andOperator) {
                parser.setDefaultOperator(BooleanClause.Occur.MUST);
            }
            var termsQuery = parser.parse(queryTextEsc);
            builder.add(termsQuery, BooleanClause.Occur.MUST);
        }
    }
    if (countryCode != null) {
        var countryCodeEsc = QueryParserBase.escape(countryCode);
        if (!countryCodeEsc.isBlank()) {
            var countryCodeQuery = new TermQuery(new Term("countryCode", countryCodeEsc));
            builder.add(countryCodeQuery, BooleanClause.Occur.MUST);
        }
    }
    if (!Strings.isNullOrEmpty(featureCode)) {
        var featureCodeQuery = new TermQuery(new Term("featureCode", featureCode));
        builder.add(featureCodeQuery, BooleanClause.Occur.MUST);
    }
    if (scoringByPopulation) {
        var query = builder.build();
        Expression expr = JavascriptCompiler.compile("_score + ln(1+population)");
        var bindings = new SimpleBindings();
        bindings.add("_score", DoubleValuesSource.SCORES);
        bindings.add("population", DoubleValuesSource.fromIntField("population"));
        return new FunctionScoreQuery(query, expr.getDoubleValuesSource(bindings));
    }
    return builder.build();
}

@Test
void testAndQueryNoHits() throws Exception {
    var geonamesQuery = new GeonamesQueryBuilder().queryText("bains cheseaux").andOperator().countryCode("CH").build();
    var topDocs = searcher.search(geonamesQuery, 1);
    assertEquals(0, topDocs.totalHits.value);
}


@Test
void read() throws IOException {
    var data = TestFiles.resolve("baremaps-testing/data/geonames/sample.txt");
    try (var inputStream = Files.newInputStream(data)) {
        var reader = new GeonamesReader();
        var stream = reader.stream(inputStream);
        var list = stream.toList();
        assertEquals(5, list.size());
        var entry = list.get(0);
        assertEquals(1, entry.getGeonameid());
        assertEquals("HEIG", entry.getAsciiname());
    }
}

public Stream<GeonamesEntry> stream(InputStream inputStream) throws IOException {
    CsvMapper mapper = new CsvMapper();
    CsvSchema schema = CsvSchema.builder().addColumn("geonameid").addColumn("name").addColumn("asciiname").addColumn("alternatenames").addColumn("latitude").addColumn("longitude").addColumn("featureClass").addColumn("featureCode").addColumn("countryCode").addColumn("cc2").addColumn("admin1Code").addColumn("admin2Code").addColumn("admin3Code").addColumn("admin4Code").addColumn("population").addColumn("elevation").addColumn("dem").addColumn("timezone").addColumn("modificationDate").build().withColumnSeparator('\t').withoutQuoteChar();
    var reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
    MappingIterator<GeonamesEntry> recordIterator = mapper.readerFor(GeonamesEntry.class).with(schema).readValues(reader);
    Spliterator<GeonamesEntry> recordSpliterator = Spliterators.spliteratorUnknownSize(recordIterator, 0);
    return StreamSupport.stream(recordSpliterator, false);
}


@Test
public void getByNameAlt() throws Exception {
    List<Country> countries = CountryService.getInstance().getByName("Norge", false);
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals("Norway", countries.get(0).getName());
}

public List<Country> getByName(String name, boolean fullText) {
    if (fullText) {
        return fulltextSearch(name);
    } else {
        return substringSearch(name);
    }
}

@Test
public void getByRegion() throws Exception {
    List<Country> countries = CountryService.getInstance().getByRegion("Europe");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    for (Country country : countries) {
        Assert.assertEquals("Europe", country.getRegion());
    }
}

public List<Country> getByRegion(String region) {
    List<Country> result = new ArrayList<Country>();
    for (Country country : countries) {
        if (country.getRegion().toLowerCase().equals(region.toLowerCase())) {
            result.add(country);
        }
    }
    return result;
}

@Test
public void getByLanguageCode() throws Exception {
    List<Country> countries = CountryService.getInstance().getByLanguage("es");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    for (Country country : countries) {
        for (Language language : country.getLanguages()) {
            if (language.getIso639_1().equals("es")) {
                return;
            }
        }
    }
    Assert.fail();
}

public List<Country> getByLanguage(String language) {
    List<Country> result = new ArrayList<Country>();
    if (language.length() == 2) {
        for (Country country : countries) {
            for (Language lang : country.getLanguages()) {
                if (language.toLowerCase().equals(lang.getIso639_1())) {
                    result.add(country);
                }
            }
        }
    } else if (language.length() == 3) {
        for (Country country : countries) {
            for (Language lang : country.getLanguages()) {
                if (language.toLowerCase().equals(lang.getIso639_2())) {
                    result.add(country);
                }
            }
        }
    }
    return result;
}

@Test
public void getByCodeList() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCodeList("CO;NOR;EE");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals(3, countries.size());
    for (Country country : countries) {
        Assert.assertTrue(country.getAlpha2Code().equals("CO") || country.getAlpha2Code().equals("NO") || country.getAlpha2Code().equals("EE"));
    }
}

public List<Country> getByCodeList(String codelist) {
    List<Country> result = new ArrayList<Country>();
    if (codelist == null)
        return result;
    List<String> codes = Arrays.asList(codelist.split(ICountryRestSymbols.SEMICOLON));
    for (String code : codes) {
        Country country = this.getByAlpha(code);
        if (!result.contains(country))
            result.add(country);
    }
    return result;
}

@Test
public void getByAlpha2() throws Exception {
    Country country = CountryService.getInstance().getByAlpha("CO");
    Assert.assertNotNull(country);
    Assert.assertEquals("CO", country.getAlpha2Code());
}

public Country getByAlpha(String alpha) {
    int alphaLength = alpha.length();
    for (Country country : countries) {
        if (alphaLength == 2) {
            if (country.getAlpha2Code().toLowerCase().equals(alpha.toLowerCase())) {
                return country;
            }
        } else if (alphaLength == 3) {
            if (country.getAlpha3Code().toLowerCase().equals(alpha.toLowerCase())) {
                return country;
            }
        }
    }
    return null;
}

@Test
public void getByName() throws Exception {
    List<Country> countries = CountryService.getInstance().getByName("Norway", false);
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals("Norway", countries.get(0).getName());
}

@Test
public void getAll() throws Exception {
    List<Country> countries = CountryService.getInstance().getAll();
    Assert.assertFalse(countries.isEmpty());
    System.out.println("TOTAL Countries " + countries.size());
}

public List<Country> getAll() {
    return countries;
}

@Test
public void getByAlpha3() throws Exception {
    Country country = CountryService.getInstance().getByAlpha("COL");
    Assert.assertNotNull(country);
    Assert.assertEquals("COL", country.getAlpha3Code());
}

@Test
public void getByCapital() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCapital("Tallinn");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals(1, countries.size());
    Assert.assertEquals("EE", countries.get(0).getAlpha2Code());
    Assert.assertEquals("Eesti", countries.get(0).getNativeName());
}

public List<Country> getByCapital(String capital) {
    List<Country> result = new ArrayList<Country>();
    for (Country country : countries) {
        if (normalize(country.getCapital().toLowerCase()).contains(normalize(capital.toLowerCase()))) {
            result.add(country);
        }
    }
    return result;
}

@Test
public void getByCurrency() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCurrency("EUR");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    for (Country country : countries) {
        for (Currency currency : country.getCurrencies()) {
            currency.getCode().equals("EUR");
        }
    }
}

public List<Country> getByCurrency(String currency) {
    List<Country> result = new ArrayList<Country>();
    for (Country country : countries) {
        for (Currency curr : country.getCurrencies()) {
            if (curr.getCode() != null && currency.toLowerCase().equals(curr.getCode().toLowerCase())) {
                result.add(country);
            }
        }
    }
    return result;
}

@Test
public void getByNameFullText() throws Exception {
    List<Country> countries = CountryService.getInstance().getByName("Russian Federation", true);
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals("Russia", countries.get(0).getName());
}

@Test
public void getByNameFullTextNotFound() throws Exception {
    List<Country> countries = CountryService.getInstance().getByName("Russian Fed", true);
    Assert.assertNotNull(countries);
    Assert.assertTrue(countries.isEmpty());
}

@Test
public void getByNamePriority() throws Exception {
    List<Country> countries = CountryService.getInstance().getByName("Iran", false);
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals("Iran", countries.get(0).getName());
    countries = CountryService.getInstance().getByName("United", false);
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals("United States Minor Outlying Islands", countries.get(0).getName());
}

@Test
public void getByCallingCode() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCallingCode("57");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals(1, countries.size());
    Assert.assertEquals("CO", countries.get(0).getAlpha2Code());
}

public List<Country> getByCallingCode(String callingcode) {
    List<Country> result = new ArrayList<Country>();
    for (Country country : countries) {
        for (String callingCode : country.getCallingCodes()) {
            if (callingCode.equals(callingcode))
                result.add(country);
        }
    }
    return result;
}


@Test
public void getByCurrency() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCurrency("EUR");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    for (Country country : countries) {
        Assert.assertTrue(country.getCurrency().contains("EUR"));
    }
}

public List<Country> getByCurrency(String currency) {
    List<Country> result = new ArrayList<Country>();
    for (Country country : countries) {
        if (country.getCurrency().toLowerCase().contains(currency.toLowerCase())) {
            result.add(country);
        }
    }
    return result;
}

@Test
public void getByCallingCode() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCallingcode("57");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals(1, countries.size());
    Assert.assertEquals("CO", countries.get(0).getCca2());
}

public List<Country> getByCallingcode(String callingcode) {
    List<Country> result = new ArrayList<Country>();
    for (Country country : countries) {
        if (country.getCallingCode().equals(callingcode))
            result.add(country);
    }
    return result;
}

@Test
public void getByLanguageCode() throws Exception {
    List<Country> countries = CountryService.getInstance().getByLanguage("en");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    for (Country country : countries) {
        Assert.assertTrue(country.getLanguages().contains("en"));
    }
}

public List<Country> getByLanguage(String language) {
    List<Country> result = new ArrayList<Country>();
    for (Country country : countries) {
        for (String lang : country.getLanguages()) {
            if (lang.toLowerCase().equals(language.toLowerCase())) {
                result.add(country);
            }
        }
    }
    return result;
}

@Test
public void getBySubregion() throws Exception {
    List<Country> countries = CountryService.getInstance().getBySubregion("Northern Europe");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    for (Country country : countries) {
        Assert.assertEquals("Northern Europe", country.getSubregion());
    }
}

public List<Country> getBySubregion(String subregion) {
    List<Country> result = new ArrayList<Country>();
    for (Country country : countries) {
        if (country.getSubregion().toLowerCase().equals(subregion.toLowerCase())) {
            result.add(country);
        }
    }
    return result;
}

@Test
public void getByCapital() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCapital("Tallinn");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals(1, countries.size());
    Assert.assertEquals("EE", countries.get(0).getCca2());
}

@Test
public void getAll() throws Exception {
    List<Country> countries = CountryService.getInstance().getAll();
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertTrue(countries.size() >= 250);
}

@Test
public void getByName() throws Exception {
    List<Country> countries = CountryService.getInstance().getByName("Norway");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals("Norway", countries.get(0).getName());
}

public List<Country> getByName(String name) {
    List<Country> result = new ArrayList<Country>();
    for (Country country : countries) {
        if (normalize(country.getName().toLowerCase()).contains(normalize(name.toLowerCase()))) {
            result.add(country);
        }
        if (normalize(country.getAltSpellings().toLowerCase()).contains(normalize(name.toLowerCase())) && !result.contains(country)) {
            result.add(country);
        }
    }
    return result;
}

@Test
public void getByNameAlt() throws Exception {
    List<Country> countries = CountryService.getInstance().getByName("Norge");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals("Norway", countries.get(0).getName());
}

@Test
public void getByAlpha2() throws Exception {
    Country country = CountryService.getInstance().getByAlpha("CO");
    Assert.assertNotNull(country);
    Assert.assertEquals("CO", country.getCca2());
}

public Country getByAlpha(String alpha) {
    int alphaLength = alpha.length();
    for (Country country : countries) {
        if (alphaLength == 2) {
            if (country.getCca2().toLowerCase().equals(alpha.toLowerCase())) {
                return country;
            }
        } else if (alphaLength == 3) {
            if (country.getCca3().toLowerCase().equals(alpha.toLowerCase())) {
                return country;
            }
        }
    }
    return null;
}

@Test
public void getByAlpha3() throws Exception {
    Country country = CountryService.getInstance().getByAlpha("COL");
    Assert.assertNotNull(country);
    Assert.assertEquals("COL", country.getCca3());
}

@Test
public void getByCodeList() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCodeList("CO;NOR;EE");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals(3, countries.size());
    for (Country country : countries) {
        Assert.assertTrue(country.getCca2().equals("CO") || country.getCca2().equals("NO") || country.getCca2().equals("EE"));
    }
}


@Test
public void fail_fast_throws_terminal_failure_exception_without_reason_when_using_default_assertions_without_explicit_reason() {
    exception.expect(TerminalFailureException.class);
    exception.expectMessage(containsString("was less than <10>"));
    AtomicInteger ai = new AtomicInteger(0);
    new Thread(() -> {
        while (true) {
            ai.incrementAndGet();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }).start();
    ThrowingRunnable failFastAssertion = () -> assertThat(ai.get(), greaterThan(10));
    Awaitility.setDefaultFailFastCondition(failFastAssertion);
    await().timeout(Duration.ofSeconds(5)).until(ai::get, equalTo(-1));
}

public static void setDefaultFailFastCondition(ThrowingRunnable defaultFailFastAssertion) {
    setDefaultFailFastCondition(null, defaultFailFastAssertion);
}

@Test
public void fail_fast_throws_terminal_failure_exception_without_reason_when_using_default_assertions_with_explicit_reason() {
    exception.expect(TerminalFailureException.class);
    exception.expectMessage("fail fast reason");
    exception.expectCause(instanceOf(AssertionError.class));
    AtomicInteger ai = new AtomicInteger(0);
    new Thread(() -> {
        while (true) {
            ai.incrementAndGet();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }).start();
    ThrowingRunnable failFastAssertion = () -> assertThat(ai.get(), greaterThan(10));
    Awaitility.setDefaultFailFastCondition("fail fast reason", failFastAssertion);
    await().timeout(Duration.ofSeconds(5)).until(ai::get, equalTo(-1));
}

public static void setDefaultFailFastCondition(String failFastFailureReason, ThrowingRunnable defaultFailFastAssertion) {
    Awaitility.defaultFailFastCondition = new FailFastAssertion(failFastFailureReason, defaultFailFastAssertion);
}

@Test
public void fail_fast_throws_terminal_failure_exception_without_reason_when_using_assertions_with_explicit_reason() {
    exception.expect(TerminalFailureException.class);
    exception.expectMessage("fail fast reason");
    exception.expectCause(instanceOf(AssertionError.class));
    AtomicInteger ai = new AtomicInteger(0);
    new Thread(() -> {
        while (true) {
            ai.incrementAndGet();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }).start();
    ThrowingRunnable failFastAssertion = () -> assertThat(ai.get(), greaterThan(10));
    await().timeout(Duration.ofSeconds(5)).failFast("fail fast reason", failFastAssertion).until(ai::get, equalTo(-1));
}

public ConditionFactory failFast(String failFastFailureReason, final ThrowingRunnable failFastAssertion) {
    if (failFastAssertion == null) {
        throw new IllegalArgumentException("failFastAssertion cannot be null");
    }
    return new ConditionFactory(alias, timeoutConstraint, pollInterval, pollDelay, catchUncaughtExceptions, exceptionsIgnorer, conditionEvaluationListener, executorLifecycle, new FailFastAssertion(failFastFailureReason, failFastAssertion));
}

@Test
public void fail_fast_throws_terminal_failure_exception_without_reason_when_using_assertions_without_explicit_reason() {
    exception.expect(TerminalFailureException.class);
    exception.expectMessage(containsString("was less than <10>"));
    AtomicInteger ai = new AtomicInteger(0);
    new Thread(() -> {
        while (true) {
            ai.incrementAndGet();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }).start();
    ThrowingRunnable failFastAssertion = () -> assertThat(ai.get(), greaterThan(10));
    await().timeout(Duration.ofSeconds(5)).failFast(failFastAssertion).until(ai::get, equalTo(-1));
}

public ConditionFactory failFast(final ThrowingRunnable failFastAssertion) {
    return failFast(null, failFastAssertion);
}


@Test(expected = TerminalFailureException.class)
public void fail_fast() {
    AtomicInteger ai = new AtomicInteger(0);
    new Thread(() -> {
        while (true) {
            ai.incrementAndGet();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }).start();
    Callable<Boolean> failFastCondition = () -> ai.get() >= 10;
    await().timeout(Duration.ofSeconds(5)).failFast(failFastCondition, () -> new Exception("System crash")).until(() -> ai.get(), equalTo(-1));
}

public ConditionFactory failFast(final Callable<Boolean> failFastCondition, final Callable<Exception> failureReason) {
    if (failFastCondition == null) {
        throw new IllegalArgumentException("failFastCondition cannot be null");
    }
    return new ConditionFactory(alias, timeoutConstraint, pollInterval, pollDelay, catchUncaughtExceptions, exceptionsIgnorer, conditionEvaluationListener, executorLifecycle, new FailFastCondition(failFastCondition, failureReason));
}


@Test
public void shouldNotMessageInWrongOrder() {
    Message c1 = (Message) channel1.readOutbound();
    channel2.writeInbound(c1);
    Message c2 = (Message) channel2.readOutbound();
    channel1.writeInbound(c2);
    channel2.writeInbound(c1);
    assertTrue(channel2.readOutbound() instanceof FailureMessage);
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    try {
        checkIfMessage(msg);
        if (msg instanceof FailureMessage) {
            System.out.println("In Failure: " + ((FailureMessage) msg).getFailure());
        } else {
            Message message = (Message) msg;
            message.verify();
            if (processor.consumesInboundMessage(message)) {
                processor.onInboundMessage(message);
            } else {
                messageExecutor.sendMessageDownwards(message);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    try {
        checkIfMessage(msg);
        if (msg instanceof FailureMessage) {
            System.out.println("In Failure: " + ((FailureMessage) msg).getFailure());
        } else {
            Message message = (Message) msg;
            message.verify();
            if (processor.consumesInboundMessage(message)) {
                processor.onInboundMessage(message);
            } else {
                messageExecutor.sendMessageDownwards(message);
            }
        }
    } catch (LNException e1) {
        if (e1.shouldDisconnect()) {
            ctx.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}


@Test
public void shouldSendMessageC() {
    channel2.writeInbound(channel1.readOutbound());
    channel1.writeInbound(channel2.readOutbound());
    LNEstablishCMessage message = (LNEstablishCMessage) channel1.readOutbound();
    Channel channel1 = processor1.channel;
    Channel channel2 = processor2.channel;
    assertTrue(Arrays.equals(message.anchorHash, channel1.getAnchorTransactionServer(walletHelper2).getHash().getBytes()));
    Transaction escape = channel2.getEscapeTransactionServer();
    Transaction fastEscape = channel2.getFastEscapeTransactionServer();
    Sha256Hash hashEscape = escape.hashForSignature(0, channel2.getScriptAnchorOutputServer(), Transaction.SigHash.ALL, false);
    Sha256Hash hashFastEscape = fastEscape.hashForSignature(0, channel2.getScriptAnchorOutputServer(), Transaction.SigHash.ALL, false);
    assertTrue(channel2.getKeyClientA().verify(hashEscape, TransactionSignature.decodeFromBitcoin(message.signatureEscape, true)));
    assertTrue(channel2.getKeyClientA().verify(hashFastEscape, TransactionSignature.decodeFromBitcoin(message.signatureFastEscape, true)));
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    try {
        checkIfMessage(msg);
        if (msg instanceof FailureMessage) {
            System.out.println("In Failure: " + ((FailureMessage) msg).getFailure());
        } else {
            System.out.println(layerName + " I: " + msg);
            Message message = (Message) msg;
            message.verify();
            processor.onInboundMessage(message);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    try {
        checkIfMessage(msg);
        if (msg instanceof FailureMessage) {
            System.out.println("In Failure: " + ((FailureMessage) msg).getFailure());
        } else {
            Message message = (Message) msg;
            message.verify();
            if (processor.consumesInboundMessage(message)) {
                processor.onInboundMessage(message);
            } else {
                messageExecutor.sendMessageDownwards(message);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

@Test
public void shouldSendMessageD() {
    channel2.writeInbound(channel1.readOutbound());
    channel1.writeInbound(channel2.readOutbound());
    channel2.writeInbound(channel1.readOutbound());
    Channel channel = processor1.channel;
    LNEstablishDMessage message = (LNEstablishDMessage) channel2.readOutbound();
    Transaction escape = channel.getEscapeTransactionServer();
    Transaction fastEscape = channel.getFastEscapeTransactionServer();
    Sha256Hash hashEscape = escape.hashForSignature(0, channel.getScriptAnchorOutputServer(), Transaction.SigHash.ALL, false);
    Sha256Hash hashFastEscape = fastEscape.hashForSignature(0, channel.getScriptAnchorOutputServer(), Transaction.SigHash.ALL, false);
    assertTrue(channel.getKeyClientA().verify(hashEscape, TransactionSignature.decodeFromBitcoin(message.signatureEscape, true)));
    assertTrue(channel.getKeyClientA().verify(hashFastEscape, TransactionSignature.decodeFromBitcoin(message.signatureFastEscape, true)));
}

@Test
public void shouldNotAcceptSignature() {
    channel2.writeInbound(channel1.readOutbound());
    channel1.writeInbound(channel2.readOutbound());
    LNEstablishCMessage message = (LNEstablishCMessage) channel1.readOutbound();
    LNEstablishCMessage messageSwappedSign = new LNEstablishCMessage(message.signatureFastEscape, message.signatureEscape, message.anchorHash);
    channel2.writeInbound(messageSwappedSign);
    System.out.println(channel2.readOutbound());
}

@Test
public void shouldSendMessageB() {
    channel2.writeInbound(channel1.readOutbound());
    LNEstablishBMessage message = (LNEstablishBMessage) channel2.readOutbound();
    Channel channel = processor2.channel;
    assertTrue(Arrays.equals(message.pubKeyEscape, channel.getKeyServer().getPubKey()));
    assertTrue(Arrays.equals(message.pubKeyFastEscape, channel.getKeyServerA().getPubKey()));
    assertTrue(Arrays.equals(message.secretHashFastEscape, channel.getAnchorSecretHashServer()));
    assertTrue(Arrays.equals(message.anchorHash, channel.getAnchorTransactionServer(walletHelper1).getHash().getBytes()));
    assertEquals(message.serverAmount, channel.getAmountServer());
}


@Test
public void testConvertColumns() {
    df.convert(null, Long.class, Number.class);
    assertEquals(String.class, df.get("row1", "string").getClass());
    assertEquals(Long.class, df.get("row1", "long").getClass());
    assertEquals(Double.class, df.get("row1", "double").getClass());
    assertEquals(String.class, df.get("row1", "date").getClass());
    assertEquals(String.class, df.get("row1", "bool").getClass());
}

@SafeVarargs
public final DataFrame<V> convert(final Class<? extends V>... columnTypes) {
    Conversion.convert(this, columnTypes);
    return this;
}

@Test(expected = ClassCastException.class)
public void testConvertFails() {
    final DataFrame<String> bad = new DataFrame<>(Arrays.<String>asList("row1", "row2", "row3", "row4", "row5", "row6"), Arrays.<String>asList("string", "long", "double", "date"), Arrays.<List<String>>asList(Arrays.<String>asList("one", "two", "three", "four", "five", "six"), Arrays.<String>asList("1", "2", "3", "4", "5", "6"), Arrays.<String>asList("1.1", "2.2", "3.3", "4.4", "5.5", "6.6"), Arrays.<String>asList("2014-01-01", "2014-01-02", "2014-01-03", "2014-01-04", "2014-01-05", "2014-01-06")));
    bad.convert();
    @SuppressWarnings("unused")
    final String tmp = bad.get("row1", "long");
}

public DataFrame<V> convert() {
    Conversion.convert(this);
    return this;
}

@Test
public void testNotNull() {
    final DataFrame<Boolean> nonnulls = df.notnull();
    final Object[] expected = new Boolean[] { true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false };
    assertArrayEquals(expected, nonnulls.toArray());
}

public DataFrame<Boolean> notnull() {
    return Conversion.notnull(this);
}

@Test
public void testConvert() {
    df.convert();
    assertEquals(String.class, df.get("row1", "string").getClass());
    assertEquals(Long.class, df.get("row1", "long").getClass());
    assertEquals(Double.class, df.get("row1", "double").getClass());
    assertEquals(Date.class, df.get("row1", "date").getClass());
    assertEquals(Boolean.class, df.get("row1", "bool").getClass());
}

@Test
public void testIsNull() {
    final DataFrame<Boolean> nulls = df.isnull();
    final Object[] expected = new Boolean[] { false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true };
    assertArrayEquals(expected, nulls.toArray());
}

public DataFrame<Boolean> isnull() {
    return Conversion.isnull(this);
}

public DataFrame<V> transpose() {
    return new DataFrame<>(index.names(), columns.names(), new Views.ListView<>(this, true));
}


@Test
public void testNotNull() {
    final DataFrame<Boolean> nonnulls = df.notnull();
    final Object[] expected = new Boolean[] { true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false };
    assertArrayEquals(expected, nonnulls.transpose().toArray());
}

@Test
public void testIsNull() {
    final DataFrame<Boolean> nulls = df.isnull();
    final Object[] expected = new Boolean[] { false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true };
    assertArrayEquals(expected, nulls.transpose().toArray());
}

public Object[] toArray() {
    return toArray(new Object[size() * length()]);
}


@Test
public void testSelectNoMatch() {
    assertEquals(0, df.select(new DataFrame.Predicate<Object>() {

        @Override
        public Boolean apply(final List<Object> row) {
            return false;
        }
    }).length());
}

public DataFrame<V> select(final Predicate<V> predicate) {
    final BitSet selected = Selection.select(this, predicate);
    return new DataFrame<V>(Selection.select(index, selected), columns, Selection.select(data, selected), new Grouping());
}

@Test
public void testSelectIndex() {
    assertArrayEquals(new String[] { "row15" }, df.select(new DataFrame.Predicate<Object>() {

        @Override
        public Boolean apply(final List<Object> row) {
            return new Integer(150).equals(row.get(1));
        }
    }).index().toArray());
}

@Test
public void testSelectMatch() {
    assertEquals(1, df.select(new DataFrame.Predicate<Object>() {

        @Override
        public Boolean apply(final List<Object> row) {
            return new Integer(150).equals(row.get(1));
        }
    }).length());
}


@Test(expected = ClassCastException.class)
public void testCastFails() {
    final DataFrame<Date> dates = df.cast(Date.class);
    @SuppressWarnings("unused")
    final Date dt = dates.get(0, 0);
}

public V get(final int row, final int col) {
    return data.get(col, row);
}


@Test
public void testIterRows() {
    int i = 0;
    final ListIterator<List<Object>> it = df.iterrows();
    while (it.hasNext()) {
        final List<Object> row = it.next();
        assertEquals(i % 2 == 0 ? "alpha" : "beta", row.get(0));
        assertEquals(String.valueOf(i + 1), row.get(1));
        i++;
    }
    assertEquals(df.length(), i);
    while (it.hasPrevious()) {
        i--;
        final List<Object> row = it.previous();
        assertEquals(i % 2 == 0 ? "alpha" : "beta", row.get(0));
        assertEquals(String.valueOf(i + 1), row.get(1));
    }
    assertEquals(0, i);
}

public ListIterator<List<V>> iterrows() {
    return new Views.ListView<>(this, true).listIterator();
}

@Test
public void testIterValues() {
    int i = 0;
    final ListIterator<Object> it = df.itervalues();
    while (it.hasNext()) {
        it.next();
        i++;
    }
    assertEquals(df.size() * df.length(), i);
    while (it.hasPrevious()) {
        i--;
        it.previous();
    }
    assertEquals(0, i);
}

public ListIterator<V> itervalues() {
    return new Views.FlatView<>(this).listIterator();
}

@Test
public void testIterMap() {
    int i = 0;
    final ListIterator<Map<String, Object>> it = df.itermap();
    while (it.hasNext()) {
        final Map<String, Object> row = it.next();
        assertEquals(i % 2 == 0 ? "alpha" : "beta", row.get("name"));
        assertEquals(String.valueOf(i + 1), row.get("value"));
        i++;
    }
    assertEquals(df.length(), i);
    while (it.hasPrevious()) {
        i--;
        final Map<String, Object> row = it.previous();
        assertEquals(i % 2 == 0 ? "alpha" : "beta", row.get("name"));
        assertEquals(String.valueOf(i + 1), row.get("value"));
    }
    assertEquals(0, i);
}

public ListIterator<Map<String, V>> itermap() {
    return new Views.MapView<>(this, true).listIterator();
}

@Test
public void testIterCols() {
    int i = 0;
    final Iterator<List<Object>> it = df.itercols();
    while (it.hasNext()) {
        final List<Object> col = it.next();
        assertEquals(4, col.size());
        i++;
    }
    assertEquals(i, df.size());
}

public ListIterator<List<V>> itercols() {
    return new Views.ListView<>(this, false).listIterator();
}