package com.atlassian.stash.internal.backup;

import com.atlassian.bitbucket.dbunit.DatabaseConnectionFactory;
import com.atlassian.stash.internal.AbstractDaoTest;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.dbunit.DatabaseUnitException;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.*;
import org.dbunit.operation.DatabaseOperation;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.List;

import static java.sql.Types.*;

/**
 * Ensures that the {@link DatabaseTable} enum is ordered so that performing inserts in that order will not
 * violate any foreign key constraints in the database.
 */
public class DatabaseTableOrderTest extends AbstractDaoTest {

    private static final byte[] SAMPLE_BINARY_VALUE = {
            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff
    };
    private static final Timestamp SAMPLE_TIMESTAMP = new Timestamp(1338979220L);
    private static final String SAMPLE_LOB_VALUE = "value";
    private static final String SAMPLE_CHAR_VALUE = "z";
    private static final int SAMPLE_NUMBER = 1;

    /**
     * A function that produces a sample value for a given column.
     * <p>
     * Since all of the values are the same for each column type, any foreign key relationships will be satisfied,
     * providing that the table they reference has been previously restored.
     */
    private static final Function<Column, Object> VALUE_FOR_COLUMN = column -> {
        switch (column.getDataType().getSqlType()) {
            case BIT:
            case BOOLEAN:
                return Boolean.FALSE;
            case TINYINT:
            case SMALLINT:
            case INTEGER:
            case BIGINT:
            case FLOAT:
            case REAL:
            case DOUBLE:
            case NUMERIC:
            case DECIMAL:
                return SAMPLE_NUMBER;
            case DATE:
            case TIME:
            case TIMESTAMP:
                return SAMPLE_TIMESTAMP;
            case CHAR:
            case VARCHAR:
            case LONGVARCHAR:
            case NCHAR:
            case NVARCHAR:
            case LONGNVARCHAR:
                return SAMPLE_CHAR_VALUE;
            case BINARY:
            case VARBINARY:
            case LONGVARBINARY:
            case BLOB:
                return SAMPLE_BINARY_VALUE;
            case CLOB:
            case NCLOB:
                return SAMPLE_LOB_VALUE;
            default:
                throw new IllegalStateException("dbunit reported unsupported SQL column type (" +
                                                column.getSqlTypeName() + "')");
        }
    };

    /**
     * A function that creates a new DBUnit table having the same structure as the table of the same name in the given
     * data set, and populated with one row of sample data.
     *
     * @param dataSet the template data set specifying the structure of the table to create
     * @return a function that converts a table name to a DBUnit table
     */
    private static Function<String, ITable> toTable(final IDataSet dataSet) {
        return new Function<String, ITable>() {

            @Override
            public ITable apply(String tableName) {
                try {
                    ITableMetaData metaData = dataSet.getTable(tableName).getTableMetaData();
                    List<Object> row = Lists.transform(Arrays.asList(metaData.getColumns()), VALUE_FOR_COLUMN);
                    return newTable(metaData, row);
                } catch (DataSetException e) {
                    throw new RuntimeException("Failed to create and/or populate " + tableName, e);
                }
            }

            /**
             * Creates a new DBUnit table with a structure defined by the given meta data, and containing the given row.
             */
            private ITable newTable(ITableMetaData metaData, List<Object> row) throws DataSetException {
                DefaultTable table = new DefaultTable(metaData);
                addRow(table, row);
                return table;
            }

            private void addRow(DefaultTable table, List<Object> row) throws DataSetException {
                table.addRow(Iterables.toArray(row, Object.class));
            }
        };
    }

    @Autowired
    private DatabaseConnectionFactory connectionFactory;

    /**
     * This test inserts a single row of sample data into every {@link DatabaseTable database table}.
     * <p>
     * The same value is used for every integer column, so provided the tables are ordered in a manner that doesn't
     * violate foreign key constraints, each successive insert should work. If a row is inserted into a table that has a
     * foreign key reference to a table that has not yet been populated, this test will fail.
     * <p>
     * We use the natural order of {@link DatabaseTable} as the insertion order for tables when restoring the database
     * during a migration, so if this test is failing, it probably means that our migration process is probably broken
     * too. See the documentation on {@link DatabaseTable} for more info.
     */
    @Test
    public void testDatabaseTableOrderCompatibleWithForeignKeyConstraints() throws DatabaseUnitException, SQLException {
        IDatabaseConnection connection = connectionFactory.newConnection();
        IDataSet template = connection.createDataSet();
        DatabaseOperation.CLEAN_INSERT.execute(connection, dataSetWithSampleData(template));
    }

    /**
     * Creates a new data set based on the given template data set, and having one row of sample data in each table.
     *
     * @param template the data set specifying the structure of tables in the returned data set
     * @return a new data set with sample data
     */
    private IDataSet dataSetWithSampleData(IDataSet template) throws DataSetException {
        List<ITable> tables = Lists.transform(DatabaseTable.getTableNames(), toTable(template));

        return new DefaultDataSet(Iterables.toArray(tables, ITable.class), true);
    }
}
