/*
 * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.testing.tests.flashback;

import java.util.*;
import java.sql.*;
import org.eclipse.persistence.sessions.*;
import org.eclipse.persistence.platform.database.OraclePlatform;
import org.eclipse.persistence.queries.*;
import org.eclipse.persistence.history.*;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.expressions.*;
import org.eclipse.persistence.testing.tests.expressions.*;
import org.eclipse.persistence.testing.tests.clientserver.*;
import org.eclipse.persistence.testing.tests.sessionbroker.*;
import org.eclipse.persistence.testing.framework.*;
import org.eclipse.persistence.testing.models.employee.domain.*;
import org.eclipse.persistence.testing.models.employee.relational.EmployeeSystem;
import org.eclipse.persistence.testing.tests.employee.EmployeeBasicTestModel;

/**
 * Tests Oracle Flashback support.
 * Note this model is also subclassed for generic history support
 * and support both historical models.
 */
public class FlashbackTestModel extends TestModel {
    protected Number systemChangeNumber;
    public static java.sql.Timestamp timestamp;
    protected AsOfClause asOfClause;
    public final static boolean QUICK_SETUP = true;
    public final static boolean STANDARD_SETUP = true;

    public FlashbackTestModel() {
        setDescription("Tests the new flashback query tests.  Includes running the expression test suite as of a past time.");
    }

    public Expression adaptExpression(Expression expression) {
        return expression.asOf(getAsOfClause());
    }

    public void adaptQuery(ObjectLevelReadQuery query) {
        if (getSystemChangeNumber() != null) {
            query.setAsOfClause(getAsOfClause());
        }
    }

    public org.eclipse.persistence.sessions.Session adaptSession(Session session) {
        return session.acquireHistoricalSession(getAsOfClause());
    }

    public void addTests() {
        // Add ExpressionTestSuite...
        //
        //assert getSCNValue() != null;
        ExpressionTestSuite expressionTestSuite = new ExpressionTestSuite();

        //expressionTestSuite.setExecutor(getExecutor());
        //expressionTestSuite.addTests();
        for (Iterator iter = expressionTestSuite.getTests().iterator(); iter.hasNext();) {
            TestEntity baseTest = (TestEntity)iter.next();
            if (baseTest instanceof ReadAllExpressionTest) {
                ReadAllExpressionTest test = (ReadAllExpressionTest)baseTest;
                if (test.getExpression() == null) {
                    //System.out.println("This test does not have an expression yet: " + test.getName());
                    iter.remove();
                } else {
                    if (test.getReferenceClass().getPackage() != Employee.class.getPackage()) {
                        iter.remove();
                    } else {
                        // For these test setting it on the builder.
                        test.getQuery(true).dontMaintainCache();
                        test.getQuery().cascadeAllParts();
                        test.getExpression().getBuilder().asOf(getAsOfClause());
                    }
                }
            } else {
                iter.remove();
            }
        }
        addTest(expressionTestSuite);

        ExpressionSubSelectTestSuite subSelectTestSuite = new ExpressionSubSelectTestSuite();
        subSelectTestSuite.setExecutor(getExecutor());

        for (Iterator iter = subSelectTestSuite.getTests().iterator(); iter.hasNext();) {
            TestEntity baseTest = (TestEntity)iter.next();
            if (baseTest instanceof ReadAllExpressionTest) {
                ReadAllExpressionTest test = (ReadAllExpressionTest)baseTest;
                if (test.getExpression() == null) {
                    //System.out.println("This test does not have an expression yet: " + test.getName());
                    iter.remove();
                } else {
                    if ((test.getReferenceClass().getPackage() != Employee.class.getPackage()) || test.getName().equals("SubSelectCustomSQLTest")) {
                        iter.remove();
                    } else {
                        // For these must set on the entire expression, as multiple builders.
                        test.getQuery(true).dontMaintainCache();
                        test.getQuery().cascadeAllParts();
                        test.getExpression().asOf(getAsOfClause());
                    }
                }
            } else {
                iter.remove();
            }
        }
        addTest(subSelectTestSuite);

        /**ExpressionOuterJoinTestSuite outerJoinTestSuite = new ExpressionOuterJoinTestSuite();
        outerJoinTestSuite.setExecutor(getExecutor());
        outerJoinTestSuite.addTests();

        for (Iterator iter = outerJoinTestSuite.getTests().iterator(); iter.hasNext();) {
            TestEntity baseTest = (TestEntity)iter.next();
            if (baseTest instanceof ReadAllExpressionTest) {
                ReadAllExpressionTest test = (ReadAllExpressionTest)baseTest;
                if (test.getExpression() == null) {
                    //System.out.println("This test does not have an expression yet: " + test.getName());
                    iter.remove();
                } else {
                    if (test.getReferenceClass().getPackage() != Employee.class.getPackage()) {
                        iter.remove();
                    } else {
                        // For these test setting it on the builder.
                        test.getQuery(true).dontMaintainCache();
                        test.getExpression().getBuilder().asOf(getAsOfClause());
                    }
                }
            } else {
                iter.remove();
            }
        }
        addTest(outerJoinTestSuite);
        */

        // Run ReadObjectTestSuite using HistoricalSession, to test relationship
        // reading.
        TestSuite suite = EmployeeBasicTestModel.getReadObjectTestSuite();
        suite.setDescription("Tests object relationships in a HistoricalSession.");
        Vector suiteTests = suite.getTests();
        Vector newTests = new Vector(suiteTests.size());
        int numTests = suiteTests.size();
        HistoricalSessionTest hsTest;
        AutoVerifyTestCase wrappedTest;
        for (int i = 0; i < numTests; i++) {
            wrappedTest = (AutoVerifyTestCase)suiteTests.get(i);
            if (!(wrappedTest.getName().indexOf("Call") > 0)) {
                hsTest = new HistoricalSessionTest(wrappedTest, getAsOfClause());
                newTests.add(hsTest);
            }
        }
        suiteTests.clear();
        suiteTests.addAll(newTests);
        addTest(suite);

        // Run ReadObjectTestSuite using HistoricalSession acquired from either
        // a ClientSession or a ClientSessionBroker.
        suite = EmployeeBasicTestModel.getReadObjectTestSuite();
        suite.setName("AnySessionsTestSuite");
        suite.setDescription("Tests flashback on other session setups.");
        suiteTests = suite.getTests();
        newTests = new Vector(suiteTests.size());
        numTests = suiteTests.size();
        ClientSessionTestAdapter csTest;
        ClientSessionBrokerTestAdapter csbTest;
        boolean oneForYou = false;
        for (int i = 0; i < numTests; i++) {
            wrappedTest = (AutoVerifyTestCase)suiteTests.get(i);
            if (!(wrappedTest.getName().indexOf("Call") > 0)) {
                hsTest = new HistoricalSessionTest(wrappedTest, getAsOfClause());
                if (oneForYou) {
                    csTest = new ClientSessionTestAdapter(hsTest);
                    newTests.add(csTest);
                } else {
                    csbTest = new ClientSessionBrokerTestAdapter(hsTest);
                    newTests.add(csbTest);
                }
                oneForYou = !oneForYou;
            }
        }
        suiteTests.clear();
        suiteTests.addAll(newTests);
        addTest(suite);

        // Run read all test suite as of a past time using a HistoricalSession.
        // These tests came up with nothing useful at the time.
        suite = EmployeeBasicTestModel.getReadAllTestSuite();
        suiteTests = suite.getTests();
        newTests.clear();
        numTests = suiteTests.size();
        for (int i = 0; i < numTests; i++) {
            wrappedTest = (AutoVerifyTestCase)suiteTests.get(i);
            if (!(wrappedTest.getName().indexOf("Call") > 0)) {
                hsTest = new HistoricalSessionTest(wrappedTest, getAsOfClause());
                newTests.add(hsTest);
            }
        }
        suiteTests.clear();
        suiteTests.addAll(newTests);
        addTest(suite);
        // Now add some individual tests in here.
        TestSuite flashbackSuite = new FlashbackUnitTestSuite();
        flashbackSuite.setExecutor(getExecutor());
        addTest(flashbackSuite);
    }

    public void buildAsOfClause() {
        if (getSystemChangeNumber() != null) {
            asOfClause = new AsOfSCNClause(getSystemChangeNumber());
        } else {
            asOfClause = new AsOfClause(getTimestamp());
        }
    }

    /**
     * Find the system change number that reflects the initial fully
     * populated state of the database.
     * Assumes that when called the database is in its start state.
     */
    protected void calculateSystemChangeNumber() throws Exception {
        OraclePlatform platform = (OraclePlatform)getSession().getPlatform();
        ValueReadQuery scnQuery = platform.getSystemChangeNumberQuery();
        ReadAllQuery query = new ReadAllQuery(Employee.class);
        query.dontMaintainCache();
        int safetyCount = 1400;
        boolean validSCN = false;
        while ((safetyCount-- > 0) && !validSCN) {
            this.systemChangeNumber = (Number)getSession().executeQuery(scnQuery);
            //clonedQuery = (ReadAllQuery)query.clone();
            //query.setSelectionCriteria(query.getExpressionBuilder().get("salary").greaterThan(safetyCount));
            query.setAsOfClause(new AsOfSCNClause(systemChangeNumber));
            try {
                Vector result = (Vector)getSession().executeQuery(query);
                validSCN = true;
            } catch (Exception e) {
                // keep going...
                Thread.sleep(1000 * 30);
            }
        }
    }

    /**
     * By caching the result of this call, it is insured that
     * a new timestamp is not calculated after rows are deleted.
     * Furthermore, an asof time that maps to a desired snapshot of
     * the database must be had.
     */
    public void calculateTimestamp() {
        if (getTimestamp() == null) {
            OraclePlatform platform = (OraclePlatform)getSession().getPlatform();
            ValueReadQuery timestampQuery = platform.getTimestampQuery();
            ReadAllQuery query = new ReadAllQuery(Employee.class);
            query.setSelectionCriteria(query.getExpressionBuilder().get("firstName").notEqual("Eun Kyung"));
            ReadAllQuery clonedQuery = null;
            int safetyCount = 1400;
            while ((safetyCount-- > 0) && (getTimestamp() == null)) {
                java.util.Date timestamp = (java.util.Date)getSession().executeQuery(timestampQuery);
                clonedQuery = query;
                clonedQuery.setAsOfClause(new AsOfClause(timestamp));
                Vector result = (Vector)getSession().executeQuery(clonedQuery);
                if (result.size() == 12) {
                    setTimestamp((java.sql.Timestamp)timestamp);
                } else {
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        throw new TestErrorException("Unable to wait for valid timstamp.", e);
                    }
                }
            }
        }
    }

    /**
     * This idea of calculateTimestamp is to get a snapshot at which time
     * the database is fully populated.
     * Tries going further and further back in time to find one.
     * Note if go back too far, or before the last time a table was altered, an
     * exception will result.
     */
    @SuppressWarnings("deprecation")
    public void calculateTimestampHopefully() {
        if (getTimestamp() != null) {
            return;
        }
        OraclePlatform platform = (OraclePlatform)getSession().getPlatform();
        ValueReadQuery timestampQuery = platform.getTimestampQuery();
        ReadObjectQuery query = new ReadObjectQuery(Employee.class);
        query.dontMaintainCache();
        query.addPartialAttribute("id");
        ReadObjectQuery asOfQuery = null;
        try {
            long asOfTime = ((Timestamp)getSession().executeQuery(timestampQuery)).getTime();
            long timeToGoBack = 0;
            while (getTimestamp() == null) {
                asOfQuery = (ReadObjectQuery)query.clone();
                asOfQuery.setAsOfClause(new AsOfClause(new java.sql.Timestamp(asOfTime - timeToGoBack)));
                if (getSession().executeQuery(asOfQuery) != null) {
                    setTimestamp(new Timestamp(asOfTime - timeToGoBack));
                } else {
                    // go back 10 minutes each time.
                    timeToGoBack += ((timeToGoBack == 0) ? (1000 * 60) : timeToGoBack);
                }
            }
        } catch (Exception e) {
            System.out.println("Went too far back, so can't tell this.");
        }
    }

    private void configure() throws Exception {
        if (!getSession().getPlatform().isOracle()) {
            throw new TestWarningException("Flashback is only supported on Oracle 9R2 or later databases.");
        }
        TestSystem system = new EmployeeSystem();

        if (STANDARD_SETUP) {
            system.run(getSession());
            // This delay here is nasty but inevitable.  Every time the tables get
            // recreated (i.e. by every test model) flashback is disabled for
            // 5 minutes.
            // Now sleeps for 5 seconds only - seem to be ok.
            System.out.println("Starting to sleep for 5 seconds.");
            Thread.sleep(1000 * 5);
            System.out.println("Have stopped sleeping for 5 seconds.");
            calculateSystemChangeNumber();
            //calculateTimestamp();
            buildAsOfClause();
            depopulate();
            return;
        }

        // Only in the non standard case, try to avoid the five minute setup hit.
        // Only useable when run individually at a separate time.
        boolean mustAddTables = false;
        Vector existingEmployees = null;

        // First check that descriptors exist?
        try {
            Object result = getSession().readObject(Employee.class);
        } catch (QueryException qe) {
            system.addDescriptors((DatabaseSession)getSession());
        } catch (Exception goNextStep) {
        }

        // Now do tables exist on database?
        try {
            existingEmployees = getSession().readAllObjects(Employee.class);
        } catch (DatabaseException de) {
            mustAddTables = true;
        }
        if (mustAddTables || !QUICK_SETUP) {
            if (mustAddTables) {
                system.createTables((DatabaseSession)getSession());
            }
            system.populate((DatabaseSession)getSession());
            // Must init identity maps as contains same objects as in
            // population manager!
            getSession().getIdentityMapAccessor().initializeAllIdentityMaps();
            // Wait for a snapshot that shows the newly added items.
            // This is going to take five minutes.
            calculateSystemChangeNumber();
            //calculateTimestamp();
            depopulate();
        } else {
            depopulate(true);
            system.populate((DatabaseSession)getSession());
            getSession().getIdentityMapAccessor().initializeAllIdentityMaps();
            calculateSystemChangeNumber();
            depopulate();

            //calculateTimestampHopefully();
            // Must at this time fill up the PopulationManager.
            // This is pretty useless though, as they all have the
            // wrong primary keys.
            //EmployeePopulator populator = new EmployeePopulator();
            //populator.buildExamples();
            //if (existingEmployees != null) {
            //depopulate();
            //}
        }
        buildAsOfClause();
        return;
    }

    public void depopulate() throws Exception {
        depopulate(false);
    }

    public void depopulate(boolean fully) throws Exception {
        try {
            // Now here is the tricky part: corrupt all the objects
            // that exist right now!  Or at least some of them.
            UnitOfWork uow = getSession().acquireUnitOfWork();
            Vector employees = uow.readAllObjects(Employee.class);
            Vector projects = uow.readAllObjects(LargeProject.class);
            Vector smallProjects = uow.readAllObjects(SmallProject.class);
            Vector addresses = uow.readAllObjects(Address.class);
            for (Enumeration enumtr = employees.elements(); enumtr.hasMoreElements();) {
                Employee emp = (Employee)enumtr.nextElement();

                //emp.setAddress(null);
                emp.setProjects(null);
                emp.setManager(null);
                emp.setManagedEmployees(new Vector());
                emp.setAddress(null);
            }
            for (Enumeration enumtr = projects.elements(); enumtr.hasMoreElements();) {
                LargeProject project = (LargeProject)enumtr.nextElement();
                project.setTeamLeader(null);
            }

            //uow.commit();
            //getSession().getIdentityMapAccessor().initializeAllIdentityMaps();
            //uow = getSession().acquireUnitOfWork();
            //employees = (Vector)uow.readAllObjects(Employee.class);
            uow.deleteAllObjects(employees);
            if (fully) {
                uow.deleteAllObjects(projects);
                uow.deleteAllObjects(smallProjects);
                uow.deleteAllObjects(addresses);
            }
            uow.commit();
        } catch (EclipseLinkException e) {
            throw e;
        } catch (Exception e) {
            System.out.println("Could not depopulate table.");
            System.out.println("Exception:"+e);
            e.printStackTrace();
        }
    }

    public Number getSystemChangeNumber() {
        return systemChangeNumber;
    }

    public AsOfClause getAsOfClause() {
        return asOfClause;
    }

    public static java.sql.Timestamp getTimestamp() {
        return timestamp;
    }

    public static long getTimestampValue() {
        return getTimestamp().getTime();
    }

    public void setAsOfClause(AsOfClause asOfClause) {
        this.asOfClause = asOfClause;
    }

    public static void setTimestamp(java.sql.Timestamp newTimestamp) {
        timestamp = newTimestamp;
    }

    /**
     * Assume setup() is called prior to addTests.  This seems bizarre
     * but is the way it works.
     */
    public void setup() {
        // Must do configuration here...
        if (getTimestamp() != null) {
            return;
        }
        try {
            configure();
        } catch (EclipseLinkException te) {
            throw te;
        } catch (Exception ignore) {
            ignore.printStackTrace();
        }
    }

    public void reset() {
        if (getTimestamp() == null) {
            return;
        }
        try {
            if (QUICK_SETUP) {
                TestSystem system = new EmployeeSystem();
                system.populate((DatabaseSession)getSession());
                getSession().getIdentityMapAccessor().initializeAllIdentityMaps();
            }
        } catch (Exception ignore) {
        }
    }
}
