/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.bookkeeper.meta;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.Lists;
import java.io.IOException;
import java.util.Base64;
import java.util.Optional;
import java.util.Random;
import org.apache.bookkeeper.client.LedgerMetadataBuilder;
import org.apache.bookkeeper.client.api.DigestType;
import org.apache.bookkeeper.client.api.LedgerMetadata;
import org.apache.bookkeeper.net.BookieSocketAddress;
import org.junit.Assert;
import org.junit.Test;

/**
 * Test Ledger Metadata serialization and deserialization.
 */
public class TestLedgerMetadataSerDe {
    // as used in 4.0.x & 4.1.x
    private static final String version1 =
        "Qm9va2llTWV0YWRhdGFGb3JtYXRWZXJzaW9uCTEKMgozCjAKMAkxOTIuMC4yLjE6MTIzNAkxOTIu"
        + "MC4yLjI6MTIzNAkxOTIuMC4yLjM6MTIzNAotMTAyCUNMT1NFRA==";

    // as used in 4.2.x & 4.3.x (text protobuf based metadata, password and digest introduced)
    private static final String version2 =
        "Qm9va2llTWV0YWRhdGFGb3JtYXRWZXJzaW9uCTIKcXVvcnVtU2l6ZTogMgplbnNlbWJsZVNpemU6I"
        + "DMKbGVuZ3RoOiAwCmxhc3RFbnRyeUlkOiAtMQpzdGF0ZTogSU5fUkVDT1ZFUlkKc2VnbWVudCB7"
        + "CiAgZW5zZW1ibGVNZW1iZXI6ICIxOTIuMC4yLjE6MTIzNCIKICBlbnNlbWJsZU1lbWJlcjogIjE"
        + "5Mi4wLjIuMjoxMjM0IgogIGVuc2VtYmxlTWVtYmVyOiAiMTkyLjAuMi4zOjEyMzQiCiAgZmlyc3"
        + "RFbnRyeUlkOiAwCn0KZGlnZXN0VHlwZTogQ1JDMzIKcGFzc3dvcmQ6ICJwYXNzd2QiCmFja1F1b"
        + "3J1bVNpemU6IDIK";

    // version 2 + ctime, as used in 4.4.x to 4.8.x (ctime is optional from 4.6.x onwards)
    private static final String version2ctime =
        "Qm9va2llTWV0YWRhdGFGb3JtYXRWZXJzaW9uCTIKcXVvcnVtU2l6ZTogMgplbnNlbWJsZVNpemU6I"
        + "DMKbGVuZ3RoOiAwCmxhc3RFbnRyeUlkOiAtMQpzdGF0ZTogSU5fUkVDT1ZFUlkKc2VnbWVudCB7"
        + "CiAgZW5zZW1ibGVNZW1iZXI6ICIxOTIuMC4yLjE6MTIzNCIKICBlbnNlbWJsZU1lbWJlcjogIjE"
        + "5Mi4wLjIuMjoxMjM0IgogIGVuc2VtYmxlTWVtYmVyOiAiMTkyLjAuMi4zOjEyMzQiCiAgZmlyc3"
        + "RFbnRyeUlkOiAwCn0KZGlnZXN0VHlwZTogQ1JDMzIKcGFzc3dvcmQ6ICJwYXNzd2QiCmFja1F1b"
        + "3J1bVNpemU6IDIKY3RpbWU6IDE1NDQwMDIzODMwNzUK";

    // version 3, since 4.9.x, protobuf binary format
    private static final String version3 =
        "Qm9va2llTWV0YWRhdGFGb3JtYXRWZXJzaW9uCTMKYAgCEAMYACD///////////8BKAEyMgoOMTkyL"
        + "jAuMi4xOjMxODEKDjE5Mi4wLjIuMjozMTgxCg4xOTIuMC4yLjM6MzE4MRAAOANCBmZvb2JhckgB"
        + "UP///////////wFgAA==";

    private static void testDecodeEncode(String encoded) throws Exception {
        LedgerMetadataSerDe serDe = new LedgerMetadataSerDe();
        LedgerMetadata md = serDe.parseConfig(Base64.getDecoder().decode(encoded), 59L, Optional.empty());
        String reserialized = Base64.getEncoder().encodeToString(serDe.serialize(md));

        Assert.assertEquals(encoded, reserialized);
    }

    @Test
    public void testVersion1SerDe() throws Exception {
        testDecodeEncode(version1);
    }

    @Test
    public void testVersion2SerDe() throws Exception {
        testDecodeEncode(version2);
    }

    @Test
    public void testVersion2CtimeSerDe() throws Exception {
        testDecodeEncode(version2ctime);
    }

    @Test
    public void testVersion3SerDe() throws Exception {
        testDecodeEncode(version3);
    }

    @Test(expected = IOException.class)
    public void testJunkSerDe() throws Exception {
        LedgerMetadataSerDe serDe = new LedgerMetadataSerDe();
        String junk = "";
        serDe.parseConfig(junk.getBytes(UTF_8), 59L, Optional.empty());
    }

    @Test(expected = IOException.class)
    public void testJunk2SerDe() throws Exception {
        byte[] randomBytes = new byte[1000];
        new Random().nextBytes(randomBytes);
        LedgerMetadataSerDe serDe = new LedgerMetadataSerDe();
        serDe.parseConfig(randomBytes, 59L, Optional.empty());
    }

    @Test(expected = IOException.class)
    public void testJunkVersionSerDe() throws Exception {
        byte[] junkVersion = "BookieMetadataFormatVersion\tfoobar\nblahblah".getBytes(UTF_8);
        LedgerMetadataSerDe serDe = new LedgerMetadataSerDe();
        serDe.parseConfig(junkVersion, 59L, Optional.empty());
    }

    @Test(expected = IOException.class)
    public void testVeryLongVersionSerDe() throws Exception {
        byte[] veryLongVersion = "BookieMetadataFormatVersion\t123456789123456789\nblahblah".getBytes(UTF_8);
        LedgerMetadataSerDe serDe = new LedgerMetadataSerDe();
        serDe.parseConfig(veryLongVersion, 59L, Optional.empty());
    }

    @Test
    public void testPeggedToV3SerDe() throws Exception {
        LedgerMetadataSerDe serDe = new LedgerMetadataSerDe();
        LedgerMetadata metadata = LedgerMetadataBuilder.create().withId(13L)
            .withEnsembleSize(3).withWriteQuorumSize(2).withAckQuorumSize(1)
            .withPassword("foobar".getBytes(UTF_8)).withDigestType(DigestType.CRC32C)
            .newEnsembleEntry(0L, Lists.newArrayList(new BookieSocketAddress("192.0.2.1", 3181).toBookieId(),
                                                     new BookieSocketAddress("192.0.2.2", 3181).toBookieId(),
                                                     new BookieSocketAddress("192.0.2.3", 3181).toBookieId()))
            .build();
        byte[] encoded = serDe.serialize(metadata);

        LedgerMetadata decoded = serDe.parseConfig(encoded, 59L, Optional.empty());
        Assert.assertEquals(LedgerMetadataSerDe.METADATA_FORMAT_VERSION_3, decoded.getMetadataFormatVersion());
    }

    @Test
    public void testStoreSystemtimeAsLedgerCtimeEnabledWithNewerVersion()
            throws Exception {
        LedgerMetadata lm = LedgerMetadataBuilder.create().withId(13L)
            .withEnsembleSize(3).withWriteQuorumSize(2).withAckQuorumSize(1)
            .withPassword("foobar".getBytes(UTF_8)).withDigestType(DigestType.CRC32C)
            .newEnsembleEntry(0L, Lists.newArrayList(new BookieSocketAddress("192.0.2.1", 1234).toBookieId(),
                                      new BookieSocketAddress("192.0.2.2", 1234).toBookieId(),
                                      new BookieSocketAddress("192.0.2.3", 1234).toBookieId()))
            .withCreationTime(123456L)
            .storingCreationTime(true)
            .build();
        LedgerMetadataSerDe serDe = new LedgerMetadataSerDe();
        byte[] serialized = serDe.serialize(lm);
        LedgerMetadata deserialized = serDe.parseConfig(serialized, 59L, Optional.of(654321L));
        Assert.assertEquals(deserialized.getCtime(), 123456L);

        // give it another round
        LedgerMetadata deserialized2 = serDe.parseConfig(serDe.serialize(deserialized), 59L, Optional.of(98765L));
        Assert.assertEquals(deserialized2.getCtime(), 123456L);
    }

    @Test
    public void testStoreSystemtimeAsLedgerCtimeDisabledWithNewerVersion()
            throws Exception {
        LedgerMetadata lm = LedgerMetadataBuilder.create().withId(13L)
            .withEnsembleSize(3).withWriteQuorumSize(2).withAckQuorumSize(1)
            .withPassword("foobar".getBytes(UTF_8)).withDigestType(DigestType.CRC32C)
            .newEnsembleEntry(0L, Lists.newArrayList(new BookieSocketAddress("192.0.2.1", 1234).toBookieId(),
                                      new BookieSocketAddress("192.0.2.2", 1234).toBookieId(),
                                      new BookieSocketAddress("192.0.2.3", 1234).toBookieId()))
            .build();
        LedgerMetadataSerDe serDe = new LedgerMetadataSerDe();
        byte[] serialized = serDe.serialize(lm);
        LedgerMetadata deserialized = serDe.parseConfig(serialized, 59L, Optional.of(654321L));
        Assert.assertEquals(654321L, deserialized.getCtime());

        // give it another round
        LedgerMetadata deserialized2 = serDe.parseConfig(serDe.serialize(deserialized), 59L, Optional.of(98765L));
        Assert.assertEquals(98765L, deserialized2.getCtime());
    }
}
