/*
 * Copyright 2004-2023 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.store;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.util.HashSet;

import org.h2.api.ErrorCode;
import org.h2.engine.ConnectionInfo;
import org.h2.engine.Constants;
import org.h2.engine.Database;
import org.h2.engine.SessionLocal;
import org.h2.message.DbException;
import org.h2.store.fs.FileUtils;
import org.h2.store.fs.Recorder;
import org.h2.store.fs.rec.FilePathRec;
import org.h2.tools.Recover;
import org.h2.util.IOUtils;
import org.h2.util.StringUtils;
import org.h2.util.Utils;

/**
 * A tool that simulates a crash while writing to the database, and then
 * verifies the database doesn't get corrupt.
 */
public class RecoverTester implements Recorder {

    private static final RecoverTester instance = new RecoverTester();

    private String testDatabase = "memFS:reopen";
    private int writeCount = Utils.getProperty("h2.recoverTestOffset", 0);
    private int testEvery = Utils.getProperty("h2.recoverTest", 64);
    private final long maxFileSize = Utils.getProperty(
            "h2.recoverTestMaxFileSize", Integer.MAX_VALUE) * 1024L * 1024;
    private int verifyCount;
    private final HashSet<String> knownErrors = new HashSet<>();
    private volatile boolean testing;

    /**
     * Initialize the recover test.
     *
     * @param recoverTest the value of the recover test parameter
     */
    public static synchronized void init(String recoverTest) {
        if (StringUtils.isNumber(recoverTest)) {
            instance.setTestEvery(Integer.parseInt(recoverTest));
        }
        FilePathRec.setRecorder(instance);
    }

    @Override
    public void log(int op, String fileName, byte[] data, long x) {
        if (op != Recorder.WRITE && op != Recorder.TRUNCATE) {
            return;
        }
        if (!fileName.endsWith(Constants.SUFFIX_MV_FILE)) {
            return;
        }
        writeCount++;
        if ((writeCount % testEvery) != 0) {
            return;
        }
        if (FileUtils.size(fileName) > maxFileSize) {
            // System.out.println(fileName + " " + IOUtils.length(fileName));
            return;
        }
        if (testing) {
            // avoid deadlocks
            return;
        }
        testing = true;
        PrintWriter out = null;
        try {
            out = new PrintWriter(
                    new OutputStreamWriter(
                    FileUtils.newOutputStream(fileName + ".log", true)));
            testDatabase(fileName, out);
        } catch (IOException e) {
            throw DbException.convertIOException(e, null);
        } finally {
            IOUtils.closeSilently(out);
            testing = false;
        }
    }

    private synchronized void testDatabase(String fileName, PrintWriter out) {
        out.println("+ write #" + writeCount + " verify #" + verifyCount);
        try {
            IOUtils.copyFiles(fileName, testDatabase + Constants.SUFFIX_MV_FILE);
            verifyCount++;
            // avoid using the Engine class to avoid deadlocks
            ConnectionInfo ci = new ConnectionInfo("jdbc:h2:" + testDatabase +
                    ";FILE_LOCK=NO;TRACE_LEVEL_FILE=0", null, "", "");
            Database database = new Database(ci, null);
            // close the database
            SessionLocal sysSession = database.getSystemSession();
            sysSession.prepare("script to '" + testDatabase + ".sql'").query(0);
            sysSession.prepare("shutdown immediately").update();
            database.removeSession(null);
            // everything OK - return
            return;
        } catch (DbException e) {
            SQLException e2 = DbException.toSQLException(e);
            int errorCode = e2.getErrorCode();
            if (errorCode == ErrorCode.WRONG_USER_OR_PASSWORD) {
                return;
            } else if (errorCode == ErrorCode.FILE_ENCRYPTION_ERROR_1) {
                return;
            }
            e.printStackTrace(System.out);
        } catch (Exception e) {
            // failed
            int errorCode = 0;
            if (e instanceof SQLException) {
                errorCode = ((SQLException) e).getErrorCode();
            }
            if (errorCode == ErrorCode.WRONG_USER_OR_PASSWORD) {
                return;
            } else if (errorCode == ErrorCode.FILE_ENCRYPTION_ERROR_1) {
                return;
            }
            e.printStackTrace(System.out);
        }
        out.println("begin ------------------------------ " + writeCount);
        try {
            Recover.execute(fileName.substring(0, fileName.lastIndexOf('/')), null);
        } catch (SQLException e) {
            // ignore
        }
        testDatabase += "X";
        try {
            IOUtils.copyFiles(fileName, testDatabase + Constants.SUFFIX_MV_FILE);
            // avoid using the Engine class to avoid deadlocks
            ConnectionInfo ci = new ConnectionInfo("jdbc:h2:" +
                        testDatabase + ";FILE_LOCK=NO", null, null, null);
            Database database = new Database(ci, null);
            // close the database
            database.removeSession(null);
        } catch (Exception e) {
            int errorCode = 0;
            if (e instanceof DbException) {
                e = ((DbException) e).getSQLException();
                errorCode = ((SQLException) e).getErrorCode();
            }
            if (errorCode == ErrorCode.WRONG_USER_OR_PASSWORD) {
                return;
            } else if (errorCode == ErrorCode.FILE_ENCRYPTION_ERROR_1) {
                return;
            }
            StringBuilder buff = new StringBuilder();
            StackTraceElement[] list = e.getStackTrace();
            for (int i = 0; i < 10 && i < list.length; i++) {
                buff.append(list[i].toString()).append('\n');
            }
            String s = buff.toString();
            if (!knownErrors.contains(s)) {
                out.println(writeCount + " code: " + errorCode + " " + e.toString());
                e.printStackTrace(System.out);
                knownErrors.add(s);
            } else {
                out.println(writeCount + " code: " + errorCode);
            }
        }
    }

    public void setTestEvery(int testEvery) {
        this.testEvery = testEvery;
    }

}