package server

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/netbirdio/netbird/management/internals/controllers/network_map"
	"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
	"github.com/netbirdio/netbird/management/server/affectedpeers"
	"github.com/netbirdio/netbird/management/server/groups"
	"github.com/netbirdio/netbird/management/server/networks"
	"github.com/netbirdio/netbird/management/server/networks/resources"
	resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
	"github.com/netbirdio/netbird/management/server/networks/routers"
	routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
	networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
	"github.com/netbirdio/netbird/management/server/permissions"
	"github.com/netbirdio/netbird/management/server/store"
	"github.com/netbirdio/netbird/management/server/types"
)

// routerScenario captures the topology from the bug report:
//
//	network ── router (routing peer) ── resource (in resourceGroup)
//	independent peer ──(policy: source -> resource)──> resource
//
// The routing peer must be refreshed when a policy grants a source peer access
// to the resource, because the network map connects the source peer to the
// routing peer at compute time (Account.GetPoliciesForNetworkResource +
// addNetworksRoutingPeers). The routing peer is NOT a member of the resource
// group, so static group/peer resolution alone cannot find it.
type routerScenario struct {
	manager       *DefaultAccountManager
	updateManager *update_channel.PeersUpdateManager
	accountID     string
	networkID     string

	sourcePeerID  string // independent peer that the policy grants access from
	sourceGroupID string // group containing the source peer

	routerPeerID      string // peer acting as the routing peer (direct router.Peer)
	routerGroupPeerID string // peer that is a member of routerPeerGroup
	routerPeerGroupID string // group used for router.PeerGroups

	resourceID      string // network resource
	resourceGroupID string // group whose member is the resource (no peers)

	unrelatedPeerID string // peer in no relevant entity
}

// setupRouterScenario builds the topology above with the default policy removed
// and channels NOT yet created, so callers control exactly when updates can flow.
func setupRouterScenario(t *testing.T, directRouterPeer bool) *routerScenario {
	t.Helper()

	manager, updateManager, err := createManager(t)
	require.NoError(t, err)

	ctx := context.Background()

	account, err := createAccount(manager, "router_scenario", userID, "")
	require.NoError(t, err)
	accountID := account.Id

	// Remove the default policy so AddPeer/CreateGroup don't schedule unrelated updates.
	policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID)
	require.NoError(t, err)
	for _, p := range policies {
		require.NoError(t, manager.Store.DeletePolicy(ctx, accountID, p.ID))
	}

	setupKey, err := manager.CreateSetupKey(ctx, accountID, "rs-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
	require.NoError(t, err)

	sourcePeer := addPeerToAccount(t, manager, accountID, setupKey.Key)
	routerPeer := addPeerToAccount(t, manager, accountID, setupKey.Key)
	routerGroupPeer := addPeerToAccount(t, manager, accountID, setupKey.Key)
	unrelatedPeer := addPeerToAccount(t, manager, accountID, setupKey.Key)

	const (
		sourceGroupID     = "rs-source-grp"
		routerPeerGroupID = "rs-router-grp"
		resourceGroupID   = "rs-resource-grp"
	)

	for _, g := range []*types.Group{
		{ID: sourceGroupID, Name: "rs-source", Peers: []string{sourcePeer.ID}},
		{ID: routerPeerGroupID, Name: "rs-router", Peers: []string{routerGroupPeer.ID}},
		{ID: resourceGroupID, Name: "rs-resource"}, // intentionally peerless; the resource is its only member
	} {
		require.NoError(t, manager.CreateGroup(ctx, accountID, userID, g))
	}

	permissionsManager := permissions.NewManager(manager.Store)
	groupsManager := groups.NewManager(manager.Store, permissionsManager, manager)
	resourcesManager := resources.NewManager(manager.Store, permissionsManager, groupsManager, manager, manager.serviceManager)
	routersManager := routers.NewManager(manager.Store, permissionsManager, manager)
	networksManager := networks.NewManager(manager.Store, permissionsManager, resourcesManager, routersManager, manager)

	network, err := networksManager.CreateNetwork(ctx, userID, &networkTypes.Network{
		ID:        "rs-network",
		AccountID: accountID,
		Name:      "rs-network",
	})
	require.NoError(t, err)

	resource, err := resourcesManager.CreateResource(ctx, userID, &resourceTypes.NetworkResource{
		AccountID: accountID,
		NetworkID: network.ID,
		Name:      "rs-resource-host",
		Address:   "10.20.30.0/24",
		GroupIDs:  []string{resourceGroupID},
		Enabled:   true,
	})
	require.NoError(t, err)

	router := &routerTypes.NetworkRouter{
		ID:         "rs-router",
		NetworkID:  network.ID,
		AccountID:  accountID,
		Masquerade: true,
		Metric:     9999,
		Enabled:    true,
	}
	if directRouterPeer {
		router.Peer = routerPeer.ID
	} else {
		router.PeerGroups = []string{routerPeerGroupID}
	}
	_, err = routersManager.CreateRouter(ctx, userID, router)
	require.NoError(t, err)

	return &routerScenario{
		manager:           manager,
		updateManager:     updateManager,
		accountID:         accountID,
		networkID:         network.ID,
		sourcePeerID:      sourcePeer.ID,
		sourceGroupID:     sourceGroupID,
		routerPeerID:      routerPeer.ID,
		routerGroupPeerID: routerGroupPeer.ID,
		routerPeerGroupID: routerPeerGroupID,
		resourceID:        resource.ID,
		resourceGroupID:   resourceGroupID,
		unrelatedPeerID:   unrelatedPeer.ID,
	}
}

// peerToResourcePolicy builds a policy granting the source group access to the
// resource, referencing the resource by its group in the rule destination.
func peerToResourcePolicyByGroup(sourceGroupID, resourceGroupID string) *types.Policy {
	return &types.Policy{
		Enabled: true,
		Name:    "peer-to-resource-by-group",
		Rules: []*types.PolicyRule{
			{
				Enabled:      true,
				Sources:      []string{sourceGroupID},
				Destinations: []string{resourceGroupID},
				Action:       types.PolicyTrafficActionAccept,
			},
		},
	}
}

// peerToResourcePolicyByResource builds a policy referencing the resource
// directly via DestinationResource rather than its group.
func peerToResourcePolicyByResource(sourceGroupID, resourceID string) *types.Policy {
	return &types.Policy{
		Enabled: true,
		Name:    "peer-to-resource-by-resource",
		Rules: []*types.PolicyRule{
			{
				Enabled:             true,
				Sources:             []string{sourceGroupID},
				DestinationResource: types.Resource{ID: resourceID, Type: types.ResourceTypeHost},
				Action:              types.PolicyTrafficActionAccept,
			},
		},
	}
}

// resolvePolicyAffected mirrors SavePolicy's resolution: resolve the affected
// peers for the given policy.
func (s *routerScenario) resolvePolicyAffected(ctx context.Context, policy *types.Policy) []string {
	change := affectedpeers.Change{Policies: []*types.Policy{policy}}
	snap, err := affectedpeers.Load(ctx, s.manager.Store, s.accountID, change)
	if err != nil {
		return nil
	}
	return snap.Expand(ctx, s.accountID, change)
}

func TestAffectedPeers_SourcePeer_DirectRouter(t *testing.T) {
	s := setupRouterScenario(t, true)
	ctx := context.Background()

	policy := peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID)
	affected := s.resolvePolicyAffected(ctx, policy)

	assert.Contains(t, affected, s.sourcePeerID, "source peer must be affected")
}

func TestAffectedPeers_RoutingPeer_DirectRouter(t *testing.T) {
	s := setupRouterScenario(t, true)
	ctx := context.Background()

	policy := peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID)
	affected := s.resolvePolicyAffected(ctx, policy)

	// BUG: the direct routing peer serves the resource's subnet to the source
	// peer, so it must be refreshed when the policy is created. The policy path
	// only resolves the literal rule groups (source group + resource group);
	// the resource group has no peer members and the router peer is reachable
	// only through the network, so it is dropped.
	assert.Contains(t, affected, s.routerPeerID,
		"routing peer (router.Peer) serving the resource must be affected by a policy granting access to it")
}

func TestAffectedPeers_RoutingPeer_RouterPeerGroups(t *testing.T) {
	s := setupRouterScenario(t, false)
	ctx := context.Background()

	policy := peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID)
	affected := s.resolvePolicyAffected(ctx, policy)

	// Router defined via PeerGroups instead of a direct peer.
	assert.Contains(t, affected, s.routerGroupPeerID,
		"routing peer (router.PeerGroups member) serving the resource must be affected")
}

func TestAffectedPeers_DestResource_RoutingPeer_DirectRouter(t *testing.T) {
	s := setupRouterScenario(t, true)
	ctx := context.Background()

	policy := peerToResourcePolicyByResource(s.sourceGroupID, s.resourceID)
	affected := s.resolvePolicyAffected(ctx, policy)

	// When the resource is referenced via DestinationResource, RuleGroups()
	// returns only the source group and the resource ID is not a peer, so
	// collectPolicyAffectedGroupsAndPeers yields nothing for the destination at
	// all. The routing peer is dropped here too.
	assert.Contains(t, affected, s.routerPeerID,
		"routing peer must be affected when the resource is referenced via DestinationResource")
}

func TestAffectedPeers_DestResource_RoutingPeer_RouterPeerGroups(t *testing.T) {
	s := setupRouterScenario(t, false)
	ctx := context.Background()

	policy := peerToResourcePolicyByResource(s.sourceGroupID, s.resourceID)
	affected := s.resolvePolicyAffected(ctx, policy)

	assert.Contains(t, affected, s.routerGroupPeerID,
		"routing peer (PeerGroups) must be affected when the resource is referenced via DestinationResource")
}

func TestAffectedPeers_SourceResourcePeer_RoutingPeer(t *testing.T) {
	s := setupRouterScenario(t, true)
	ctx := context.Background()

	// Source expressed as a direct peer (SourceResource), destination as resource group.
	policy := &types.Policy{
		Enabled: true,
		Name:    "sourceResource-peer-to-resource",
		Rules: []*types.PolicyRule{
			{
				Enabled:        true,
				SourceResource: types.Resource{ID: s.sourcePeerID, Type: types.ResourceTypePeer},
				Destinations:   []string{s.resourceGroupID},
				Action:         types.PolicyTrafficActionAccept,
			},
		},
	}
	affected := s.resolvePolicyAffected(ctx, policy)

	// The direct source peer IS picked up (collectPolicyAffectedGroupsAndPeers
	// handles SourceResource peers), but the routing peer is still missing.
	assert.Contains(t, affected, s.sourcePeerID, "direct source peer must be affected")
	assert.Contains(t, affected, s.routerPeerID, "routing peer must be affected")
}

func TestAffectedPeers_PolicyToResource_UnrelatedPeerNotAffected(t *testing.T) {
	s := setupRouterScenario(t, true)
	ctx := context.Background()

	policy := peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID)
	affected := s.resolvePolicyAffected(ctx, policy)

	// Guard against an over-broad fix: a peer in no relevant entity must never
	// be pulled in.
	assert.NotContains(t, affected, s.unrelatedPeerID, "unrelated peer must not be affected")
}

func TestAffectedPeers_ResourceSideBridgesToRoutingPeer_DirectRouter(t *testing.T) {
	s := setupRouterScenario(t, true)
	ctx := context.Background()

	// A pre-existing policy grants the source group access to the resource.
	_, err := s.manager.SavePolicy(ctx, s.accountID, userID, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID), true)
	require.NoError(t, err)

	// Drive an update through the resource manager and assert the routing peer
	// is among the affected set by observing the channel. This path walks
	// policies whose destinations reference the resource's groups, folds in the
	// source groups, and loads the network's routers, so it reaches both the
	// source peer and the routing peer.
	permissionsManager := permissions.NewManager(s.manager.Store)
	groupsManager := groups.NewManager(s.manager.Store, permissionsManager, s.manager)
	rm := resources.NewManager(s.manager.Store, permissionsManager, groupsManager, s.manager, s.manager.serviceManager)

	srcCh := s.updateManager.CreateChannel(ctx, s.sourcePeerID)
	routerCh := s.updateManager.CreateChannel(ctx, s.routerPeerID)
	t.Cleanup(func() {
		s.updateManager.CloseChannel(ctx, s.sourcePeerID)
		s.updateManager.CloseChannel(ctx, s.routerPeerID)
	})

	done := make(chan struct{})
	go func() {
		peerShouldReceiveUpdate(t, srcCh)
		peerShouldReceiveUpdate(t, routerCh)
		close(done)
	}()

	_, err = rm.UpdateResource(ctx, userID, &resourceTypes.NetworkResource{
		ID:        s.resourceID,
		AccountID: s.accountID,
		NetworkID: s.networkID,
		Name:      "rs-resource-host",
		Address:   "10.20.30.0/24",
		GroupIDs:  []string{s.resourceGroupID},
		Enabled:   true,
	})
	require.NoError(t, err)

	select {
	case <-done:
	case <-time.After(peerUpdateTimeout):
		t.Error("timeout: resource update did not refresh source peer + routing peer")
	}
}

// settleAffectedUpdates waits for in-flight async updates to arrive, then drains
// every given channel so subsequent assertions start from a clean slate.
//
// Setup (CreateNetwork/CreateResource/CreateRouter) fires async UpdateAffectedPeers
// goroutines; draining first means the assertion only observes updates from the
// action under test, not setup stragglers.
func settleAffectedUpdates(chans ...<-chan *network_map.UpdateMessage) {
	time.Sleep(300 * time.Millisecond)
	for _, ch := range chans {
		drainPeerUpdates(ch)
	}
}

func TestAffectedPeers_E2E_CreatePolicy_RoutingPeer_DirectRouter(t *testing.T) {
	s := setupRouterScenario(t, true)
	ctx := context.Background()

	srcCh := s.updateManager.CreateChannel(ctx, s.sourcePeerID)
	routerCh := s.updateManager.CreateChannel(ctx, s.routerPeerID)
	unrelatedCh := s.updateManager.CreateChannel(ctx, s.unrelatedPeerID)
	t.Cleanup(func() {
		s.updateManager.CloseChannel(ctx, s.sourcePeerID)
		s.updateManager.CloseChannel(ctx, s.routerPeerID)
		s.updateManager.CloseChannel(ctx, s.unrelatedPeerID)
	})

	settleAffectedUpdates(srcCh, routerCh, unrelatedCh)

	done := make(chan struct{})
	go func() {
		peerShouldReceiveUpdate(t, srcCh)
		peerShouldReceiveUpdate(t, routerCh)
		peerShouldNotReceiveUpdate(t, unrelatedCh)
		close(done)
	}()

	_, err := s.manager.SavePolicy(ctx, s.accountID, userID, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID), true)
	require.NoError(t, err)

	select {
	case <-done:
	case <-time.After(peerUpdateTimeout):
		t.Error("timeout: creating peer->resource policy did not refresh the routing peer")
	}
}

func TestAffectedPeers_E2E_CreatePolicy_RoutingPeer_RouterPeerGroups(t *testing.T) {
	s := setupRouterScenario(t, false)
	ctx := context.Background()

	srcCh := s.updateManager.CreateChannel(ctx, s.sourcePeerID)
	routerCh := s.updateManager.CreateChannel(ctx, s.routerGroupPeerID)
	t.Cleanup(func() {
		s.updateManager.CloseChannel(ctx, s.sourcePeerID)
		s.updateManager.CloseChannel(ctx, s.routerGroupPeerID)
	})

	settleAffectedUpdates(srcCh, routerCh)

	done := make(chan struct{})
	go func() {
		peerShouldReceiveUpdate(t, srcCh)
		peerShouldReceiveUpdate(t, routerCh)
		close(done)
	}()

	_, err := s.manager.SavePolicy(ctx, s.accountID, userID, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID), true)
	require.NoError(t, err)

	select {
	case <-done:
	case <-time.After(peerUpdateTimeout):
		t.Error("timeout: routing peer (PeerGroups) not refreshed on policy create")
	}
}

func TestAffectedPeers_E2E_DestResource_RoutingPeer(t *testing.T) {
	s := setupRouterScenario(t, true)
	ctx := context.Background()

	srcCh := s.updateManager.CreateChannel(ctx, s.sourcePeerID)
	routerCh := s.updateManager.CreateChannel(ctx, s.routerPeerID)
	t.Cleanup(func() {
		s.updateManager.CloseChannel(ctx, s.sourcePeerID)
		s.updateManager.CloseChannel(ctx, s.routerPeerID)
	})

	settleAffectedUpdates(srcCh, routerCh)

	done := make(chan struct{})
	go func() {
		peerShouldReceiveUpdate(t, srcCh)
		peerShouldReceiveUpdate(t, routerCh)
		close(done)
	}()

	_, err := s.manager.SavePolicy(ctx, s.accountID, userID, peerToResourcePolicyByResource(s.sourceGroupID, s.resourceID), true)
	require.NoError(t, err)

	select {
	case <-done:
	case <-time.After(peerUpdateTimeout):
		t.Error("timeout: routing peer not refreshed when policy targets DestinationResource")
	}
}

func TestAffectedPeers_E2E_DeletePolicy_RoutingPeer(t *testing.T) {
	s := setupRouterScenario(t, true)
	ctx := context.Background()

	policy, err := s.manager.SavePolicy(ctx, s.accountID, userID, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID), true)
	require.NoError(t, err)

	srcCh := s.updateManager.CreateChannel(ctx, s.sourcePeerID)
	routerCh := s.updateManager.CreateChannel(ctx, s.routerPeerID)
	t.Cleanup(func() {
		s.updateManager.CloseChannel(ctx, s.sourcePeerID)
		s.updateManager.CloseChannel(ctx, s.routerPeerID)
	})

	settleAffectedUpdates(srcCh, routerCh)

	done := make(chan struct{})
	go func() {
		peerShouldReceiveUpdate(t, srcCh)
		peerShouldReceiveUpdate(t, routerCh)
		close(done)
	}()

	require.NoError(t, s.manager.DeletePolicy(ctx, s.accountID, policy.ID, userID))

	select {
	case <-done:
	case <-time.After(peerUpdateTimeout):
		t.Error("timeout: deleting peer->resource policy did not refresh the routing peer")
	}
}

func (s *routerScenario) managers() (resources.Manager, routers.Manager, networks.Manager) {
	permissionsManager := permissions.NewManager(s.manager.Store)
	groupsManager := groups.NewManager(s.manager.Store, permissionsManager, s.manager)
	resourcesManager := resources.NewManager(s.manager.Store, permissionsManager, groupsManager, s.manager, s.manager.serviceManager)
	routersManager := routers.NewManager(s.manager.Store, permissionsManager, s.manager)
	networksManager := networks.NewManager(s.manager.Store, permissionsManager, resourcesManager, routersManager, s.manager)
	return resourcesManager, routersManager, networksManager
}

type secondTopology struct {
	networkID       string
	resourceID      string
	resourceGroupID string
	routerPeerID    string
}

func (s *routerScenario) addSecondTopology(t *testing.T, suffix string) secondTopology {
	t.Helper()
	ctx := context.Background()
	resourcesManager, routersManager, networksManager := s.managers()

	setupKey, err := s.manager.CreateSetupKey(ctx, s.accountID, "rs-key-"+suffix, types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
	require.NoError(t, err)
	routerPeer := addPeerToAccount(t, s.manager, s.accountID, setupKey.Key)

	resourceGroupID := "rs-resource-grp-" + suffix
	require.NoError(t, s.manager.CreateGroup(ctx, s.accountID, userID, &types.Group{
		ID: resourceGroupID, Name: "rs-resource-" + suffix,
	}))

	network, err := networksManager.CreateNetwork(ctx, userID, &networkTypes.Network{
		ID:        "rs-network-" + suffix,
		AccountID: s.accountID,
		Name:      "rs-network-" + suffix,
	})
	require.NoError(t, err)

	resource, err := resourcesManager.CreateResource(ctx, userID, &resourceTypes.NetworkResource{
		AccountID: s.accountID,
		NetworkID: network.ID,
		Name:      "rs-resource-host-" + suffix,
		Address:   "10.40.50.0/24",
		GroupIDs:  []string{resourceGroupID},
		Enabled:   true,
	})
	require.NoError(t, err)

	_, err = routersManager.CreateRouter(ctx, userID, &routerTypes.NetworkRouter{
		NetworkID:  network.ID,
		AccountID:  s.accountID,
		Peer:       routerPeer.ID,
		Masquerade: true,
		Metric:     9999,
		Enabled:    true,
	})
	require.NoError(t, err)

	return secondTopology{
		networkID:       network.ID,
		resourceID:      resource.ID,
		resourceGroupID: resourceGroupID,
		routerPeerID:    routerPeer.ID,
	}
}

func TestAffectedPeers_E2E_UpdatePolicy_BothRoutingPeers(t *testing.T) {
	s := setupRouterScenario(t, true)
	second := s.addSecondTopology(t, "b")
	ctx := context.Background()

	policy, err := s.manager.SavePolicy(ctx, s.accountID, userID, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID), true)
	require.NoError(t, err)

	srcCh := s.updateManager.CreateChannel(ctx, s.sourcePeerID)
	routerACh := s.updateManager.CreateChannel(ctx, s.routerPeerID)
	routerBCh := s.updateManager.CreateChannel(ctx, second.routerPeerID)
	t.Cleanup(func() {
		s.updateManager.CloseChannel(ctx, s.sourcePeerID)
		s.updateManager.CloseChannel(ctx, s.routerPeerID)
		s.updateManager.CloseChannel(ctx, second.routerPeerID)
	})

	settleAffectedUpdates(srcCh, routerACh, routerBCh)

	done := make(chan struct{})
	go func() {
		peerShouldReceiveUpdate(t, srcCh)
		peerShouldReceiveUpdate(t, routerACh)
		peerShouldReceiveUpdate(t, routerBCh)
		close(done)
	}()

	policy.Rules[0].Destinations = []string{second.resourceGroupID}
	_, err = s.manager.SavePolicy(ctx, s.accountID, userID, policy, false)
	require.NoError(t, err)

	select {
	case <-done:
	case <-time.After(peerUpdateTimeout):
		t.Error("timeout: re-pointing the policy destination did not refresh both routing peers")
	}
}

func TestAffectedPeers_E2E_UpdatePolicy_AddSource(t *testing.T) {
	s := setupRouterScenario(t, true)
	ctx := context.Background()

	const secondSourceGroupID = "rs-source-grp-2"
	setupKey, err := s.manager.CreateSetupKey(ctx, s.accountID, "rs-key-2", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
	require.NoError(t, err)
	secondSourcePeer := addPeerToAccount(t, s.manager, s.accountID, setupKey.Key)
	require.NoError(t, s.manager.CreateGroup(ctx, s.accountID, userID, &types.Group{
		ID: secondSourceGroupID, Name: "rs-source-2", Peers: []string{secondSourcePeer.ID},
	}))

	policy, err := s.manager.SavePolicy(ctx, s.accountID, userID, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID), true)
	require.NoError(t, err)

	newSrcCh := s.updateManager.CreateChannel(ctx, secondSourcePeer.ID)
	routerCh := s.updateManager.CreateChannel(ctx, s.routerPeerID)
	t.Cleanup(func() {
		s.updateManager.CloseChannel(ctx, secondSourcePeer.ID)
		s.updateManager.CloseChannel(ctx, s.routerPeerID)
	})

	settleAffectedUpdates(newSrcCh, routerCh)

	done := make(chan struct{})
	go func() {
		peerShouldReceiveUpdate(t, newSrcCh)
		peerShouldReceiveUpdate(t, routerCh)
		close(done)
	}()

	policy.Rules[0].Sources = []string{s.sourceGroupID, secondSourceGroupID}
	_, err = s.manager.SavePolicy(ctx, s.accountID, userID, policy, false)
	require.NoError(t, err)

	select {
	case <-done:
	case <-time.After(peerUpdateTimeout):
		t.Error("timeout: adding a source group did not refresh the new source peer + routing peer")
	}
}

func TestAffectedPeers_E2E_DestResource_RouterPeerGroups(t *testing.T) {
	s := setupRouterScenario(t, false)
	ctx := context.Background()

	srcCh := s.updateManager.CreateChannel(ctx, s.sourcePeerID)
	routerCh := s.updateManager.CreateChannel(ctx, s.routerGroupPeerID)
	t.Cleanup(func() {
		s.updateManager.CloseChannel(ctx, s.sourcePeerID)
		s.updateManager.CloseChannel(ctx, s.routerGroupPeerID)
	})

	settleAffectedUpdates(srcCh, routerCh)

	done := make(chan struct{})
	go func() {
		peerShouldReceiveUpdate(t, srcCh)
		peerShouldReceiveUpdate(t, routerCh)
		close(done)
	}()

	_, err := s.manager.SavePolicy(ctx, s.accountID, userID, peerToResourcePolicyByResource(s.sourceGroupID, s.resourceID), true)
	require.NoError(t, err)

	select {
	case <-done:
	case <-time.After(peerUpdateTimeout):
		t.Error("timeout: DestinationResource policy with PeerGroups router did not refresh the routing peer")
	}
}

func TestAffectedPeers_AllRoutingPeers_Network(t *testing.T) {
	s := setupRouterScenario(t, true)
	ctx := context.Background()

	_, routersManager, _ := s.managers()
	setupKey, err := s.manager.CreateSetupKey(ctx, s.accountID, "rs-key-r2", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
	require.NoError(t, err)
	secondRouterPeer := addPeerToAccount(t, s.manager, s.accountID, setupKey.Key)
	_, err = routersManager.CreateRouter(ctx, userID, &routerTypes.NetworkRouter{
		NetworkID:  s.networkID,
		AccountID:  s.accountID,
		Peer:       secondRouterPeer.ID,
		Masquerade: true,
		Metric:     9998,
		Enabled:    true,
	})
	require.NoError(t, err)

	affected := s.resolvePolicyAffected(ctx, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID))

	assert.Contains(t, affected, s.routerPeerID, "first routing peer must be affected")
	assert.Contains(t, affected, secondRouterPeer.ID, "second routing peer on the same network must also be affected")
}

// A disabled router in the snapshot routes to nobody, so it is skipped when the
// walk scans existing account data: a policy edit still folds the literal source
// group, but not the disabled router's peer.
func TestAffectedPeers_DisabledRouter(t *testing.T) {
	s := setupRouterScenario(t, true)
	ctx := context.Background()

	routers, err := s.manager.Store.GetNetworkRoutersByNetID(ctx, store.LockingStrengthNone, s.accountID, s.networkID)
	require.NoError(t, err)
	require.Len(t, routers, 1)
	routers[0].Enabled = false
	require.NoError(t, s.manager.Store.UpdateNetworkRouter(ctx, routers[0]))

	affected := s.resolvePolicyAffected(ctx, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID))

	assert.Contains(t, affected, s.sourcePeerID, "source peer (literal policy source group) must be affected")
	assert.NotContains(t, affected, s.routerPeerID,
		"a disabled router routes to nobody, so its peer must not be folded from snapshot data")
}

// A disabled resource in the snapshot is skipped: the policy edit still folds the
// literal source group, but the resource no longer bridges to its network's router.
func TestAffectedPeers_DisabledResource(t *testing.T) {
	s := setupRouterScenario(t, true)
	ctx := context.Background()

	res, err := s.manager.Store.GetNetworkResourceByID(ctx, store.LockingStrengthNone, s.accountID, s.resourceID)
	require.NoError(t, err)
	res.Enabled = false
	require.NoError(t, s.manager.Store.SaveNetworkResource(ctx, res))

	affected := s.resolvePolicyAffected(ctx, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID))

	assert.Contains(t, affected, s.sourcePeerID, "source peer (literal policy source group) must be affected")
	assert.NotContains(t, affected, s.routerPeerID,
		"a disabled resource routes to nobody, so its network's router must not be folded from snapshot data")
}

func TestAffectedPeers_DisabledRule(t *testing.T) {
	s := setupRouterScenario(t, true)
	ctx := context.Background()

	policy := peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID)
	policy.Rules[0].Enabled = false

	affected := s.resolvePolicyAffected(ctx, policy)

	assert.Contains(t, affected, s.routerPeerID,
		"disabled rule must still resolve the routing peer: Enabled must not gate affected-peers")
}

func TestAffectedPeers_MultiRule(t *testing.T) {
	s := setupRouterScenario(t, true)
	second := s.addSecondTopology(t, "c")
	ctx := context.Background()

	policy := &types.Policy{
		Enabled: true,
		Name:    "multi-rule-two-resources",
		Rules: []*types.PolicyRule{
			{
				Enabled:      true,
				Sources:      []string{s.sourceGroupID},
				Destinations: []string{s.resourceGroupID},
				Action:       types.PolicyTrafficActionAccept,
			},
			{
				Enabled:      true,
				Sources:      []string{s.sourceGroupID},
				Destinations: []string{second.resourceGroupID},
				Action:       types.PolicyTrafficActionAccept,
			},
		},
	}

	affected := s.resolvePolicyAffected(ctx, policy)

	assert.Contains(t, affected, s.routerPeerID, "routing peer for resource A must be affected")
	assert.Contains(t, affected, second.routerPeerID, "routing peer for resource B must be affected")
}

func TestAffectedPeers_RouterOtherNetwork(t *testing.T) {
	s := setupRouterScenario(t, true)
	second := s.addSecondTopology(t, "d")
	ctx := context.Background()

	affected := s.resolvePolicyAffected(ctx, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID))

	assert.Contains(t, affected, s.routerPeerID, "network A's routing peer must be affected")
	assert.NotContains(t, affected, second.routerPeerID,
		"a router in an unrelated network must not be affected by a policy that does not target its resource")
}
