package com.atlassian.bitbucket.mesh.app;

import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.junit.TemporaryDirectory;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import java.io.IOException;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.Files;
import java.nio.file.Path;

import static org.junit.Assert.*;

public class DirectoryLockTest {

    @Rule
    public final TemporaryDirectory tempDir = new TemporaryDirectory();

    private ExternalProcessDirectoryLock externalLock;
    private DirectoryLock homeLock;
    private Path homeDir;

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

    @After
    public void tearDown() throws Exception {
        if (homeLock != null) {
            homeLock.close();
        }
        if (externalLock != null) {
            externalLock.close();
        }
    }

    @Test(expected = IOException.class)
    public void testLockWhenFileCreationFails() throws Exception {
        try {
            Files.delete(homeDir);
        } catch (IOException e) {
            fail("Could not delete the home directory");
        }

        homeLock = new DirectoryLock(homeDir, false);
        homeLock.lock();
    }

    @Test(expected = IOException.class)
    public void testLockWhenLockFileIsADirectory() throws Exception {
        MoreFiles.mkdir(homeDir, ".lock");

        homeLock = new DirectoryLock(homeDir, false);
        homeLock.lock();
    }

    @Test
    public void testLockRelockAndUnlock() throws Exception {
        homeLock = new DirectoryLock(homeDir, false);
        assertTrue("Exclusive lock should be acquired", homeLock.lock());

        try {
            homeLock.lock(); // Attempting to lock a second time should throw an exception
            fail("An OverlappingFileLockException should have been thrown");
        } catch (OverlappingFileLockException e) {
            // ignore
        }

        homeLock.close();

        assertTrue("Exclusive lock should be acquired after closed", homeLock.lock());
        homeLock.close();

        homeLock.close(); // Unlocking a second time should do nothing
    }

    @Test
    public void testLockWithExclusiveLocks() throws Exception {
        externalLock = new ExternalProcessDirectoryLock(homeDir, false);
        homeLock = new DirectoryLock(homeDir, false);
        assertTrue("First exclusive lock should be acquired", externalLock.lock());
        assertFalse("Second exclusive lock should not be acquired", homeLock.lock());
    }

    @Test
    public void testLockWithExclusiveLocksCanBeReAcquiredAfterClose() throws Exception {
        homeLock = new DirectoryLock(homeDir, false);
        assertTrue("First exclusive lock should be acquired", homeLock.lock());
        homeLock.close();
        assertTrue("Second exclusive lock should be acquired after first is closed", homeLock.lock());
    }

    @Test
    public void testLockWithExclusiveAndSharedLocks() throws Exception {
        externalLock = new ExternalProcessDirectoryLock(homeDir, false);
        homeLock = new DirectoryLock(homeDir, true);
        assertTrue("Exclusive lock should be acquired", externalLock.lock());
        assertFalse("Shared lock should noy be acquired", homeLock.lock());
    }

    @Test
    public void testLockWithExclusiveAndSharedLocksAfterClose() throws Exception {
        homeLock = new DirectoryLock(homeDir, false);
        assertTrue("Exclusive lock should be acquired", homeLock.lock());
        homeLock.close();
        homeLock = new DirectoryLock(homeDir, true);
        assertTrue("Shared lock should be acquired after exclusive is closed", homeLock.lock());
    }

    @Test
    public void testLockWithSharedAndExclusiveLocks() throws Exception {
        externalLock = new ExternalProcessDirectoryLock(homeDir, true);
        homeLock = new DirectoryLock(homeDir, false);
        assertTrue("Shared lock should be acquired", externalLock.lock());
        assertFalse("Exclusive lock should noy be acquired", homeLock.lock());
    }

    @Test
    public void testLockWithSharedAndExclusiveLocksAfterClose() throws Exception {
        homeLock = new DirectoryLock(homeDir, true);
        assertTrue("Shared lock should be acquired", homeLock.lock());
        homeLock.close();
        homeLock = new DirectoryLock(homeDir, false);
        assertTrue("Exclusive lock should be acquired after shared is closed", homeLock.lock());
    }

    @Test
    public void testLockWithSharedLocks() throws Exception {
        externalLock = new ExternalProcessDirectoryLock(homeDir, true);
        homeLock = new DirectoryLock(homeDir, true);

        assertTrue("First shared lock should be acquired", externalLock.lock());
        assertTrue("Second shared lock should be acquired", homeLock.lock());

        homeLock.close();
        homeLock = new DirectoryLock(homeDir, false);
        assertFalse("Exclusive lock should not be acquired after only 1 of 2 shared locks are closed", homeLock.lock());
    }
}