package restic_test

import (
	"os"
	"testing"
	"time"

	"github.com/restic/restic"
	"github.com/restic/restic/backend"
	"github.com/restic/restic/repository"
	. "github.com/restic/restic/test"
)

func TestLock(t *testing.T) {
	repo := SetupRepo()
	defer TeardownRepo(repo)

	lock, err := restic.NewLock(repo)
	OK(t, err)

	OK(t, lock.Unlock())
}

func TestDoubleUnlock(t *testing.T) {
	repo := SetupRepo()
	defer TeardownRepo(repo)

	lock, err := restic.NewLock(repo)
	OK(t, err)

	OK(t, lock.Unlock())

	err = lock.Unlock()
	Assert(t, err != nil,
		"double unlock didn't return an error, got %v", err)
}

func TestMultipleLock(t *testing.T) {
	repo := SetupRepo()
	defer TeardownRepo(repo)

	lock1, err := restic.NewLock(repo)
	OK(t, err)

	lock2, err := restic.NewLock(repo)
	OK(t, err)

	OK(t, lock1.Unlock())
	OK(t, lock2.Unlock())
}

func TestLockExclusive(t *testing.T) {
	repo := SetupRepo()
	defer TeardownRepo(repo)

	elock, err := restic.NewExclusiveLock(repo)
	OK(t, err)
	OK(t, elock.Unlock())
}

func TestLockOnExclusiveLockedRepo(t *testing.T) {
	repo := SetupRepo()
	defer TeardownRepo(repo)

	elock, err := restic.NewExclusiveLock(repo)
	OK(t, err)

	lock, err := restic.NewLock(repo)
	Assert(t, err != nil,
		"create normal lock with exclusively locked repo didn't return an error")
	Assert(t, restic.IsAlreadyLocked(err),
		"create normal lock with exclusively locked repo didn't return the correct error")

	OK(t, lock.Unlock())
	OK(t, elock.Unlock())
}

func TestExclusiveLockOnLockedRepo(t *testing.T) {
	repo := SetupRepo()
	defer TeardownRepo(repo)

	elock, err := restic.NewLock(repo)
	OK(t, err)

	lock, err := restic.NewExclusiveLock(repo)
	Assert(t, err != nil,
		"create normal lock with exclusively locked repo didn't return an error")
	Assert(t, restic.IsAlreadyLocked(err),
		"create normal lock with exclusively locked repo didn't return the correct error")

	OK(t, lock.Unlock())
	OK(t, elock.Unlock())
}

func createFakeLock(repo *repository.Repository, t time.Time, pid int) (backend.ID, error) {
	newLock := &restic.Lock{Time: t, PID: pid}
	return repo.SaveJSONUnpacked(backend.Lock, &newLock)
}

func removeLock(repo *repository.Repository, id backend.ID) error {
	return repo.Backend().Remove(backend.Lock, id.String())
}

var staleLockTests = []struct {
	timestamp time.Time
	stale     bool
	pid       int
}{
	{
		timestamp: time.Now(),
		stale:     false,
		pid:       os.Getpid(),
	},
	{
		timestamp: time.Now().Add(-time.Hour),
		stale:     true,
		pid:       os.Getpid(),
	},
	{
		timestamp: time.Now().Add(3 * time.Minute),
		stale:     false,
		pid:       os.Getpid(),
	},
	{
		timestamp: time.Now(),
		stale:     true,
		pid:       os.Getpid() + 500,
	},
}

func TestLockStale(t *testing.T) {
	for i, test := range staleLockTests {
		lock := restic.Lock{
			Time: test.timestamp,
			PID:  test.pid,
		}

		Assert(t, lock.Stale() == test.stale,
			"TestStaleLock: test %d failed: expected stale: %v, got %v",
			i, test.stale, !test.stale)
	}
}

func lockExists(repo *repository.Repository, t testing.TB, id backend.ID) bool {
	exists, err := repo.Backend().Test(backend.Lock, id.String())
	OK(t, err)

	return exists
}

func TestLockWithStaleLock(t *testing.T) {
	repo := SetupRepo()
	defer TeardownRepo(repo)

	id1, err := createFakeLock(repo, time.Now().Add(-time.Hour), os.Getpid())
	OK(t, err)

	id2, err := createFakeLock(repo, time.Now().Add(-time.Minute), os.Getpid())
	OK(t, err)

	id3, err := createFakeLock(repo, time.Now().Add(-time.Minute), os.Getpid()+500)
	OK(t, err)

	OK(t, restic.RemoveStaleLocks(repo))

	Assert(t, lockExists(repo, t, id1) == false,
		"stale lock still exists after RemoveStaleLocks was called")
	Assert(t, lockExists(repo, t, id2) == true,
		"non-stale lock was removed by RemoveStaleLocks")
	Assert(t, lockExists(repo, t, id3) == false,
		"stale lock still exists after RemoveStaleLocks was called")

	OK(t, removeLock(repo, id2))
}

func TestRemoveAllLocks(t *testing.T) {
	repo := SetupRepo()
	defer TeardownRepo(repo)

	id1, err := createFakeLock(repo, time.Now().Add(-time.Hour), os.Getpid())
	OK(t, err)

	id2, err := createFakeLock(repo, time.Now().Add(-time.Minute), os.Getpid())
	OK(t, err)

	id3, err := createFakeLock(repo, time.Now().Add(-time.Minute), os.Getpid()+500)
	OK(t, err)

	OK(t, restic.RemoveAllLocks(repo))

	Assert(t, lockExists(repo, t, id1) == false,
		"lock still exists after RemoveAllLocks was called")
	Assert(t, lockExists(repo, t, id2) == false,
		"lock still exists after RemoveAllLocks was called")
	Assert(t, lockExists(repo, t, id3) == false,
		"lock still exists after RemoveAllLocks was called")
}

func TestLockRefresh(t *testing.T) {
	repo := SetupRepo()
	defer TeardownRepo(repo)

	lock, err := restic.NewLock(repo)
	OK(t, err)

	var lockID backend.ID
	for id := range repo.List(backend.Lock, nil) {
		if lockID != nil {
			t.Error("more than one lock found")
		}
		lockID = id
	}

	OK(t, lock.Refresh())

	var lockID2 backend.ID
	for id := range repo.List(backend.Lock, nil) {
		if lockID2 != nil {
			t.Error("more than one lock found")
		}
		lockID2 = id
	}

	Assert(t, !lockID.Equal(lockID2),
		"expected a new ID after lock refresh, got the same")
	OK(t, lock.Unlock())
}