// API:
//
// Open(initalDeposit int64) *Account
// (Account) Close() (payout int64, ok bool)
// (Account) Balance() (balance int64, ok bool)
// (Account) Deposit(amount uint64) (newBalance int64, ok bool)
//
// If Open is given a negative initial deposit, it must return nil.
// Deposit must handle a negative amount as a withdrawal.
// If any Account method is called on an closed account, it must not modify
// the account and must return ok = false.

package account

import (
	"runtime"
	"sync"
	"sync/atomic"
	"testing"
	"time"
)

func TestSeqOpenBalanceClose(t *testing.T) {
	// open account
	const amt = 10
	a := Open(amt)
	if a == nil {
		t.Fatalf("Open(%d) = nil, want non-nil *Account.", amt)
	}
	t.Logf("Account 'a' opened with initial balance of %d.", amt)

	// verify balance after open
	switch b, ok := a.Balance(); {
	case !ok:
		t.Fatal("a.Balance() returned !ok, want ok.")
	case b != amt:
		t.Fatalf("a.Balance() = %d, want %d", b, amt)
	}

	// close account
	switch p, ok := a.Close(); {
	case !ok:
		t.Fatalf("a.Close() returned !ok, want ok.")
	case p != amt:
		t.Fatalf("a.Close() returned payout = %d, want %d.", p, amt)
	}
	t.Log("Account 'a' closed.")

	// verify balance no longer accessible
	if b, ok := a.Balance(); ok {
		t.Log("Balance still available on closed account.")
		t.Fatalf("a.Balance() = %d, %t.  Want ok == false", b, ok)
	}
}

func TestSeqOpenDepositClose(t *testing.T) {
	// open account
	const openAmt = 10
	a := Open(openAmt)
	if a == nil {
		t.Fatalf("Open(%d) = nil, want non-nil *Account.", openAmt)
	}
	t.Logf("Account 'a' opened with initial balance of %d.", openAmt)

	// deposit
	const depAmt = 20
	const newAmt = openAmt + depAmt
	switch b, ok := a.Deposit(depAmt); {
	case !ok:
		t.Fatalf("a.Deposit(%d) returned !ok, want ok.", depAmt)
	case b != openAmt+depAmt:
		t.Fatalf("a.Deposit(%d) = %d, want new balance = %d", depAmt, b, newAmt)
	}
	t.Logf("Deposit of %d accepted to account 'a'", depAmt)

	// close account
	switch p, ok := a.Close(); {
	case !ok:
		t.Fatalf("a.Close() returned !ok, want ok.")
	case p != newAmt:
		t.Fatalf("a.Close() returned payout = %d, want %d.", p, newAmt)
	}
	t.Log("Account 'a' closed.")

	// verify deposits no longer accepted
	if b, ok := a.Deposit(1); ok {
		t.Log("Deposit accepted on closed account.")
		t.Fatalf("a.Deposit(1) = %d, %t.  Want ok == false", b, ok)
	}
}

func TestMoreSeqCases(t *testing.T) {
	// open account 'a' as before
	const openAmt = 10
	a := Open(openAmt)
	if a == nil {
		t.Fatalf("Open(%d) = nil, want non-nil *Account.", openAmt)
	}
	t.Logf("Account 'a' opened with initial balance of %d.", openAmt)

	// open account 'z' with zero balance
	z := Open(0)
	if z == nil {
		t.Fatal("Open(0) = nil, want non-nil *Account.")
	}
	t.Log("Account 'z' opened with initial balance of 0.")

	// attempt to open account with negative opening balance
	if Open(-10) != nil {
		t.Fatal("Open(-10) seemed to work, " +
			"want nil result for negative opening balance.")
	}

	// verify both balances a and z still there
	switch b, ok := a.Balance(); {
	case !ok:
		t.Fatal("a.Balance() returned !ok, want ok.")
	case b != openAmt:
		t.Fatalf("a.Balance() = %d, want %d", b, openAmt)
	}
	switch b, ok := z.Balance(); {
	case !ok:
		t.Fatal("z.Balance() returned !ok, want ok.")
	case b != 0:
		t.Fatalf("z.Balance() = %d, want 0", b)
	}

	// withdrawals
	const wAmt = 3
	const newAmt = openAmt - wAmt
	switch b, ok := a.Deposit(-wAmt); {
	case !ok:
		t.Fatalf("a.Deposit(%d) returned !ok, want ok.", -wAmt)
	case b != newAmt:
		t.Fatalf("a.Deposit(%d) = %d, want new balance = %d", -wAmt, b, newAmt)
	}
	t.Logf("Withdrawal of %d accepted from account 'a'", wAmt)
	if _, ok := z.Deposit(-1); ok {
		t.Fatal("z.Deposit(-1) returned ok, want !ok.")
	}

	// verify both balances
	switch b, ok := a.Balance(); {
	case !ok:
		t.Fatal("a.Balance() returned !ok, want ok.")
	case b != newAmt:
		t.Fatalf("a.Balance() = %d, want %d", b, newAmt)
	}
	switch b, ok := z.Balance(); {
	case !ok:
		t.Fatal("z.Balance() returned !ok, want ok.")
	case b != 0:
		t.Fatalf("z.Balance() = %d, want 0", b)
	}

	// close just z
	switch p, ok := z.Close(); {
	case !ok:
		t.Fatalf("z.Close() returned !ok, want ok.")
	case p != 0:
		t.Fatalf("z.Close() returned payout = %d, want 0.", p)
	}
	t.Log("Account 'z' closed.")

	// verify 'a' balance one more time
	switch b, ok := a.Balance(); {
	case !ok:
		t.Fatal("a.Balance() returned !ok, want ok.")
	case b != newAmt:
		t.Fatalf("a.Balance() = %d, want %d", b, newAmt)
	}
}

func TestConcClose(t *testing.T) {
	if runtime.NumCPU() < 2 {
		t.Skip("Multiple CPU cores required for concurrency tests.")
	}
	if runtime.GOMAXPROCS(0) < 2 {
		runtime.GOMAXPROCS(2)
	}

	// test competing close attempts
	for rep := 0; rep < 1000; rep++ {
		const openAmt = 10
		a := Open(openAmt)
		if a == nil {
			t.Fatalf("Open(%d) = nil, want non-nil *Account.", openAmt)
		}
		var start sync.WaitGroup
		start.Add(1)
		const closeAttempts = 10
		res := make(chan string)
		for i := 0; i < closeAttempts; i++ {
			go func() { // on your mark,
				start.Wait() // get set...
				switch p, ok := a.Close(); {
				case !ok:
					if p != 0 {
						t.Errorf("a.Close() = %d, %t.  "+
							"Want payout = 0 for unsuccessful close", p, ok)
						res <- "fail"
					} else {
						res <- "already closed"
					}
				case p != openAmt:
					t.Errorf("a.Close() = %d, %t.  "+
						"Want payout = %d for successful close", p, ok, openAmt)
					res <- "fail"
				default:
					res <- "close" // exactly one goroutine should reach here
				}
			}()
		}
		start.Done() // ...go
		var closes, fails int
		for i := 0; i < closeAttempts; i++ {
			switch <-res {
			case "close":
				closes++
			case "fail":
				fails++
			}
		}
		switch {
		case fails > 0:
			t.FailNow() // error already logged by other goroutine
		case closes == 0:
			t.Fatal("Concurrent a.Close() attempts all failed.  " +
				"Want one to succeed.")
		case closes > 1:
			t.Fatalf("%d concurrent a.Close() attempts succeeded, "+
				"each paying out %d!.  Want just one to succeed.",
				closes, openAmt)
		}
	}
}

func TestConcDeposit(t *testing.T) {
	if runtime.NumCPU() < 2 {
		t.Skip("Multiple CPU cores required for concurrency tests.")
	}
	if runtime.GOMAXPROCS(0) < 2 {
		runtime.GOMAXPROCS(2)
	}
	a := Open(0)
	if a == nil {
		t.Fatal("Open(0) = nil, want non-nil *Account.")
	}
	const amt = 10
	const c = 1000
	var negBal int32
	var start, g sync.WaitGroup
	start.Add(1)
	g.Add(3 * c)
	for i := 0; i < c; i++ {
		go func() { // deposit
			start.Wait()
			a.Deposit(amt) // ignore return values
			g.Done()
		}()
		go func() { // withdraw
			start.Wait()
			for {
				if _, ok := a.Deposit(-amt); ok {
					break
				}
				time.Sleep(time.Microsecond) // retry
			}
			g.Done()
		}()
		go func() { // watch that balance stays >= 0
			start.Wait()
			if p, _ := a.Balance(); p < 0 {
				atomic.StoreInt32(&negBal, 1)
			}
			g.Done()
		}()
	}
	start.Done()
	g.Wait()
	if negBal == 1 {
		t.Fatal("Balance went negative with concurrent deposits and " +
			"withdrawals.  Want balance always >= 0.")
	}
	if p, ok := a.Balance(); !ok || p != 0 {
		t.Fatalf("After equal concurrent deposits and withdrawals, "+
			"a.Balance = %d, %t.  Want 0, true", p, ok)
	}
}
