package migration24

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"math/rand"
	"testing"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/wire"
	lnwire "github.com/lightningnetwork/lnd/channeldb/migration/lnwire21"
	mig "github.com/lightningnetwork/lnd/channeldb/migration_01_to_11"
	"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,
	}
	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,
	}
	_, pubKey = btcec.PrivKeyFromBytes(key[:])
)

// TestMigrateFwdPkgCleanup asserts that the migration will delete all the
// forwarding packages for close channels, and leave the rest untouched.
func TestMigrateFwdPkgCleanup(t *testing.T) {
	migrationTests := []struct {
		name                string
		beforeMigrationFunc func(kvdb.RwTx) error
		afterMigrationFunc  func(kvdb.RwTx) error
	}{
		{
			// No closed channels summeries in the db.
			// This leaves the fwdpkg untouched.
			name: "no closed channel summeries",
			beforeMigrationFunc: genBeforeMigration(
				false, []int{}, []int{1},
			),
			afterMigrationFunc: genAfterMigration(
				[]int{}, []int{1},
			),
		},
		{
			// One closed summery found, and the forwarding package
			// shares the same channel ID.
			// This makes the fwdpkg removed.
			name: "remove one fwdpkg",
			beforeMigrationFunc: genBeforeMigration(
				false, []int{1}, []int{1},
			),
			afterMigrationFunc: genAfterMigration(
				[]int{1}, []int{},
			),
		},
		{
			// One closed summery with pending status found, and the
			// forwarding package shares the same channel ID.
			// This leaves the fwdpkg untouched.
			name: "no action if closed status is pending",
			beforeMigrationFunc: genBeforeMigration(
				true, []int{1}, []int{1},
			),
			afterMigrationFunc: genAfterMigration(
				[]int{}, []int{1},
			),
		},
		{
			// One closed summery found, while the forwarding
			// package has a different channel ID.
			// This leaves the fwdpkg untouched.
			name: "no fwdpkg removed",
			beforeMigrationFunc: genBeforeMigration(
				false, []int{1}, []int{2},
			),
			afterMigrationFunc: genAfterMigration(
				[]int{}, []int{2},
			),
		},
		{
			// Multiple closed summeries and fwdpkg nested buckets
			// found. Only the matched fwdPkg nested buckets are
			// removed.
			name: "only matching fwdpkg removed",
			beforeMigrationFunc: genBeforeMigration(false,
				[]int{1, 2, 3},
				[]int{1, 2, 4},
			),
			afterMigrationFunc: genAfterMigration(
				[]int{1, 2},
				[]int{4},
			),
		},
	}

	for _, tt := range migrationTests {
		test := tt
		t.Run(test.name, func(t *testing.T) {
			migtest.ApplyMigration(
				t,
				test.beforeMigrationFunc,
				test.afterMigrationFunc,
				MigrateFwdPkgCleanup,
				false,
			)
		})
	}
}

func genBeforeMigration(isPending bool,
	closeSummeryChanIDs, fwdPkgChanIDs []int) func(kvdb.RwTx) error {

	return func(tx kvdb.RwTx) error {
		// Create closed channel summeries
		for _, id := range closeSummeryChanIDs {
			chanID := lnwire.NewShortChanIDFromInt(uint64(id))
			if err := createTestCloseChannelSummery(
				tx, isPending, chanID,
			); err != nil {
				return err
			}
		}

		// Create fwdPkg nested buckets
		for _, id := range fwdPkgChanIDs {
			chanID := lnwire.NewShortChanIDFromInt(uint64(id))
			err := createTestFwdPkgBucket(tx, chanID)
			if err != nil {
				return err
			}
		}

		return nil
	}
}

func genAfterMigration(deleted, untouched []int) func(kvdb.RwTx) error {
	return func(tx kvdb.RwTx) error {
		fwdPkgBkt := tx.ReadBucket(fwdPackagesKey)
		if fwdPkgBkt == nil {
			return errors.New("unable to find bucket")
		}

		// Reading deleted buckets should return nil
		for _, id := range deleted {
			chanID := lnwire.NewShortChanIDFromInt(uint64(id))
			sourceKey := MakeLogKey(chanID.ToUint64())
			sourceBkt := fwdPkgBkt.NestedReadBucket(sourceKey[:])
			if sourceBkt != nil {
				return fmt.Errorf(
					"expected bucket to be deleted: %v",
					id,
				)
			}
		}

		// Reading untouched buckets should return not nil
		for _, id := range untouched {
			chanID := lnwire.NewShortChanIDFromInt(uint64(id))
			sourceKey := MakeLogKey(chanID.ToUint64())
			sourceBkt := fwdPkgBkt.NestedReadBucket(sourceKey[:])
			if sourceBkt == nil {
				return fmt.Errorf(
					"expected bucket to not be deleted: %v",
					id,
				)
			}
		}

		return nil
	}
}

// createTestCloseChannelSummery creates a CloseChannelSummery for testing.
func createTestCloseChannelSummery(tx kvdb.RwTx, isPending bool,
	chanID lnwire.ShortChannelID) error {

	closedChanBucket, err := tx.CreateTopLevelBucket(closedChannelBucket)
	if err != nil {
		return err
	}

	outputPoint := wire.OutPoint{Hash: key, Index: rand.Uint32()}

	ccs := &mig.ChannelCloseSummary{
		ChanPoint:      outputPoint,
		ShortChanID:    chanID,
		ChainHash:      key,
		ClosingTXID:    testTx.TxHash(),
		CloseHeight:    100,
		RemotePub:      pubKey,
		Capacity:       btcutil.Amount(10000),
		SettledBalance: btcutil.Amount(50000),
		CloseType:      mig.RemoteForceClose,
		IsPending:      isPending,
		// Optional fields
		RemoteCurrentRevocation: pubKey,
		RemoteNextRevocation:    pubKey,
	}
	var b bytes.Buffer
	if err := serializeChannelCloseSummary(&b, ccs); err != nil {
		return err
	}

	var chanPointBuf bytes.Buffer
	if err := writeOutpoint(&chanPointBuf, &outputPoint); err != nil {
		return err
	}

	return closedChanBucket.Put(chanPointBuf.Bytes(), b.Bytes())
}

func createTestFwdPkgBucket(tx kvdb.RwTx, chanID lnwire.ShortChannelID) error {
	fwdPkgBkt, err := tx.CreateTopLevelBucket(fwdPackagesKey)
	if err != nil {
		return err
	}

	source := MakeLogKey(chanID.ToUint64())
	if _, err := fwdPkgBkt.CreateBucketIfNotExists(source[:]); err != nil {
		return err
	}

	return nil
}

func serializeChannelCloseSummary(
	w io.Writer, cs *mig.ChannelCloseSummary) error {

	err := mig.WriteElements(
		w,
		cs.ChanPoint, cs.ShortChanID, cs.ChainHash, cs.ClosingTXID,
		cs.CloseHeight, cs.RemotePub, cs.Capacity, cs.SettledBalance,
		cs.TimeLockedBalance, cs.CloseType, cs.IsPending,
	)
	if err != nil {
		return err
	}
	return nil
}

// writeOutpoint writes an outpoint to the passed writer using the minimal
// amount of bytes possible.
func writeOutpoint(w io.Writer, o *wire.OutPoint) error {
	if _, err := w.Write(o.Hash[:]); err != nil {
		return err
	}
	if err := binary.Write(w, binary.BigEndian, o.Index); err != nil {
		return err
	}

	return nil
}
