package migration21

import (
	"bytes"
	"encoding/hex"
	"fmt"
	"reflect"
	"testing"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/btcec/v2/ecdsa"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/wire"
	"github.com/davecgh/go-spew/spew"
	lnwire "github.com/lightningnetwork/lnd/channeldb/migration/lnwire21"
	"github.com/lightningnetwork/lnd/channeldb/migration21/common"
	"github.com/lightningnetwork/lnd/channeldb/migration21/current"
	"github.com/lightningnetwork/lnd/channeldb/migration21/legacy"
	"github.com/lightningnetwork/lnd/channeldb/migtest"
	"github.com/lightningnetwork/lnd/kvdb"
)

var (
	key = [chainhash.HashSize]byte{
		0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda,
		0x68, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17,
		0xd, 0xe7, 0x93, 0xe4, 0xb7, 0x25, 0xb8, 0x4d,
		0x1e, 0xb, 0x4c, 0xf9, 0x9e, 0xc5, 0x8c, 0xe9,
	}

	_, pubKey = btcec.PrivKeyFromBytes(key[:])

	wireSig, _ = lnwire.NewSigFromSignature(testSig)

	testRBytes, _ = hex.DecodeString("8ce2bc69281ce27da07e6683571319d18e949ddfa2965fb6caa1bf0314f882d7")
	testSBytes, _ = hex.DecodeString("299105481d63e0f4bc2a88121167221b6700d72a0ead154c03be696a292d24ae")
	testRScalar   = new(btcec.ModNScalar)
	testSScalar   = new(btcec.ModNScalar)
	_             = testRScalar.SetByteSlice(testRBytes)
	_             = testSScalar.SetByteSlice(testSBytes)
	testSig       = ecdsa.NewSignature(testRScalar, testSScalar)

	testTx = &wire.MsgTx{
		Version: 1,
		TxIn: []*wire.TxIn{
			{
				PreviousOutPoint: wire.OutPoint{
					Hash:  chainhash.Hash{},
					Index: 0xffffffff,
				},
				SignatureScript: []byte{0x04, 0x31, 0xdc, 0x00, 0x1b, 0x01, 0x62},
				Sequence:        0xffffffff,
			},
		},
		TxOut: []*wire.TxOut{
			{
				Value: 5000000000,
				PkScript: []byte{
					0x41, // OP_DATA_65
					0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5,
					0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42,
					0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1,
					0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24,
					0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97,
					0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78,
					0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20,
					0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63,
					0xa6, // 65-byte signature
					0xac, // OP_CHECKSIG
				},
			},
		},
		LockTime: 5,
	}

	testCommitDiff = &common.CommitDiff{
		Commitment: common.ChannelCommitment{
			CommitTx:  testTx,
			CommitSig: make([]byte, 0),
		},
		CommitSig: &lnwire.CommitSig{
			ChanID:    lnwire.ChannelID(key),
			CommitSig: wireSig,
			HtlcSigs: []lnwire.Sig{
				wireSig,
				wireSig,
			},
		},
		LogUpdates: []common.LogUpdate{
			{
				LogIndex: 1,
				UpdateMsg: &lnwire.UpdateAddHTLC{
					ID:     1,
					Amount: lnwire.NewMSatFromSatoshis(100),
					Expiry: 25,
				},
			},
			{
				LogIndex: 2,
				UpdateMsg: &lnwire.UpdateAddHTLC{
					ID:     2,
					Amount: lnwire.NewMSatFromSatoshis(200),
					Expiry: 50,
				},
			},
		},
		OpenedCircuitKeys: []common.CircuitKey{},
		ClosedCircuitKeys: []common.CircuitKey{},
	}

	testNetworkResult = &common.NetworkResult{
		Msg:          testCommitDiff.CommitSig,
		Unencrypted:  true,
		IsResolution: true,
	}

	testChanCloseSummary = &common.ChannelCloseSummary{
		RemotePub:               pubKey,
		Capacity:                9,
		RemoteCurrentRevocation: pubKey,
		RemoteNextRevocation:    pubKey,
		LastChanSyncMsg: &lnwire.ChannelReestablish{
			LocalUnrevokedCommitPoint: pubKey,
		},
	}

	netResultKey = []byte{3}

	chanID = lnwire.NewChanIDFromOutPoint(&wire.OutPoint{})

	adds = []common.LogUpdate{
		{
			LogIndex: 0,
			UpdateMsg: &lnwire.UpdateAddHTLC{
				ChanID:      chanID,
				ID:          1,
				Amount:      100,
				Expiry:      1000,
				PaymentHash: [32]byte{0},
			},
		},
		{
			LogIndex: 1,
			UpdateMsg: &lnwire.UpdateAddHTLC{
				ChanID:      chanID,
				ID:          1,
				Amount:      101,
				Expiry:      1001,
				PaymentHash: [32]byte{1},
			},
		},
	}

	settleFails = []common.LogUpdate{
		{
			LogIndex: 2,
			UpdateMsg: &lnwire.UpdateFulfillHTLC{
				ChanID:          chanID,
				ID:              0,
				PaymentPreimage: [32]byte{0},
			},
		},
		{
			LogIndex: 3,
			UpdateMsg: &lnwire.UpdateFailHTLC{
				ChanID: chanID,
				ID:     1,
				Reason: []byte{},
			},
		},
	}
)

// TestMigrateDatabaseWireMessages tests that we're able to properly migrate
// all the wire messages in the database which are written without a length
// prefix in front of them. At the time this test was written we need to
// migrate three areas: open channel commit diffs, open channel unacked updates,
// and network results in the switch.
func TestMigrateDatabaseWireMessages(t *testing.T) {
	var pub [33]byte
	copy(pub[:], key[:])

	migtest.ApplyMigration(
		t,
		func(tx kvdb.RwTx) error {
			t.Helper()

			// First, we'll insert a new fake channel (well just
			// the commitment diff) at the expected location
			// on-disk.
			openChanBucket, err := tx.CreateTopLevelBucket(
				openChannelBucket,
			)
			if err != nil {
				return err
			}
			nodeBucket, err := openChanBucket.CreateBucket(pub[:])
			if err != nil {
				return err
			}
			chainBucket, err := nodeBucket.CreateBucket(key[:])
			if err != nil {
				return err
			}
			chanBucket, err := chainBucket.CreateBucket(key[:])
			if err != nil {
				return err
			}

			var b bytes.Buffer
			err = legacy.SerializeCommitDiff(&b, testCommitDiff)
			if err != nil {
				return err
			}

			err = chanBucket.Put(commitDiffKey, b.Bytes())
			if err != nil {
				return err
			}

			var logUpdateBuf bytes.Buffer
			err = legacy.SerializeLogUpdates(
				&logUpdateBuf, testCommitDiff.LogUpdates,
			)
			if err != nil {
				return err
			}

			// We'll re-use the same log updates to insert as a set
			// of un-acked and unsigned pending log updateas as well.
			err = chanBucket.Put(
				unsignedAckedUpdatesKey, logUpdateBuf.Bytes(),
			)
			if err != nil {
				return err
			}

			err = chanBucket.Put(
				remoteUnsignedLocalUpdatesKey, logUpdateBuf.Bytes(),
			)
			if err != nil {
				return err
			}

			// Next, we'll insert a sample closed channel summary
			// for the 2nd part of our migration.
			closedChanBucket, err := tx.CreateTopLevelBucket(
				closedChannelBucket,
			)
			if err != nil {
				return err
			}

			var summaryBuf bytes.Buffer
			err = legacy.SerializeChannelCloseSummary(
				&summaryBuf, testChanCloseSummary,
			)
			if err != nil {
				return err
			}

			err = closedChanBucket.Put(key[:], summaryBuf.Bytes())
			if err != nil {
				return err
			}

			// Create a few forwarding packages to migrate.
			for i := uint64(100); i < 200; i++ {
				shortChanID := lnwire.NewShortChanIDFromInt(i)
				packager := legacy.NewChannelPackager(shortChanID)
				fwdPkg := common.NewFwdPkg(shortChanID, 0, adds, settleFails)

				if err := packager.AddFwdPkg(tx, fwdPkg); err != nil {
					return err
				}
			}

			// Finally, we need to insert a sample network result
			// as well for the final component of our migration.
			var netResBuf bytes.Buffer
			err = legacy.SerializeNetworkResult(
				&netResBuf, testNetworkResult,
			)
			if err != nil {
				return err
			}

			networkResults, err := tx.CreateTopLevelBucket(
				networkResultStoreBucketKey,
			)
			if err != nil {
				return err
			}

			return networkResults.Put(
				netResultKey, netResBuf.Bytes(),
			)
		},
		func(tx kvdb.RwTx) error {
			t.Helper()

			// We'll now read the commit diff from disk using the
			// _new_ decoding method. This should match the commit
			// diff we inserted in the pre-migration step.
			openChanBucket := tx.ReadWriteBucket(openChannelBucket)
			nodeBucket := openChanBucket.NestedReadWriteBucket(
				pub[:],
			)
			chainBucket := nodeBucket.NestedReadWriteBucket(key[:])
			chanBucket := chainBucket.NestedReadWriteBucket(key[:])

			commitDiffBytes := chanBucket.Get(commitDiffKey)
			if commitDiffBytes == nil {
				return fmt.Errorf("no commit diff found")
			}

			newCommitDiff, err := current.DeserializeCommitDiff(
				bytes.NewReader(commitDiffBytes),
			)
			if err != nil {
				return fmt.Errorf("unable to decode commit "+
					"diff: %v", err)
			}

			if !reflect.DeepEqual(newCommitDiff, testCommitDiff) {
				return fmt.Errorf("diff mismatch: expected "+
					"%v, got %v", spew.Sdump(testCommitDiff),
					spew.Sdump(newCommitDiff))
			}

			// Next, we'll ensure that the un-acked updates match
			// up as well.
			updateBytes := chanBucket.Get(unsignedAckedUpdatesKey)
			if updateBytes == nil {
				return fmt.Errorf("no update bytes found")
			}

			newUpdates, err := current.DeserializeLogUpdates(
				bytes.NewReader(updateBytes),
			)
			if err != nil {
				return err
			}

			if !reflect.DeepEqual(
				newUpdates, testCommitDiff.LogUpdates,
			) {

				return fmt.Errorf("updates mismatch: expected "+
					"%v, got %v",
					spew.Sdump(testCommitDiff.LogUpdates),
					spew.Sdump(newUpdates))
			}

			updateBytes = chanBucket.Get(remoteUnsignedLocalUpdatesKey)
			if updateBytes == nil {
				return fmt.Errorf("no update bytes found")
			}

			newUpdates, err = current.DeserializeLogUpdates(
				bytes.NewReader(updateBytes),
			)
			if err != nil {
				return err
			}

			if !reflect.DeepEqual(
				newUpdates, testCommitDiff.LogUpdates,
			) {

				return fmt.Errorf("updates mismatch: expected "+
					"%v, got %v",
					spew.Sdump(testCommitDiff.LogUpdates),
					spew.Sdump(newUpdates))
			}

			// Next, we'll ensure that the inserted close channel
			// summary bytes also mach up with what we inserted in
			// the prior step.
			closedChanBucket := tx.ReadWriteBucket(
				closedChannelBucket,
			)
			if closedChannelBucket == nil {
				return fmt.Errorf("no closed channels found")
			}

			chanSummaryBytes := closedChanBucket.Get(key[:])
			newChanCloseSummary, err := current.DeserializeCloseChannelSummary(
				bytes.NewReader(chanSummaryBytes),
			)
			if err != nil {
				return err
			}

			if !reflect.DeepEqual(
				newChanCloseSummary, testChanCloseSummary,
			) {

				return fmt.Errorf("summary mismatch: expected "+
					"%v, got %v",
					spew.Sdump(testChanCloseSummary),
					spew.Sdump(newChanCloseSummary))
			}

			// Fetch all forwarding packages.
			for i := uint64(100); i < 200; i++ {
				shortChanID := lnwire.NewShortChanIDFromInt(i)
				packager := current.NewChannelPackager(shortChanID)

				fwdPkgs, err := packager.LoadFwdPkgs(tx)
				if err != nil {
					return err
				}

				if len(fwdPkgs) != 1 {
					return fmt.Errorf("expected 1 pkg")
				}

				og := common.NewFwdPkg(shortChanID, 0, adds, settleFails)

				// Check that we deserialized the packages correctly.
				if !reflect.DeepEqual(fwdPkgs[0], og) {
					return fmt.Errorf("res mismatch: expected "+
						"%v, got %v",
						spew.Sdump(fwdPkgs[0]),
						spew.Sdump(og))
				}
			}

			// Finally, we'll check the network results to ensure
			// that was migrated properly as well.
			networkResults := tx.ReadBucket(
				networkResultStoreBucketKey,
			)
			if networkResults == nil {
				return fmt.Errorf("no net results found")
			}

			netResBytes := networkResults.Get(netResultKey)
			if netResBytes == nil {
				return fmt.Errorf("no network res found")
			}

			newNetRes, err := current.DeserializeNetworkResult(
				bytes.NewReader(netResBytes),
			)
			if err != nil {
				return err
			}

			if !reflect.DeepEqual(newNetRes, testNetworkResult) {
				return fmt.Errorf("res mismatch: expected "+
					"%v, got %v",
					spew.Sdump(testNetworkResult),
					spew.Sdump(newNetRes))
			}

			return nil
		},
		MigrateDatabaseWireMessages,
		false,
	)
}
