// Copyright (c) 2017 The btcsuite developers
// Copyright (c) 2018 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package blockchain

import (
	"fmt"
	"reflect"
	"testing"
)

// String returns the block node as a human-readable name.
func (node blockNode) String() string {
	return fmt.Sprintf("%s(%d)", node.hash, node.height)
}

// zipLocators is a convenience function that returns a single block locator
// given a variable number of them and is used in the tests.
func zipLocators(locators ...BlockLocator) BlockLocator {
	var hashes BlockLocator
	for _, locator := range locators {
		hashes = append(hashes, locator...)
	}
	return hashes
}

// TestChainView ensures all of the exported functionality of chain views works
// as intended with the exception of some special cases which are handled in
// other tests.
func TestChainView(t *testing.T) {
	// Construct a synthetic block index consisting of the following
	// structure.
	// 0 -> 1 -> 2  -> 3  -> 4
	//       \-> 2a -> 3a -> 4a  -> 5a -> 6a -> 7a -> ... -> 26a
	//             \-> 3a'-> 4a' -> 5a'
	branch0Nodes := chainedFakeNodes(nil, 5)
	branch1Nodes := chainedFakeNodes(branch0Nodes[1], 25)
	branch2Nodes := chainedFakeNodes(branch1Nodes[0], 3)

	tests := []struct {
		name       string
		view       *chainView   // active view
		genesis    *blockNode   // expected genesis block of active view
		tip        *blockNode   // expected tip of active view
		side       *chainView   // side chain view
		sideTip    *blockNode   // expected tip of side chain view
		fork       *blockNode   // expected fork node
		contains   []*blockNode // expected nodes in active view
		noContains []*blockNode // expected nodes NOT in active view
		equal      *chainView   // view expected equal to active view
		unequal    *chainView   // view expected NOT equal to active
		locator    BlockLocator // expected locator for active view tip
	}{
		{
			// Create a view for branch 0 as the active chain and
			// another view for branch 1 as the side chain.
			name:       "chain0-chain1",
			view:       newChainView(branchTip(branch0Nodes)),
			genesis:    branch0Nodes[0],
			tip:        branchTip(branch0Nodes),
			side:       newChainView(branchTip(branch1Nodes)),
			sideTip:    branchTip(branch1Nodes),
			fork:       branch0Nodes[1],
			contains:   branch0Nodes,
			noContains: branch1Nodes,
			equal:      newChainView(branchTip(branch0Nodes)),
			unequal:    newChainView(branchTip(branch1Nodes)),
			locator:    locatorHashes(branch0Nodes, 4, 3, 2, 1, 0),
		},
		{
			// Create a view for branch 1 as the active chain and
			// another view for branch 2 as the side chain.
			name:       "chain1-chain2",
			view:       newChainView(branchTip(branch1Nodes)),
			genesis:    branch0Nodes[0],
			tip:        branchTip(branch1Nodes),
			side:       newChainView(branchTip(branch2Nodes)),
			sideTip:    branchTip(branch2Nodes),
			fork:       branch1Nodes[0],
			contains:   branch1Nodes,
			noContains: branch2Nodes,
			equal:      newChainView(branchTip(branch1Nodes)),
			unequal:    newChainView(branchTip(branch2Nodes)),
			locator: zipLocators(
				locatorHashes(branch1Nodes, 24, 23, 22, 21, 20,
					19, 18, 17, 16, 15, 14, 13, 11, 7),
				locatorHashes(branch0Nodes, 1, 0)),
		},
		{
			// Create a view for branch 2 as the active chain and
			// another view for branch 0 as the side chain.
			name:       "chain2-chain0",
			view:       newChainView(branchTip(branch2Nodes)),
			genesis:    branch0Nodes[0],
			tip:        branchTip(branch2Nodes),
			side:       newChainView(branchTip(branch0Nodes)),
			sideTip:    branchTip(branch0Nodes),
			fork:       branch0Nodes[1],
			contains:   branch2Nodes,
			noContains: branch0Nodes[2:],
			equal:      newChainView(branchTip(branch2Nodes)),
			unequal:    newChainView(branchTip(branch0Nodes)),
			locator: zipLocators(
				locatorHashes(branch2Nodes, 2, 1, 0),
				locatorHashes(branch1Nodes, 0),
				locatorHashes(branch0Nodes, 1, 0)),
		},
	}
testLoop:
	for _, test := range tests {
		// Ensure the active and side chain heights are the expected
		// values.
		if test.view.Height() != test.tip.height {
			t.Errorf("%s: unexpected active view height -- got "+
				"%d, want %d", test.name, test.view.Height(),
				test.tip.height)
			continue
		}
		if test.side.Height() != test.sideTip.height {
			t.Errorf("%s: unexpected side view height -- got %d, "+
				"want %d", test.name, test.side.Height(),
				test.sideTip.height)
			continue
		}

		// Ensure the active and side chain genesis block is the
		// expected value.
		if test.view.Genesis() != test.genesis {
			t.Errorf("%s: unexpected active view genesis -- got "+
				"%v, want %v", test.name, test.view.Genesis(),
				test.genesis)
			continue
		}
		if test.side.Genesis() != test.genesis {
			t.Errorf("%s: unexpected side view genesis -- got %v, "+
				"want %v", test.name, test.view.Genesis(),
				test.genesis)
			continue
		}

		// Ensure the active and side chain tips are the expected nodes.
		if test.view.Tip() != test.tip {
			t.Errorf("%s: unexpected active view tip -- got %v, "+
				"want %v", test.name, test.view.Tip(), test.tip)
			continue
		}
		if test.side.Tip() != test.sideTip {
			t.Errorf("%s: unexpected active view tip -- got %v, "+
				"want %v", test.name, test.side.Tip(),
				test.sideTip)
			continue
		}

		// Ensure that regardless of the order the two chains are
		// compared they both return the expected fork point.
		forkNode := test.view.FindFork(test.side.Tip())
		if forkNode != test.fork {
			t.Errorf("%s: unexpected fork node (view, side) -- "+
				"got %v, want %v", test.name, forkNode,
				test.fork)
			continue
		}
		forkNode = test.side.FindFork(test.view.Tip())
		if forkNode != test.fork {
			t.Errorf("%s: unexpected fork node (side, view) -- "+
				"got %v, want %v", test.name, forkNode,
				test.fork)
			continue
		}

		// Ensure that the fork point for a node that is already part
		// of the chain view is the node itself.
		forkNode = test.view.FindFork(test.view.Tip())
		if forkNode != test.view.Tip() {
			t.Errorf("%s: unexpected fork node (view, tip) -- "+
				"got %v, want %v", test.name, forkNode,
				test.view.Tip())
			continue
		}

		// Ensure all expected nodes are contained in the active view.
		for _, node := range test.contains {
			if !test.view.Contains(node) {
				t.Errorf("%s: expected %v in active view",
					test.name, node)
				continue testLoop
			}
		}

		// Ensure all nodes from side chain view are NOT contained in
		// the active view.
		for _, node := range test.noContains {
			if test.view.Contains(node) {
				t.Errorf("%s: unexpected %v in active view",
					test.name, node)
				continue testLoop
			}
		}

		// Ensure equality of different views into the same chain works
		// as intended.
		if !test.view.Equals(test.equal) {
			t.Errorf("%s: unexpected unequal views", test.name)
			continue
		}
		if test.view.Equals(test.unequal) {
			t.Errorf("%s: unexpected equal views", test.name)
			continue
		}

		// Ensure all nodes contained in the view return the expected
		// next node.
		for i, node := range test.contains {
			// Final node expects nil for the next node.
			var expected *blockNode
			if i < len(test.contains)-1 {
				expected = test.contains[i+1]
			}
			if next := test.view.Next(node); next != expected {
				t.Errorf("%s: unexpected next node -- got %v, "+
					"want %v", test.name, next, expected)
				continue testLoop
			}
		}

		// Ensure nodes that are not contained in the view do not
		// produce a successor node.
		for _, node := range test.noContains {
			if next := test.view.Next(node); next != nil {
				t.Errorf("%s: unexpected next node -- got %v, "+
					"want nil", test.name, next)
				continue testLoop
			}
		}

		// Ensure all nodes contained in the view can be retrieved by
		// height.
		for _, wantNode := range test.contains {
			node := test.view.NodeByHeight(wantNode.height)
			if node != wantNode {
				t.Errorf("%s: unexpected node for height %d -- "+
					"got %v, want %v", test.name,
					wantNode.height, node, wantNode)
				continue testLoop
			}
		}

		// Ensure the block locator for the tip of the active view
		// consists of the expected hashes.
		locator := test.view.BlockLocator(test.view.tip())
		if !reflect.DeepEqual(locator, test.locator) {
			t.Errorf("%s: unexpected locator -- got %v, want %v",
				test.name, locator, test.locator)
			continue
		}
	}
}

// TestChainViewForkCorners ensures that finding the fork between two chains
// works in some corner cases such as when the two chains have completely
// unrelated histories.
func TestChainViewForkCorners(t *testing.T) {
	// Construct two unrelated single branch synthetic block indexes.
	branchNodes := chainedFakeNodes(nil, 5)
	unrelatedBranchNodes := chainedFakeNodes(nil, 7)

	// Create chain views for the two unrelated histories.
	view1 := newChainView(branchTip(branchNodes))
	view2 := newChainView(branchTip(unrelatedBranchNodes))

	// Ensure attempting to find a fork point with a node that doesn't exist
	// doesn't produce a node.
	if fork := view1.FindFork(nil); fork != nil {
		t.Fatalf("FindFork: unexpected fork -- got %v, want nil", fork)
	}

	// Ensure attempting to find a fork point in two chain views with
	// totally unrelated histories doesn't produce a node.
	for _, node := range branchNodes {
		if fork := view2.FindFork(node); fork != nil {
			t.Fatalf("FindFork: unexpected fork -- got %v, want nil",
				fork)
		}
	}
	for _, node := range unrelatedBranchNodes {
		if fork := view1.FindFork(node); fork != nil {
			t.Fatalf("FindFork: unexpected fork -- got %v, want nil",
				fork)
		}
	}
}

// TestChainViewSetTip ensures changing the tip works as intended including
// capacity changes.
func TestChainViewSetTip(t *testing.T) {
	// Construct a synthetic block index consisting of the following
	// structure.
	// 0 -> 1 -> 2  -> 3  -> 4
	//       \-> 2a -> 3a -> 4a  -> 5a -> 6a -> 7a -> ... -> 26a
	branch0Nodes := chainedFakeNodes(nil, 5)
	branch1Nodes := chainedFakeNodes(branch0Nodes[1], 25)

	tests := []struct {
		name     string
		view     *chainView     // active view
		tips     []*blockNode   // tips to set
		contains [][]*blockNode // expected nodes in view for each tip
	}{
		{
			// Create an empty view and set the tip to increasingly
			// longer chains.
			name: "increasing",
			view: newChainView(nil),
			tips: []*blockNode{branchTip(branch0Nodes),
				branchTip(branch1Nodes)},
			contains: [][]*blockNode{branch0Nodes, branch1Nodes},
		},
		{
			// Create a view with a longer chain and set the tip to
			// increasingly shorter chains.
			name:     "decreasing",
			view:     newChainView(branchTip(branch1Nodes)),
			tips:     []*blockNode{branchTip(branch0Nodes), nil},
			contains: [][]*blockNode{branch0Nodes, nil},
		},
		{
			// Create a view with a shorter chain and set the tip to
			// a longer chain followed by setting it back to the
			// shorter chain.
			name: "small-large-small",
			view: newChainView(branchTip(branch0Nodes)),
			tips: []*blockNode{branchTip(branch1Nodes),
				branchTip(branch0Nodes)},
			contains: [][]*blockNode{branch1Nodes, branch0Nodes},
		},
		{
			// Create a view with a longer chain and set the tip to
			// a smaller chain followed by setting it back to the
			// longer chain.
			name: "large-small-large",
			view: newChainView(branchTip(branch1Nodes)),
			tips: []*blockNode{branchTip(branch0Nodes),
				branchTip(branch1Nodes)},
			contains: [][]*blockNode{branch0Nodes, branch1Nodes},
		},
	}

testLoop:
	for _, test := range tests {
		for i, tip := range test.tips {
			// Ensure the view tip is the expected node.
			test.view.SetTip(tip)
			if test.view.Tip() != tip {
				t.Errorf("%s: unexpected view tip -- got %v, "+
					"want %v", test.name, test.view.Tip(),
					tip)
				continue testLoop
			}

			// Ensure all expected nodes are contained in the view.
			for _, node := range test.contains[i] {
				if !test.view.Contains(node) {
					t.Errorf("%s: expected %v in active view",
						test.name, node)
					continue testLoop
				}
			}
		}
	}
}

// TestChainViewNil ensures that creating and accessing a nil chain view behaves
// as expected.
func TestChainViewNil(t *testing.T) {
	// Ensure two uninitialized views are considered equal.
	view := newChainView(nil)
	if !view.Equals(newChainView(nil)) {
		t.Fatal("uninitialized nil views unequal")
	}

	// Ensure the genesis of an uninitialized view does not produce a node.
	if genesis := view.Genesis(); genesis != nil {
		t.Fatalf("Genesis: unexpected genesis -- got %v, want nil",
			genesis)
	}

	// Ensure the tip of an uninitialized view does not produce a node.
	if tip := view.Tip(); tip != nil {
		t.Fatalf("Tip: unexpected tip -- got %v, want nil", tip)
	}

	// Ensure the height of an uninitialized view is the expected value.
	if height := view.Height(); height != -1 {
		t.Fatalf("Height: unexpected height -- got %d, want -1", height)
	}

	// Ensure attempting to get a node for a height that does not exist does
	// not produce a node.
	if node := view.NodeByHeight(10); node != nil {
		t.Fatalf("NodeByHeight: unexpected node -- got %v, want nil", node)
	}

	// Ensure an uninitialized view does not report it contains nodes.
	fakeNode := chainedFakeNodes(nil, 1)[0]
	if view.Contains(fakeNode) {
		t.Fatalf("Contains: view claims it contains node %v", fakeNode)
	}

	// Ensure the next node for a node that does not exist does not produce
	// a node.
	if next := view.Next(nil); next != nil {
		t.Fatalf("Next: unexpected next node -- got %v, want nil", next)
	}

	// Ensure the next node for a node that exists does not produce a node.
	if next := view.Next(fakeNode); next != nil {
		t.Fatalf("Next: unexpected next node -- got %v, want nil", next)
	}

	// Ensure attempting to find a fork point with a node that doesn't exist
	// doesn't produce a node.
	if fork := view.FindFork(nil); fork != nil {
		t.Fatalf("FindFork: unexpected fork -- got %v, want nil", fork)
	}

	// Ensure attempting to get a block locator for the tip doesn't produce
	// one since the tip is nil.
	if locator := view.BlockLocator(nil); locator != nil {
		t.Fatalf("BlockLocator: unexpected locator -- got %v, want nil",
			locator)
	}

	// Ensure attempting to get a block locator for a node that exists still
	// works as intended.
	branchNodes := chainedFakeNodes(nil, 50)
	wantLocator := locatorHashes(branchNodes, 49, 48, 47, 46, 45, 44, 43,
		42, 41, 40, 39, 38, 36, 32, 24, 8, 0)
	locator := view.BlockLocator(branchTip(branchNodes))
	if !reflect.DeepEqual(locator, wantLocator) {
		t.Fatalf("BlockLocator: unexpected locator -- got %v, want %v",
			locator, wantLocator)
	}
}
