/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed 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.modeshape.sequencer.ddl;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.modeshape.jcr.api.JcrConstants.NT_UNSTRUCTURED;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.CONSTRAINT_TYPE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.DATATYPE_LENGTH;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.DATATYPE_NAME;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.DATATYPE_PRECISION;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.DATATYPE_SCALE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.GRANTEE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.GRANT_PRIVILEGE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.PARSER_ID;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.PROPERTY_VALUE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_COLUMN_DEFINITION;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_COLUMN_REFERENCE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_CONSTRAINT_ATTRIBUTE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_CREATE_ASSERTION_STATEMENT;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_CREATE_SCHEMA_STATEMENT;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_CREATE_TABLE_STATEMENT;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_CREATE_VIEW_STATEMENT;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_FK_COLUMN_REFERENCE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_GRANT_ON_TABLE_STATEMENT;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_REVOKE_ON_TABLE_STATEMENT;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_TABLE_CONSTRAINT;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_TABLE_REFERENCE;
import java.util.ArrayList;
import java.util.List;
import javax.jcr.Node;
import org.junit.Test;
import org.modeshape.sequencer.ddl.dialect.oracle.OracleDdlParser;

/**
 * Unit test for the behaviour of the ddl sequencer, when standard ddl files are sequenced
 * 
 * @author Horia Chiorean
 */
public class StandardDdlSequencerTest extends AbstractDdlSequencerTest {

    @Test
    public void shouldSequenceCreateSchema() throws Exception {
        // CREATE SCHEMA hollywood
        // CREATE TABLE films (title varchar(255), release date, producerName varchar(255))
        // CREATE VIEW winners AS
        // SELECT title, release FROM films WHERE producerName IS NOT NULL;
        Node statementsNode = sequenceDdl("ddl/create_schema.ddl");
        assertThat(statementsNode.getNodes().getSize(), is(1l));

        verifyPrimaryType(statementsNode, NT_UNSTRUCTURED);
        verifyProperty(statementsNode, PARSER_ID, "POSTGRES");

        Node schemaNode = statementsNode.getNode("hollywood");
        assertNotNull(schemaNode);
        verifyBaseProperties(schemaNode, NT_UNSTRUCTURED, "1", "1", "0", 2);
        verifyMixinType(schemaNode, TYPE_CREATE_SCHEMA_STATEMENT);
        verifyExpression(schemaNode, "CREATE SCHEMA hollywood");

        Node filmsNode = schemaNode.getNode("films");
        assertNotNull(filmsNode);
        verifyBaseProperties(filmsNode, NT_UNSTRUCTURED, "2", "5", "28", 3);
        verifyMixinType(filmsNode, TYPE_CREATE_TABLE_STATEMENT);
        verifyExpression(filmsNode, "CREATE TABLE films (title varchar(255), release date, producerName varchar(255))");

        Node winnersNode = schemaNode.getNode("winners");
        assertNotNull(winnersNode);
        verifyBaseProperties(winnersNode, NT_UNSTRUCTURED, "3", "5", "113", 0);
        verifyMixinType(winnersNode, TYPE_CREATE_VIEW_STATEMENT);
        verifyExpression(winnersNode, "CREATE VIEW winners AS SELECT title, release FROM films WHERE producerName IS NOT NULL;");

        // Check Column Properties
        Node titleNode = filmsNode.getNode("title");
        assertNotNull(titleNode);
        verifyPrimaryType(titleNode, NT_UNSTRUCTURED);
        verifyProperty(titleNode, DATATYPE_NAME, "VARCHAR");
        verifyProperty(titleNode, DATATYPE_LENGTH, "255");
        verifyMixinType(titleNode, TYPE_COLUMN_DEFINITION);

        Node releaseNode = filmsNode.getNode("release");
        assertNotNull(releaseNode);
        verifyPrimaryType(releaseNode, NT_UNSTRUCTURED);
        verifyProperty(releaseNode, DATATYPE_NAME, "DATE");
        verifyHasProperty(releaseNode, DATATYPE_LENGTH);
        verifyMixinType(titleNode, TYPE_COLUMN_DEFINITION);
    }

    @Test
    public void shouldSequenceCreateTable() throws Exception {
        // CREATE TABLE IDTABLE
        // (
        // IDCONTEXT VARCHAR(20) NOT NULL PRIMARY KEY,
        // NEXTID NUMERIC
        // );
        String targetExpression = "CREATE TABLE IDTABLE\n" + "(\n" + "  IDCONTEXT  VARCHAR(20) NOT NULL PRIMARY KEY,\n"
                                  + "  NEXTID     NUMERIC\n" + ");";

        Node statementsNode = sequenceDdl("ddl/create_table.ddl");
        assertThat(statementsNode.getNodes().getSize(), is(1l));
        verifyPrimaryType(statementsNode, NT_UNSTRUCTURED);
        verifyProperty(statementsNode, PARSER_ID, "SQL92");

        Node tableNode = statementsNode.getNode("IDTABLE");
        assertNotNull(tableNode);
        verifyBaseProperties(tableNode, NT_UNSTRUCTURED, "1", "1", "0", 3);
        verifyMixinType(tableNode, TYPE_CREATE_TABLE_STATEMENT);
        verifyExpression(tableNode, targetExpression);

        // Check Column Properties
        Node idcontextNode = tableNode.getNode("IDCONTEXT");
        assertNotNull(idcontextNode);
        verifyPrimaryType(idcontextNode, NT_UNSTRUCTURED);
        verifyProperty(idcontextNode, DATATYPE_NAME, "VARCHAR");
        verifyProperty(idcontextNode, DATATYPE_LENGTH, "20");
        verifyMixinType(idcontextNode, TYPE_COLUMN_DEFINITION);

        Node nextidNode = tableNode.getNode("NEXTID");
        assertNotNull(nextidNode);
        verifyPrimaryType(nextidNode, NT_UNSTRUCTURED);
        verifyProperty(nextidNode, DATATYPE_NAME, "NUMERIC");
        verifyProperty(nextidNode, DATATYPE_PRECISION, "0");
        verifyProperty(nextidNode, DATATYPE_SCALE, "0");
        verifyHasProperty(nextidNode, DATATYPE_LENGTH);
        verifyMixinType(nextidNode, TYPE_COLUMN_DEFINITION);

        Node pk_1_Node = tableNode.getNode("PK_1");
        assertNotNull(pk_1_Node);
        verifyPrimaryType(pk_1_Node, NT_UNSTRUCTURED);
        verifyProperty(pk_1_Node, CONSTRAINT_TYPE, "PRIMARY KEY");
        verifyMixinType(pk_1_Node, TYPE_TABLE_CONSTRAINT);

        // One column reference
        assertThat(pk_1_Node.getNodes().getSize(), is(1l));
        Node idcontectRefNode = pk_1_Node.getNode("IDCONTEXT");
        assertNotNull(idcontectRefNode);
        verifyPrimaryType(idcontectRefNode, NT_UNSTRUCTURED);
        verifyMixinType(idcontectRefNode, TYPE_COLUMN_REFERENCE);
    }

    @Test
    public void shouldGenerateNodeTypesForCreateTables() throws Exception {
        // Check one table
        // CREATE TABLE RT_MDLS
        // (
        // MDL_UID NUMERIC(20) NOT NULL,
        // MDL_UUID VARCHAR(64) NOT NULL,
        // MDL_NM VARCHAR(255) NOT NULL,
        // MDL_VERSION VARCHAR(50),
        // DESCRIPTION VARCHAR(255),
        // MDL_URI VARCHAR(255),
        // MDL_TYPE NUMERIC(3),
        // IS_PHYSICAL CHAR(1) NOT NULL,
        // MULTI_SOURCED CHAR(1) DEFAULT '0',
        // VISIBILITY NUMERIC(3)
        //
        // );
        Node statementsNode = sequenceDdl("ddl/createTables.ddl");

        assertThat(statementsNode.getNodes().getSize(), is(20l));
        verifyPrimaryType(statementsNode, NT_UNSTRUCTURED);
        verifyProperty(statementsNode, PARSER_ID, "SQL92");

        Node tableNode = statementsNode.getNode("RT_MDLS");
        assertNotNull(tableNode);
        verifyBaseProperties(tableNode, NT_UNSTRUCTURED, "81", "1", "2212", 10);
        verifyMixinType(tableNode, TYPE_CREATE_TABLE_STATEMENT);

        // Check Column Properties
        Node node_1 = tableNode.getNode("MDL_UUID");
        assertNotNull(node_1);
        verifyPrimaryType(node_1, NT_UNSTRUCTURED);
        verifyProperty(node_1, DATATYPE_NAME, "VARCHAR");
        verifyProperty(node_1, DATATYPE_LENGTH, "64");
        verifyMixinType(node_1, TYPE_COLUMN_DEFINITION);

        Node node_2 = tableNode.getNode("MDL_TYPE");
        assertNotNull(node_2);
        verifyPrimaryType(node_2, NT_UNSTRUCTURED);
        verifyProperty(node_2, DATATYPE_NAME, "NUMERIC");
        verifyProperty(node_2, DATATYPE_PRECISION, "3");
        verifyProperty(node_2, DATATYPE_SCALE, "0");
        verifyHasProperty(node_2, DATATYPE_LENGTH);
        verifyMixinType(node_2, TYPE_COLUMN_DEFINITION);
    }

    @Test
    public void shouldSequenceStandardDdlFile() throws Exception {
        Node statementsNode = sequenceDdl("ddl/standard_test_statements.ddl");
        assertEquals(44l, statementsNode.getNodes().getSize());

        Node stmtNode = findNode(statementsNode, "assertNotNull", TYPE_CREATE_ASSERTION_STATEMENT);
        Node constraintAttribute = findNode(stmtNode, "CONSTRAINT_ATTRIBUTE", TYPE_CONSTRAINT_ATTRIBUTE);
        verifyProperty(constraintAttribute, PROPERTY_VALUE, "NOT DEFERRABLE");

        stmtNode = findNode(statementsNode, "assertIsZero", TYPE_CREATE_ASSERTION_STATEMENT);
        constraintAttribute = findNode(stmtNode, "CONSTRAINT_ATTRIBUTE", TYPE_CONSTRAINT_ATTRIBUTE);
        verifyProperty(constraintAttribute, PROPERTY_VALUE, "INITIALLY DEFERRED");

        Node tableNode = findNode(statementsNode, "employee", TYPE_CREATE_TABLE_STATEMENT);
        Node columnNode = findNode(tableNode, "empname", TYPE_COLUMN_DEFINITION);
        verifyProperty(columnNode, DATATYPE_NAME, "CHAR");
        verifyProperty(columnNode, DATATYPE_LENGTH, 10);

        Node constraintNode = findNode(tableNode, "emp_fk1", TYPE_TABLE_CONSTRAINT);
        constraintAttribute = findNode(constraintNode, "CONSTRAINT_ATTRIBUTE", TYPE_CONSTRAINT_ATTRIBUTE);
        verifyProperty(constraintAttribute, PROPERTY_VALUE, "INITIALLY IMMEDIATE");
        findNode(constraintNode, "deptno[1]", TYPE_COLUMN_REFERENCE);
        findNode(constraintNode, "dept", TYPE_TABLE_REFERENCE);
        findNode(constraintNode, "deptno[2]", TYPE_FK_COLUMN_REFERENCE);

        Node viewNode = findNode(statementsNode, "view_1", TYPE_CREATE_VIEW_STATEMENT);
        findNode(viewNode, "col1", TYPE_COLUMN_REFERENCE);

        tableNode = findNode(statementsNode, "table_5", TYPE_CREATE_TABLE_STATEMENT);
        assertEquals(18, tableNode.getNodes().getSize());
    }

    @Test
    public void shouldSequenceStandardDdlGrantStatements() throws Exception {
        Node statementsNode = sequenceDdl("ddl/grant_test_statements.ddl");
        assertEquals(4, statementsNode.getNodes().getSize());

        // GRANT SELECT ON TABLE purchaseOrders TO maria,harry;
        Node grantNode = findNode(statementsNode, "purchaseOrders", TYPE_GRANT_ON_TABLE_STATEMENT);
        findNode(grantNode, "maria", GRANTEE);
        Node privNode = findNode(grantNode, "privilege", GRANT_PRIVILEGE);
        verifyProperty(privNode, TYPE, "SELECT");

        // GRANT UPDATE, USAGE ON TABLE purchaseOrders FROM anita,zhi;
        grantNode = findNode(statementsNode, "billedOrders", TYPE_GRANT_ON_TABLE_STATEMENT);
        privNode = findNode(grantNode, "privilege", GRANT_PRIVILEGE);
        verifyProperty(privNode, TYPE, "UPDATE");
        findNode(grantNode, "anita", GRANTEE);
    }

    @Test
    public void shouldSequenceStandardDdlRevokeStatements() throws Exception {
        Node statementsNode = sequenceDdl("ddl/revoke_test_statements.ddl");
        assertEquals(4, statementsNode.getNodes().getSize());

        // REVOKE SELECT ON TABLE purchaseOrders FROM maria,harry;
        Node revokeNode = findNode(statementsNode, "purchaseOrders", TYPE_REVOKE_ON_TABLE_STATEMENT);
        findNode(revokeNode, "maria", GRANTEE);
        Node privNode = findNode(revokeNode, "privilege", GRANT_PRIVILEGE);
        verifyProperty(privNode, TYPE, "SELECT");

        // REVOKE UPDATE, USAGE ON TABLE purchaseOrders FROM anita,zhi CASCADE;
        revokeNode = findNode(statementsNode, "orderDetails", TYPE_REVOKE_ON_TABLE_STATEMENT);
        privNode = findNode(revokeNode, "privilege", GRANT_PRIVILEGE);
        verifyProperty(privNode, TYPE, "UPDATE");
        findNode(revokeNode, "anita", GRANTEE);
    }

    protected String[] builtInGrammars() {
        List<String> builtInParserNames = new ArrayList<String>();
        for (DdlParser parser : DdlParsers.BUILTIN_PARSERS) {
            builtInParserNames.add(parser.getId().toLowerCase());
        }
        return builtInParserNames.toArray(new String[builtInParserNames.size()]);
    }

    @Test
    public void shouldHaveDefaultListOfGrammars() {
        DdlSequencer sequencer = new DdlSequencer();
        String[] grammars = sequencer.getGrammars();
        assertThat(grammars, is(builtInGrammars()));
    }

    @Test
    public void shouldCreateListOfDdlParserInstancesForDefaultListOfGrammars() {
        DdlSequencer sequencer = new DdlSequencer();
        List<DdlParser> parsers = sequencer.getParserList();
        assertThat(parsers, is(DdlParsers.BUILTIN_PARSERS));
    }

    @Test
    public void shouldAllowSettingGrammarsWithEmptyArray() {
        DdlSequencer sequencer = new DdlSequencer();
        sequencer.setGrammars(new String[] {});
        assertThat(sequencer.getGrammars(), is(builtInGrammars()));
    }

    @Test
    public void shouldAllowSettingGrammarsWithNullArray() {
        DdlSequencer sequencer = new DdlSequencer();
        sequencer.setGrammars(null);
        assertThat(sequencer.getGrammars(), is(builtInGrammars()));
    }

    @Test
    public void shouldAllowSettingGrammarsToNonEmptyArrayOfValidBuiltInGrammars() {
        DdlSequencer sequencer = new DdlSequencer();
        String[] grammars = new String[] {new OracleDdlParser().getId(), new StandardDdlParser().getId()};
        sequencer.setGrammars(grammars);
        assertThat(sequencer.getGrammars(), is(grammars));
    }

    @Test
    public void shouldAllowSettingGrammarsToNonEmptyArrayOfValidAndNonExistantBuiltInGrammars() {
        DdlSequencer sequencer = new DdlSequencer();
        String[] grammars = new String[] {new OracleDdlParser().getId(), new StandardDdlParser().getId(), "argle"};
        sequencer.setGrammars(grammars);
        assertThat(sequencer.getGrammars(), is(grammars));
    }

    @Test
    public void shouldCreateDdlParserInstancesForAllValidBuiltInGrammars() {
        DdlSequencer sequencer = new DdlSequencer();
        String[] grammars = new String[] {new OracleDdlParser().getId(), new StandardDdlParser().getId(), "argle"};
        sequencer.setGrammars(grammars);
        assertThat(sequencer.getGrammars(), is(grammars));
        List<DdlParser> parsers = sequencer.getParserList();
        assertThat(parsers.get(0), is((DdlParser)new OracleDdlParser()));
        assertThat(parsers.get(1), is((DdlParser)new StandardDdlParser()));
        assertThat(parsers.size(), is(2));
    }

    @Test
    public void shouldCreateDdlParserInstancesForAllValidBuiltInGrammarsAndInstantiableParser() {
        DdlSequencer sequencer = new DdlSequencer();
        String[] grammars = new String[] {new OracleDdlParser().getId(), new StandardDdlParser().getId(),
            ArgleDdlParser.class.getName()};
        sequencer.setGrammars(grammars);
        assertThat(sequencer.getGrammars(), is(grammars));
        List<DdlParser> parsers = sequencer.getParserList();
        assertThat(parsers.get(0), is((DdlParser)new OracleDdlParser()));
        assertThat(parsers.get(1), is((DdlParser)new StandardDdlParser()));
        assertThat(parsers.get(2), is((DdlParser)new ArgleDdlParser()));
        assertThat(parsers.size(), is(3));
    }

    protected static class ArgleDdlParser extends StandardDdlParser {
        @Override
        public String getId() {
            return "ARGLE";
        }
    }
}
