/*
 * Copyright 2004-2022 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (https://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.test.db;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.h2.api.Trigger;
import org.h2.engine.Constants;
import org.h2.test.TestBase;
import org.h2.test.TestDb;
import org.h2.util.Task;

/**
 * Tests the sequence feature of this database.
 */
public class TestSequence extends TestDb {

    /**
     * Run just this test.
     *
     * @param a ignored
     */
    public static void main(String... a) throws Exception {
        TestBase.createCaller().init().testFromMain();
    }

    @Override
    public void test() throws Exception {
        testConcurrentCreate();
        testConcurrentNextAndCurrentValue();
        testSchemaSearchPath();
        testAlterSequenceColumn();
        testAlterSequence();
        testCache();
        testTwo();
        testMetaTable();
        testCreateWithMinValue();
        testCreateWithMaxValue();
        testCreationErrors();
        testCreateSql();
        testDefaultMinMax();
        deleteDb("sequence");
    }

    private void testConcurrentCreate() throws Exception {
        deleteDb("sequence");
        final String url = getURL("sequence;LOCK_TIMEOUT=2000", true);
        Connection conn = getConnection(url);
        Task[] tasks = new Task[2];
        try {
            Statement stat = conn.createStatement();
            stat.execute("create table dummy(id bigint primary key)");
            stat.execute("create table test(id bigint primary key)");
            stat.execute("create sequence test_seq cache 2");
            for (int i = 0; i < tasks.length; i++) {
                final int x = i;
                tasks[i] = new Task() {
                    @Override
                    public void call() throws Exception {
                        try (Connection conn = getConnection(url)) {
                            PreparedStatement prep = conn.prepareStatement(
                                    "insert into test(id) values(next value for test_seq)");
                            PreparedStatement prep2 = conn.prepareStatement(
                                    "delete from test");
                            while (!stop) {
                                prep.execute();
                                if (Math.random() < 0.01) {
                                    prep2.execute();
                                }
                                if (Math.random() < 0.01) {
                                    createDropTrigger(conn);
                                }
                            }
                        }
                    }

                    private void createDropTrigger(Connection conn) throws Exception {
                        String triggerName = "t_" + x;
                        Statement stat = conn.createStatement();
                        stat.execute("create trigger " + triggerName +
                                " before insert on dummy call \"" +
                                TriggerTest.class.getName() + "\"");
                        stat.execute("drop trigger " + triggerName);
                    }

                }.execute();
            }
            Thread.sleep(1000);
            for (Task t : tasks) {
                t.get();
            }
        } finally {
            for (Task t : tasks) {
                t.join();
            }
            conn.close();
        }
    }

    private void testConcurrentNextAndCurrentValue() throws Exception {
        deleteDb("sequence");
        final String url = getURL("sequence", true);
        Connection conn = getConnection(url);
        Task[] tasks = new Task[2];
        try {
            Statement stat = conn.createStatement();
            stat.execute("CREATE SEQUENCE SEQ1");
            stat.execute("CREATE SEQUENCE SEQ2");
            for (int i = 0; i < tasks.length; i++) {
                tasks[i] = new Task() {
                    @Override
                    public void call() throws Exception {
                        try (Connection conn = getConnection(url)) {
                            PreparedStatement next1 = conn.prepareStatement("CALL NEXT VALUE FOR SEQ1");
                            PreparedStatement next2 = conn.prepareStatement("CALL NEXT VALUE FOR SEQ2");
                            PreparedStatement current1 = conn.prepareStatement("CALL CURRENT VALUE FOR SEQ1");
                            PreparedStatement current2 = conn.prepareStatement("CALL CURRENT VALUE FOR SEQ2");
                            while (!stop) {
                                long v1, v2;
                                try (ResultSet rs = next1.executeQuery()) {
                                    rs.next();
                                    v1 = rs.getLong(1);
                                }
                                try (ResultSet rs = next2.executeQuery()) {
                                    rs.next();
                                    v2 = rs.getLong(1);
                                }
                                try (ResultSet rs = current1.executeQuery()) {
                                    rs.next();
                                    if (v1 != rs.getLong(1)) {
                                        throw new RuntimeException("Unexpected CURRENT VALUE FOR SEQ1");
                                    }
                                }
                                try (ResultSet rs = current2.executeQuery()) {
                                    rs.next();
                                    if (v2 != rs.getLong(1)) {
                                        throw new RuntimeException("Unexpected CURRENT VALUE FOR SEQ2");
                                    }
                                }
                            }
                        }
                    }
                }.execute();
            }
            Thread.sleep(1000);
            for (Task t : tasks) {
                Exception e = t.getException();
                if (e != null) {
                    throw new AssertionError(e.getMessage());
                }
            }
        } finally {
            for (Task t : tasks) {
                t.join();
            }
            conn.close();
        }
    }

    private void testSchemaSearchPath() throws SQLException {
        deleteDb("sequence");
        Connection conn = getConnection("sequence");
        Statement stat = conn.createStatement();
        stat.execute("CREATE SCHEMA TEST");
        stat.execute("CREATE SEQUENCE TEST.TEST_SEQ");
        stat.execute("SET SCHEMA_SEARCH_PATH PUBLIC, TEST");
        stat.execute("CALL NEXT VALUE FOR TEST_SEQ");
        stat.execute("CALL CURRENT VALUE FOR TEST_SEQ");
        conn.close();
    }

    private void testAlterSequenceColumn() throws SQLException {
        deleteDb("sequence");
        Connection conn = getConnection("sequence");
        Statement stat = conn.createStatement();
        stat.execute("CREATE TABLE TEST(ID INT , NAME VARCHAR(255))");
        stat.execute("INSERT INTO TEST VALUES(1, 'Hello')");
        stat.execute("ALTER TABLE TEST ALTER COLUMN ID INT GENERATED BY DEFAULT AS IDENTITY");
        stat.execute("ALTER TABLE test ALTER COLUMN ID RESTART WITH 3");
        stat.execute("INSERT INTO TEST (name) VALUES('Other World')");
        conn.close();
    }

    private void testAlterSequence() throws SQLException {
        test("create sequence s; alter sequence s restart with 2", null, 2, 3, 4);
        test("create sequence s; alter sequence s restart with 7", null, 7, 8, 9, 10);
        test("create sequence s; alter sequence s start with 3 restart with 11 minvalue 3 maxvalue 12 cycle",
                null, 11, 12, 3, 4);
        test("create sequence s; alter sequence s restart with 5 cache 2",
                null, 5, 6, 7, 8);
        test("create sequence s; alter sequence s restart with 9 " +
                "maxvalue 12 nocycle nocache",
                "Sequence \"S\" has run out of numbers", 9, 10, 11, 12);
    }

    private void testCache() throws SQLException {
        if (config.memory) {
            return;
        }
        deleteDb("sequence");
        Connection conn = getConnection("sequence");
        Statement stat = conn.createStatement();
        stat.execute("create sequence test_Sequence");
        stat.execute("create sequence test_Sequence3 cache 3");
        conn.close();
        conn = getConnection("sequence");
        stat = conn.createStatement();
        stat.execute("call next value for test_Sequence");
        stat.execute("call next value for test_Sequence3");
        ResultSet rs = stat.executeQuery("select * from " +
                "information_schema.sequences order by sequence_name");
        rs.next();
        assertEquals("TEST_SEQUENCE", rs.getString("SEQUENCE_NAME"));
        assertEquals("32", rs.getString("CACHE"));
        rs.next();
        assertEquals("TEST_SEQUENCE3", rs.getString("SEQUENCE_NAME"));
        assertEquals("3", rs.getString("CACHE"));
        assertFalse(rs.next());
        conn.close();
    }

    private void testMetaTable() throws SQLException {
        deleteDb("sequence");
        Connection conn = getConnection("sequence");
        Statement stat = conn.createStatement();
        stat.execute("create sequence a");
        stat.execute("create sequence b start with 7 minvalue 5 " +
                "maxvalue 9 cycle increment by 2 nocache");
        stat.execute("create sequence c start with -4 minvalue -9 " +
                "maxvalue -3 no cycle increment by -2 cache 3");

        if (!config.memory) {
            conn.close();
            conn = getConnection("sequence");
        }

        stat = conn.createStatement();
        ResultSet rs = stat.executeQuery("select * from " +
                "information_schema.sequences order by sequence_name");
        rs.next();
        assertEquals("SEQUENCE", rs.getString("SEQUENCE_CATALOG"));
        assertEquals("PUBLIC", rs.getString("SEQUENCE_SCHEMA"));
        assertEquals("A", rs.getString("SEQUENCE_NAME"));
        assertEquals(1, rs.getLong("BASE_VALUE"));
        assertEquals(1, rs.getLong("INCREMENT"));
        assertNull(rs.getString("REMARKS"));
        assertEquals(32, rs.getLong("CACHE"));
        assertEquals(1, rs.getLong("MINIMUM_VALUE"));
        assertEquals(Long.MAX_VALUE, rs.getLong("MAXIMUM_VALUE"));
        assertEquals("NO", rs.getString("CYCLE_OPTION"));
        rs.next();
        assertEquals("SEQUENCE", rs.getString("SEQUENCE_CATALOG"));
        assertEquals("PUBLIC", rs.getString("SEQUENCE_SCHEMA"));
        assertEquals("B", rs.getString("SEQUENCE_NAME"));
        assertEquals(7, rs.getLong("BASE_VALUE"));
        assertEquals(2, rs.getLong("INCREMENT"));
        assertNull(rs.getString("REMARKS"));
        assertEquals(1, rs.getLong("CACHE"));
        assertEquals(5, rs.getLong("MINIMUM_VALUE"));
        assertEquals(9, rs.getLong("MAXIMUM_VALUE"));
        assertEquals("YES", rs.getString("CYCLE_OPTION"));
        rs.next();
        assertEquals("SEQUENCE", rs.getString("SEQUENCE_CATALOG"));
        assertEquals("PUBLIC", rs.getString("SEQUENCE_SCHEMA"));
        assertEquals("C", rs.getString("SEQUENCE_NAME"));
        assertEquals(-4, rs.getLong("BASE_VALUE"));
        assertEquals(-2, rs.getLong("INCREMENT"));
        assertNull(rs.getString("REMARKS"));
        assertEquals(3, rs.getLong("CACHE"));
        assertEquals(-9, rs.getLong("MINIMUM_VALUE"));
        assertEquals(-3, rs.getLong("MAXIMUM_VALUE"));
        assertEquals("NO", rs.getString("CYCLE_OPTION"));
        assertFalse(rs.next());
        conn.close();
    }

    private void testCreateWithMinValue() throws SQLException {
        test("create sequence s minvalue 3", null, 3, 4, 5, 6);
        test("create sequence s minvalue -3 increment by -1 cycle",
                null, -1, -2, -3, -1);
        test("create sequence s minvalue -3 increment by -1",
                "Sequence \"S\" has run out of numbers", -1, -2, -3);
        test("create sequence s minvalue -3 increment by -1 nocycle",
                "Sequence \"S\" has run out of numbers", -1, -2, -3);
        test("create sequence s minvalue -3 increment by -1 no cycle",
                "Sequence \"S\" has run out of numbers", -1, -2, -3);
        test("create sequence s minvalue -3 increment by -1 nocache cycle",
                null, -1, -2, -3, -1);
        test("create sequence s minvalue -3 increment by -1 nocache",
                "Sequence \"S\" has run out of numbers", -1, -2, -3);
        test("create sequence s minvalue -3 increment by -1 nocache nocycle",
                "Sequence \"S\" has run out of numbers", -1, -2, -3);
        test("create sequence s minvalue -3 increment by -1 no cache no cycle",
                "Sequence \"S\" has run out of numbers", -1, -2, -3);
    }

    private void testCreateWithMaxValue() throws SQLException {
        test("create sequence s maxvalue -3 increment by -1",
                null, -3, -4, -5, -6);
        test("create sequence s maxvalue 3 cycle", null, 1, 2, 3, 1);
        test("create sequence s maxvalue 3",
                "Sequence \"S\" has run out of numbers", 1, 2, 3);
        test("create sequence s maxvalue 3 nocycle",
                "Sequence \"S\" has run out of numbers", 1, 2, 3);
        test("create sequence s maxvalue 3 no cycle",
                "Sequence \"S\" has run out of numbers", 1, 2, 3);
        test("create sequence s maxvalue 3 nocache cycle",
                null, 1, 2, 3, 1);
        test("create sequence s maxvalue 3 nocache",
                "Sequence \"S\" has run out of numbers", 1, 2, 3);
        test("create sequence s maxvalue 3 nocache nocycle",
                "Sequence \"S\" has run out of numbers", 1, 2, 3);
        test("create sequence s maxvalue 3 no cache no cycle",
                "Sequence \"S\" has run out of numbers", 1, 2, 3);
    }

    private void testCreationErrors() throws SQLException {
        deleteDb("sequence");
        Connection conn = getConnection("sequence");
        Statement stat = conn.createStatement();
        expectError(
                stat,
                "create sequence a minvalue 5 start with 2",
                "Unable to create or alter sequence \"A\" because of " +
                "invalid attributes (base value \"2\", start value \"2\", " +
                "min value \"5\", max value \"" + Long.MAX_VALUE +
                "\", increment \"1\", cache size \"32\")");
        expectError(
                stat,
                "create sequence b maxvalue 5 start with 7",
                "Unable to create or alter sequence \"B\" because of " +
                "invalid attributes (base value \"7\", start value \"7\", " +
                        "min value \"1\", max value \"5\", increment \"1\", cache size \"32\")");
        expectError(
                stat,
                "create sequence c minvalue 5 maxvalue 2",
                "Unable to create or alter sequence \"C\" because of " +
                "invalid attributes (base value \"5\", start value \"5\", " +
                "min value \"5\", max value \"2\", increment \"1\", cache size \"32\")");
        expectError(
                stat,
                "create sequence d increment by 0",
                "Unable to create or alter sequence \"D\" because of " +
                "invalid attributes (base value \"1\", start value \"1\", " +
                "min value \"1\", max value \"" +
                Long.MAX_VALUE + "\", increment \"0\", cache size \"32\")");
        expectError(stat,
                "create sequence e minvalue 1 maxvalue 5 increment 99",
                "Unable to create or alter sequence \"E\" because of " +
                "invalid attributes (base value \"1\", start value \"1\", " +
                "min value \"1\", max value \"5\", increment \"99\", cache size \"32\")");
        conn.close();
    }

    private void testCreateSql() throws SQLException {
        deleteDb("sequence");
        Connection conn = getConnection("sequence");
        Statement stat = conn.createStatement();
        stat.execute("create sequence a");
        stat.execute("create sequence b start with 5 increment by 2 " +
                "minvalue 3 maxvalue 7 cycle nocache");
        stat.execute("create sequence c start with 3 increment by 1 " +
                "minvalue 2 maxvalue 9 nocycle cache 2");
        stat.execute("create sequence d nomaxvalue no minvalue no cache nocycle");
        stat.execute("create sequence e cache 1");
        List<String> script = new ArrayList<>();
        ResultSet rs = stat.executeQuery("script nodata");
        while (rs.next()) {
            script.add(rs.getString(1));
        }
        Collections.sort(script);
        assertEquals("-- H2 " + Constants.VERSION + ";", script.get(0));
        assertEquals("CREATE SEQUENCE \"PUBLIC\".\"A\" START WITH 1;", script.get(1));
        assertEquals("CREATE SEQUENCE \"PUBLIC\".\"B\" START " +
                "WITH 5 INCREMENT BY 2 " +
                "MINVALUE 3 MAXVALUE 7 CYCLE NO CACHE;", script.get(2));
        assertEquals("CREATE SEQUENCE \"PUBLIC\".\"C\" START " +
                "WITH 3 MINVALUE 2 MAXVALUE 9 CACHE 2;",
                script.get(3));
        assertEquals("CREATE SEQUENCE \"PUBLIC\".\"D\" START " +
                "WITH 1 NO CACHE;", script.get(4));
        assertEquals("CREATE SEQUENCE \"PUBLIC\".\"E\" START " +
                "WITH 1 NO CACHE;", script.get(5));
        conn.close();
    }

    private void testDefaultMinMax() throws SQLException {
        // test that we calculate default MIN and MAX values correctly
        deleteDb("sequence");
        Connection conn = getConnection("sequence");
        Statement stat = conn.createStatement();
        stat.execute("create sequence a START WITH -7320917853639540658");
        stat.execute("create sequence b START WITH 7320917853639540658 INCREMENT -1");
        conn.close();
    }

    private void testTwo() throws SQLException {
        deleteDb("sequence");
        Connection conn = getConnection("sequence");
        Statement stat = conn.createStatement();
        stat.execute("create sequence s");
        conn.setAutoCommit(false);

        Connection conn2 = getConnection("sequence");
        Statement stat2 = conn2.createStatement();
        conn2.setAutoCommit(false);

        long last = 0;
        for (int i = 0; i < 100; i++) {
            long v1 = getNext(stat);
            assertTrue(v1 > last);
            last = v1;
            for (int j = 0; j < 100; j++) {
                long v2 = getNext(stat2);
                assertTrue(v2 > last);
                last = v2;
            }
        }

        conn2.close();
        conn.close();
    }

    private void test(String setupSql, String finalError, long... values)
            throws SQLException {

        deleteDb("sequence");

        Connection conn = getConnection("sequence");
        Statement stat = conn.createStatement();
        stat.execute(setupSql);

        if (!config.memory) {
            conn.close();
            conn = getConnection("sequence");
        }

        stat = conn.createStatement();
        for (long value : values) {
            assertEquals(value, getNext(stat));
        }

        if (finalError != null) {
            try {
                getNext(stat);
                fail("Expected error: " + finalError);
            } catch (SQLException e) {
                assertContains(e.getMessage(), finalError);
            }
        }

        conn.close();
    }

    private void expectError(Statement stat, String sql, String error) {
        try {
            stat.execute(sql);
            fail("Expected error: " + error);
        } catch (SQLException e) {
            assertContains(e.getMessage(), error);
        }
    }

    private static long getNext(Statement stat) throws SQLException {
        ResultSet rs = stat.executeQuery("call next value for s");
        rs.next();
        long value = rs.getLong(1);
        return value;
    }

    /**
     * A test trigger.
     */
    public static class TriggerTest implements Trigger {

        @Override
        public void init(Connection conn, String schemaName,
                String triggerName, String tableName, boolean before, int type)
                throws SQLException {
            conn.createStatement().executeQuery("call next value for test_seq");
        }

        @Override
        public void fire(Connection conn, Object[] oldRow, Object[] newRow)
                throws SQLException {
            // ignore
        }

    }

}
