package com.vladmihalcea.book.hpjp.hibernate.concurrency;

import com.vladmihalcea.book.hpjp.util.AbstractTest;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.Session;
import org.hibernate.jpa.AvailableSettings;
import org.junit.Before;
import org.junit.Test;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import static org.junit.Assert.assertEquals;


/**
 * CascadeLockTest - Test to check CascadeType.LOCK
 *
 * @author Vlad Mihalcea
 */
public class CascadeLockUnidirectionalOneToManyTest extends AbstractTest {

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

    @Before
    public void init() {
        super.init();
        doInJPA(entityManager -> {
            Post post = new Post();
            post.setTitle("Hibernate Master Class");
            entityManager.persist(post);

            post.addDetails(new PostDetails());
            post.addComment(new PostComment("Good post!"));
            post.addComment(new PostComment("Nice post!"));
        });
    }

    @Test
    public void testCascadeLockOnManagedEntityWithScope() throws InterruptedException {
        LOGGER.info("Test lock cascade for managed entity");
        doInJPA(entityManager -> {
            Post post = entityManager.find(Post.class, 1L);
            entityManager.unwrap(Session.class)
            .buildLockRequest(
                new LockOptions(LockMode.PESSIMISTIC_WRITE))
            .setScope(true)
            .lock(post);
        });
    }

    @Test
    public void testCascadeLockOnManagedEntityWithJPA() throws InterruptedException {
        LOGGER.info("Test lock cascade for managed entity");
        doInJPA(entityManager -> {
            Post post = entityManager.find(Post.class, 1L);
            entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, Collections.singletonMap(
                AvailableSettings.LOCK_SCOPE, PessimisticLockScope.EXTENDED
            ));
        });
    }

    @Test
    public void testCascadeLockOnManagedEntityWithQuery() throws InterruptedException {
        LOGGER.info("Test lock cascade for managed entity");
        doInJPA(entityManager -> {
            Post post = entityManager.createQuery(
                "select p " +
                "from Post p " +
                "join fetch p.details " +
                "join fetch p.comments " +
                "where p.id = :id", Post.class)
            .setParameter("id", 1L)
            .setLockMode(LockModeType.PESSIMISTIC_WRITE)
            .getSingleResult();
        });
    }

    @Test
    public void testCascadeLockOnManagedEntityWithAssociationsInitialzied() throws InterruptedException {
        LOGGER.info("Test lock cascade for managed entity");
        doInJPA(entityManager -> {
            Session session = entityManager.unwrap(Session.class);
            Post post = (Post) entityManager.createQuery(
                    "select p " +
                            "from Post p " +
                            "join fetch p.details " +
                            "join fetch p.comments " +
                            "where " +
                            "   p.id = :id"
            ).setParameter("id", 1L)
                    .getSingleResult();
            session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).setScope(true).lock(post);
        });
    }

    @Test
    public void testCascadeLockOnManagedEntityWithAssociationsInitializedAndJpa() throws InterruptedException {
        LOGGER.info("Test lock cascade for managed entity");
        doInJPA(entityManager -> {
            Post post = entityManager.createQuery(
                    "select p " +
                            "from Post p " +
                            "join fetch p.details " +
                            "where p.id = :id", Post.class)
                    .setParameter("id", 1L)
                    .getSingleResult();
            entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, Collections.singletonMap(
                AvailableSettings.LOCK_SCOPE, PessimisticLockScope.EXTENDED
            ));
        });
    }

    private void containsPost(EntityManager entityManager, Post post, boolean expected) {
        assertEquals(expected, entityManager.contains(post));
        assertEquals(expected, (entityManager.contains(post.getDetails())));
        for(PostComment comment : post.getComments()) {
            assertEquals(expected, (entityManager.contains(comment)));
        }
    }

    @Test
    public void testCascadeLockOnDetachedEntityWithoutScope() {
        LOGGER.info("Test lock cascade for detached entity without scope");

        //Load the Post entity, which will become detached
        Post post = doInJPA(entityManager ->
            (Post) entityManager.createQuery(
                "select p " +
                "from Post p " +
                "join fetch p.details " +
                "join fetch p.comments " +
                "where p.id = :id"
        ).setParameter("id", 1L)
        .getSingleResult());

        //Change the detached entity state
        post.setTitle("Hibernate Training");
        doInJPA(entityManager -> {
            Session session = entityManager.unwrap(Session.class);
            //The Post entity graph is detached
            containsPost(entityManager, post, false);

            //The Lock request associates the entity graph and locks the requested entity
            session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(post);

            //Hibernate doesn't know if the entity is dirty
            assertEquals("Hibernate Training", post.getTitle());

            //The Post entity graph is attached
            containsPost(entityManager, post, true);
        });
        doInJPA(entityManager -> {
            //The detached Post entity changes have been lost
            Post _post = (Post) entityManager.find(Post.class, 1L);
            assertEquals("Hibernate Master Class", _post.getTitle());
        });
    }

    @Test
    public void testCascadeLockOnDetachedEntityWithScope() {
        LOGGER.info("Test lock cascade for detached entity with scope");

        //Load the Post entity, which will become detached
        Post post = doInJPA(entityManager -> (Post) entityManager.createQuery(
            "select p " +
            "from Post p " +
            "join fetch p.details " +
            "join fetch p.comments " +
            "where p.id = :id", Post.class)
        .setParameter("id", 1L)
        .getSingleResult());

        doInJPA(entityManager -> {
            LOGGER.info("Reattach and lock");
            entityManager.unwrap(Session.class)
            .buildLockRequest(
                new LockOptions(LockMode.PESSIMISTIC_WRITE))
            .setScope(true)
            .lock(post);

            //The Post entity graph is attached
            containsPost(entityManager, post, true);
        });
        doInJPA(entityManager -> {
            //The detached Post entity changes have been lost
            Post _post = (Post) entityManager.find(Post.class, 1L);
            assertEquals("Hibernate Master Class", _post.getTitle());
        });
    }

    @Test
    public void testCascadeLockOnDetachedEntityUninitializedWithScope() {
        LOGGER.info("Test lock cascade for detached entity with scope");

        //Load the Post entity, which will become detached
        Post post = doInJPA(entityManager -> (Post) entityManager.find(Post.class, 1L));

        doInJPA(entityManager -> {
            LOGGER.info("Reattach and lock entity with associations not initialized");
            entityManager.unwrap(Session.class)
                    .buildLockRequest(
                            new LockOptions(LockMode.PESSIMISTIC_WRITE))
                    .setScope(true)
                    .lock(post);

            LOGGER.info("Check entities are reattached");
            //The Post entity graph is attached
            containsPost(entityManager, post, true);
        });
    }

    @Test
    public void testCascadeLockOnDetachedChildEntityUninitializedWithScope() {
        LOGGER.info("Test lock cascade for detached entity with scope");

        //Load the Post entity, which will become detached
        PostComment postComment = doInJPA(entityManager -> (PostComment) entityManager.find(PostComment.class, 3L));

        doInJPA(entityManager -> {
            LOGGER.info("Reattach and lock entity with associations not initialized");
            entityManager.unwrap(Session.class)
                    .buildLockRequest(
                            new LockOptions(LockMode.PESSIMISTIC_WRITE))
                    .lock(postComment);
        });
    }

    @Test
    public void testUpdateOnDetachedEntity() {
        LOGGER.info("Test update for detached entity");
        //Load the Post entity, which will become detached
        Post post = doInJPA(entityManager -> (Post) entityManager.createQuery(
            "select p " +
            "from Post p " +
            "join fetch p.details " +
            "join fetch p.comments " +
            "where p.id = :id", Post.class)
        .setParameter("id", 1L)
        .getSingleResult());

        //Change the detached entity state
        post.setTitle("Hibernate Training");

        doInJPA(entityManager -> {
            Session session = entityManager.unwrap(Session.class);
            //The Post entity graph is detached
            containsPost(entityManager, post, false);

            //The update will trigger an entity state flush and attach the entity graph
            session.update(post);

            //The Post entity graph is attached
            containsPost(entityManager, post, true);
        });
        doInJPA(entityManager -> {
            Post _post = (Post) entityManager.find(Post.class, 1L);
            assertEquals("Hibernate Training", _post.getTitle());
        });
    }

    @Entity(name = "Post")
    @Table(name = "post")
    public static class Post {

        @Id
        @GeneratedValue
        private Long id;

        private String title;

        private String body;

        @Version
        private int version;

        public Post() {}

        public Post(Long id) {
            this.id = id;
        }

        public Post(String title) {
            this.title = title;
        }

        @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
        private List<PostComment> comments = new ArrayList<>();

        @OneToOne(cascade = CascadeType.ALL, mappedBy = "post",
                orphanRemoval = true, fetch = FetchType.LAZY, optional = false)
        private PostDetails details;

        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 List<PostComment> getComments() {
            return comments;
        }

        public PostDetails getDetails() {
            return details;
        }

        public void addComment(PostComment comment) {
            comments.add(comment);
        }

        public void addDetails(PostDetails details) {
            this.details = details;
            details.setPost(this);
        }

        public void removeDetails() {
            this.details.setPost(null);
            this.details = null;
        }
    }

    @Entity(name = "PostDetails")
    @Table(name = "post_details")
    public static class PostDetails {

        @Id
        private Long id;

        @Column(name = "created_on")
        private Date createdOn;

        @Column(name = "created_by")
        private String createdBy;

        @Version
        private int version;

        public PostDetails() {
            createdOn = new Date();
        }

        @OneToOne(fetch = FetchType.LAZY)
        @MapsId
        private Post post;

        public Long getId() {
            return id;
        }

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

        public Post getPost() {
            return post;
        }

        public void setPost(Post post) {
            this.post = post;
        }

        public Date getCreatedOn() {
            return createdOn;
        }

        public void setCreatedOn(Date createdOn) {
            this.createdOn = createdOn;
        }

        public String getCreatedBy() {
            return createdBy;
        }

        public void setCreatedBy(String createdBy) {
            this.createdBy = createdBy;
        }
    }

    @Entity(name = "PostComment")
    @Table(name = "post_comment")
    public static class PostComment {

        @Id
        @GeneratedValue
        private Long id;

        private String review;

        @Version
        private int version;

        public PostComment() {}

        public PostComment(String review) {
            this.review = review;
        }

        public Long getId() {
            return id;
        }

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

        public String getReview() {
            return review;
        }

        public void setReview(String review) {
            this.review = review;
        }
    }
}