/*
* 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.cassandra.db.marshal;

import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.util.*;

import com.google.common.collect.Lists;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
import static org.junit.Assert.fail;
import static org.junit.Assert.assertEquals;
import static org.quicktheories.QuickTheory.qt;

import org.apache.cassandra.SchemaLoader;
import org.apache.cassandra.Util;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.partitions.ImmutableBTreePartition;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.SyntaxException;
import org.apache.cassandra.schema.KeyspaceParams;
import org.apache.cassandra.serializers.MarshalException;
import org.apache.cassandra.serializers.UUIDSerializer;
import org.apache.cassandra.utils.*;
import org.assertj.core.api.Assertions;
import org.quicktheories.generators.SourceDSL;

public class CompositeTypeTest
{
    private static final String KEYSPACE1 = "CompositeTypeTest";
    private static final String CF_STANDARDCOMPOSITE = "StandardComposite";
    private static final CompositeType comparator;
    static
    {
        List<AbstractType<?>> subComparators = new ArrayList<AbstractType<?>>();
        subComparators.add(BytesType.instance);
        subComparators.add(TimeUUIDType.instance);
        subComparators.add(IntegerType.instance);
        comparator = CompositeType.getInstance(subComparators);
    }

    private static final int UUID_COUNT = 3;
    private static final TimeUUID[] uuids = new TimeUUID[UUID_COUNT];
    static
    {
        for (int i = 0; i < UUID_COUNT; ++i)
            uuids[i] = nextTimeUUID();
    }

    @BeforeClass
    public static void defineSchema() throws ConfigurationException
    {
        AbstractType<?> composite = CompositeType.getInstance(Arrays.asList(new AbstractType<?>[]{BytesType.instance, TimeUUIDType.instance, IntegerType.instance}));
        SchemaLoader.prepareServer();
        SchemaLoader.createKeyspace(KEYSPACE1,
                                    KeyspaceParams.simple(1),
                                    SchemaLoader.standardCFMD(KEYSPACE1, CF_STANDARDCOMPOSITE,
                                                              0,
                                                              AsciiType.instance, // key
                                                              AsciiType.instance, // value
                                                              composite)); // clustering
    }

    @Test
    public void testEndOfComponent()
    {
        ByteBuffer[] cnames = {
            createCompositeKey("test1", uuids[0], -1, false),
            createCompositeKey("test1", uuids[1], 24, false),
            createCompositeKey("test1", uuids[1], 42, false),
            createCompositeKey("test1", uuids[1], 83, false),
            createCompositeKey("test1", uuids[2], -1, false),
            createCompositeKey("test1", uuids[2], 42, false),
        };

        ByteBuffer start = createCompositeKey("test1", uuids[1], -1, false);
        ByteBuffer stop = createCompositeKey("test1", uuids[1], -1, true);

        for (int i = 0; i < 1; ++i)
        {
            assert comparator.compare(start, cnames[i]) > 0;
            assert comparator.compare(stop, cnames[i]) > 0;
        }
        for (int i = 1; i < 4; ++i)
        {
            assert comparator.compare(start, cnames[i]) < 0;
            assert comparator.compare(stop, cnames[i]) > 0;
        }
        for (int i = 4; i < cnames.length; ++i)
        {
            assert comparator.compare(start, cnames[i]) < 0;
            assert comparator.compare(stop, cnames[i]) < 0;
        }
    }

    @Test
    public void testGetString()
    {
        String test1Hex = ByteBufferUtil.bytesToHex(ByteBufferUtil.bytes("test1"));
        ByteBuffer key = createCompositeKey("test1", uuids[1], 42, false);
        assert comparator.getString(key).equals(test1Hex + ":" + uuids[1] + ":42");

        key = createCompositeKey("test1", uuids[1], -1, true);
        assert comparator.getString(key).equals(test1Hex + ":" + uuids[1] + ":!");
    }

    @Test
    public void testFromString()
    {
        String test1Hex = ByteBufferUtil.bytesToHex(ByteBufferUtil.bytes("test1"));
        ByteBuffer key = createCompositeKey("test1", uuids[1], 42, false);
        assert key.equals(comparator.fromString(test1Hex + ":" + uuids[1] + ":42"));

        key = createCompositeKey("test1", uuids[1], -1, true);
        assert key.equals(comparator.fromString(test1Hex + ":" + uuids[1] + ":!"));
    }

    @Test
    public void testValidate()
    {
        ByteBuffer key = createCompositeKey("test1", uuids[1], 42, false);
        comparator.validate(key);

        key = createCompositeKey("test1", (ByteBuffer) null, -1, false);
        comparator.validate(key);

        key = createCompositeKey("test1", uuids[2], -1, true);
        comparator.validate(key);

        key.get(); // make sure we're not aligned anymore
        try
        {
            comparator.validate(key);
            fail("Should not validate");
        }
        catch (MarshalException e) {}

        key = ByteBuffer.allocate(3 + "test1".length() + 3 + 14);
        key.putShort((short) "test1".length());
        key.put(ByteBufferUtil.bytes("test1"));
        key.put((byte) 0);
        key.putShort((short) 14);
        key.rewind();
        try
        {
            comparator.validate(key);
            fail("Should not validate");
        }
        catch (MarshalException e)
        {
            assert e.toString().contains("should be 16 or 0 bytes");
        }

        key = createCompositeKey("test1", UUIDSerializer.instance.serialize(UUID.randomUUID()), 42, false);
        try
        {
            comparator.validate(key);
            fail("Should not validate");
        }
        catch (MarshalException e)
        {
            assert e.toString().contains("Invalid version for TimeUUID type");
        }
    }

    @Test
    public void testFullRound() throws Exception
    {
        Keyspace keyspace = Keyspace.open(KEYSPACE1);
        ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF_STANDARDCOMPOSITE);

        ByteBuffer cname1 = createCompositeKey("test1", (ByteBuffer) null, -1, false);
        ByteBuffer cname2 = createCompositeKey("test1", uuids[0], 24, false);
        ByteBuffer cname3 = createCompositeKey("test1", uuids[0], 42, false);
        ByteBuffer cname4 = createCompositeKey("test2", uuids[0], -1, false);
        ByteBuffer cname5 = createCompositeKey("test2", uuids[1], 42, false);

        ByteBuffer key = ByteBufferUtil.bytes("k");

        long ts = FBUtilities.timestampMicros();
        new RowUpdateBuilder(cfs.metadata(), ts, key).clustering(cname5).add("val", "cname5").build().applyUnsafe();
        new RowUpdateBuilder(cfs.metadata(), ts, key).clustering(cname1).add("val", "cname1").build().applyUnsafe();
        new RowUpdateBuilder(cfs.metadata(), ts, key).clustering(cname4).add("val", "cname4").build().applyUnsafe();
        new RowUpdateBuilder(cfs.metadata(), ts, key).clustering(cname2).add("val", "cname2").build().applyUnsafe();
        new RowUpdateBuilder(cfs.metadata(), ts, key).clustering(cname3).add("val", "cname3").build().applyUnsafe();

        ColumnMetadata cdef = cfs.metadata().getColumn(ByteBufferUtil.bytes("val"));

        ImmutableBTreePartition readPartition = Util.getOnlyPartitionUnfiltered(Util.cmd(cfs, key).build());
        Iterator<Row> iter = readPartition.iterator();

        compareValues(iter.next().getCell(cdef), "cname1");
        compareValues(iter.next().getCell(cdef), "cname2");
        compareValues(iter.next().getCell(cdef), "cname3");
        compareValues(iter.next().getCell(cdef), "cname4");
        compareValues(iter.next().getCell(cdef), "cname5");
    }
    private void compareValues(Cell<?> c, String r) throws CharacterCodingException
    {
        assert ByteBufferUtil.string(c.buffer()).equals(r) : "Expected: {" + ByteBufferUtil.string(c.buffer()) + "} got: {" + r + "}";
    }

    @Test
    public void testEmptyParametersNotallowed()
    {
        try
        {
            TypeParser.parse("CompositeType");
            fail("Shouldn't work");
        }
        catch (ConfigurationException e) {}
        catch (SyntaxException e) {}

        try
        {
            TypeParser.parse("CompositeType()");
            fail("Shouldn't work");
        }
        catch (ConfigurationException e) {}
        catch (SyntaxException e) {}
    }

    @Test
    public void testCompatibility() throws Exception
    {
        assert TypeParser.parse("CompositeType(IntegerType, BytesType)").isCompatibleWith(TypeParser.parse("CompositeType(IntegerType)"));
        assert TypeParser.parse("CompositeType(IntegerType, BytesType)").isCompatibleWith(TypeParser.parse("CompositeType(IntegerType, BytesType)"));
        assert TypeParser.parse("CompositeType(BytesType, BytesType)").isCompatibleWith(TypeParser.parse("CompositeType(AsciiType, BytesType)"));

        assert !TypeParser.parse("CompositeType(IntegerType)").isCompatibleWith(TypeParser.parse("CompositeType(IntegerType, BytesType)"));
        assert !TypeParser.parse("CompositeType(IntegerType)").isCompatibleWith(TypeParser.parse("CompositeType(BytesType)"));
    }

    @Test
    public void testEscapeUnescape()
    {
        List<AbstractType<?>> subComparators = new ArrayList<AbstractType<?>>(){{;
            add(UTF8Type.instance);
            add(UTF8Type.instance);
        }};
        CompositeType comp = CompositeType.getInstance(subComparators);

        String[][] inputs = new String[][]{
            new String[]{ "foo", "bar" },
            new String[]{ "", "" },
            new String[]{ "foo\\", "bar" },
            new String[]{ "foo\\:", "bar" },
            new String[]{ "foo:", "bar" },
            new String[]{ "foo", "b:ar" },
            new String[]{ "foo!", "b:ar" },
        };


        for (String[] input : inputs)
        {
            ByteBuffer[] bbs = new ByteBuffer[input.length];
            for (int i = 0; i < input.length; i++)
                bbs[i] = UTF8Type.instance.fromString(input[i]);

            ByteBuffer value = comp.fromString(comp.getString(CompositeType.build(ByteBufferAccessor.instance, bbs)));
            ByteBuffer[] splitted = comp.split(value);
            for (int i = 0; i < splitted.length; i++)
                assertEquals(input[i], UTF8Type.instance.getString(splitted[i]));
        }
    }

    private ByteBuffer createCompositeKey(String s, TimeUUID uuid, int i, boolean lastIsOne)
    {
        return createCompositeKey(s, uuid == null ? null : uuid.toBytes(), i, lastIsOne);
    }

    private ByteBuffer createCompositeKey(String s, ByteBuffer uuid, int i, boolean lastIsOne)
    {
        ByteBuffer bytes = ByteBufferUtil.bytes(s);
        int totalSize = 0;
        if (s != null)
        {
            totalSize += 2 + bytes.remaining() + 1;
            if (uuid != null)
            {
                totalSize += 2 + 16 + 1;
                if (i != -1)
                {
                    totalSize += 2 + 1 + 1;
                }
            }
        }

        ByteBuffer bb = ByteBuffer.allocate(totalSize);

        if (s != null)
        {
            bb.putShort((short) bytes.remaining());
            bb.put(bytes);
            bb.put(uuid == null && lastIsOne ? (byte)1 : (byte)0);
            if (uuid != null)
            {
                bb.putShort((short) 16);
                bb.put(uuid);
                bb.put(i == -1 && lastIsOne ? (byte)1 : (byte)0);
                if (i != -1)
                {
                    // We are putting a byte only because our test use ints that fit in a byte *and* IntegerType.fromString() will
                    // return something compatible (i.e, putting a full int here would break 'fromStringTest')
                    bb.putShort((short) 1);
                    bb.put((byte)i);
                    bb.put(lastIsOne ? (byte)1 : (byte)0);
                }
            }
        }
        bb.rewind();
        return bb;
    }

    private static <V> void testToFromString(ByteBuffer bytes, ValueAccessor<V> accessor, CompositeType type)
    {
        V value = accessor.valueOf(bytes);
        String s = type.getString(value, accessor);
        ByteBuffer fromString = type.fromString(s);
        Assert.assertEquals(bytes, fromString);
    }


    @Test
    public void testLargeValues()
    {
        CompositeType type = CompositeType.getInstance(Lists.newArrayList(BytesType.instance));
        ByteBuffer expected = ByteBuffer.allocate(0xFFFE);
        new Random(0).nextBytes(expected.array());
        ByteBuffer serialized = CompositeType.build(ByteBufferAccessor.instance, expected);
        for (ValueAccessor<?> accessor : ValueAccessors.ACCESSORS)
            testToFromString(serialized, accessor, type);
    }

    @Test
    public void testSizeHeader()
    {
        ByteBuffer bb = ByteBuffer.allocate(2);
        qt().forAll(SourceDSL.integers().between(0, FBUtilities.MAX_UNSIGNED_SHORT)).checkAssert(size -> {
            bb.clear();

            ByteBufferUtil.writeShortLength(bb, size);
            bb.flip();
            Assertions.assertThat(ByteBufferAccessor.instance.getUnsignedShort(bb, 0))
                      .isEqualTo(ByteBufferUtil.readShortLength(bb))
                      .isEqualTo(size);
        });
    }
}
