package com.atlassian.bitbucket.mesh.app;

import com.atlassian.bitbucket.mesh.junit.LogListener;
import com.atlassian.bitbucket.mesh.junit.TemporaryDirectory;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.beans.DirectFieldAccessor;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.*;

@RunWith(MockitoJUnitRunner.class)
public class HomeLockAcquirerTest {

    @Rule
    public final LogListener logListener = new LogListener(HomeLockAcquirer.class);
    @Rule
    public final TemporaryDirectory tempDir = new TemporaryDirectory();

    private HomeLockAcquirer acquirer;
    private Path directory;

    @Before
    public void setup() throws Exception {
        directory = tempDir.newFolder();

        acquirer = new HomeLockAcquirer(directory);
    }

    @Test
    public void testLockFailed() throws Exception {
        //The easiest way to get lock creation to fail with an IOException is to remove the directory it's going to try
        //and create the lock file in, so that's what this test does.
        Files.delete(directory);

        assertThrows(HomeLockFailedException.class, acquirer::lock);
        logListener.assertError("cannot be obtained in home directory");
    }

    @Test
    public void testLockRelockAndUnlock() {
        acquirer.lock();

        DirectFieldAccessor accessor = new DirectFieldAccessor(acquirer);
        assertNotNull(accessor.getPropertyValue("lock"));

        //Attempting to lock a second time should throw an exception
        IllegalStateException e = assertThrows(IllegalStateException.class, acquirer::lock);
        assertTrue(e.getMessage().contains("already held"));

        acquirer.unlock();

        //Releasing the lock should
        assertNull(accessor.getPropertyValue("lock"));

        acquirer.unlock(); //Unlocking a second time should do nothing
    }

    @Test
    public void testMultipleLocks() {
        acquirer.lock(); //Acquire the lock successfully

        HomeLockAcquirer secondAcquirer = new HomeLockAcquirer(directory);
        //Should trigger an OverlappingFileLockException
        assertThrows(HomeLockFailedException.class, secondAcquirer::lock);

        logListener.assertError("The current JVM holds an overlapping lock on directory");
    }

    /**
     * This test is a compromise between the desire to test the logic of {@code DirectoryLockAcquirer.onLockDenied} and
     * the fact that only way to get it to happen for real in a unit test is to fork another Java process and try to
     * lock the file within a separate JVM. Because that is incredibly complicated for a unit test, discretion seems to
     * be the better part of valour here.
     * <p>
     * This test directly accesses the private method and invokes it, allowing us to verify its behaviour without the
     * difficulty of actually getting the related state to occur in the wild.
     *
     * @throws Exception May be thrown due to use of Reflection to access the private onLockDenied method.
     */
    @Test
    public void testOnLockDenied() throws Exception {
        Method method = HomeLockAcquirer.class.getDeclaredMethod("onLockDenied", DirectoryLock.class);
        method.setAccessible(true);

        try (DirectoryLock lock = new DirectoryLock(directory, false)) {
            InvocationTargetException e = assertThrows(InvocationTargetException.class,
                    () -> method.invoke(acquirer, lock));

            assertThat("Invoking onLockDenied should have thrown a DirectoryLockDeniedException", e.getCause(),
                    instanceOf(HomeLockDeniedException.class));
        }
    }
}
