package com.vladmihalcea.hibernate.masterclass.laboratory.concurrency;

import com.vladmihalcea.hibernate.masterclass.laboratory.util.AbstractTest;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.StaleObjectStateException;
import org.hibernate.annotations.Immutable;
import org.junit.Before;
import org.junit.Test;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

import static org.junit.Assert.fail;


/**
 * LockModePessimisticForceIncrementTest - Test to check LockMode.PESSIMISTIC_FORCE_INCREMENT
 *
 * @author Vlad Mihalcea
 */
public class LockModePessimisticForceIncrementTest extends AbstractTest {

    private final CountDownLatch startLatch = new CountDownLatch(1);
    private final CountDownLatch endLatch = new CountDownLatch(1);

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

    @Before
    public void init() {
        super.init();
        doInTransaction(session -> {
            Repository repository = new Repository("Hibernate-Master-Class");
            session.persist(repository);
            session.flush();
        });
    }

    @Test
    public void testPessimisticForceIncrementLocking() throws InterruptedException {
        LOGGER.info("Test Single PESSIMISTIC_FORCE_INCREMENT Lock Mode ");
        doInTransaction(session -> {
            Repository repository = (Repository) session.get(Repository.class, 1L);
            session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(repository);
            Commit commit = new Commit(repository);
            commit.getChanges().add(new Change("README.txt", "0a1,5..."));
            commit.getChanges().add(new Change("web.xml", "17c17..."));
            session.persist(commit);
        });
    }

    @Test
    public void testConcurrentPessimisticForceIncrementLockingWithLockWaiting() throws InterruptedException {
        LOGGER.info("Test Concurrent PESSIMISTIC_FORCE_INCREMENT Lock Mode With Lock Waiting");
        doInTransaction(session -> {
            try {
                Repository repository = (Repository) session.get(Repository.class, 1L);
                session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(repository);

                executeAsync(() -> doInTransaction(_session -> {
                    LOGGER.info("Try to get the Repository row");
                    startLatch.countDown();
                    Repository _repository = (Repository) _session.get(Repository.class, 1L);
                    _session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(_repository);
                    Commit _commit = new Commit(_repository);
                    _commit.getChanges().add(new Change("index.html", "0a1,2..."));
                    _session.persist(_commit);
                    _session.flush();
                    endLatch.countDown();
                }));
                startLatch.await();
                LOGGER.info("Sleep for 500ms to delay the other transaction PESSIMISTIC_FORCE_INCREMENT Lock Mode acquisition");
                Thread.sleep(500);
                Commit commit = new Commit(repository);
                commit.getChanges().add(new Change("README.txt", "0a1,5..."));
                commit.getChanges().add(new Change("web.xml", "17c17..."));
                session.persist(commit);
            } catch (InterruptedException e) {
                fail("Unexpected failure");
            }
        });
        endLatch.await();
    }

    @Test
    public void testConcurrentPessimisticForceIncrementLockingFailFast() throws InterruptedException {
        LOGGER.info("Test Concurrent PESSIMISTIC_FORCE_INCREMENT Lock Mode fail fast");
        doInTransaction(session -> {
            try {
                Repository repository = (Repository) session.get(Repository.class, 1L);

                executeSync(() -> {
                    doInTransaction(_session -> {
                        Repository _repository = (Repository) _session.get(Repository.class, 1L);
                        _session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(_repository);
                        Commit _commit = new Commit(_repository);
                        _commit.getChanges().add(new Change("index.html", "0a1,2..."));
                        _session.persist(_commit);
                        _session.flush();
                    });
                });
                session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(repository);
                fail("Should have thrown StaleObjectStateException!");
            } catch (StaleObjectStateException expected) {
                LOGGER.info("Failure: ", expected);
            }
        });
    }

    /**
     * Repository - Repository
     *
     * @author Vlad Mihalcea
     */
    @Entity(name = "repository")
    @Table(name = "repository")
    public static class Repository {

        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;

        private String name;

        public Repository() {
        }

        public Repository(String name) {
            this.name = name;
        }

        @Version
        private int version;

        public Long getId() {
            return id;
        }

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

    /**
     * OrderLine - Order Line
     *
     * @author Vlad Mihalcea
     */
    @Entity(name = "Commit")
    @Table(name = "commit")
    @Immutable
    public static class Commit {

        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;

        @ManyToOne
        private Repository repository;

        @ElementCollection
        @CollectionTable(
                name = "commit_change",
                joinColumns = @JoinColumn(name = "commit_id")
        )
        private List<Change> changes = new ArrayList<>();

        public Commit() {
        }

        public Commit(Repository repository) {
            this.repository = repository;
        }

        public Repository getRepository() {
            return repository;
        }

        public List<Change> getChanges() {
            return changes;
        }
    }

    /**
     * OrderLine - Order Line
     *
     * @author Vlad Mihalcea
     */
    @Embeddable
    public static class Change {

        private String path;

        private String diff;

        public Change() {
        }

        public Change(String path, String diff) {
            this.path = path;
            this.diff = diff;
        }

        public String getPath() {
            return path;
        }

        public String getDiff() {
            return diff;
        }
    }
}