/*
 * 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.cql3.functions;

import org.junit.Test;

import org.apache.cassandra.exceptions.InvalidRequestException;

import static java.util.List.of;
import static org.apache.cassandra.cql3.CQL3Type.Native.ASCII;
import static org.apache.cassandra.cql3.CQL3Type.Native.BIGINT;
import static org.apache.cassandra.cql3.CQL3Type.Native.INT;
import static org.apache.cassandra.cql3.CQL3Type.Native.SMALLINT;
import static org.apache.cassandra.cql3.CQL3Type.Native.TEXT;
import static org.apache.cassandra.cql3.CQL3Type.Native.TINYINT;
import static org.apache.cassandra.cql3.CQL3Type.Native.VARINT;
import static org.apache.cassandra.cql3.functions.FormatFcts.format;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.quicktheories.QuickTheory.qt;
import static org.quicktheories.generators.SourceDSL.integers;

public class FormatBytesFctTest extends AbstractFormatFctTest
{
    @Test
    public void testOneValueArgumentExact()
    {
        createTable(of(INT), new Object[][]{ { 1, 1073741825 },
                                             { 2, 1073741823 },
                                             { 3, 0 } }); // 0 B
        assertRows(execute("select format_bytes(col1) from %s where pk = 1"), row("1 GiB"));
        assertRows(execute("select format_bytes(col1) from %s where pk = 2"), row("1024 MiB"));
        assertRows(execute("select format_bytes(col1) from %s where pk = 3"), row("0 B"));
    }

    @Test
    public void testOneValueArgumentDecimalRoundup()
    {
        createTable(of(INT), new Object[][]{ { 1, 1563401650 },
                                             { 2, 1072441589 },
                                             { 3, 102775 },
                                             { 4, 102 } });
        assertRows(execute("select format_bytes(col1) from %s where pk = 1"), row("1.46 GiB")); // 1.4560
        assertRows(execute("select format_bytes(col1) from %s where pk = 2"), row("1022.76 MiB")); // 1022.7599
        assertRows(execute("select format_bytes(col1) from %s where pk = 3"), row("100.37 KiB")); // 100.3662
        assertRows(execute("select format_bytes(col1) from %s where pk = 4"), row("102 B"));
    }

    @Test
    public void testOneValueArgumentDecimalRoundDown()
    {
        createTable(of(INT), new Object[][]{ { 1, 1557999386 },
                                             { 2, 1072433201 },
                                             { 3, 102769 },
                                             { 4, 102 } });
        assertRows(execute("select format_bytes(col1) from %s where pk = 1"), row("1.45 GiB")); // 1.451
        assertRows(execute("select format_bytes(col1) from %s where pk = 2"), row("1022.75 MiB")); // 1022.752
        assertRows(execute("select format_bytes(col1) from %s where pk = 3"), row("100.36 KiB")); // 100.3613
        assertRows(execute("select format_bytes(col1) from %s where pk = 4"), row("102 B"));
    }

    @Test
    public void testValueAndUnitArgumentsExact()
    {
        createTable(of(INT), new Object[][]{ { 1, 1073741825 },
                                             { 2, 0 } });
        assertRows(execute("select format_bytes(col1, 'B') from %s where pk = 1"), row("1073741825 B"));
        assertRows(execute("select format_bytes(col1, 'KiB') from %s where pk = 1"), row("1048576 KiB"));
        assertRows(execute("select format_bytes(col1, 'MiB') from %s where pk = 1"), row("1024 MiB"));
        assertRows(execute("select format_bytes(col1, 'GiB') from %s where pk = 1"), row("1 GiB"));

        assertRows(execute("select format_bytes(col1, 'B') from %s where pk = 2"), row("0 B"));
        assertRows(execute("select format_bytes(col1, 'KiB') from %s where pk = 2"), row("0 KiB"));
        assertRows(execute("select format_bytes(col1, 'MiB') from %s where pk = 2"), row("0 MiB"));
        assertRows(execute("select format_bytes(col1, 'GiB') from %s where pk = 2"), row("0 GiB"));
    }

    @Test
    public void testValueAndUnitArgumentsDecimal()
    {
        createTable(of(INT), new Object[][]{ { 1, 1563401650 },
                                             { 2, 1557999336 } });
        assertRows(execute("select format_bytes(col1, 'B') from %s where pk = 1"), row("1563401650 B"));
        assertRows(execute("select format_bytes(col1, 'KiB') from %s where pk = 1"), row("1526759.42 KiB"));
        assertRows(execute("select format_bytes(col1, 'MiB') from %s where pk = 1"), row("1490.98 MiB"));
        assertRows(execute("select format_bytes(col1, 'GiB') from %s where pk = 1"), row("1.46 GiB"));

        assertRows(execute("select format_bytes(col1, 'B') from %s where pk = 2"), row("1557999336 B"));
        assertRows(execute("select format_bytes(col1, 'KiB') from %s where pk = 2"), row("1521483.73 KiB"));
        assertRows(execute("select format_bytes(col1, 'MiB') from %s where pk = 2"), row("1485.82 MiB"));
        assertRows(execute("select format_bytes(col1, 'GiB') from %s where pk = 2"), row("1.45 GiB"));
    }

    @Test
    public void testValueWithSourceAndTargetArgumentExact()
    {
        createTable(of(INT), new Object[][]{ { 1, 1073741825 },
                                             { 2, 1 },
                                             { 3, 0 } });
        assertRows(execute("select format_bytes(col1, 'B',   'B') from %s where pk = 1"), row("1073741825 B"));
        assertRows(execute("select format_bytes(col1, 'B', 'KiB') from %s where pk = 1"), row("1048576 KiB"));
        assertRows(execute("select format_bytes(col1, 'B', 'MiB') from %s where pk = 1"), row("1024 MiB"));
        assertRows(execute("select format_bytes(col1, 'B', 'GiB') from %s where pk = 1"), row("1 GiB"));

        assertRows(execute("select format_bytes(col1, 'GiB', 'GiB') from %s where pk = 2"), row("1 GiB"));
        assertRows(execute("select format_bytes(col1, 'GiB', 'MiB') from %s where pk = 2"), row("1024 MiB"));
        assertRows(execute("select format_bytes(col1, 'GiB', 'KiB') from %s where pk = 2"), row("1048576 KiB"));
        assertRows(execute("select format_bytes(col1, 'GiB',   'B') from %s where pk = 2"), row("1073741824 B"));

        assertRows(execute("select format_bytes(col1, 'GiB', 'GiB') from %s where pk = 3"), row("0 GiB"));
        assertRows(execute("select format_bytes(col1, 'GiB', 'MiB') from %s where pk = 3"), row("0 MiB"));
        assertRows(execute("select format_bytes(col1, 'GiB', 'KiB') from %s where pk = 3"), row("0 KiB"));
        assertRows(execute("select format_bytes(col1, 'GiB',   'B') from %s where pk = 3"), row("0 B"));
    }

    @Test
    public void testValueWithSourceAndTargetArgumentDecimal()
    {
        createTable(of(INT), new Object[][]{ { 1, 1563401650 },
                                             { 2, 1557999336 },});
        assertRows(execute("select format_bytes(col1, 'B',   'B') from %s where pk = 1"), row("1563401650 B"));
        assertRows(execute("select format_bytes(col1, 'B', 'KiB') from %s where pk = 1"), row("1526759.42 KiB"));
        assertRows(execute("select format_bytes(col1, 'B', 'MiB') from %s where pk = 1"), row("1490.98 MiB"));
        assertRows(execute("select format_bytes(col1, 'B', 'GiB') from %s where pk = 1"), row("1.46 GiB"));

        assertRows(execute("select format_bytes(col1, 'B', 'B') from %s where pk = 2"), row("1557999336 B"));
        assertRows(execute("select format_bytes(col1, 'B', 'KiB') from %s where pk = 2"), row("1521483.73 KiB"));
        assertRows(execute("select format_bytes(col1, 'B', 'MiB') from %s where pk = 2"), row("1485.82 MiB"));
        assertRows(execute("select format_bytes(col1, 'B', 'GiB') from %s where pk = 2"), row("1.45 GiB"));
    }

    @Test
    public void testFuzzNumberGenerators()
    {
        createTable("CREATE TABLE %s (pk int primary key, col1 int)");

        qt().withExamples(1024).forAll(integers().allPositive()).checkAssert(
        (randInt) -> {
            execute("INSERT INTO %s (pk, col1) VALUES (?, ?)", 1, randInt);

            assertRows(execute("select format_bytes(col1, 'MiB') from %s where pk = 1"), row(format(randInt / 1024.0 / 1024.0) + " MiB"));
            assertRows(execute("select format_bytes(col1, 'KiB', 'GiB') from %s where pk = 1"), row(format(randInt / 1024.0 / 1024.0) + " GiB"));
            assertRows(execute("select format_bytes(col1, 'B', 'GiB') from %s where pk = 1"), row(format(randInt / 1024.0 / 1024.0 / 1024.0 ) + " GiB"));
        });
    }

    @Test
    public void testOverflow()
    {
        createTable(of(BIGINT, INT, SMALLINT, TINYINT),
                    new Object[][]{ { 1,
                                      1073741825L * 1024 + 1,
                                      Integer.MAX_VALUE - 1,
                                      Short.MAX_VALUE - 1,
                                      Byte.MAX_VALUE - 1 },
                                    { 2,
                                      1073741825L * 1024 + 1,
                                      Integer.MAX_VALUE,
                                      Short.MAX_VALUE,
                                      Byte.MAX_VALUE } });

        // this will stop at Long.MAX_VALUE
        assertRows(execute("select format_bytes(col1, 'GiB', 'B') from %s where pk = 1"), row("9223372036854776000 B"));
        assertRows(execute("select format_bytes(col2, 'GiB', 'B') from %s where pk = 1"), row("2305843007066210300 B"));
        assertRows(execute("select format_bytes(col3, 'GiB', 'B') from %s where pk = 1"), row("35182224605184 B"));
        assertRows(execute("select format_bytes(col4, 'GiB', 'B') from %s where pk = 1"), row("135291469824 B"));

        assertRows(execute("select format_bytes(col2, 'GiB', 'B') from %s where pk = 2"), row("2305843008139952130 B"));
        assertRows(execute("select format_bytes(col3, 'GiB', 'B') from %s where pk = 2"), row("35183298347008 B"));
        assertRows(execute("select format_bytes(col4, 'GiB', 'B') from %s where pk = 2"), row("136365211648 B"));
    }

    @Test
    public void testAllSupportedColumnTypes()
    {
        createTable(of(INT, TINYINT, SMALLINT, BIGINT, VARINT, ASCII, TEXT),
                    new Object[][]{ { 1,
                                      Integer.MAX_VALUE,
                                      Byte.MAX_VALUE,
                                      Short.MAX_VALUE,
                                      Long.MAX_VALUE,
                                      Integer.MAX_VALUE,
                                      '\'' + Integer.valueOf(Integer.MAX_VALUE).toString() + '\'',
                                      '\'' + Integer.valueOf(Integer.MAX_VALUE).toString() + '\'',
                                      } });

        assertRows(execute("select format_bytes(col1) from %s where pk = 1"), row("2 GiB"));
        assertRows(execute("select format_bytes(col2) from %s where pk = 1"), row("127 B"));
        assertRows(execute("select format_bytes(col3) from %s where pk = 1"), row("32 KiB"));
        assertRows(execute("select format_bytes(col4) from %s where pk = 1"), row("8589934592 GiB"));
        assertRows(execute("select format_bytes(col5) from %s where pk = 1"), row("2 GiB"));
        assertRows(execute("select format_bytes(col6) from %s where pk = 1"), row("2 GiB"));
        assertRows(execute("select format_bytes(col7) from %s where pk = 1"), row("2 GiB"));
    }

    @Test
    public void testNegativeValueIsInvalid()
    {
        createDefaultTable(new Object[][]{ { "1", "-1", "-2" } });
        assertThatThrownBy(() -> execute("select format_bytes(col1) from %s where pk = 1"))
        .isInstanceOf(InvalidRequestException.class)
        .hasMessageContaining("value must be non-negative");
    }

    @Test
    public void testUnparsableTextIsInvalid()
    {
        createTable(of(TEXT), new Object[][]{ { 1, "'abc'" }, { 2, "'-1'" } });

        assertThatThrownBy(() -> execute("select format_bytes(col1) from %s where pk = 1"))
        .isInstanceOf(InvalidRequestException.class)
        .hasMessageContaining("unable to convert string 'abc' to a value of type long");

        assertThatThrownBy(() -> execute("select format_bytes(col1) from %s where pk = 2"))
        .isInstanceOf(InvalidRequestException.class)
        .hasMessageContaining("value must be non-negative");
    }

    @Test
    public void testInvalidUnits()
    {
        createDefaultTable(new Object[][]{ { "1", "1", "2" } });
        for (String functionCall : new String[] {
        "format_bytes(col1, 'abc')",
        "format_bytes(col1, 'B', 'abc')",
        "format_bytes(col1, 'abc', 'B')",
        "format_bytes(col1, 'abc', 'abc')"
        })
        {
            assertThatThrownBy(() -> execute("select " + functionCall + " from %s where pk = 1"))
            .isInstanceOf(InvalidRequestException.class)
            .hasMessageContaining("Unsupported data storage unit: abc. Supported units are: B, KiB, MiB, GiB");
        }
    }

    @Test
    public void testInvalidArgumentsSize()
    {
        createDefaultTable(new Object[][]{ { "1", "1", "2" } });
        
        // Test arguemnt size = 0
        assertThatThrownBy(() -> execute("select format_bytes() from %s where pk = 1"))
        .isInstanceOf(InvalidRequestException.class)
        .hasMessageContaining("Invalid number of arguments for function system.format_bytes([int|tinyint|smallint|bigint|varint|ascii|text], [ascii], [ascii])");

        // Test argument size > 3
        assertThatThrownBy(() -> execute("select format_bytes(col1, 'B', 'KiB', 'GiB') from %s where pk = 1"))
        .isInstanceOf(InvalidRequestException.class)
        .hasMessageContaining("Invalid number of arguments for function system.format_bytes([int|tinyint|smallint|bigint|varint|ascii|text], [ascii], [ascii])");
    }

    @Test
    public void testHandlingNullValues()
    {
        createTable(of(TEXT, ASCII, INT),
                    new Object[][]{ { 1, null, null, null } });

        assertRows(execute("select format_bytes(col1), format_bytes(col2), format_bytes(col3) from %s where pk = 1"),
                   row(null, null, null));

        assertRows(execute("select format_bytes(col1, 'B') from %s where pk = 1"), row((Object) null));
        assertRows(execute("select format_bytes(col1, 'B', 'KiB') from %s where pk = 1"), row((Object) null));
    }

    @Test
    public void testHandlingNullArguments()
    {
        createTable(of(TEXT, ASCII, INT),
                    new Object[][]{ { 1, null, null, null },
                                    { 2, "'1'", "'2'", 3 } });

        assertRows(execute("select format_bytes(col1, null) from %s where pk = 1"), row((Object) null));

        for (String functionCall : new String[] {
        "format_bytes(col3, null)",
        "format_bytes(col3, null, null)",
        "format_bytes(col3, null, 'KiB')",
        "format_bytes(col3, 'KiB', null)"
        })
        {
            assertThatThrownBy(() -> execute("select " + functionCall + " from %s where pk = 2"))
            .isInstanceOf(InvalidRequestException.class)
            .hasMessageContaining("none of the arguments may be null");
        }
    }

    @Test
    public void testSizeSmallerThan1KibiByte()
    {
        createDefaultTable(new Object[][]{ { "1", "900", "2000" } });
        assertRows(execute("select format_bytes(col1) from %s where pk = 1"), row("900 B"));
    }
}
