package rpctest

import (
	"bytes"
	crand "crypto/rand"
	"fmt"
	"math/rand"
	"testing"
	"time"

	. "github.com/tendermint/go-common"
	"github.com/tendermint/go-wire"
	ctypes "github.com/tendermint/tendermint/rpc/core/types"
	"github.com/tendermint/tendermint/types"
	"github.com/tendermint/abci/example/dummy"
	abci "github.com/tendermint/abci/types"
)

//--------------------------------------------------------------------------------
// Test the HTTP client
// These tests assume the dummy app
//--------------------------------------------------------------------------------

//--------------------------------------------------------------------------------
// status

func TestURIStatus(t *testing.T) {
	tmResult := new(ctypes.TMResult)
	_, err := clientURI.Call("status", map[string]interface{}{}, tmResult)
	if err != nil {
		panic(err)
	}
	testStatus(t, tmResult)
}

func TestJSONStatus(t *testing.T) {
	tmResult := new(ctypes.TMResult)
	_, err := clientJSON.Call("status", []interface{}{}, tmResult)
	if err != nil {
		panic(err)
	}
	testStatus(t, tmResult)
}

func testStatus(t *testing.T, statusI interface{}) {
	tmRes := statusI.(*ctypes.TMResult)
	status := (*tmRes).(*ctypes.ResultStatus)
	if status.NodeInfo.Network != chainID {
		panic(Fmt("ChainID mismatch: got %s expected %s",
			status.NodeInfo.Network, chainID))
	}
}

//--------------------------------------------------------------------------------
// broadcast tx sync

// random bytes (excluding byte('='))
func randBytes() []byte {
	n := rand.Intn(10) + 2
	buf := make([]byte, n)
	_, err := crand.Read(buf)
	if err != nil {
		panic(err)
	}
	return bytes.Replace(buf, []byte("="), []byte{100}, -1)
}

func TestURIBroadcastTxSync(t *testing.T) {
	config.Set("block_size", 0)
	defer config.Set("block_size", -1)
	tmResult := new(ctypes.TMResult)
	tx := randBytes()
	_, err := clientURI.Call("broadcast_tx_sync", map[string]interface{}{"tx": tx}, tmResult)
	if err != nil {
		panic(err)
	}
	testBroadcastTxSync(t, tmResult, tx)
}

func TestJSONBroadcastTxSync(t *testing.T) {
	config.Set("block_size", 0)
	defer config.Set("block_size", -1)
	tmResult := new(ctypes.TMResult)
	tx := randBytes()
	_, err := clientJSON.Call("broadcast_tx_sync", []interface{}{tx}, tmResult)
	if err != nil {
		panic(err)
	}
	testBroadcastTxSync(t, tmResult, tx)
}

func testBroadcastTxSync(t *testing.T, resI interface{}, tx []byte) {
	tmRes := resI.(*ctypes.TMResult)
	res := (*tmRes).(*ctypes.ResultBroadcastTx)
	if res.Code != abci.CodeType_OK {
		panic(Fmt("BroadcastTxSync got non-zero exit code: %v. %X; %s", res.Code, res.Data, res.Log))
	}
	mem := node.MempoolReactor().Mempool
	if mem.Size() != 1 {
		panic(Fmt("Mempool size should have been 1. Got %d", mem.Size()))
	}

	txs := mem.Reap(1)
	if !bytes.Equal(txs[0], tx) {
		panic(Fmt("Tx in mempool does not match test tx. Got %X, expected %X", txs[0], tx))
	}

	mem.Flush()
}

//--------------------------------------------------------------------------------
// query

func testTxKV() ([]byte, []byte, []byte) {
	k := randBytes()
	v := randBytes()
	return k, v, []byte(Fmt("%s=%s", k, v))
}

func sendTx() ([]byte, []byte) {
	tmResult := new(ctypes.TMResult)
	k, v, tx := testTxKV()
	_, err := clientJSON.Call("broadcast_tx_commit", []interface{}{tx}, tmResult)
	if err != nil {
		panic(err)
	}
	fmt.Println("SENT TX", tx)
	fmt.Printf("SENT TX %X\n", tx)
	fmt.Printf("k %X; v %X", k, v)
	return k, v
}

func TestURIABCIQuery(t *testing.T) {
	k, v := sendTx()
	time.Sleep(time.Second)
	tmResult := new(ctypes.TMResult)
	_, err := clientURI.Call("abci_query", map[string]interface{}{"query": k}, tmResult)
	if err != nil {
		panic(err)
	}
	testABCIQuery(t, tmResult, v)
}

func TestJSONABCIQuery(t *testing.T) {
	k, v := sendTx()
	tmResult := new(ctypes.TMResult)
	_, err := clientJSON.Call("abci_query", []interface{}{k}, tmResult)
	if err != nil {
		panic(err)
	}
	testABCIQuery(t, tmResult, v)
}

func testABCIQuery(t *testing.T, statusI interface{}, value []byte) {
	tmRes := statusI.(*ctypes.TMResult)
	query := (*tmRes).(*ctypes.ResultABCIQuery)
	if query.Result.IsErr() {
		panic(Fmt("Query returned an err: %v", query))
	}

	qResult := new(dummy.QueryResult)
	if err := wire.ReadJSONBytes(query.Result.Data, qResult); err != nil {
		t.Fatal(err)
	}
	// XXX: specific to value returned by the dummy
	if qResult.Exists != true {
		panic(Fmt("Query error. Expected to find 'exists=true'. Got: %v", qResult))
	}
}

//--------------------------------------------------------------------------------
// broadcast tx commit

func TestURIBroadcastTxCommit(t *testing.T) {
	tmResult := new(ctypes.TMResult)
	tx := randBytes()
	_, err := clientURI.Call("broadcast_tx_commit", map[string]interface{}{"tx": tx}, tmResult)
	if err != nil {
		panic(err)
	}
	testBroadcastTxCommit(t, tmResult, tx)
}

func TestJSONBroadcastTxCommit(t *testing.T) {
	tmResult := new(ctypes.TMResult)
	tx := randBytes()
	_, err := clientJSON.Call("broadcast_tx_commit", []interface{}{tx}, tmResult)
	if err != nil {
		panic(err)
	}
	testBroadcastTxCommit(t, tmResult, tx)
}

func testBroadcastTxCommit(t *testing.T, resI interface{}, tx []byte) {
	tmRes := resI.(*ctypes.TMResult)
	res := (*tmRes).(*ctypes.ResultBroadcastTxCommit)
	checkTx := res.CheckTx
	if checkTx.Code != abci.CodeType_OK {
		panic(Fmt("BroadcastTxCommit got non-zero exit code from CheckTx: %v. %X; %s", checkTx.Code, checkTx.Data, checkTx.Log))
	}
	deliverTx := res.DeliverTx
	if deliverTx.Code != abci.CodeType_OK {
		panic(Fmt("BroadcastTxCommit got non-zero exit code from CheckTx: %v. %X; %s", deliverTx.Code, deliverTx.Data, deliverTx.Log))
	}
	mem := node.MempoolReactor().Mempool
	if mem.Size() != 0 {
		panic(Fmt("Mempool size should have been 0. Got %d", mem.Size()))
	}

	// TODO: find tx in block
}

//--------------------------------------------------------------------------------
// Test the websocket service

var wsTyp = "JSONRPC"

// make a simple connection to the server
func TestWSConnect(t *testing.T) {
	wsc := newWSClient(t)
	wsc.Stop()
}

// receive a new block message
func TestWSNewBlock(t *testing.T) {
	wsc := newWSClient(t)
	eid := types.EventStringNewBlock()
	subscribe(t, wsc, eid)
	defer func() {
		unsubscribe(t, wsc, eid)
		wsc.Stop()
	}()
	waitForEvent(t, wsc, eid, true, func() {}, func(eid string, b interface{}) error {
		fmt.Println("Check:", b)
		return nil
	})
}

// receive a few new block messages in a row, with increasing height
func TestWSBlockchainGrowth(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping test in short mode.")
	}
	wsc := newWSClient(t)
	eid := types.EventStringNewBlock()
	subscribe(t, wsc, eid)
	defer func() {
		unsubscribe(t, wsc, eid)
		wsc.Stop()
	}()

	// listen for NewBlock, ensure height increases by 1

	var initBlockN int
	for i := 0; i < 3; i++ {
		waitForEvent(t, wsc, eid, true, func() {}, func(eid string, eventData interface{}) error {
			block := eventData.(types.EventDataNewBlock).Block
			if i == 0 {
				initBlockN = block.Header.Height
			} else {
				if block.Header.Height != initBlockN+i {
					return fmt.Errorf("Expected block %d, got block %d", initBlockN+i, block.Header.Height)
				}
			}

			return nil
		})
	}
}

func TestWSTxEvent(t *testing.T) {
	wsc := newWSClient(t)
	tx := randBytes()

	// listen for the tx I am about to submit
	eid := types.EventStringTx(types.Tx(tx))
	subscribe(t, wsc, eid)
	defer func() {
		unsubscribe(t, wsc, eid)
		wsc.Stop()
	}()

	// send an tx
	tmResult := new(ctypes.TMResult)
	_, err := clientJSON.Call("broadcast_tx_sync", []interface{}{tx}, tmResult)
	if err != nil {
		t.Fatal("Error submitting event")
	}

	waitForEvent(t, wsc, eid, true, func() {}, func(eid string, b interface{}) error {
		evt, ok := b.(types.EventDataTx)
		if !ok {
			t.Fatal("Got wrong event type", b)
		}
		if bytes.Compare([]byte(evt.Tx), tx) != 0 {
			t.Error("Event returned different tx")
		}
		if evt.Code != abci.CodeType_OK {
			t.Error("Event returned tx error code", evt.Code)
		}
		return nil
	})
}

/* TODO: this with dummy app..
func TestWSDoubleFire(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping test in short mode.")
	}
	con := newWSCon(t)
	eid := types.EventStringAccInput(user[0].Address)
	subscribe(t, con, eid)
	defer func() {
		unsubscribe(t, con, eid)
		con.Close()
	}()
	amt := int64(100)
	toAddr := user[1].Address
	// broadcast the transaction, wait to hear about it
	waitForEvent(t, con, eid, true, func() {
		tx := makeDefaultSendTxSigned(t, wsTyp, toAddr, amt)
		broadcastTx(t, wsTyp, tx)
	}, func(eid string, b []byte) error {
		return nil
	})
	// but make sure we don't hear about it twice
	waitForEvent(t, con, eid, false, func() {
	}, func(eid string, b []byte) error {
		return nil
	})
}*/

//--------------------------------------------------------------------------------
// unsafe_set_config

var stringVal = "my string"
var intVal = 987654321
var boolVal = true

// don't change these
var testCasesUnsafeSetConfig = [][]string{
	[]string{"string", "key1", stringVal},
	[]string{"int", "key2", fmt.Sprintf("%v", intVal)},
	[]string{"bool", "key3", fmt.Sprintf("%v", boolVal)},
}

func TestURIUnsafeSetConfig(t *testing.T) {
	for _, testCase := range testCasesUnsafeSetConfig {
		tmResult := new(ctypes.TMResult)
		_, err := clientURI.Call("unsafe_set_config", map[string]interface{}{
			"type":  testCase[0],
			"key":   testCase[1],
			"value": testCase[2],
		}, tmResult)
		if err != nil {
			panic(err)
		}
	}
	testUnsafeSetConfig(t)
}

func TestJSONUnsafeSetConfig(t *testing.T) {
	for _, testCase := range testCasesUnsafeSetConfig {
		tmResult := new(ctypes.TMResult)
		_, err := clientJSON.Call("unsafe_set_config", []interface{}{testCase[0], testCase[1], testCase[2]}, tmResult)
		if err != nil {
			panic(err)
		}
	}
	testUnsafeSetConfig(t)
}

func testUnsafeSetConfig(t *testing.T) {
	s := config.GetString("key1")
	if s != stringVal {
		panic(Fmt("got %v, expected %v", s, stringVal))
	}

	i := config.GetInt("key2")
	if i != intVal {
		panic(Fmt("got %v, expected %v", i, intVal))
	}

	b := config.GetBool("key3")
	if b != boolVal {
		panic(Fmt("got %v, expected %v", b, boolVal))
	}
}
