package example;
import jakarta.persistence.*;
import java.sql.SQLException;
import java.util.function.Consumer;
import org.hibernate.Hibernate;
import org.hibernate.LazyInitializationException;
import org.hibernate.SessionFactory;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;

public final class JpaLab {
    private JpaLab() {}
    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 number(String sql) throws Exception {
        try(var c=Factories.dataSource().getConnection();var s=c.createStatement();var r=s.executeQuery(sql)){
            r.next();return r.getLong(1);
        }
    }
    static void reset() throws Exception {
        sql("DELETE FROM order_line");
        sql("DELETE FROM purchase_order WHERE id>=100");
        sql("INSERT INTO purchase_order VALUES (1,0,'seed','original',500),(2,0,'replace','replace-old',100) ON CONFLICT(id) DO UPDATE SET version=0,title=EXCLUDED.title,amount_cents=EXCLUDED.amount_cents");
        sql("INSERT INTO order_line VALUES (11,1,'sku-A')");
    }
    static void tx(EntityManagerFactory f,Consumer<EntityManager> body) {
        try(var em=f.createEntityManager()){
            var transaction=em.getTransaction(); transaction.begin();
            try { body.accept(em);transaction.commit(); }
            catch(RuntimeException|Error failure) {
                if(transaction.isActive())try{transaction.rollback();}catch(RuntimeException rb){failure.addSuppressed(rb);}
                throw failure;
            }
        }
    }
    static void first(EntityManagerFactory f) {
        final Long[] id=new Long[1];
        tx(f,em->{
            var order=new PurchaseOrder("first","created",1250);
            order.addLine("sku-first"); em.persist(order); id[0]=order.getId();
            check(id[0]!=null && em.contains(order),"persist managed");
        });
        try(var em=f.createEntityManager()){
            var a=em.find(PurchaseOrder.class,id[0]);var b=em.find(PurchaseOrder.class,id[0]);
            check(a==b && a.getAmountCents()==1250,"identity");
            check(a.getLines().size()==1,"cascade");
            System.out.println("persisted=true amountCents=1250 sameContextSameObject=true cascadeLineRows=1");
        }
    }
    static void flush(EntityManagerFactory f) throws Exception {
        var stats=f.unwrap(SessionFactory.class).getStatistics();stats.clear();
        try(var em=f.createEntityManager()){
            var t=em.getTransaction();t.begin();
            try {
                var order=em.find(PurchaseOrder.class,1L);order.setAmountCents(700);
                check(stats.getEntityUpdateCount()==0,"no SQL before flush");
                em.flush();
                check(stats.getEntityUpdateCount()==1,"dirty update");
                check(number("SELECT amount_cents FROM purchase_order WHERE id=1")==500,"external old before commit");
                t.commit();
                check(number("SELECT amount_cents FROM purchase_order WHERE id=1")==700,"external new after commit");
                System.out.println("dirtyUpdateBeforeFlush=0 afterFlush=1 externalBeforeCommit=500 externalAfterCommit=700");
            } finally { if(t.isActive())t.rollback(); }
        }
        try(var em=f.createEntityManager()){
            var t=em.getTransaction();t.begin();
            try{em.find(PurchaseOrder.class,1L).setAmountCents(900);em.flush();}
            finally{t.rollback();}
        }
        check(number("SELECT amount_cents FROM purchase_order WHERE id=1")==700,"rollback undoes flush");
        System.out.println("flushedUpdateRolledBack=true amountCents=700");
    }
    static void merge(EntityManagerFactory f) throws Exception {
        PurchaseOrder detached;
        try(var em=f.createEntityManager()){detached=em.find(PurchaseOrder.class,1L);}
        detached.setAmountCents(650);
        tx(f,em->{
            var managed=em.merge(detached);
            check(managed!=detached && em.contains(managed) && !em.contains(detached),"merge identities");
            detached.setAmountCents(999);
        });
        check(number("SELECT amount_cents FROM purchase_order WHERE id=1")==650,"detached later change ignored");
        System.out.println("mergeReturnedManagedCopy=true originalRemainedDetached=true savedAmountCents=650");
    }
    static void bulk(EntityManagerFactory f) {
        tx(f,em->{
            var loaded=em.find(PurchaseOrder.class,1L);long version=loaded.getVersion();
            int count=em.createQuery("update PurchaseOrder o set o.amountCents=800 where o.id=1").executeUpdate();
            check(count==1 && loaded.getAmountCents()==500,"bulk leaves loaded object old");
            em.clear();var fresh=em.find(PurchaseOrder.class,1L);
            check(fresh.getAmountCents()==800 && fresh.getVersion()==version,"bulk no automatic version");
            System.out.println("bulkRows=1 loadedObjectStayedOld=true clearThenFresh=true versionUnchanged=true");
        });
    }
    static void lazy(EntityManagerFactory f) {
        PurchaseOrder detached;
        try(var em=f.createEntityManager()){
            detached=em.find(PurchaseOrder.class,1L);check(!Hibernate.isInitialized(detached.getLines()),"lazy before close");
        }
        boolean rejected=false;
        try{detached.getLines().size();}catch(LazyInitializationException expected){rejected=true;}
        check(rejected,"closed context lazy failure");
        PurchaseOrder fetched;
        try(var em=f.createEntityManager()){
            fetched=em.createQuery("select o from PurchaseOrder o left join fetch o.lines where o.id=1",PurchaseOrder.class).getSingleResult();
        }
        check(fetched.getLines().size()==1,"fetch plan repair");
        System.out.println("closedContextLazyRejected=true fetchJoinUsableAfterClose=true lines=1");
    }
    static void optimistic(EntityManagerFactory f) {
        try(var a=f.createEntityManager();var b=f.createEntityManager()){
            var ta=a.getTransaction();var tb=b.getTransaction();ta.begin();tb.begin();
            try{
                var first=a.find(PurchaseOrder.class,1L);var second=b.find(PurchaseOrder.class,1L);
                check(first.getVersion().equals(second.getVersion()),"same version");
                first.setAmountCents(600);ta.commit();
                second.setAmountCents(700);boolean rejected=false;
                try{b.flush();}catch(OptimisticLockException expected){rejected=true;}
                check(rejected,"stale version rejected");
                System.out.println("twoContextsSameInitialVersion=true staleUpdateRejected=true");
            }finally{if(ta.isActive())ta.rollback();if(tb.isActive())tb.rollback();}
        }
    }
    static boolean sqlState(Throwable failure,String wanted) {
        for(Throwable c=failure;c!=null;c=c.getCause()){
            if(c instanceof SQLException sql && wanted.equals(sql.getSQLState()))return true;
        }return false;
    }
    static void ordering(EntityManagerFactory f) throws Exception {
        boolean rejected=false;
        try {
            tx(f,em->{em.remove(em.find(PurchaseOrder.class,2L));em.persist(new PurchaseOrder("replace","new",200));em.flush();});
        }catch(PersistenceException expected){rejected=sqlState(expected,"23505");}
        check(rejected && number("SELECT count(*) FROM purchase_order WHERE id=2")==1,"insert preceded delete");
        tx(f,em->{
            em.remove(em.find(PurchaseOrder.class,2L));em.flush();
            em.persist(new PurchaseOrder("replace","new",200));
        });
        check(number("SELECT count(*) FROM purchase_order WHERE external_key='replace' AND amount_cents=200")==1,"delete flush repair");
        System.out.println("insertBeforeDeleteUniqueRejected=true sqlState=23505 explicitDeleteFlushRepaired=true");
    }
    static void orphan(EntityManagerFactory f) throws Exception {
        tx(f,em->{var order=em.find(PurchaseOrder.class,1L);order.removeLine(order.getLines().get(0));});
        check(number("SELECT count(*) FROM order_line WHERE order_id=1")==0,"orphan removal");
        System.out.println("collectionRemovalDeletedOrphan=true remainingLines=0");
    }
    static void repository(EntityManagerFactory f) throws Exception {
        PurchaseOrder fresh=new PurchaseOrder("repository","repo",250);
        tx(f,em->{
            var repository=new JpaRepositoryFactory(em).getRepository(OrderRepository.class);
            check(repository.save(fresh)==fresh && em.contains(fresh),"new save persist");
        });
        // fresh is detached after the transaction's EntityManager closes.
        fresh.setAmountCents(350);
        tx(f,em->{
            var repository=new JpaRepositoryFactory(em).getRepository(OrderRepository.class);
            var merged=repository.save(fresh);
            check(merged!=fresh && em.contains(merged) && !em.contains(fresh),"existing save merge");
            fresh.setAmountCents(999);
        });
        check(number("SELECT amount_cents FROM purchase_order WHERE external_key='repository'")==350,"repository merge result");
        System.out.println("repositoryNewSaveSameObject=true existingSaveManagedCopy=true savedAmountCents=350");
    }
    public static void main(String[] args) throws Exception {
        if("root".equals(System.getProperty("user.name")))throw new IllegalStateException("Use non-root runtime");
        String mode=args.length==0?"all":args[0];
        try(var f=Factories.create()){
            String[] modes="all".equals(mode)?new String[]{"first","flush","merge","bulk","lazy","optimistic","ordering","orphan","repository"}:new String[]{mode};
            for(String current:modes){reset();try{
                switch(current){
                    case "first"->first(f);case "flush"->flush(f);case "merge"->merge(f);case "bulk"->bulk(f);
                    case "lazy"->lazy(f);case "optimistic"->optimistic(f);case "ordering"->ordering(f);
                    case "orphan"->orphan(f);case "repository"->repository(f);
                    default->throw new IllegalArgumentException("Unknown mode: "+current);
                }
            }finally{reset();}}
        }
    }
}
