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

import static com.datastax.driver.core.ProtocolOptions.DEFAULT_MAX_SCHEMA_AGREEMENT_WAIT_SECONDS;
import static com.google.common.collect.FluentIterable.from;
import static org.apache.zeppelin.cassandra.CassandraInterpreter.CASSANDRA_CLUSTER_NAME;
import static org.apache.zeppelin.cassandra.CassandraInterpreter.CASSANDRA_COMPRESSION_PROTOCOL;
import static org.apache.zeppelin.cassandra.CassandraInterpreter.CASSANDRA_CREDENTIALS_PASSWORD;
import static org.apache.zeppelin.cassandra.CassandraInterpreter.CASSANDRA_CREDENTIALS_USERNAME;
import static org.apache.zeppelin.cassandra.CassandraInterpreter.CASSANDRA_HOSTS;
import static org.apache.zeppelin.cassandra.CassandraInterpreter.CASSANDRA_PORT;
import static org.apache.zeppelin.cassandra.CassandraInterpreter.CASSANDRA_PROTOCOL_VERSION;
import static org.apache.zeppelin.cassandra.CassandraInterpreter.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.when;

import com.datastax.driver.core.Cluster;
import com.datastax.driver.core.ProtocolVersion;
import com.datastax.driver.core.Session;

import info.archinnov.achilles.embedded.CassandraEmbeddedServerBuilder;

import org.apache.zeppelin.display.AngularObjectRegistry;
import org.apache.zeppelin.interpreter.Interpreter;
import org.apache.zeppelin.interpreter.InterpreterContext;
import org.apache.zeppelin.interpreter.InterpreterResult;
import org.apache.zeppelin.interpreter.InterpreterResult.Code;
import org.junit.*;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Properties;

@RunWith(MockitoJUnitRunner.class)
public class CassandraInterpreterTest {

    private static final String ARTISTS_TABLE = "zeppelin.artists";

    public static Session session = CassandraEmbeddedServerBuilder
        .noEntityPackages()
        .withKeyspaceName("zeppelin")
        .withScript("prepare_schema.cql")
        .withScript("prepare_data.cql")
        .withProtocolVersion(ProtocolVersion.V3)
        .buildNativeSessionOnly();

    private static CassandraInterpreter interpreter;

    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private InterpreterContext intrContext;

    @BeforeClass
    public static void setUp() {
        Properties properties = new Properties();
        final Cluster cluster = session.getCluster();

        properties.setProperty(CASSANDRA_CLUSTER_NAME, cluster.getClusterName());
        properties.setProperty(CASSANDRA_COMPRESSION_PROTOCOL, "NONE");
        properties.setProperty(CASSANDRA_CREDENTIALS_USERNAME, "none");
        properties.setProperty(CASSANDRA_CREDENTIALS_PASSWORD, "none");

        properties.setProperty(CASSANDRA_PROTOCOL_VERSION, "3");
        properties.setProperty(CASSANDRA_LOAD_BALANCING_POLICY, "DEFAULT");
        properties.setProperty(CASSANDRA_RETRY_POLICY, "DEFAULT");
        properties.setProperty(CASSANDRA_RECONNECTION_POLICY, "DEFAULT");
        properties.setProperty(CASSANDRA_SPECULATIVE_EXECUTION_POLICY, "DEFAULT");

        properties.setProperty(CASSANDRA_MAX_SCHEMA_AGREEMENT_WAIT_SECONDS,
                DEFAULT_MAX_SCHEMA_AGREEMENT_WAIT_SECONDS + "");

        properties.setProperty(CASSANDRA_POOLING_NEW_CONNECTION_THRESHOLD_LOCAL, "100");
        properties.setProperty(CASSANDRA_POOLING_NEW_CONNECTION_THRESHOLD_REMOTE, "100");
        properties.setProperty(CASSANDRA_POOLING_CORE_CONNECTION_PER_HOST_LOCAL, "2");
        properties.setProperty(CASSANDRA_POOLING_CORE_CONNECTION_PER_HOST_REMOTE, "1");
        properties.setProperty(CASSANDRA_POOLING_MAX_CONNECTION_PER_HOST_LOCAL, "8");
        properties.setProperty(CASSANDRA_POOLING_MAX_CONNECTION_PER_HOST_REMOTE, "2");
        properties.setProperty(CASSANDRA_POOLING_MAX_REQUESTS_PER_CONNECTION_LOCAL, "1024");
        properties.setProperty(CASSANDRA_POOLING_MAX_REQUESTS_PER_CONNECTION_REMOTE, "256");

        properties.setProperty(CASSANDRA_POOLING_IDLE_TIMEOUT_SECONDS, "120");
        properties.setProperty(CASSANDRA_POOLING_POOL_TIMEOUT_MILLIS, "5000");
        properties.setProperty(CASSANDRA_POOLING_HEARTBEAT_INTERVAL_SECONDS, "30");

        properties.setProperty(CASSANDRA_QUERY_DEFAULT_CONSISTENCY, "ONE");
        properties.setProperty(CASSANDRA_QUERY_DEFAULT_SERIAL_CONSISTENCY, "SERIAL");
        properties.setProperty(CASSANDRA_QUERY_DEFAULT_FETCH_SIZE, "5000");

        properties.setProperty(CASSANDRA_SOCKET_CONNECTION_TIMEOUT_MILLIS, "5000");
        properties.setProperty(CASSANDRA_SOCKET_READ_TIMEOUT_MILLIS, "12000");
        properties.setProperty(CASSANDRA_SOCKET_TCP_NO_DELAY, "true");

        properties.setProperty(CASSANDRA_HOSTS, from(cluster.getMetadata().getAllHosts()).first().get().getAddress().getHostAddress());
        properties.setProperty(CASSANDRA_PORT, cluster.getConfiguration().getProtocolOptions().getPort()+"");
        interpreter = new CassandraInterpreter(properties);
        interpreter.open();
    }

    @AfterClass
    public static void tearDown() {
        interpreter.close();
    }

    @Before
    public void prepareContext() {
        when(intrContext.getParagraphTitle()).thenReturn("Paragraph1");
    }

    @Test
    public void should_create_cluster_and_session_upon_call_to_open() throws Exception {
        assertThat(interpreter.cluster).isNotNull();
        assertThat(interpreter.cluster.getClusterName()).isEqualTo(session.getCluster().getClusterName());
        assertThat(interpreter.session).isNotNull();
        assertThat(interpreter.helper).isNotNull();
    }

    @Test
    public void should_interpret_simple_select() throws Exception {
        //Given

        //When
        final InterpreterResult actual = interpreter.interpret("SELECT * FROM " + ARTISTS_TABLE + " LIMIT 10;", intrContext);

        //Then
        assertThat(actual).isNotNull();
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
        assertThat(actual.message()).isEqualTo("name\tborn\tcountry\tdied\tgender\tstyles\ttype\n" +
                "Bogdan Raczynski\t1977-01-01\tPoland\tnull\tMale\t[Dance, Electro]\tPerson\n" +
                "Krishna Das\t1947-05-31\tUSA\tnull\tMale\t[Unknown]\tPerson\n" +
                "Sheryl Crow\t1962-02-11\tUSA\tnull\tFemale\t[Classic, Rock, Country, Blues, Pop, Folk]\tPerson\n" +
                "Doof\t1968-08-31\tUnited Kingdom\tnull\tnull\t[Unknown]\tPerson\n" +
                "House of Large Sizes\t1986-01-01\tUSA\t2003\tnull\t[Unknown]\tGroup\n" +
                "Fanfarlo\t2006-01-01\tUnited Kingdom\tnull\tnull\t[Rock, Indie, Pop, Classic]\tGroup\n" +
                "Jeff Beck\t1944-06-24\tUnited Kingdom\tnull\tMale\t[Rock, Pop, Classic]\tPerson\n" +
                "Los Paranoias\tnull\tUnknown\tnull\tnull\t[Unknown]\tnull\n" +
                "…And You Will Know Us by the Trail of Dead\t1994-01-01\tUSA\tnull\tnull\t[Rock, Pop, Classic]\tGroup\n");

    }

    @Test
    public void should_interpret_select_statement() throws Exception {
        //Given

        //When
        final InterpreterResult actual = interpreter.interpret("SELECT * FROM " + ARTISTS_TABLE + " LIMIT 2;", intrContext);

        //Then
        assertThat(actual).isNotNull();
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
        assertThat(actual.message()).isEqualTo("name\tborn\tcountry\tdied\tgender\tstyles\ttype\n" +
                "Bogdan Raczynski\t1977-01-01\tPoland\tnull\tMale\t[Dance, Electro]\tPerson\n" +
                "Krishna Das\t1947-05-31\tUSA\tnull\tMale\t[Unknown]\tPerson\n");

    }

    @Test
    public void should_interpret_multiple_statements_with_single_line_logged_batch() throws Exception {
        //Given
        String statements = "CREATE TABLE IF NOT EXISTS zeppelin.albums(\n" +
                "    title text PRIMARY KEY,\n" +
                "    artist text,\n" +
                "    year int\n" +
                ");\n" +
                "BEGIN BATCH"+
                "   INSERT INTO zeppelin.albums(title,artist,year) VALUES('The Impossible Dream EP','Carter the Unstoppable Sex Machine',1992);"+
                "   INSERT INTO zeppelin.albums(title,artist,year) VALUES('The Way You Are','Tears for Fears',1983);"+
                "   INSERT INTO zeppelin.albums(title,artist,year) VALUES('Primitive','Soulfly',2003);"+
                "APPLY BATCH;\n"+
                "SELECT * FROM zeppelin.albums;";
        //When
        final InterpreterResult actual = interpreter.interpret(statements, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
        assertThat(actual.message()).isEqualTo("title\tartist\tyear\n" +
                "The Impossible Dream EP\tCarter the Unstoppable Sex Machine\t1992\n" +
                "The Way You Are\tTears for Fears\t1983\n" +
                "Primitive\tSoulfly\t2003\n");
    }
    
    @Test
    public void should_throw_statement_not_having_semi_colon() throws Exception {
        //Given
        String statement = "SELECT * zeppelin.albums";

        //When
        final InterpreterResult actual = interpreter.interpret(statement, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.ERROR);
        assertThat(actual.message())
                .contains("Error parsing input:\n" +
                        "\t'SELECT * zeppelin.albums'\n" +
                        "Did you forget to add ; (semi-colon) at the end of each CQL statement ?");
    }

    @Test
    public void should_validate_statement() throws Exception {
        //Given
        String statement = "SELECT * zeppelin.albums;";

        //When
        final InterpreterResult actual = interpreter.interpret(statement, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.ERROR);
        assertThat(actual.message()).contains("line 1:9 missing K_FROM at 'zeppelin' (SELECT * [zeppelin]....)");
    }

    @Test
    public void should_execute_statement_with_consistency_option() throws Exception {
        //Given
        String statement = "@consistency=THREE\n" +
                "SELECT * FROM zeppelin.artists LIMIT 1;";

        //When
        final InterpreterResult actual = interpreter.interpret(statement, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.ERROR);
        assertThat(actual.message())
                .contains("Not enough replicas available for query at consistency THREE (3 required but only 1 alive)");
    }

    @Test
    public void should_execute_statement_with_serial_consistency_option() throws Exception {
        //Given
        String statement = "@serialConsistency=SERIAL\n" +
                "SELECT * FROM zeppelin.artists LIMIT 1;";

        //When
        final InterpreterResult actual = interpreter.interpret(statement, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
    }

    @Test
    public void should_execute_statement_with_timestamp_option() throws Exception {
        //Given
        String statement1 = "INSERT INTO zeppelin.ts(key,val) VALUES('k','v1');";
        String statement2 = "@timestamp=15\n" +
                "INSERT INTO zeppelin.ts(key,val) VALUES('k','v2');";

        // Insert v1 with current timestamp
        interpreter.interpret(statement1, intrContext);

        Thread.sleep(1);

        //When
        // Insert v2 with past timestamp
        interpreter.interpret(statement2, intrContext);
        final String actual = session.execute("SELECT * FROM zeppelin.ts LIMIT 1").one().getString("val");

        //Then
        assertThat(actual).isEqualTo("v1");
    }

    @Test
    public void should_execute_statement_with_retry_policy() throws Exception {
        //Given
        String statement = "@retryPolicy=" + interpreter.LOGGING_DOWNGRADING_RETRY + "\n" +
                "@consistency=THREE\n" +
                "SELECT * FROM zeppelin.artists LIMIT 1;";

        //When
        final InterpreterResult actual = interpreter.interpret(statement, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
    }

    @Test
    public void should_execute_statement_with_request_timeout() throws Exception {
        //Given
        String statement = "@requestTimeOut=10000000\n" +
                "SELECT * FROM zeppelin.artists;";

        //When
        final InterpreterResult actual = interpreter.interpret(statement, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
    }

    @Test
    public void should_execute_prepared_and_bound_statements() throws Exception {
        //Given
        String queries = "@prepare[ps]=INSERT INTO zeppelin.prepared(key,val) VALUES(?,?)\n" +
                "@prepare[select]=SELECT * FROM zeppelin.prepared WHERE key=:key\n" +
                "@bind[ps]='myKey','myValue'\n" +
                "@bind[select]='myKey'";

        //When
        final InterpreterResult actual = interpreter.interpret(queries, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
        assertThat(actual.message()).isEqualTo("key\tval\n" +
                "myKey\tmyValue\n");
    }

    @Test
    public void should_execute_bound_statement() throws Exception {
        //Given
        String queries = "@prepare[users_insert]=INSERT INTO zeppelin.users" +
                "(login,firstname,lastname,addresses,location)" +
                "VALUES(:login,:fn,:ln,:addresses,:loc)\n" +
                "@bind[users_insert]='jdoe','John','DOE'," +
                "{street_number: 3, street_name: 'Beverly Hills Bld', zip_code: 90209," +
                " country: 'USA', extra_info: ['Right on the hills','Next to the post box']," +
                " phone_numbers: {'home': 2016778524, 'office': 2015790847}}," +
                "('USA', 90209, 'Beverly Hills')\n" +
                "SELECT * FROM zeppelin.users WHERE login='jdoe';";
        //When
        final InterpreterResult actual = interpreter.interpret(queries, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
        assertThat(actual.message()).isEqualTo(
                "login\taddresses\tage\tdeceased\tfirstname\tlast_update\tlastname\tlocation\n" +
                        "jdoe\t" +
                        "{street_number:3,street_name:'Beverly Hills Bld',zip_code:90209," +
                        "country:'USA',extra_info:['Right on the hills','Next to the post box']," +
                        "phone_numbers:{'office':2015790847,'home':2016778524}}\tnull\t" +
                        "null\t" +
                        "John\t" +
                        "null\t" +
                        "DOE\t" +
                        "('USA',90209,'Beverly Hills')\n");
    }

    @Test
    public void should_exception_when_executing_unknown_bound_statement() throws Exception {
        //Given
        String queries = "@bind[select_users]='jdoe'";

        //When
        final InterpreterResult actual = interpreter.interpret(queries, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.ERROR);
        assertThat(actual.message())
                .isEqualTo("The statement 'select_users' can not be bound to values. " +
                        "Are you sure you did prepare it with @prepare[select_users] ?");
    }

    @Test
    public void should_extract_variable_from_statement() throws Exception {
        //Given
        AngularObjectRegistry angularObjectRegistry = new AngularObjectRegistry("cassandra", null);
        when(intrContext.getAngularObjectRegistry()).thenReturn(angularObjectRegistry);
        when(intrContext.getGui().input("login", "hsue")).thenReturn("hsue");
        when(intrContext.getGui().input("age", "27")).thenReturn("27");

        String queries = "@prepare[test_insert_with_variable]=" +
                "INSERT INTO zeppelin.users(login,firstname,lastname,age) VALUES(?,?,?,?)\n" +
                "@bind[test_insert_with_variable]='{{login=hsue}}','Helen','SUE',{{age=27}}\n" +
                "SELECT firstname,lastname,age FROM zeppelin.users WHERE login='hsue';";
        //When
        final InterpreterResult actual = interpreter.interpret(queries, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
        assertThat(actual.message()).isEqualTo("firstname\tlastname\tage\n" +
                "Helen\tSUE\t27\n");

    }

    @Test
    public void should_just_prepare_statement() throws Exception {
        //Given
        String queries = "@prepare[just_prepare]=SELECT name,country,styles FROM zeppelin.artists LIMIT 3";
        final String expected = reformatHtml(
                readTestResource("/scalate/NoResult.html"));

        //When
        final InterpreterResult actual = interpreter.interpret(queries, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
        assertThat(reformatHtml(actual.message())).isEqualTo(expected);
    }

    @Test
    public void should_execute_bound_statement_with_no_bound_value() throws Exception {
        //Given
        String queries = "@prepare[select_no_bound_value]=SELECT name,country,styles FROM zeppelin.artists LIMIT 3\n" +
                "@bind[select_no_bound_value]";

        //When
        final InterpreterResult actual = interpreter.interpret(queries, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
        assertThat(actual.message()).isEqualTo("name\tcountry\tstyles\n" +
                "Bogdan Raczynski\tPoland\t[Dance, Electro]\n" +
                "Krishna Das\tUSA\t[Unknown]\n" +
                "Sheryl Crow\tUSA\t[Classic, Rock, Country, Blues, Pop, Folk]\n");

    }

    @Test
    public void should_parse_date_value() throws Exception {
        //Given
        String queries = "@prepare[parse_date]=INSERT INTO zeppelin.users(login,last_update) VALUES(?,?)\n" +
                "@bind[parse_date]='last_update','2015-07-30 12:00:01'\n" +
                "SELECT last_update FROM zeppelin.users WHERE login='last_update';";
        //When
        final InterpreterResult actual = interpreter.interpret(queries, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
        assertThat(actual.message()).contains("last_update\n" +
                "Thu Jul 30 12:00:01");
    }

    @Test
    public void should_bind_null_value() throws Exception {
        //Given
        String queries = "@prepare[bind_null]=INSERT INTO zeppelin.users(login,firstname,lastname) VALUES(?,?,?)\n" +
                "@bind[bind_null]='bind_null',null,'NULL'\n" +
                "SELECT firstname,lastname FROM zeppelin.users WHERE login='bind_null';";
        //When
        final InterpreterResult actual = interpreter.interpret(queries, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
        assertThat(actual.message()).isEqualTo("firstname\tlastname\n" +
                "null\tNULL\n");
    }

    @Test
    public void should_bind_boolean_value() throws Exception {
        //Given
        String queries = "@prepare[bind_boolean]=INSERT INTO zeppelin.users(login,deceased) VALUES(?,?)\n" +
                "@bind[bind_boolean]='bind_bool',false\n" +
                "SELECT login,deceased FROM zeppelin.users WHERE login='bind_bool';";
        //When
        final InterpreterResult actual = interpreter.interpret(queries, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
        assertThat(actual.message()).isEqualTo("login\tdeceased\n" +
                "bind_bool\tfalse\n");
    }

    @Test
    public void should_fail_when_executing_a_removed_prepared_statement() throws Exception {
        //Given
        String prepare_first = "@prepare[to_be_removed]=INSERT INTO zeppelin.users(login,deceased) VALUES(?,?)";
        interpreter.interpret(prepare_first, intrContext);
        String remove_prepared = "@remove_prepare[to_be_removed]\n" +
                "@bind[to_be_removed]='bind_bool'";

        //When
        final InterpreterResult actual = interpreter.interpret(remove_prepared, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.ERROR);
        assertThat(actual.message()).isEqualTo("The statement 'to_be_removed' can not be bound to values. " +
                "Are you sure you did prepare it with @prepare[to_be_removed] ?");
    }

    @Test
    public void should_display_statistics_for_non_select_statement() throws Exception {
        //Given
        String query = "USE zeppelin;\nCREATE TABLE IF NOT EXISTS no_select(id int PRIMARY KEY);";
        final String rawResult = reformatHtml(readTestResource("/scalate/NoResultWithExecutionInfo.html"));

        //When
        final InterpreterResult actual = interpreter.interpret(query, intrContext);
        final Cluster cluster = session.getCluster();
        final int port = cluster.getConfiguration().getProtocolOptions().getPort();
        final String address = cluster.getMetadata().getAllHosts().iterator().next()
                .getAddress().getHostAddress()
                .replaceAll("/", "").replaceAll("\\[", "").replaceAll("\\]","");
        //Then
        final String expected = rawResult.replaceAll("TRIED_HOSTS", address+":"+port)
                .replaceAll("QUERIED_HOSTS", address +":"+port);


        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
        assertThat(reformatHtml(actual.message())).isEqualTo(expected);
    }

    @Test
    public void should_error_and_display_stack_trace() throws Exception {
        //Given
        String query = "@consistency=THREE\n" +
                "SELECT * FROM zeppelin.users LIMIT 3;";
        //When
        final InterpreterResult actual = interpreter.interpret(query, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.ERROR);
        assertThat(actual.message()).contains("All host(s) tried for query failed");
    }

    @Test
    public void should_describe_cluster() throws Exception {
        //Given

        String query = "DESCRIBE CLUSTER;";
        final String expected = reformatHtml(
                readTestResource("/scalate/DescribeCluster.html"));

        //When
        final InterpreterResult actual = interpreter.interpret(query, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);

        assertThat(reformatHtml(actual.message())).isEqualTo(expected);
    }

    @Test
    public void should_describe_keyspaces() throws Exception {
        //Given
        String query = "DESCRIBE KEYSPACES;";
        final String expected = reformatHtml(
                readTestResource("/scalate/DescribeKeyspaces.html"));

        //When
        final InterpreterResult actual = interpreter.interpret(query, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);

        assertThat(reformatHtml(actual.message())).isEqualTo(expected);
    }

    @Test
         public void should_describe_keyspace() throws Exception {
        //Given
        String query = "DESCRIBE KEYSPACE live_data;";
        final String expected = reformatHtml(
                readTestResource("/scalate/DescribeKeyspace_live_data.html"));

        //When
        final InterpreterResult actual = interpreter.interpret(query, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);

        assertThat(reformatHtml(actual.message())).isEqualTo(expected);
    }

    @Test
    @Ignore
    //TODO activate test when using Java 8 and C* 3.x
    public void should_describe_function() throws Exception {
        //Given
        Properties properties = new Properties();
        properties.setProperty(CASSANDRA_HOSTS, "127.0.0.1");
        properties.setProperty(CASSANDRA_PORT,  "9042");
        Interpreter interpreter = new CassandraInterpreter(properties);
        interpreter.open();

        String createFunction = "CREATE FUNCTION zeppelin.maxof(val1 int,val2 int) " +
                "RETURNS NULL ON NULL INPUT " +
                "RETURNS int " +
                "LANGUAGE java " +
                "AS $$" +
                "    return Math.max(val1, val2);\n" +
                "$$;";
        interpreter.interpret(createFunction, intrContext);
        String query = "DESCRIBE FUNCTION zeppelin.maxOf;";

        //When
        final InterpreterResult actual = interpreter.interpret(query, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
        assertThat(actual.message()).isEqualTo("xxxxx");
    }

    @Test
    @Ignore
    //TODO activate test when using Java 8 and C* 3.x
    public void should_describe_aggregate() throws Exception {
        //Given
        Properties properties = new Properties();
        properties.setProperty(CASSANDRA_HOSTS, "127.0.0.1");
        properties.setProperty(CASSANDRA_PORT,  "9042");
        Interpreter interpreter = new CassandraInterpreter(properties);
        interpreter.open();

        final String query = "DESCRIBE AGGREGATES;";

        //When
        final InterpreterResult actual = interpreter.interpret(query, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);

    }

    @Test
    @Ignore
    //TODO activate test when using Java 8 and C* 3.x
    public void should_describe_materialized_view() throws Exception {
        //Given
        Properties properties = new Properties();
        properties.setProperty(CASSANDRA_HOSTS, "127.0.0.1");
        properties.setProperty(CASSANDRA_PORT,  "9042");
        Interpreter interpreter = new CassandraInterpreter(properties);
        interpreter.open();

        final String query = "DESCRIBE MATERIALIZED VIEWS;";

        //When
        final InterpreterResult actual = interpreter.interpret(query, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
    }

    @Test
    public void should_describe_table() throws Exception {
        //Given
        String query = "DESCRIBE TABLE live_data.complex_table;";
        final String expected = reformatHtml(
                readTestResource("/scalate/DescribeTable_live_data_complex_table.html"));

        //When
        final InterpreterResult actual = interpreter.interpret(query, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);

        assertThat(reformatHtml(actual.message())).isEqualTo(expected);
    }

    @Test
    public void should_describe_udt() throws Exception {
        //Given
        String query = "DESCRIBE TYPE live_data.address;";
        final String expected = reformatHtml(
                readTestResource("/scalate/DescribeType_live_data_address.html"));

        //When
        final InterpreterResult actual = interpreter.interpret(query, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);

        assertThat(reformatHtml(actual.message())).isEqualTo(expected);
    }

    @Test
    public void should_describe_udt_withing_logged_in_keyspace() throws Exception {
        //Given
        String query = "USE live_data;\n" +
                "DESCRIBE TYPE address;";
        final String expected = reformatHtml(
                readTestResource("/scalate/DescribeType_live_data_address_within_current_keyspace.html"));

        //When
        final InterpreterResult actual = interpreter.interpret(query, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);

        assertThat(reformatHtml(actual.message())).isEqualTo(expected);
    }

    @Test
    public void should_error_describing_non_existing_table() throws Exception {
        //Given
        String query = "USE system;\n" +
                "DESCRIBE TABLE complex_table;";

        //When
        final InterpreterResult actual = interpreter.interpret(query, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.ERROR);
        assertThat(actual.message()).contains("Cannot find table system.complex_table");
    }

    @Test
    public void should_error_describing_non_existing_udt() throws Exception {
        //Given
        String query = "USE system;\n" +
                "DESCRIBE TYPE address;";

        //When
        final InterpreterResult actual = interpreter.interpret(query, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.ERROR);
        assertThat(actual.message()).contains("Cannot find type system.address");
    }

    @Test
    public void should_show_help() throws Exception {
        //Given
        String query = "HELP;";
        final String expected = reformatHtml(readTestResource("/scalate/Help.html"));

        //When
        final InterpreterResult actual = interpreter.interpret(query, intrContext);

        //Then
        assertThat(actual.code()).isEqualTo(Code.SUCCESS);
        assertThat(reformatHtml(actual.message())).isEqualTo(expected);
    }

    private static String reformatHtml(String rawHtml) {
        return  rawHtml
                .replaceAll("\\s*\n\\s*","")
                .replaceAll(">\\s+<", "><")
                .replaceAll("(?s)data-target=\"#[a-f0-9-]+(?:_asCQL|_indices_asCQL)?\"", "")
                .replaceAll("(?s)id=\"[a-f0-9-]+(?:_asCQL|_indices_asCQL)?\"", "")
                .trim();
    }

    private static String readTestResource(String testResource) {
        StringBuilder builder = new StringBuilder();
        InputStream stream = testResource.getClass().getResourceAsStream(testResource);

        try (BufferedReader br = new BufferedReader(new InputStreamReader(stream))) {
            String line;
            while ((line = br.readLine()) != null) {
                builder.append(line).append("\n");
            }
        } catch (Exception ex) {
            throw  new RuntimeException(ex);
        }

        return builder.toString();
    }
}