package itest

import (
	"flag"
	"fmt"
	"io"
	"math"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcd/integration/rpctest"
	"github.com/lightningnetwork/lnd/lnrpc"
	"github.com/lightningnetwork/lnd/lntest"
	"github.com/lightningnetwork/lnd/lntest/node"
	"github.com/lightningnetwork/lnd/lntest/port"
	"github.com/lightningnetwork/lnd/lntest/wait"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc/grpclog"
)

const (
	// defaultSplitTranches is the default number of tranches we split the
	// test cases into.
	defaultSplitTranches uint = 1

	// defaultRunTranche is the default index of the test cases tranche that
	// we run.
	defaultRunTranche uint = 0

	defaultTimeout = wait.DefaultTimeout
	itestLndBinary = "../lnd-itest"

	// TODO(yy): remove the following defined constants and put them in the
	// specific tests where they are used?
	testFeeBase    = 1e+6
	anchorSize     = 330
	defaultCSV     = node.DefaultCSV
	noFeeLimitMsat = math.MaxInt64

	AddrTypeWitnessPubkeyHash = lnrpc.AddressType_WITNESS_PUBKEY_HASH
	AddrTypeNestedPubkeyHash  = lnrpc.AddressType_NESTED_PUBKEY_HASH
	AddrTypeTaprootPubkey     = lnrpc.AddressType_TAPROOT_PUBKEY
)

var (
	harnessNetParams = &chaincfg.RegressionNetParams

	// testCasesSplitParts is the number of tranches the test cases should
	// be split into. By default this is set to 1, so no splitting happens.
	// If this value is increased, then the -runtranche flag must be
	// specified as well to indicate which part should be run in the current
	// invocation.
	testCasesSplitTranches = flag.Uint(
		"splittranches", defaultSplitTranches, "split the test cases "+
			"in this many tranches and run the tranche at "+
			"0-based index specified by the -runtranche flag",
	)

	// testCasesRunTranche is the 0-based index of the split test cases
	// tranche to run in the current invocation.
	testCasesRunTranche = flag.Uint(
		"runtranche", defaultRunTranche, "run the tranche of the "+
			"split test cases with the given (0-based) index",
	)

	// dbBackendFlag specifies the backend to use.
	dbBackendFlag = flag.String("dbbackend", "bbolt", "Database backend "+
		"(bbolt, etcd, postgres)")

	nativeSQLFlag = flag.Bool("nativesql", false, "Database backend to "+
		"use native SQL when applicable (only for sqlite and postgres")

	// lndExecutable is the full path to the lnd binary.
	lndExecutable = flag.String(
		"lndexec", itestLndBinary, "full path to lnd binary",
	)
)

// TestLightningNetworkDaemon performs a series of integration tests amongst a
// programmatically driven network of lnd nodes.
func TestLightningNetworkDaemon(t *testing.T) {
	// If no tests are registered, then we can exit early.
	if len(allTestCases) == 0 {
		t.Skip("integration tests not selected with flag 'integration'")
	}

	// Get the test cases to be run in this tranche.
	testCases, trancheIndex, trancheOffset := getTestCaseSplitTranche()

	// Create a simple fee service.
	feeService := lntest.NewFeeService(t)

	// Get the binary path and setup the harness test.
	binary := getLndBinary(t)
	harnessTest := lntest.SetupHarness(
		t, binary, *dbBackendFlag, *nativeSQLFlag, feeService,
	)
	defer harnessTest.Stop()

	// Setup standby nodes, Alice and Bob, which will be alive and shared
	// among all the test cases.
	harnessTest.SetupStandbyNodes()

	// Run the subset of the test cases selected in this tranche.
	for idx, testCase := range testCases {
		testCase := testCase
		name := fmt.Sprintf("tranche%02d/%02d-of-%d/%s/%s",
			trancheIndex, trancheOffset+uint(idx)+1,
			len(allTestCases), harnessTest.ChainBackendName(),
			testCase.Name)

		success := t.Run(name, func(t1 *testing.T) {
			// Create a separate harness test for the testcase to
			// avoid overwriting the external harness test that is
			// tied to the parent test.
			ht := harnessTest.Subtest(t1)

			// TODO(yy): split log files.
			cleanTestCaseName := strings.ReplaceAll(
				testCase.Name, " ", "_",
			)
			ht.SetTestName(cleanTestCaseName)

			logLine := fmt.Sprintf(
				"STARTING ============ %v ============\n",
				testCase.Name,
			)

			ht.Alice.AddToLogf(logLine)
			ht.Bob.AddToLogf(logLine)

			ht.EnsureConnected(ht.Alice, ht.Bob)

			ht.RunTestCase(testCase)
		})

		// Stop at the first failure. Mimic behavior of original test
		// framework.
		if !success {
			// Log failure time to help relate the lnd logs to the
			// failure.
			t.Logf("Failure time: %v", time.Now().Format(
				"2006-01-02 15:04:05.000",
			))
			break
		}
	}

	height := harnessTest.CurrentHeight()
	t.Logf("=========> tests finished for tranche: %v, tested %d "+
		"cases, end height: %d\n", trancheIndex, len(testCases), height)
}

// getTestCaseSplitTranche returns the sub slice of the test cases that should
// be run as the current split tranche as well as the index and slice offset of
// the tranche.
func getTestCaseSplitTranche() ([]*lntest.TestCase, uint, uint) {
	numTranches := defaultSplitTranches
	if testCasesSplitTranches != nil {
		numTranches = *testCasesSplitTranches
	}
	runTranche := defaultRunTranche
	if testCasesRunTranche != nil {
		runTranche = *testCasesRunTranche
	}

	// There's a special flake-hunt mode where we run the same test multiple
	// times in parallel. In that case the tranche index is equal to the
	// thread ID, but we need to actually run all tests for the regex
	// selection to work.
	threadID := runTranche
	if numTranches == 1 {
		runTranche = 0
	}

	numCases := uint(len(allTestCases))
	testsPerTranche := numCases / numTranches
	trancheOffset := runTranche * testsPerTranche
	trancheEnd := trancheOffset + testsPerTranche
	if trancheEnd > numCases || runTranche == numTranches-1 {
		trancheEnd = numCases
	}

	return allTestCases[trancheOffset:trancheEnd], threadID,
		trancheOffset
}

func getLndBinary(t *testing.T) string {
	binary := itestLndBinary
	lndExec := ""
	if lndExecutable != nil && *lndExecutable != "" {
		lndExec = *lndExecutable
	}
	if lndExec == "" && runtime.GOOS == "windows" {
		// Windows (even in a bash like environment like git bash as on
		// Travis) doesn't seem to like relative paths to exe files...
		currentDir, err := os.Getwd()
		require.NoError(t, err, "unable to get working directory")

		targetPath := filepath.Join(currentDir, "../../lnd-itest.exe")
		binary, err = filepath.Abs(targetPath)
		require.NoError(t, err, "unable to get absolute path")
	} else if lndExec != "" {
		binary = lndExec
	}

	return binary
}

func init() {
	// Before we start any node, we need to make sure that any btcd node
	// that is started through the RPC harness uses a unique port as well
	// to avoid any port collisions.
	rpctest.ListenAddressGenerator =
		port.GenerateSystemUniqueListenerAddresses

	// Swap out grpc's default logger with our fake logger which drops the
	// statements on the floor.
	fakeLogger := grpclog.NewLoggerV2(io.Discard, io.Discard, io.Discard)
	grpclog.SetLoggerV2(fakeLogger)
}
