package lockservice

import "testing"
import "runtime"

import "math/rand"
import "os"
import "strconv"

import "time"
import "fmt"

func tl(t *testing.T, ck *Clerk, lockname string, expected bool) {
	x := ck.Lock(lockname)
	if x != expected {
		t.Fatalf("Lock(%v) returned %v; expected %v", lockname, x, expected)
	}
}

func tu(t *testing.T, ck *Clerk, lockname string, expected bool) {
	x := ck.Unlock(lockname)
	if x != expected {
		t.Fatalf("Unlock(%v) returned %v; expected %v", lockname, x, expected)
	}
}

//
// cook up a unique-ish UNIX-domain socket name
// in /var/tmp. can't use current directory since
// AFS doesn't support UNIX-domain sockets.
//
func port(suffix string) string {
	s := "/var/tmp/824-"
	s += strconv.Itoa(os.Getuid()) + "/"
	os.Mkdir(s, 0777)
	s += strconv.Itoa(os.Getpid()) + "-"
	s += suffix
	return s
}

func TestBasic(t *testing.T) {
	fmt.Printf("Test: Basic lock/unlock ...\n")

	runtime.GOMAXPROCS(4)

	phost := port("p")
	bhost := port("b")
	p := StartServer(phost, bhost, true)  // primary
	b := StartServer(phost, bhost, false) // backup

	ck := MakeClerk(phost, bhost)

	tl(t, ck, "a", true)
	tu(t, ck, "a", true)

	tl(t, ck, "a", true)
	tl(t, ck, "b", true)
	tu(t, ck, "a", true)
	tu(t, ck, "b", true)

	tl(t, ck, "a", true)
	tl(t, ck, "a", false)
	tu(t, ck, "a", true)
	tu(t, ck, "a", false)

	p.kill()
	b.kill()

	fmt.Printf("  ... Passed\n")
}

func TestPrimaryFail1(t *testing.T) {
	fmt.Printf("Test: Primary failure ...\n")
	runtime.GOMAXPROCS(4)

	phost := port("p")
	bhost := port("b")
	p := StartServer(phost, bhost, true)  // primary
	b := StartServer(phost, bhost, false) // backup

	ck := MakeClerk(phost, bhost)

	tl(t, ck, "a", true)

	tl(t, ck, "b", true)
	tu(t, ck, "b", true)

	tl(t, ck, "c", true)
	tl(t, ck, "c", false)

	tl(t, ck, "d", true)
	tu(t, ck, "d", true)
	tl(t, ck, "d", true)

	p.kill()

	tl(t, ck, "a", false)
	tu(t, ck, "a", true)

	tu(t, ck, "b", false)
	tl(t, ck, "b", true)

	tu(t, ck, "c", true)

	tu(t, ck, "d", true)

	b.kill()
	fmt.Printf("  ... Passed\n")
}

func TestPrimaryFail2(t *testing.T) {
	fmt.Printf("Test: Primary failure just before reply #1 ...\n")
	runtime.GOMAXPROCS(4)

	phost := port("p")
	bhost := port("b")
	p := StartServer(phost, bhost, true)  // primary
	b := StartServer(phost, bhost, false) // backup

	ck1 := MakeClerk(phost, bhost)
	ck2 := MakeClerk(phost, bhost)

	tl(t, ck1, "a", true)
	tl(t, ck1, "b", true)

	p.dying = true

	tl(t, ck2, "c", true)
	tl(t, ck1, "c", false)
	tu(t, ck2, "c", true)
	tl(t, ck1, "c", true)

	b.kill()
	fmt.Printf("  ... Passed\n")
}

func TestPrimaryFail3(t *testing.T) {
	fmt.Printf("Test: Primary failure just before reply #2 ...\n")
	runtime.GOMAXPROCS(4)

	phost := port("p")
	bhost := port("b")
	p := StartServer(phost, bhost, true)  // primary
	b := StartServer(phost, bhost, false) // backup

	ck1 := MakeClerk(phost, bhost)

	tl(t, ck1, "a", true)
	tl(t, ck1, "b", true)

	p.dying = true

	tl(t, ck1, "b", false)

	b.kill()
	fmt.Printf("  ... Passed\n")
}

func TestPrimaryFail4(t *testing.T) {
	fmt.Printf("Test: Primary failure just before reply #3 ...\n")
	runtime.GOMAXPROCS(4)

	phost := port("p")
	bhost := port("b")
	p := StartServer(phost, bhost, true)  // primary
	b := StartServer(phost, bhost, false) // backup

	ck1 := MakeClerk(phost, bhost)
	ck2 := MakeClerk(phost, bhost)

	tl(t, ck1, "a", true)
	tl(t, ck1, "b", true)

	p.dying = true

	tl(t, ck2, "b", false)

	b.kill()
	fmt.Printf("  ... Passed\n")
}

func TestPrimaryFail5(t *testing.T) {
	fmt.Printf("Test: Primary failure just before reply #4 ...\n")
	runtime.GOMAXPROCS(4)

	phost := port("p")
	bhost := port("b")
	p := StartServer(phost, bhost, true)  // primary
	b := StartServer(phost, bhost, false) // backup

	ck1 := MakeClerk(phost, bhost)
	ck2 := MakeClerk(phost, bhost)

	tl(t, ck1, "a", true)
	tl(t, ck1, "b", true)
	tu(t, ck1, "b", true)

	p.dying = true

	tu(t, ck1, "b", false)
	tl(t, ck2, "b", true)

	b.kill()
	fmt.Printf("  ... Passed\n")
}

func TestPrimaryFail6(t *testing.T) {
	fmt.Printf("Test: Primary failure just before reply #5 ...\n")
	runtime.GOMAXPROCS(4)

	phost := port("p")
	bhost := port("b")
	p := StartServer(phost, bhost, true)  // primary
	b := StartServer(phost, bhost, false) // backup

	ck1 := MakeClerk(phost, bhost)
	ck2 := MakeClerk(phost, bhost)

	tl(t, ck1, "a", true)
	tu(t, ck1, "a", true)
	tu(t, ck2, "a", false)
	tl(t, ck1, "b", true)

	p.dying = true

	tu(t, ck2, "b", true)
	tl(t, ck1, "b", true)

	b.kill()
	fmt.Printf("  ... Passed\n")
}

func TestPrimaryFail7(t *testing.T) {
  fmt.Printf("Test: Primary failure just before reply #6 ...\n")
  runtime.GOMAXPROCS(4)

  phost := port("p")
  bhost := port("b")
  p := StartServer(phost, bhost, true)  // primary
  b := StartServer(phost, bhost, false) // backup

  ck1 := MakeClerk(phost, bhost)
  ck2 := MakeClerk(phost, bhost)

  tl(t, ck1, "a", true)
  tu(t, ck1, "a", true)
  tu(t, ck2, "a", false)
  tl(t, ck1, "b", true)

  p.dying = true

  ch := make(chan bool)
  go func() {
    ok := false
    defer func() { ch <- ok }()
    tu(t, ck2, "b", true) // 2 second delay until retry
    ok = true
  }()
  time.Sleep(1 * time.Second)
  tl(t, ck1, "b", true)

  ok := <- ch
  if ok == false {
    t.Fatalf("re-sent Unlock did not return true")
  }

  tu(t, ck1, "b", true)

  b.kill()
  fmt.Printf("  ... Passed\n")
}

func TestPrimaryFail8(t *testing.T) {
  fmt.Printf("Test: Primary failure just before reply #7 ...\n")
  runtime.GOMAXPROCS(4)

  phost := port("p")
  bhost := port("b")
  p := StartServer(phost, bhost, true)  // primary
  b := StartServer(phost, bhost, false) // backup

  ck1 := MakeClerk(phost, bhost)
  ck2 := MakeClerk(phost, bhost)

  tl(t, ck1, "a", true)
  tu(t, ck1, "a", true)

  p.dying = true

  ch := make(chan bool)
  go func() {
    ok := false
    defer func() { ch <- ok }()
    tu(t, ck2, "a", false) // 2 second delay until retry
    ok = true
  }()
  time.Sleep(1 * time.Second)
  tl(t, ck1, "a", true)

  ok := <- ch
  if ok == false {
    t.Fatalf("re-sent Unlock did not return false")
  }

  tu(t, ck1, "a", true)

  b.kill()
  fmt.Printf("  ... Passed\n")
}

func TestBackupFail(t *testing.T) {
	fmt.Printf("Test: Backup failure ...\n")
	runtime.GOMAXPROCS(4)

	phost := port("p")
	bhost := port("b")
	p := StartServer(phost, bhost, true)  // primary
	b := StartServer(phost, bhost, false) // backup

	ck := MakeClerk(phost, bhost)

	tl(t, ck, "a", true)

	tl(t, ck, "b", true)
	tu(t, ck, "b", true)

	tl(t, ck, "c", true)
	tl(t, ck, "c", false)

	tl(t, ck, "d", true)
	tu(t, ck, "d", true)
	tl(t, ck, "d", true)

	b.kill()

	tl(t, ck, "a", false)
	tu(t, ck, "a", true)

	tu(t, ck, "b", false)
	tl(t, ck, "b", true)

	tu(t, ck, "c", true)

	tu(t, ck, "d", true)

	p.kill()
	fmt.Printf("  ... Passed\n")
}

func TestMany(t *testing.T) {
	fmt.Printf("Test: Multiple clients with primary failure ...\n")
	runtime.GOMAXPROCS(4)

	phost := port("p")
	bhost := port("b")
	p := StartServer(phost, bhost, true)  // primary
	b := StartServer(phost, bhost, false) // backup

	const nclients = 2
	const nlocks = 10
	done := false
	var state [nclients][nlocks]bool
	var acks [nclients]bool

	for xi := 0; xi < nclients; xi++ {
		go func(i int) {
			ck := MakeClerk(phost, bhost)
			rr := rand.New(rand.NewSource(int64(os.Getpid() + i)))
			for done == false {
				locknum := (rr.Int() % nlocks)
				lockname := strconv.Itoa(locknum + (i * 1000))
				what := rr.Int() % 2
				if what == 0 {
					ck.Lock(lockname)
					state[i][locknum] = true
				} else {
					ck.Unlock(lockname)
					state[i][locknum] = false
				}
			}
			acks[i] = true
		}(xi)
	}

	time.Sleep(2 * time.Second)
	p.kill()
	time.Sleep(2 * time.Second)
	done = true
	time.Sleep(time.Second)
	ck := MakeClerk(phost, bhost)
	for xi := 0; xi < nclients; xi++ {
		if acks[xi] == false {
			t.Fatal("one client didn't complete")
		}
		for locknum := 0; locknum < nlocks; locknum++ {
			lockname := strconv.Itoa(locknum + (xi * 1000))
			locked := !ck.Lock(lockname)
			if locked != state[xi][locknum] {
				t.Fatal("bad final state")
			}
		}
	}

	b.kill()
	fmt.Printf("  ... Passed\n")
}

func TestConcurrentCounts(t *testing.T) {
	fmt.Printf("Test: Multiple clients, single lock, primary failure ...\n")
	runtime.GOMAXPROCS(4)

	phost := port("p")
	bhost := port("b")
	p := StartServer(phost, bhost, true)  // primary
	b := StartServer(phost, bhost, false) // backup

	const nclients = 2
	const nlocks = 1
	done := false
	var acks [nclients]bool
	var locks [nclients][nlocks]int
	var unlocks [nclients][nlocks]int

	for xi := 0; xi < nclients; xi++ {
		go func(i int) {
			ck := MakeClerk(phost, bhost)
			rr := rand.New(rand.NewSource(int64(os.Getpid() + i)))
			for done == false {
				locknum := rr.Int() % nlocks
				lockname := strconv.Itoa(locknum)
				what := rr.Int() % 2
				if what == 0 {
					if ck.Lock(lockname) {
						locks[i][locknum]++
					}
				} else {
					if ck.Unlock(lockname) {
						unlocks[i][locknum]++
					}
				}
			}
			acks[i] = true
		}(xi)
	}

	time.Sleep(2 * time.Second)
	p.kill()
	time.Sleep(2 * time.Second)
	done = true
	time.Sleep(time.Second)
	for xi := 0; xi < nclients; xi++ {
		if acks[xi] == false {
			t.Fatal("one client didn't complete")
		}
	}
	ck := MakeClerk(phost, bhost)
	for locknum := 0; locknum < nlocks; locknum++ {
		nl := 0
		nu := 0
		for xi := 0; xi < nclients; xi++ {
			nl += locks[xi][locknum]
			nu += unlocks[xi][locknum]
		}
		locked := ck.Unlock(strconv.Itoa(locknum))
		// fmt.Printf("lock=%d nl=%d nu=%d locked=%v\n",
		//   locknum, nl, nu, locked)
		if nl < nu || nl > nu+1 {
			t.Fatal("lock race 1")
		}
		if nl == nu && locked != false {
			t.Fatal("lock race 2")
		}
		if nl != nu && locked != true {
			t.Fatal("lock race 3")
		}
	}

	b.kill()
	fmt.Printf("  ... Passed\n")
}
