package chainntnfs_test

import (
	"testing"

	"github.com/lightningnetwork/lnd/chainntnfs"
	"github.com/roasbeef/btcd/chaincfg/chainhash"
	"github.com/roasbeef/btcd/wire"
	"github.com/roasbeef/btcutil"
)

var zeroHash chainhash.Hash

// TestTxConfFutureDispatch tests that the TxConfNotifier dispatches
// registered notifications when the transaction confirms after registration.
func TestTxConfFutureDispatch(t *testing.T) {
	t.Parallel()

	const (
		tx1NumConfs uint32 = 1
		tx2NumConfs uint32 = 2
	)

	var (
		tx1 = wire.MsgTx{Version: 1}
		tx2 = wire.MsgTx{Version: 2}
		tx3 = wire.MsgTx{Version: 3}
	)

	txConfNotifier := chainntnfs.NewTxConfNotifier(10, 100)

	// Create the test transactions and register them with the
	// TxConfNotifier before including them in a block to receive future
	// notifications.
	tx1Hash := tx1.TxHash()
	ntfn1 := chainntnfs.ConfNtfn{
		TxID:             &tx1Hash,
		NumConfirmations: tx1NumConfs,
		Event:            chainntnfs.NewConfirmationEvent(tx1NumConfs),
	}
	txConfNotifier.Register(&ntfn1, nil)

	tx2Hash := tx2.TxHash()
	ntfn2 := chainntnfs.ConfNtfn{
		TxID:             &tx2Hash,
		NumConfirmations: tx2NumConfs,
		Event:            chainntnfs.NewConfirmationEvent(tx2NumConfs),
	}
	txConfNotifier.Register(&ntfn2, nil)

	// We should not receive any notifications from both transactions
	// since they have not been included in a block yet.
	select {
	case <-ntfn1.Event.Updates:
		t.Fatal("Received unexpected confirmation update for tx1")
	case txConf := <-ntfn1.Event.Confirmed:
		t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
	default:
	}

	select {
	case <-ntfn2.Event.Updates:
		t.Fatal("Received unexpected confirmation update for tx2")
	case txConf := <-ntfn2.Event.Confirmed:
		t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
	default:
	}

	// Include the transactions in a block and add it to the TxConfNotifier.
	// This should confirm tx1, but not tx2.
	block1 := btcutil.NewBlock(&wire.MsgBlock{
		Transactions: []*wire.MsgTx{&tx1, &tx2, &tx3},
	})

	err := txConfNotifier.ConnectTip(
		block1.Hash(), 11, block1.Transactions(),
	)
	if err != nil {
		t.Fatalf("Failed to connect block: %v", err)
	}

	// We should only receive one update for tx1 since it only requires
	// one confirmation and it already met it.
	select {
	case numConfsLeft := <-ntfn1.Event.Updates:
		const expected = 0
		if numConfsLeft != expected {
			t.Fatalf("Received incorrect confirmation update: tx1 "+
				"expected %d confirmations left, got %d",
				expected, numConfsLeft)
		}
	default:
		t.Fatal("Expected confirmation update for tx1")
	}

	// A confirmation notification for this tranaction should be dispatched,
	// as it only required one confirmation.
	select {
	case txConf := <-ntfn1.Event.Confirmed:
		expectedConf := chainntnfs.TxConfirmation{
			BlockHash:   block1.Hash(),
			BlockHeight: 11,
			TxIndex:     0,
		}
		assertEqualTxConf(t, txConf, &expectedConf)
	default:
		t.Fatalf("Expected confirmation for tx1")
	}

	// We should only receive one update for tx2 since it only has one
	// confirmation so far and it requires two.
	select {
	case numConfsLeft := <-ntfn2.Event.Updates:
		const expected = 1
		if numConfsLeft != expected {
			t.Fatalf("Received incorrect confirmation update: tx2 "+
				"expected %d confirmations left, got %d",
				expected, numConfsLeft)
		}
	default:
		t.Fatal("Expected confirmation update for tx2")
	}

	// A confirmation notification for tx2 should not be dispatched yet, as
	// it requires one more confirmation.
	select {
	case txConf := <-ntfn2.Event.Confirmed:
		t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
	default:
	}

	// Create a new block and add it to the TxConfNotifier at the next
	// height. This should confirm tx2.
	block2 := btcutil.NewBlock(&wire.MsgBlock{
		Transactions: []*wire.MsgTx{&tx3},
	})

	err = txConfNotifier.ConnectTip(block2.Hash(), 12, block2.Transactions())
	if err != nil {
		t.Fatalf("Failed to connect block: %v", err)
	}

	// We should not receive any event notifications for tx1 since it has
	// already been confirmed.
	select {
	case <-ntfn1.Event.Updates:
		t.Fatal("Received unexpected confirmation update for tx1")
	case txConf := <-ntfn1.Event.Confirmed:
		t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
	default:
	}

	// We should only receive one update since the last at the new height,
	// indicating how many confirmations are still left.
	select {
	case numConfsLeft := <-ntfn2.Event.Updates:
		const expected = 0
		if numConfsLeft != expected {
			t.Fatalf("Received incorrect confirmation update: tx2 "+
				"expected %d confirmations left, got %d",
				expected, numConfsLeft)
		}
	default:
		t.Fatal("Expected confirmation update for tx2")
	}

	// A confirmation notification for tx2 should be dispatched, since it
	// now meets its required number of confirmations.
	select {
	case txConf := <-ntfn2.Event.Confirmed:
		expectedConf := chainntnfs.TxConfirmation{
			BlockHash:   block1.Hash(),
			BlockHeight: 11,
			TxIndex:     1,
		}
		assertEqualTxConf(t, txConf, &expectedConf)
	default:
		t.Fatalf("Expected confirmation for tx2")
	}
}

// TestTxConfHistoricalDispatch tests that the TxConfNotifier dispatches
// registered notifications when the transaction is confirmed before
// registration.
func TestTxConfHistoricalDispatch(t *testing.T) {
	t.Parallel()

	const (
		tx1NumConfs uint32 = 1
		tx2NumConfs uint32 = 3
	)

	var (
		tx1 = wire.MsgTx{Version: 1}
		tx2 = wire.MsgTx{Version: 2}
		tx3 = wire.MsgTx{Version: 3}
	)

	txConfNotifier := chainntnfs.NewTxConfNotifier(10, 100)

	// Create the test transactions at a height before the TxConfNotifier's
	// starting height so that they are confirmed once registering them.
	tx1Hash := tx1.TxHash()
	ntfn1 := chainntnfs.ConfNtfn{
		TxID:             &tx1Hash,
		NumConfirmations: tx1NumConfs,
		Event:            chainntnfs.NewConfirmationEvent(tx1NumConfs),
	}
	txConf1 := chainntnfs.TxConfirmation{
		BlockHash:   &zeroHash,
		BlockHeight: 9,
		TxIndex:     1,
	}
	txConfNotifier.Register(&ntfn1, &txConf1)

	tx2Hash := tx2.TxHash()
	txConf2 := chainntnfs.TxConfirmation{
		BlockHash:   &zeroHash,
		BlockHeight: 9,
		TxIndex:     2,
	}
	ntfn2 := chainntnfs.ConfNtfn{
		TxID:             &tx2Hash,
		NumConfirmations: tx2NumConfs,
		Event:            chainntnfs.NewConfirmationEvent(tx2NumConfs),
	}
	txConfNotifier.Register(&ntfn2, &txConf2)

	// We should only receive one update for tx1 since it only requires
	// one confirmation and it already met it.
	select {
	case numConfsLeft := <-ntfn1.Event.Updates:
		const expected = 0
		if numConfsLeft != expected {
			t.Fatalf("Received incorrect confirmation update: tx1 "+
				"expected %d confirmations left, got %d",
				expected, numConfsLeft)
		}
	default:
		t.Fatal("Expected confirmation update for tx1")
	}

	// A confirmation notification for tx1 should be dispatched, as it met
	// its required number of confirmations.
	select {
	case txConf := <-ntfn1.Event.Confirmed:
		assertEqualTxConf(t, txConf, &txConf1)
	default:
		t.Fatalf("Expected confirmation for tx1")
	}

	// We should only receive one update indicating how many confirmations
	// are left for the transaction to be confirmed.
	select {
	case numConfsLeft := <-ntfn2.Event.Updates:
		const expected = 1
		if numConfsLeft != expected {
			t.Fatalf("Received incorrect confirmation update: tx2 "+
				"expected %d confirmations left, got %d",
				expected, numConfsLeft)
		}
	default:
		t.Fatal("Expected confirmation update for tx2")
	}

	// A confirmation notification for tx2 should not be dispatched yet, as
	// it requires one more confirmation.
	select {
	case txConf := <-ntfn2.Event.Confirmed:
		t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
	default:
	}

	// Create a new block and add it to the TxConfNotifier at the next
	// height. This should confirm tx2.
	block := btcutil.NewBlock(&wire.MsgBlock{
		Transactions: []*wire.MsgTx{&tx3},
	})

	err := txConfNotifier.ConnectTip(block.Hash(), 11, block.Transactions())
	if err != nil {
		t.Fatalf("Failed to connect block: %v", err)
	}

	// We should not receive any event notifications for tx1 since it has
	// already been confirmed.
	select {
	case <-ntfn1.Event.Updates:
		t.Fatal("Received unexpected confirmation update for tx1")
	case txConf := <-ntfn1.Event.Confirmed:
		t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
	default:
	}

	// We should only receive one update for tx2 since the last one,
	// indicating how many confirmations are still left.
	select {
	case numConfsLeft := <-ntfn2.Event.Updates:
		const expected = 0
		if numConfsLeft != expected {
			t.Fatalf("Received incorrect confirmation update: tx2 "+
				"expected %d confirmations left, got %d",
				expected, numConfsLeft)
		}
	default:
		t.Fatal("Expected confirmation update for tx2")
	}

	// A confirmation notification for tx2 should be dispatched, as it met
	// its required number of confirmations.
	select {
	case txConf := <-ntfn2.Event.Confirmed:
		assertEqualTxConf(t, txConf, &txConf2)
	default:
		t.Fatalf("Expected confirmation for tx2")
	}
}

// TestTxConfChainReorg tests that TxConfNotifier dispatches Confirmed and
// NegativeConf notifications appropriately when there is a chain
// reorganization.
func TestTxConfChainReorg(t *testing.T) {
	t.Parallel()

	const (
		tx1NumConfs uint32 = 2
		tx2NumConfs uint32 = 1
		tx3NumConfs uint32 = 2
	)

	var (
		tx1 = wire.MsgTx{Version: 1}
		tx2 = wire.MsgTx{Version: 2}
		tx3 = wire.MsgTx{Version: 3}
	)

	txConfNotifier := chainntnfs.NewTxConfNotifier(7, 100)

	// Tx 1 will be confirmed in block 9 and requires 2 confs.
	tx1Hash := tx1.TxHash()
	ntfn1 := chainntnfs.ConfNtfn{
		TxID:             &tx1Hash,
		NumConfirmations: tx1NumConfs,
		Event:            chainntnfs.NewConfirmationEvent(tx1NumConfs),
	}
	txConfNotifier.Register(&ntfn1, nil)

	// Tx 2 will be confirmed in block 10 and requires 1 conf.
	tx2Hash := tx2.TxHash()
	ntfn2 := chainntnfs.ConfNtfn{
		TxID:             &tx2Hash,
		NumConfirmations: tx2NumConfs,
		Event:            chainntnfs.NewConfirmationEvent(tx2NumConfs),
	}
	txConfNotifier.Register(&ntfn2, nil)

	// Tx 3 will be confirmed in block 10 and requires 2 confs.
	tx3Hash := tx3.TxHash()
	ntfn3 := chainntnfs.ConfNtfn{
		TxID:             &tx3Hash,
		NumConfirmations: tx3NumConfs,
		Event:            chainntnfs.NewConfirmationEvent(tx3NumConfs),
	}
	txConfNotifier.Register(&ntfn3, nil)

	// Sync chain to block 10. Txs 1 & 2 should be confirmed.
	block1 := btcutil.NewBlock(&wire.MsgBlock{
		Transactions: []*wire.MsgTx{&tx1},
	})
	err := txConfNotifier.ConnectTip(nil, 8, block1.Transactions())
	if err != nil {
		t.Fatalf("Failed to connect block: %v", err)
	}
	err = txConfNotifier.ConnectTip(nil, 9, nil)
	if err != nil {
		t.Fatalf("Failed to connect block: %v", err)
	}

	block2 := btcutil.NewBlock(&wire.MsgBlock{
		Transactions: []*wire.MsgTx{&tx2, &tx3},
	})
	err = txConfNotifier.ConnectTip(nil, 10, block2.Transactions())
	if err != nil {
		t.Fatalf("Failed to connect block: %v", err)
	}

	// We should receive two updates for tx1 since it requires two
	// confirmations and it has already met them.
	for i := 0; i < 2; i++ {
		select {
		case <-ntfn1.Event.Updates:
		default:
			t.Fatal("Expected confirmation update for tx1")
		}
	}

	// A confirmation notification for tx1 should be dispatched, as it met
	// its required number of confirmations.
	select {
	case <-ntfn1.Event.Confirmed:
	default:
		t.Fatalf("Expected confirmation for tx1")
	}

	// We should only receive one update for tx2 since it only requires
	// one confirmation and it already met it.
	select {
	case <-ntfn2.Event.Updates:
	default:
		t.Fatal("Expected confirmation update for tx2")
	}

	// A confirmation notification for tx2 should be dispatched, as it met
	// its required number of confirmations.
	select {
	case <-ntfn2.Event.Confirmed:
	default:
		t.Fatalf("Expected confirmation for tx2")
	}

	// We should only receive one update for tx3 since it only has one
	// confirmation so far and it requires two.
	select {
	case <-ntfn3.Event.Updates:
	default:
		t.Fatal("Expected confirmation update for tx3")
	}

	// A confirmation notification for tx3 should not be dispatched yet, as
	// it requires one more confirmation.
	select {
	case txConf := <-ntfn3.Event.Confirmed:
		t.Fatalf("Received unexpected confirmation for tx3: %v", txConf)
	default:
	}

	// The block that included tx2 and tx3 is disconnected and two next
	// blocks without them are connected.
	err = txConfNotifier.DisconnectTip(10)
	if err != nil {
		t.Fatalf("Failed to connect block: %v", err)
	}

	err = txConfNotifier.ConnectTip(nil, 10, nil)
	if err != nil {
		t.Fatalf("Failed to connect block: %v", err)
	}

	err = txConfNotifier.ConnectTip(nil, 11, nil)
	if err != nil {
		t.Fatalf("Failed to connect block: %v", err)
	}

	select {
	case reorgDepth := <-ntfn2.Event.NegativeConf:
		if reorgDepth != 1 {
			t.Fatalf("Incorrect value for negative conf notification: "+
				"expected %d, got %d", 1, reorgDepth)
		}
	default:
		t.Fatalf("Expected negative conf notification for tx1")
	}

	// We should not receive any event notifications from all of the
	// transactions because tx1 has already been confirmed and tx2 and tx3
	// have not been included in the chain since the reorg.
	select {
	case <-ntfn1.Event.Updates:
		t.Fatal("Received unexpected confirmation update for tx1")
	case txConf := <-ntfn1.Event.Confirmed:
		t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
	default:
	}

	select {
	case <-ntfn2.Event.Updates:
		t.Fatal("Received unexpected confirmation update for tx2")
	case txConf := <-ntfn2.Event.Confirmed:
		t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
	default:
	}

	select {
	case <-ntfn3.Event.Updates:
		t.Fatal("Received unexpected confirmation update for tx3")
	case txConf := <-ntfn3.Event.Confirmed:
		t.Fatalf("Received unexpected confirmation for tx3: %v", txConf)
	default:
	}

	// Now transactions 2 & 3 are re-included in a new block.
	block3 := btcutil.NewBlock(&wire.MsgBlock{
		Transactions: []*wire.MsgTx{&tx2, &tx3},
	})
	block4 := btcutil.NewBlock(&wire.MsgBlock{})

	err = txConfNotifier.ConnectTip(block3.Hash(), 12, block3.Transactions())
	if err != nil {
		t.Fatalf("Failed to connect block: %v", err)
	}

	err = txConfNotifier.ConnectTip(block4.Hash(), 13, block4.Transactions())
	if err != nil {
		t.Fatalf("Failed to connect block: %v", err)
	}

	// We should only receive one update for tx2 since it only requires
	// one confirmation and it already met it.
	select {
	case numConfsLeft := <-ntfn2.Event.Updates:
		const expected = 0
		if numConfsLeft != expected {
			t.Fatalf("Received incorrect confirmation update: tx2 "+
				"expected %d confirmations left, got %d",
				expected, numConfsLeft)
		}
	default:
		t.Fatal("Expected confirmation update for tx2")
	}

	// A confirmation notification for tx2 should be dispatched, as it met
	// its required number of confirmations.
	select {
	case txConf := <-ntfn2.Event.Confirmed:
		expectedConf := chainntnfs.TxConfirmation{
			BlockHash:   block3.Hash(),
			BlockHeight: 12,
			TxIndex:     0,
		}
		assertEqualTxConf(t, txConf, &expectedConf)
	default:
		t.Fatalf("Expected confirmation for tx2")
	}

	// We should receive two updates for tx3 since it requires two
	// confirmations and it has already met them.
	for i := uint32(1); i <= 2; i++ {
		select {
		case numConfsLeft := <-ntfn3.Event.Updates:
			expected := tx3NumConfs - i
			if numConfsLeft != expected {
				t.Fatalf("Received incorrect confirmation update: tx3 "+
					"expected %d confirmations left, got %d",
					expected, numConfsLeft)
			}
		default:
			t.Fatal("Expected confirmation update for tx2")
		}
	}

	// A confirmation notification for tx3 should be dispatched, as it met
	// its required number of confirmations.
	select {
	case txConf := <-ntfn3.Event.Confirmed:
		expectedConf := chainntnfs.TxConfirmation{
			BlockHash:   block3.Hash(),
			BlockHeight: 12,
			TxIndex:     1,
		}
		assertEqualTxConf(t, txConf, &expectedConf)
	default:
		t.Fatalf("Expected confirmation for tx3")
	}
}

func TestTxConfTearDown(t *testing.T) {
	t.Parallel()

	var (
		tx1 = wire.MsgTx{Version: 1}
		tx2 = wire.MsgTx{Version: 2}
	)

	txConfNotifier := chainntnfs.NewTxConfNotifier(10, 100)

	// Create the test transactions and register them with the
	// TxConfNotifier to receive notifications.
	tx1Hash := tx1.TxHash()
	ntfn1 := chainntnfs.ConfNtfn{
		TxID:             &tx1Hash,
		NumConfirmations: 1,
		Event:            chainntnfs.NewConfirmationEvent(1),
	}
	txConfNotifier.Register(&ntfn1, nil)

	tx2Hash := tx2.TxHash()
	ntfn2 := chainntnfs.ConfNtfn{
		TxID:             &tx2Hash,
		NumConfirmations: 2,
		Event:            chainntnfs.NewConfirmationEvent(2),
	}
	txConfNotifier.Register(&ntfn2, nil)

	// Include the transactions in a block and add it to the TxConfNotifier.
	// This should confirm tx1, but not tx2.
	block := btcutil.NewBlock(&wire.MsgBlock{
		Transactions: []*wire.MsgTx{&tx1, &tx2},
	})

	err := txConfNotifier.ConnectTip(block.Hash(), 11, block.Transactions())
	if err != nil {
		t.Fatalf("Failed to connect block: %v", err)
	}

	// We do not care about the correctness of the notifications since they
	// are tested in other methods, but we'll still attempt to retrieve them
	// for the sake of not being able to later once the notification
	// channels are closed.
	select {
	case <-ntfn1.Event.Updates:
	default:
		t.Fatal("Expected confirmation update for tx1")
	}

	select {
	case <-ntfn1.Event.Confirmed:
	default:
		t.Fatalf("Expected confirmation for tx1")
	}

	select {
	case <-ntfn2.Event.Updates:
	default:
		t.Fatal("Expected confirmation update for tx2")
	}

	select {
	case txConf := <-ntfn2.Event.Confirmed:
		t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
	default:
	}

	// The notification channels should be closed for notifications that
	// have not been dispatched yet, so we should not expect to receive any
	// more updates.
	txConfNotifier.TearDown()

	// tx1 should not receive any more updates because it has already been
	// confirmed and the TxConfNotifier has been shut down.
	select {
	case <-ntfn1.Event.Updates:
		t.Fatal("Received unexpected confirmation update for tx1")
	case txConf := <-ntfn1.Event.Confirmed:
		t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
	default:
	}

	// tx2 should not receive any more updates after the notifications
	// channels have been closed and the TxConfNotifier shut down.
	select {
	case _, more := <-ntfn2.Event.Updates:
		if more {
			t.Fatal("Expected closed Updates channel for tx2")
		}
	case _, more := <-ntfn2.Event.Confirmed:
		if more {
			t.Fatalf("Expected closed Confirmed channel for tx2")
		}
	default:
		t.Fatalf("Expected closed notification channels for tx2")
	}
}

func assertEqualTxConf(t *testing.T,
	actualConf, expectedConf *chainntnfs.TxConfirmation) {

	if actualConf.BlockHeight != expectedConf.BlockHeight {
		t.Fatalf("Incorrect block height in confirmation details: "+
			"expected %d, got %d",
			expectedConf.BlockHeight, actualConf.BlockHeight)
	}
	if !actualConf.BlockHash.IsEqual(expectedConf.BlockHash) {
		t.Fatalf("Incorrect block hash in confirmation details: "+
			"expected %d, got %d", expectedConf.BlockHash, actualConf.BlockHash)
	}
	if actualConf.TxIndex != expectedConf.TxIndex {
		t.Fatalf("Incorrect tx index in confirmation details: "+
			"expected %d, got %d", expectedConf.TxIndex, actualConf.TxIndex)
	}
}
