package channeldb

import (
	"bytes"
	"testing"

	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightningnetwork/lnd/chainntnfs"
	"github.com/stretchr/testify/require"
)

func initHintCache(t *testing.T) *HeightHintCache {
	t.Helper()

	defaultCfg := CacheConfig{
		QueryDisable: false,
	}

	return initHintCacheWithConfig(t, defaultCfg)
}

func initHintCacheWithConfig(t *testing.T, cfg CacheConfig) *HeightHintCache {
	t.Helper()

	db, err := Open(t.TempDir())
	require.NoError(t, err, "unable to create db")
	hintCache, err := NewHeightHintCache(cfg, db.Backend)
	require.NoError(t, err, "unable to create hint cache")

	t.Cleanup(func() {
		require.NoError(t, db.Close())
	})

	return hintCache
}

// TestHeightHintCacheConfirms ensures that the height hint cache properly
// caches confirm hints for transactions.
func TestHeightHintCacheConfirms(t *testing.T) {
	t.Parallel()

	hintCache := initHintCache(t)

	// Querying for a transaction hash not found within the cache should
	// return an error indication so.
	var unknownHash chainhash.Hash
	copy(unknownHash[:], bytes.Repeat([]byte{0x01}, 32))
	unknownConfRequest := chainntnfs.ConfRequest{TxID: unknownHash}
	_, err := hintCache.QueryConfirmHint(unknownConfRequest)
	require.ErrorIs(t, err, chainntnfs.ErrConfirmHintNotFound)

	// Now, we'll create some transaction hashes and commit them to the
	// cache with the same confirm hint.
	const height = 100
	const numHashes = 5
	confRequests := make([]chainntnfs.ConfRequest, numHashes)
	for i := 0; i < numHashes; i++ {
		var txHash chainhash.Hash
		copy(txHash[:], bytes.Repeat([]byte{byte(i + 1)}, 32))
		confRequests[i] = chainntnfs.ConfRequest{TxID: txHash}
	}

	err = hintCache.CommitConfirmHint(height, confRequests...)
	require.NoError(t, err, "unable to add entries to cache")

	// With the hashes committed, we'll now query the cache to ensure that
	// we're able to properly retrieve the confirm hints.
	for _, confRequest := range confRequests {
		confirmHint, err := hintCache.QueryConfirmHint(confRequest)
		require.NoError(t, err)
		require.EqualValues(t, height, confirmHint)
	}

	// We'll also attempt to purge all of them in a single database
	// transaction.
	err = hintCache.PurgeConfirmHint(confRequests...)
	require.NoError(t, err)

	// Finally, we'll attempt to query for each hash. We should expect not
	// to find a hint for any of them.
	for _, confRequest := range confRequests {
		_, err := hintCache.QueryConfirmHint(confRequest)
		require.ErrorIs(t, err, chainntnfs.ErrConfirmHintNotFound)
	}
}

// TestHeightHintCacheSpends ensures that the height hint cache properly caches
// spend hints for outpoints.
func TestHeightHintCacheSpends(t *testing.T) {
	t.Parallel()

	hintCache := initHintCache(t)

	// Querying for an outpoint not found within the cache should return an
	// error indication so.
	unknownOutPoint := wire.OutPoint{Index: 1}
	unknownSpendRequest := chainntnfs.SpendRequest{
		OutPoint: unknownOutPoint,
	}
	_, err := hintCache.QuerySpendHint(unknownSpendRequest)
	require.ErrorIs(t, err, chainntnfs.ErrSpendHintNotFound)

	// Now, we'll create some outpoints and commit them to the cache with
	// the same spend hint.
	const height = 100
	const numOutpoints = 5
	spendRequests := make([]chainntnfs.SpendRequest, numOutpoints)
	for i := uint32(0); i < numOutpoints; i++ {
		spendRequests[i] = chainntnfs.SpendRequest{
			OutPoint: wire.OutPoint{Index: i + 1},
		}
	}

	err = hintCache.CommitSpendHint(height, spendRequests...)
	require.NoError(t, err, "unable to add entries to cache")

	// With the outpoints committed, we'll now query the cache to ensure
	// that we're able to properly retrieve the confirm hints.
	for _, spendRequest := range spendRequests {
		spendHint, err := hintCache.QuerySpendHint(spendRequest)
		require.NoError(t, err)
		require.EqualValues(t, height, spendHint)
	}

	// We'll also attempt to purge all of them in a single database
	// transaction.
	err = hintCache.PurgeSpendHint(spendRequests...)
	require.NoError(t, err)

	// Finally, we'll attempt to query for each outpoint. We should expect
	// not to find a hint for any of them.
	for _, spendRequest := range spendRequests {
		_, err = hintCache.QuerySpendHint(spendRequest)
		require.ErrorIs(t, err, chainntnfs.ErrSpendHintNotFound)
	}
}

// TestQueryDisable asserts querying for confirmation or spend hints always
// return height zero when QueryDisabled is set to true in the CacheConfig.
func TestQueryDisable(t *testing.T) {
	cfg := CacheConfig{
		QueryDisable: true,
	}

	hintCache := initHintCacheWithConfig(t, cfg)

	// Insert a new confirmation hint with a non-zero height.
	const confHeight = 100
	confRequest := chainntnfs.ConfRequest{
		TxID: chainhash.Hash{0x01, 0x02, 0x03},
	}
	err := hintCache.CommitConfirmHint(confHeight, confRequest)
	require.Nil(t, err)

	// Query for the confirmation hint, which should return zero.
	cachedConfHeight, err := hintCache.QueryConfirmHint(confRequest)
	require.Nil(t, err)
	require.Equal(t, uint32(0), cachedConfHeight)

	// Insert a new spend hint with a non-zero height.
	const spendHeight = 200
	spendRequest := chainntnfs.SpendRequest{
		OutPoint: wire.OutPoint{
			Hash:  chainhash.Hash{0x4, 0x05, 0x06},
			Index: 42,
		},
	}
	err = hintCache.CommitSpendHint(spendHeight, spendRequest)
	require.Nil(t, err)

	// Query for the spend hint, which should return zero.
	cachedSpendHeight, err := hintCache.QuerySpendHint(spendRequest)
	require.Nil(t, err)
	require.Equal(t, uint32(0), cachedSpendHeight)
}
