package lockfile

import (
	"fmt"
	"io/ioutil"
	"math/rand"
	"os"
	"path/filepath"
	"strconv"
	"testing"
)

func ExampleLockfile() {
	lock, err := New(filepath.Join(os.TempDir(), "lock.me.now.lck"))
	if err != nil {
		fmt.Printf("Cannot init lock. reason: %v", err)
		panic(err) // handle properly please!
	}

	// Error handling is essential, as we only try to get the lock.
	if err = lock.TryLock(); err != nil {
		fmt.Printf("Cannot lock %q, reason: %v", lock, err)
		panic(err) // handle properly please!
	}

	defer func() {
		if err := lock.Unlock(); err != nil {
			fmt.Printf("Cannot unlock %q, reason: %v", lock, err)
			panic(err) // handle properly please!
		}
	}()

	fmt.Println("Do stuff under lock")
	// Output: Do stuff under lock
}

func TestBasicLockUnlock(t *testing.T) {
	path, err := filepath.Abs("test_lockfile.pid")
	if err != nil {
		panic(err)
	}

	lf, err := New(path)
	if err != nil {
		t.Fail()
		fmt.Println("Error making lockfile: ", err)
		return
	}

	err = lf.TryLock()
	if err != nil {
		t.Fail()
		fmt.Println("Error locking lockfile: ", err)
		return
	}

	err = lf.Unlock()
	if err != nil {
		t.Fail()
		fmt.Println("Error unlocking lockfile: ", err)
		return
	}
}

func GetDeadPID() int {
	// I have no idea how windows handles large PIDs, or if they even exist.
	// So limit it to be less or equal to 4096 to be safe.
	const maxPid = 4095

	// limited iteration, so we finish one day
	seen := map[int]bool{}
	for len(seen) < maxPid {
		pid := rand.Intn(maxPid + 1) // see https://godoc.org/math/rand#Intn why
		if seen[pid] {
			continue
		}

		seen[pid] = true

		running, err := isRunning(pid)
		if err != nil {
			fmt.Println("Error checking PID: ", err)
			continue
		}

		if !running {
			return pid
		}
	}
	panic(fmt.Sprintf("all pids lower %d are used, cannot test this", maxPid))
}

func TestBusy(t *testing.T) {
	path, err := filepath.Abs("test_lockfile.pid")
	if err != nil {
		t.Fatal(err)
		return
	}

	pid := os.Getppid()

	if err := ioutil.WriteFile(path, []byte(strconv.Itoa(pid)+"\n"), 0666); err != nil {
		t.Fatal(err)
		return
	}
	defer os.Remove(path)

	lf, err := New(path)
	if err != nil {
		t.Fatal(err)
		return
	}

	got := lf.TryLock()
	if got != ErrBusy {
		t.Fatalf("expected error %q, got %v", ErrBusy, got)
		return
	}
}

func TestRogueDeletion(t *testing.T) {
	path, err := filepath.Abs("test_lockfile.pid")
	if err != nil {
		t.Fatal(err)
		return
	}
	lf, err := New(path)
	if err != nil {
		t.Fatal(err)
		return
	}
	err = lf.TryLock()
	if err != nil {
		t.Fatal(err)
		return
	}
	err = os.Remove(path)
	if err != nil {
		t.Fatal(err)
		return
	}

	got := lf.Unlock()
	if got != ErrRogueDeletion {
		t.Fatalf("unexpected error: %v", got)
		return
	}
}

func TestRogueDeletionDeadPid(t *testing.T) {
	path, err := filepath.Abs("test_lockfile.pid")
	if err != nil {
		t.Fatal(err)
		return
	}
	lf, err := New(path)
	if err != nil {
		t.Fatal(err)
		return
	}
	err = lf.TryLock()
	if err != nil {
		t.Fatal(err)
		return
	}

	pid := GetDeadPID()
	if err := ioutil.WriteFile(path, []byte(strconv.Itoa(pid)+"\n"), 0666); err != nil {
		t.Fatal(err)
		return
	}

	defer os.Remove(path)

	err = lf.Unlock()
	if err != ErrRogueDeletion {
		t.Fatalf("unexpected error: %v", err)
		return
	}

	if _, err := os.Stat(path); err != nil {
		if os.IsNotExist(err) {
			content, _ := ioutil.ReadFile(path)
			t.Fatalf("lockfile %q (%q) should not be deleted by us, if we didn't create it", path, content)
		}
		t.Fatalf("unexpected error %v", err)
	}
}

func TestRemovesStaleLockOnDeadOwner(t *testing.T) {
	path, err := filepath.Abs("test_lockfile.pid")
	if err != nil {
		t.Fatal(err)
		return
	}
	lf, err := New(path)
	if err != nil {
		t.Fatal(err)
		return
	}
	pid := GetDeadPID()
	if err := ioutil.WriteFile(path, []byte(strconv.Itoa(pid)+"\n"), 0666); err != nil {
		t.Fatal(err)
		return
	}
	err = lf.TryLock()
	if err != nil {
		t.Fatal(err)
		return
	}

	if err := lf.Unlock(); err != nil {
		t.Fatal(err)
		return
	}
}

func TestInvalidPidLeadToReplacedLockfileAndSuccess(t *testing.T) {
	path, err := filepath.Abs("test_lockfile.pid")
	if err != nil {
		t.Fatal(err)
		return
	}

	if err := ioutil.WriteFile(path, []byte("\n"), 0666); err != nil {
		t.Fatal(err)
		return
	}

	defer os.Remove(path)

	lf, err := New(path)
	if err != nil {
		t.Fatal(err)
		return
	}

	if err := lf.TryLock(); err != nil {
		t.Fatalf("unexpected error: %v", err)
		return
	}

	// now check if file exists and contains the correct content
	got, err := ioutil.ReadFile(path)
	if err != nil {
		t.Fatalf("unexpected error %v", err)
		return
	}
	want := fmt.Sprintf("%d\n", os.Getpid())
	if string(got) != want {
		t.Fatalf("got %q, want %q", got, want)
	}
}

func TestScanPidLine(t *testing.T) {
	tests := [...]struct {
		input []byte
		pid   int
		xfail error
	}{
		{xfail: ErrInvalidPid},
		{input: []byte(""), xfail: ErrInvalidPid},
		{input: []byte("\n"), xfail: ErrInvalidPid},
		{input: []byte("-1\n"), xfail: ErrInvalidPid},
		{input: []byte("0\n"), xfail: ErrInvalidPid},
		{input: []byte("a\n"), xfail: ErrInvalidPid},
		{input: []byte("1\n"), pid: 1},
	}

	// test positive cases first
	for step, tc := range tests {
		if tc.xfail != nil {
			continue
		}

		got, err := scanPidLine(tc.input)
		if err != nil {
			t.Fatalf("%d: unexpected error %v", step, err)
		}

		if want := tc.pid; got != want {
			t.Errorf("%d: expected pid %d, got %d", step, want, got)
		}
	}

	// test negative cases now
	for step, tc := range tests {
		if tc.xfail == nil {
			continue
		}

		_, got := scanPidLine(tc.input)
		if want := tc.xfail; got != want {
			t.Errorf("%d: expected error %v, got %v", step, want, got)
		}
	}
}
