package graphdb

import (
	"encoding/hex"
	"testing"

	"github.com/lightningnetwork/lnd/fn/v2"
	"github.com/lightningnetwork/lnd/graph/db/models"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/routing/route"
	"github.com/stretchr/testify/require"
)

var (
	pubKey1Bytes, _ = hex.DecodeString(
		"0248f5cba4c6da2e4c9e01e81d1404dfac0cbaf3ee934a4fc117d2ea9a64" +
			"22c91d",
	)
	pubKey2Bytes, _ = hex.DecodeString(
		"038155ba86a8d3b23c806c855097ca5c9fa0f87621f1e7a7d2835ad057f6" +
			"f4484f",
	)

	pubKey1, _ = route.NewVertexFromBytes(pubKey1Bytes)
	pubKey2, _ = route.NewVertexFromBytes(pubKey2Bytes)
)

// TestGraphCacheAddNode tests that a channel going from node A to node B can be
// cached correctly, independent of the direction we add the channel as.
func TestGraphCacheAddNode(t *testing.T) {
	t.Parallel()

	runTest := func(nodeA, nodeB route.Vertex) {
		t.Helper()

		isNode1A, isNode1B := true, false
		if nodeA == pubKey2 {
			isNode1A, isNode1B = false, true
		}

		inboundFee := lnwire.Fee{
			BaseFee: 10,
			FeeRate: 20,
		}

		outPolicy1 := &models.CachedEdgePolicy{
			ChannelID:  1000,
			IsNode1:    isNode1A,
			IsDisabled: false,
			ToNodePubKey: func() route.Vertex {
				return nodeB
			},
			// Define an inbound fee.
			InboundFee: fn.Some(inboundFee),
		}
		inPolicy1 := &models.CachedEdgePolicy{
			ChannelID:  1000,
			IsNode1:    isNode1B,
			IsDisabled: false,
			ToNodePubKey: func() route.Vertex {
				return nodeA
			},
		}
		cache := NewGraphCache(10)
		cache.AddNodeFeatures(nodeA, lnwire.EmptyFeatureVector())
		cache.AddChannel(&models.CachedEdgeInfo{
			ChannelID: 1000,
			// Those are direction independent!
			NodeKey1Bytes: pubKey1,
			NodeKey2Bytes: pubKey2,
			Capacity:      500,
		}, outPolicy1, inPolicy1)

		var fromChannels, toChannels []*DirectedChannel
		_ = cache.ForEachChannel(nodeA, func(c *DirectedChannel) error {
			fromChannels = append(fromChannels, c)
			return nil
		})
		_ = cache.ForEachChannel(nodeB, func(c *DirectedChannel) error {
			toChannels = append(toChannels, c)
			return nil
		})

		require.Len(t, fromChannels, 1)
		require.Len(t, toChannels, 1)

		require.Equal(t, outPolicy1 != nil, fromChannels[0].OutPolicySet)
		assertCachedPolicyEqual(t, inPolicy1, fromChannels[0].InPolicy)

		require.Equal(t, inPolicy1 != nil, toChannels[0].OutPolicySet)
		assertCachedPolicyEqual(t, outPolicy1, toChannels[0].InPolicy)

		// Now that we've inserted two nodes into the graph, check that
		// we'll recover the same set of channels during forEachNode.
		nodes := make(map[route.Vertex]struct{})
		chans := make(map[uint64]struct{})
		_ = cache.ForEachNode(func(node route.Vertex,
			edges map[uint64]*DirectedChannel) error {

			nodes[node] = struct{}{}
			for chanID, directedChannel := range edges {
				chans[chanID] = struct{}{}

				if node == nodeA {
					require.NotZero(
						t, directedChannel.InboundFee,
					)
				} else {
					require.Zero(
						t, directedChannel.InboundFee,
					)
				}
			}

			return nil
		})

		require.Len(t, nodes, 2)
		require.Len(t, chans, 1)
	}

	runTest(pubKey1, pubKey2)
	runTest(pubKey2, pubKey1)
}

func assertCachedPolicyEqual(t *testing.T, original,
	cached *models.CachedEdgePolicy) {

	require.Equal(t, original.ChannelID, cached.ChannelID)
	require.Equal(t, original.HasMaxHTLC, cached.HasMaxHTLC)
	require.Equal(t, original.IsNode1, cached.IsNode1)
	require.Equal(t, original.IsDisabled, cached.IsDisabled)
	require.Equal(t, original.TimeLockDelta, cached.TimeLockDelta)
	require.Equal(t, original.MinHTLC, cached.MinHTLC)
	require.Equal(t, original.MaxHTLC, cached.MaxHTLC)
	require.Equal(t, original.FeeBaseMSat, cached.FeeBaseMSat)
	require.Equal(
		t, original.FeeProportionalMillionths,
		cached.FeeProportionalMillionths,
	)
	if original.ToNodePubKey != nil {
		require.Equal(t, original.ToNodePubKey(), cached.ToNodePubKey())
	}
}

// TestGraphCacheDisabledPoliciesRegression is a regression test for the bug
// where channels with both policies disabled were not added to the graph cache
// during population, preventing future policy updates from working.
//
// The bug flow was:
// 1. Channel with both policies disabled exists in DB.
// 2. populateCache skips adding it to graph cache entirely.
// 3. Later, a policy update arrives enabling one direction.
// 4. UpdateEdgePolicy updates the DB successfully.
// 5. UpdateEdgePolicy tries to update graph cache but channel not found.
// 6. Channel never becomes usable for routing.
func TestGraphCacheDisabledPoliciesRegression(t *testing.T) {
	t.Parallel()

	// Create a simple cache instance.
	cache := NewGraphCache(10)

	// Simulate a channel with both policies disabled.
	chanID := uint64(12345)
	node1 := pubKey1
	node2 := pubKey2

	edgeInfo := &models.CachedEdgeInfo{
		ChannelID:     chanID,
		NodeKey1Bytes: node1,
		NodeKey2Bytes: node2,
		Capacity:      1000000,
	}

	// Create two disabled policies.
	disabledPolicy1 := &models.CachedEdgePolicy{
		ChannelID:  chanID,
		IsNode1:    true,
		IsDisabled: true,
	}
	disabledPolicy2 := &models.CachedEdgePolicy{
		ChannelID:  chanID,
		IsNode1:    false,
		IsDisabled: true,
	}

	// Add the channel with both policies disabled (simulating
	// populateCache).
	cache.AddChannel(edgeInfo, disabledPolicy1, disabledPolicy2)

	// Verify the channel structure was added to cache.
	var foundChannels []*DirectedChannel
	err := cache.ForEachChannel(node1, func(c *DirectedChannel) error {
		if c.ChannelID == chanID {
			foundChannels = append(foundChannels, c)
		}

		return nil
	})
	require.NoError(t, err)
	require.Len(t, foundChannels, 1,
		"channel structure should be in cache even when both "+
			"policies are disabled")

	// Verify policies were NOT added (both disabled).
	require.False(t, foundChannels[0].OutPolicySet,
		"disabled outgoing policy should not be set in cache")
	require.Nil(t, foundChannels[0].InPolicy,
		"disabled incoming policy should not be set in cache")

	// Now simulate receiving a fresh update enabling one direction.
	enabledPolicy1 := &models.CachedEdgePolicy{
		ChannelID:     chanID,
		IsNode1:       true,
		IsDisabled:    false,
		TimeLockDelta: 40,
		MinHTLC:       lnwire.MilliSatoshi(1000),
	}

	// Update the policy (simulating what UpdateEdgePolicy does).
	cache.UpdatePolicy(enabledPolicy1, node1, node2)

	// Verify the policy update succeeded. Before the fix, UpdatePolicy
	// would log "Channel not found in graph cache" and return early,
	// so the policy would never be added.
	foundChannels = nil
	err = cache.ForEachChannel(node1, func(c *DirectedChannel) error {
		if c.ChannelID == chanID {
			foundChannels = append(foundChannels, c)
		}

		return nil
	})
	require.NoError(t, err)
	require.Len(t, foundChannels, 1)

	// The policy should now be set.
	require.True(t, foundChannels[0].OutPolicySet,
		"REGRESSION: policy update should work even for channels that "+
			"had both policies disabled initially")

	// Verify we can also see it from node2's perspective.
	foundChannels = nil
	err = cache.ForEachChannel(node2, func(c *DirectedChannel) error {
		if c.ChannelID == chanID {
			foundChannels = append(foundChannels, c)
		}

		return nil
	})
	require.NoError(t, err)
	require.Len(t, foundChannels, 1)
	require.NotNil(t, foundChannels[0].InPolicy,
		"incoming policy should be set after policy update")
	require.Equal(t, uint16(40), foundChannels[0].InPolicy.TimeLockDelta)
}
