package com.vladmihalcea.hpjp.hibernate.concurrency;

import com.vladmihalcea.hpjp.util.AbstractTest;
import org.hibernate.Session;
import org.hibernate.StaleObjectStateException;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.OptimisticLockType;
import org.hibernate.annotations.OptimisticLocking;
import org.junit.Test;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.concurrent.CountDownLatch;

/**
 * OptimisticLockingOneRootDirtyVersioningTest - Test to check optimistic checking on a single entity being updated by many threads
 * using the dirty properties instead of a synthetic version column
 *
 * @author Vlad Mihalcea
 */
public class OptimisticLockingOneRootDirtyVersioningTest extends AbstractTest {

    private final CountDownLatch loadPostLatch = new CountDownLatch(3);
    private final CountDownLatch aliceLatch = new CountDownLatch(1);

    @Override
    protected Class<?>[] entities() {
        return new Class<?>[]{
                Post.class
        };
    }

    public class AliceTransaction implements Runnable {

        @Override
        public void run() {
            try {
                doInJPA(entityManager -> {
                    try {
                        Post post = entityManager.find(Post.class, 1L);
                        loadPostLatch.countDown();
                        loadPostLatch.await();
                        post.setTitle("JPA");
                    } catch (InterruptedException e) {
                        throw new IllegalStateException(e);
                    }
                });
            } catch (StaleObjectStateException expected) {
                LOGGER.info("Alice: Optimistic locking failure", expected);
            }
            aliceLatch.countDown();
        }
    }

    public class BobTransaction implements Runnable {

        @Override
        public void run() {
            try {
                doInJPA(entityManager -> {
                    try {
                        Post post = entityManager.find(Post.class, 1L);
                        loadPostLatch.countDown();
                        loadPostLatch.await();
                        aliceLatch.await();
                        post.incrementLikes();
                    } catch (InterruptedException e) {
                        throw new IllegalStateException(e);
                    }
                });
            } catch (StaleObjectStateException expected) {
                LOGGER.info("Bob: Optimistic locking failure", expected);
            }
        }
    }

    public class CarolTransaction implements Runnable {

        @Override
        public void run() {
            try {
                doInJPA(entityManager -> {
                    try {
                        Post post = entityManager.find(Post.class, 1L);
                        loadPostLatch.countDown();
                        loadPostLatch.await();
                        aliceLatch.await();
                        post.setViews(15);
                    } catch (InterruptedException e) {
                        throw new IllegalStateException(e);
                    }
                });
            } catch (StaleObjectStateException expected) {
                LOGGER.info("Carol: Optimistic locking failure", expected);
            }
        }
    }

    @Test
    public void testOptimisticLocking() throws InterruptedException {

        doInJPA(entityManager -> {
            Post post = new Post();
            post.setId(1L);
            post.setTitle("JDBC");
            entityManager.persist(post);
        });

        Thread alice = new Thread(new AliceTransaction());
        Thread bob = new Thread(new BobTransaction());
        Thread carol = new Thread(new CarolTransaction());

        alice.start();
        bob.start();
        carol.start();

        alice.join();
        bob.join();
        carol.join();
    }

    @Test
    public void testVersionlessOptimisticLockingWhenMerging() {
        Post detachedPost = doInJPA(entityManager -> {
            Post post = new Post();
            post.setId(1L);
            post.setTitle("JDBC");
            entityManager.persist(post);
            return post;
        });

        doInJPA(entityManager -> {
            Post post = entityManager.find(Post.class, 1L);
            post.setTitle("Hibernate");
            return post;
        });

        doInJPA(entityManager -> {
            detachedPost.setTitle("JPA");
            entityManager.merge(detachedPost);
        });
    }

    @Test
    public void testVersionlessOptimisticLockingWhenReattaching() {

        doInJPA(entityManager -> {
            Post post = new Post();
            post.setId(1L);
            post.setTitle("JDBC");
            entityManager.persist(post);
            return post;
        });

        Post detachedPost = doInJPA(entityManager -> {
            LOGGER.info("Alice loads the Post entity");
            return entityManager.find(Post.class, 1L);
        });

        executeSync(() -> {
            doInJPA(entityManager -> {
                LOGGER.info("Bob loads the Post entity and modifies it");
                Post post = entityManager.find(Post.class, 1L);
                post.setTitle("Hibernate");
            });
        });

        doInJPA(entityManager -> {
            LOGGER.info("Alice updates the Post entity");
            detachedPost.setTitle("JPA");
            entityManager.unwrap(Session.class).update(detachedPost);
        });
    }
    
    @Entity(name = "Post") @Table(name = "post")
    @OptimisticLocking(type = OptimisticLockType.DIRTY)
    @DynamicUpdate
    public static class Post {

        @Id
        private Long id;

        private String title;

        private long views;

        private int likes;

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public long getViews() {
            return views;
        }

        public int getLikes() {
            return likes;
        }

        public int incrementLikes() {
            return ++likes;
        }

        public void setViews(long views) {
            this.views = views;
        }
    }
}
