package routing

import (
	"io/ioutil"
	"os"
	"testing"
	"time"

	"github.com/lightningnetwork/lnd/channeldb/kvdb"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/routing/route"
)

// integratedRoutingContext defines the context in which integrated routing
// tests run.
type integratedRoutingContext struct {
	graph *mockGraph
	t     *testing.T

	source *mockNode
	target *mockNode

	amt         lnwire.MilliSatoshi
	finalExpiry int32

	mcCfg          MissionControlConfig
	pathFindingCfg PathFindingConfig
}

// newIntegratedRoutingContext instantiates a new integrated routing test
// context with a source and a target node.
func newIntegratedRoutingContext(t *testing.T) *integratedRoutingContext {
	// Instantiate a mock graph.
	source := newMockNode()
	target := newMockNode()

	graph := newMockGraph(t)
	graph.addNode(source)
	graph.addNode(target)
	graph.source = source

	// Initiate the test context with a set of default configuration values.
	// We don't use the lnd defaults here, because otherwise changing the
	// defaults would break the unit tests. The actual values picked aren't
	// critical to excite certain behavior, but do need to be aligned with
	// the test case assertions.
	ctx := integratedRoutingContext{
		t:           t,
		graph:       graph,
		amt:         100000,
		finalExpiry: 40,

		mcCfg: MissionControlConfig{
			PenaltyHalfLife:       30 * time.Minute,
			AprioriHopProbability: 0.6,
			AprioriWeight:         0.5,
			SelfNode:              source.pubkey,
		},

		pathFindingCfg: PathFindingConfig{
			PaymentAttemptPenalty: 1000,
		},

		source: source,
		target: target,
	}

	return &ctx
}

// testPayment launches a test payment and asserts that it is completed after
// the expected number of attempts.
func (c *integratedRoutingContext) testPayment(expectedNofAttempts int) {
	var nextPid uint64

	// Create temporary database for mission control.
	file, err := ioutil.TempFile("", "*.db")
	if err != nil {
		c.t.Fatal(err)
	}

	dbPath := file.Name()
	defer os.Remove(dbPath)

	db, err := kvdb.Open(kvdb.BoltBackendName, dbPath, true)
	if err != nil {
		c.t.Fatal(err)
	}
	defer db.Close()

	// Instantiate a new mission control with the current configuration
	// values.
	mc, err := NewMissionControl(db, &c.mcCfg)
	if err != nil {
		c.t.Fatal(err)
	}

	// Instruct pathfinding to use mission control as a probabiltiy source.
	restrictParams := RestrictParams{
		ProbabilitySource: mc.GetProbability,
		FeeLimit:          lnwire.MaxMilliSatoshi,
	}

	// Now the payment control loop starts. It will keep trying routes until
	// the payment succeeds.
	for {
		// Create bandwidth hints based on local channel balances.
		bandwidthHints := map[uint64]lnwire.MilliSatoshi{}
		for _, ch := range c.graph.nodes[c.source.pubkey].channels {
			bandwidthHints[ch.id] = ch.balance
		}

		// Find a route.
		path, err := findPathInternal(
			nil, bandwidthHints, c.graph,
			&restrictParams,
			&c.pathFindingCfg,
			c.source.pubkey, c.target.pubkey,
			c.amt, c.finalExpiry,
		)
		if err != nil {
			c.t.Fatal(err)
		}

		finalHop := finalHopParams{
			amt:       c.amt,
			cltvDelta: uint16(c.finalExpiry),
		}

		route, err := newRoute(c.source.pubkey, path, 0, finalHop)
		if err != nil {
			c.t.Fatal(err)
		}

		// Send out the htlc on the mock graph.
		pid := nextPid
		nextPid++
		htlcResult, err := c.graph.sendHtlc(route)
		if err != nil {
			c.t.Fatal(err)
		}

		// Process the result.
		if htlcResult.failure == nil {
			err := mc.ReportPaymentSuccess(pid, route)
			if err != nil {
				c.t.Fatal(err)
			}

			// If the payment is successful, the control loop can be
			// broken out of.
			break
		}

		// Failure, update mission control and retry.
		c.t.Logf("fail: %v @ %v\n", htlcResult.failure, htlcResult.failureSource)

		finalResult, err := mc.ReportPaymentFail(
			pid, route,
			getNodeIndex(route, htlcResult.failureSource),
			htlcResult.failure,
		)
		if err != nil {
			c.t.Fatal(err)
		}

		if finalResult != nil {
			c.t.Logf("final result: %v\n", finalResult)
			break
		}
	}

	c.t.Logf("Payment attempts: %v\n", nextPid)
	if expectedNofAttempts != int(nextPid) {
		c.t.Fatalf("expected %v attempts, but needed %v",
			expectedNofAttempts, nextPid)
	}
}

// getNodeIndex returns the zero-based index of the given node in the route.
func getNodeIndex(route *route.Route, failureSource route.Vertex) *int {
	if failureSource == route.SourcePubKey {
		idx := 0
		return &idx
	}

	for i, h := range route.Hops {
		if h.PubKeyBytes == failureSource {
			idx := i + 1
			return &idx
		}
	}
	return nil
}
