package example;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import org.apache.ibatis.exceptions.PersistenceException;
import org.apache.ibatis.executor.BatchExecutor;
import org.apache.ibatis.session.*;

public final class ExecutorLab {
    private ExecutorLab() {}
    static void check(boolean value, String label) {
        if (!value) throw new AssertionError(label);
    }
    static void sql(String sql) throws Exception {
        try (var c = Factories.dataSource().getConnection(); var s = c.createStatement()) { s.executeUpdate(sql); }
    }
    static long count(Connection c) throws Exception {
        try (var s = c.createStatement(); var r = s.executeQuery("SELECT count(*) FROM batch_item")) {
            r.next(); return r.getLong(1);
        }
    }
    static long externalCount() throws Exception {
        try (var c = Factories.dataSource().getConnection()) { return count(c); }
    }
    static String externalLabel() throws Exception {
        try (var c = Factories.dataSource().getConnection(); var s = c.createStatement();
                var r = s.executeQuery("SELECT label FROM item WHERE id=1")) {
            r.next(); return r.getString(1);
        }
    }
    static void local() throws Exception {
        var counter = new PrepareCounter();
        var f = Factories.create(false, LocalCacheScope.SESSION, counter);
        try (var session = f.openSession()) {
            var mapper = session.getMapper(ItemMapper.class);
            var a = mapper.find(1); var b = mapper.find(1);
            check(a == b && counter.prepares.get() == 1, "session identity and prepare");
            a.setLabel("memory-only");
            check(mapper.find(1).getLabel().equals("memory-only"), "local reference");
            check(externalLabel().equals("original"), "database unchanged");
            System.out.println("sessionSameObject=true prepares=1 memoryMutationVisible=true databaseUnchanged=true");
        }
        counter = new PrepareCounter();
        f = Factories.create(false, LocalCacheScope.STATEMENT, counter);
        try (var session = f.openSession()) {
            var a = session.getMapper(ItemMapper.class).find(1);
            var b = session.getMapper(ItemMapper.class).find(1);
            check(a != b && counter.prepares.get() == 2, "statement scope");
            System.out.println("statementScopeNewObject=true prepares=2");
        }
    }
    static void secondLevel() {
        var counter = new PrepareCounter();
        var f = Factories.create(true, LocalCacheScope.SESSION, counter);
        try (var first = f.openSession(); var second = f.openSession()) {
            first.getMapper(ItemMapper.class).find(1);
            second.getMapper(ItemMapper.class).find(1);
            check(counter.prepares.get() == 2, "unpublished cache");
            second.rollback(true); // Explicitly discard this reader's staged entry.
            first.commit();
            try (var third = f.openSession()) {
                check(third.getMapper(ItemMapper.class).find(1).getLabel().equals("original"), "published value");
                check(counter.prepares.get() == 2, "shared cache hit");
            }
            System.out.println("beforeCommitTwoPrepares=true afterCommitSharedHit=true");
        }
        counter = new PrepareCounter();
        f = Factories.create(true, LocalCacheScope.SESSION, counter);
        Item original;
        try (var read = f.openSession()) { original = read.getMapper(ItemMapper.class).find(1); }
        try (var read = f.openSession()) {
            Item copy = read.getMapper(ItemMapper.class).find(1);
            check(counter.prepares.get() == 1 && copy != original, "close publishes read-write copy");
            System.out.println("normalReadClosePublished=true serializedCopyReturned=true");
        }
        counter = new PrepareCounter();
        f = Factories.create(true, LocalCacheScope.SESSION, counter);
        try (var read = f.openSession()) { read.getMapper(ItemMapper.class).find(1); read.rollback(true); }
        try (var read = f.openSession()) { read.getMapper(ItemMapper.class).find(1); }
        check(counter.prepares.get() == 2, "forced rollback discards");
        System.out.println("forcedRollbackDiscarded=true nextReadPrepared=true");
    }
    static void stale() throws Exception {
        var counter = new PrepareCounter();
        var f = Factories.create(true, LocalCacheScope.SESSION, counter);
        try {
            try (var s = f.openSession()) { s.getMapper(ItemMapper.class).find(1); }
            sql("UPDATE item SET label='external-new' WHERE id=1");
            try (var s = f.openSession()) {
                check(s.getMapper(ItemMapper.class).find(1).getLabel().equals("original"), "stale");
            }
            check(externalLabel().equals("external-new") && counter.prepares.get() == 1, "external write");
            f.getConfiguration().getCache(ItemMapper.class.getName()).clear();
            try (var s = f.openSession()) {
                check(s.getMapper(ItemMapper.class).find(1).getLabel().equals("external-new"), "clear then fresh");
            }
            check(counter.prepares.get() == 2, "fresh prepare");
            System.out.println("externalWriteBypassedInvalidation=true staleValueObserved=true clearThenFresh=true");
        } finally { sql("UPDATE item SET label='original' WHERE id=1"); }
    }
    static void executors() {
        for (var type : List.of(ExecutorType.SIMPLE, ExecutorType.REUSE)) {
            var counter = new PrepareCounter();
            var f = Factories.create(false, LocalCacheScope.STATEMENT, counter);
            try (var s = f.openSession(type)) {
                var a = s.getMapper(ItemMapper.class).find(1);
                var b = s.getMapper(ItemMapper.class).find(1);
                int expected = type == ExecutorType.SIMPLE ? 2 : 1;
                check(counter.prepares.get() == expected && a != b, "statement reuse not result cache");
                System.out.println("executor=" + type + " prepares=" + expected + " twoResultMappings=true");
            }
        }
    }
    static void batch() throws Exception {
        sql("DELETE FROM batch_item");
        var f = Factories.create(false, LocalCacheScope.STATEMENT, new PrepareCounter());
        try (var s = f.openSession(ExecutorType.BATCH)) {
            var m = s.getMapper(ItemMapper.class);
            check(m.insert(1, "one") == BatchExecutor.BATCH_UPDATE_RETURN_VALUE, "sentinel");
            check(m.insert(2, "two") == BatchExecutor.BATCH_UPDATE_RETURN_VALUE, "sentinel");
            check(count(s.getConnection()) == 0 && externalCount() == 0, "not sent");
            var results = s.flushStatements();
            check(results.size() == 1 && results.get(0).getUpdateCounts().length == 2, "one group two commands");
            check(count(s.getConnection()) == 2 && externalCount() == 0, "sent but uncommitted");
            s.commit();
            check(externalCount() == 2, "committed");
            System.out.println("batchSentinel=true beforeFlushRows=0 afterFlushOwnRows=2 externalBeforeCommit=0 afterCommit=2");
        } finally { sql("DELETE FROM batch_item"); }
        try (var s = f.openSession(ExecutorType.BATCH)) {
            var m = s.getMapper(ItemMapper.class);
            m.insert(3, "three"); m.insert(3, "duplicate");
            boolean rejected = false;
            try { s.flushStatements(); }
            catch (PersistenceException e) {
                for (Throwable cause = e; cause != null; cause = cause.getCause()) {
                    if (cause instanceof java.sql.SQLException se && "23505".equals(se.getSQLState())) rejected = true;
                }
                s.rollback(true);
            }
            check(rejected && externalCount() == 0, "failed batch rollback");
            System.out.println("batchConstraintRejected=true sqlState=23505 rollbackRows=0");
        } finally { sql("DELETE FROM batch_item"); }
    }
    static void plugins() {
        var events = new ArrayList<String>();
        var f = Factories.create(false, LocalCacheScope.STATEMENT, new PrepareCounter(),
            new QueryTrace("A", events), new QueryTrace("B", events));
        try (var s = f.openSession()) { s.getMapper(ItemMapper.class).find(1); }
        check(events.equals(List.of("B.before", "A.before", "A.after", "B.after")), "plugin nesting");
        System.out.println("pluginEvents=" + events);
    }
    public static void main(String[] args) throws Exception {
        if ("root".equals(System.getProperty("user.name"))) throw new IllegalStateException("Use a non-root runtime");
        String mode = args.length == 0 ? "all" : args[0];
        switch (mode) {
            case "first", "local" -> local();
            case "second-level" -> secondLevel();
            case "stale" -> stale();
            case "executors" -> executors();
            case "batch" -> batch();
            case "plugins" -> plugins();
            case "all" -> { local(); secondLevel(); stale(); executors(); batch(); plugins(); }
            default -> throw new IllegalArgumentException("Unknown mode: " + mode);
        }
    }
}
