package integration

import (
	"fmt"
	"net/netip"
	"strings"
	"testing"
	"time"

	policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
	"github.com/juanfont/headscale/hscontrol/types"
	"github.com/juanfont/headscale/integration/hsic"
	"github.com/juanfont/headscale/integration/tsic"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"tailscale.com/tailcfg"
	"tailscale.com/wgengine/filter"
)

// hasCapMatchInPacketFilter checks if any [filter.Match] entry in the packet
// filter contains a [filter.CapMatch] with the given capability name.
func hasCapMatchInPacketFilter(pf []filter.Match, peerCap tailcfg.PeerCapability) bool {
	for _, m := range pf {
		for _, cm := range m.Caps {
			if cm.Cap == peerCap {
				return true
			}
		}
	}

	return false
}

// hasCapMatchForIP checks if any [filter.CapMatch] with the given capability
// has a Dst prefix that contains the given IP. This validates that
// the cap is directed at the correct node, not just present.
func hasCapMatchForIP(pf []filter.Match, peerCap tailcfg.PeerCapability, ip netip.Addr) bool {
	for _, m := range pf {
		for _, cm := range m.Caps {
			if cm.Cap == peerCap && cm.Dst.Contains(ip) {
				return true
			}
		}
	}

	return false
}

// parsePeerRelay parses a PeerRelay string of the form "ip:port:vni:N"
// and returns the [netip.AddrPort] and VNI. Returns zero values on parse failure.
func parsePeerRelay(pr string) (netip.AddrPort, string, bool) {
	// Format: "172.18.0.4:58738:vni:1"
	// Split into: host part "172.18.0.4:58738" and vni part "vni:1"
	addrStr, vni, ok := strings.Cut(pr, ":vni:")
	if !ok {
		return netip.AddrPort{}, "", false
	}

	ap, err := netip.ParseAddrPort(addrStr)
	if err != nil {
		return netip.AddrPort{}, "", false
	}

	return ap, vni, true
}

// TestGrantCapRelay validates the full peer relay lifecycle:
//  1. No direct connection between isolated clients
//  2. Cap grants compile correctly (relay + relay-target in packet filters)
//     with strict directionality and negative checks
//  3. Peer relay is used instead of DERP (PeerRelay non-empty, valid format)
//  4. Relay goes down -> fallback to DERP (PeerRelay empty, Relay non-empty,
//     DERP ping works)
//  5. Relay comes back up -> peer relay resumes (PeerRelay non-empty again)
func TestGrantCapRelay(t *testing.T) {
	IntegrationSkip(t)

	assertTimeout := 120 * time.Second

	spec := ScenarioSpec{
		NodesPerUser: 0,
		Users:        []string{"relay", "clienta", "clientb"},
		Networks: map[string]NetworkSpec{
			"usernet1": {Users: []string{"clienta"}},
			"usernet2": {Users: []string{"clientb"}},
			"usernet3": {Users: []string{"relay"}},
		},
		Versions: []string{"head"},
	}

	scenario, err := NewScenario(spec)

	require.NoErrorf(t, err, "failed to create scenario: %s", err)
	defer scenario.ShutdownAssertNoPanics(t)

	pol := &policyv2.Policy{
		TagOwners: policyv2.TagOwners{
			policyv2.Tag("tag:relay"):    policyv2.Owners{usernameOwner("relay@")},
			policyv2.Tag("tag:client-a"): policyv2.Owners{usernameOwner("clienta@")},
			policyv2.Tag("tag:client-b"): policyv2.Owners{usernameOwner("clientb@")},
		},
		Grants: []policyv2.Grant{
			// Grant 1: Basic IP connectivity between all tagged nodes.
			{
				Sources: policyv2.Aliases{
					tagp("tag:relay"), tagp("tag:client-a"), tagp("tag:client-b"),
				},
				Destinations: policyv2.Aliases{
					tagp("tag:relay"), tagp("tag:client-a"), tagp("tag:client-b"),
				},
				InternetProtocols: []policyv2.ProtocolPort{
					{Protocol: "*", Ports: []tailcfg.PortRange{tailcfg.PortRangeAny}},
				},
			},
			// Grant 2: Relay cap - clients can use relay node for UDP relaying.
			// This generates cap/relay on the relay's filter and cap/relay-target
			// (companion) on the clients' filters.
			{
				Sources:      policyv2.Aliases{tagp("tag:client-a"), tagp("tag:client-b")},
				Destinations: policyv2.Aliases{tagp("tag:relay")},
				App: tailcfg.PeerCapMap{
					tailcfg.PeerCapabilityRelay: {tailcfg.RawMessage("{}")},
				},
			},
		},
	}

	headscale, err := scenario.Headscale(
		hsic.WithTestName("grant-cap-relay"),
		hsic.WithACLPolicy(pol),
		hsic.WithPolicyMode(types.PolicyModeDB),
	)
	requireNoErrGetHeadscale(t, err)

	usernet1, err := scenario.Network("usernet1")
	require.NoError(t, err)
	usernet2, err := scenario.Network("usernet2")
	require.NoError(t, err)
	usernet3, err := scenario.Network("usernet3")
	require.NoError(t, err)

	// Create users on headscale server.
	_, err = scenario.CreateUser("relay")
	require.NoError(t, err)
	_, err = scenario.CreateUser("clienta")
	require.NoError(t, err)
	_, err = scenario.CreateUser("clientb")
	require.NoError(t, err)

	userMap, err := headscale.MapUsers()
	require.NoError(t, err)

	// --- Create Relay R on usernet3, dual-homed to usernet1+usernet2 ---
	relayR, err := scenario.CreateTailscaleNode("head",
		tsic.WithNetwork(usernet3),
	)
	require.NoError(t, err)

	defer func() { _, _, _ = relayR.Shutdown() }()

	pakRelay, err := scenario.CreatePreAuthKeyWithTags(
		userMap["relay"].GetId(), false, false, []string{"tag:relay"},
	)
	require.NoError(t, err)
	err = relayR.Login(headscale.GetEndpoint(), pakRelay.GetKey())
	require.NoError(t, err)
	err = relayR.WaitForRunning(30 * time.Second)
	require.NoError(t, err)

	// Dual-home after registration to avoid duplicate node key generation
	// from Docker network interface changes during tailscaled startup.
	err = relayR.ConnectToNetwork(usernet1)
	require.NoError(t, err)
	err = relayR.ConnectToNetwork(usernet2)
	require.NoError(t, err)

	// Enable the relay server on the relay node. Without this, the
	// relayserver extension loads but RelayServerPort is nil and the
	// server never starts listening for allocation requests.
	// Port 0 = random unused port.
	_, _, err = relayR.Execute([]string{
		"tailscale", "set", "--relay-server-port=0",
	})
	require.NoError(t, err)

	// --- Create Client A on usernet1 only ---
	clientA, err := scenario.CreateTailscaleNode("head",
		tsic.WithNetwork(usernet1),
	)
	require.NoError(t, err)

	defer func() { _, _, _ = clientA.Shutdown() }()

	pakClientA, err := scenario.CreatePreAuthKeyWithTags(
		userMap["clienta"].GetId(), false, false, []string{"tag:client-a"},
	)
	require.NoError(t, err)
	err = clientA.Login(headscale.GetEndpoint(), pakClientA.GetKey())
	require.NoError(t, err)
	err = clientA.WaitForRunning(30 * time.Second)
	require.NoError(t, err)

	// --- Create Client B on usernet2 only ---
	clientB, err := scenario.CreateTailscaleNode("head",
		tsic.WithNetwork(usernet2),
	)
	require.NoError(t, err)

	defer func() { _, _, _ = clientB.Shutdown() }()

	pakClientB, err := scenario.CreatePreAuthKeyWithTags(
		userMap["clientb"].GetId(), false, false, []string{"tag:client-b"},
	)
	require.NoError(t, err)
	err = clientB.Login(headscale.GetEndpoint(), pakClientB.GetKey())
	require.NoError(t, err)
	err = clientB.WaitForRunning(30 * time.Second)
	require.NoError(t, err)

	// ===== Phase 1: Validate isolation and peer visibility =====
	t.Log("Phase 1: Validate network isolation and peer visibility")

	allNodes := []TailscaleClient{relayR, clientA, clientB}
	for _, node := range allNodes {
		err = node.WaitForPeers(len(allNodes)-1, 60*time.Second, 1*time.Second)
		require.NoErrorf(t, err, "node %s failed to see all peers", node.Hostname())
	}

	// Restart all nodes to ensure fresh wireguard config. When nodes
	// register sequentially, early peers may arrive without DERP info
	// and get permanently skipped in wireguard config.
	for _, node := range allNodes {
		require.NoError(t, node.Restart())
		require.NoError(t, node.WaitForRunning(30*time.Second))
	}

	for _, node := range allNodes {
		err = node.WaitForPeers(len(allNodes)-1, 60*time.Second, 1*time.Second)
		require.NoErrorf(t, err, "node %s failed to see all peers after restart", node.Hostname())
	}

	// Capture keys and IPs for assertions.
	clientBKey := clientB.MustStatus().Self.PublicKey
	clientAKey := clientA.MustStatus().Self.PublicKey
	relayIPv4 := relayR.MustIPv4()
	clientAIPv4 := clientA.MustIPv4()
	clientBIPv4 := clientB.MustIPv4()

	// Verify no direct path between A and B.
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		status, err := clientA.Status()
		assert.NoError(c, err)

		peerB := status.Peer[clientBKey]
		assert.NotNil(c, peerB, "A should see B as a peer")

		if peerB != nil {
			assert.Empty(c, peerB.CurAddr, "A->B should have no direct path")
		}
	}, assertTimeout, 500*time.Millisecond, "A should have no direct path to B")

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		status, err := clientB.Status()
		assert.NoError(c, err)

		peerA := status.Peer[clientAKey]
		assert.NotNil(c, peerA, "B should see A as a peer")

		if peerA != nil {
			assert.Empty(c, peerA.CurAddr, "B->A should have no direct path")
		}
	}, assertTimeout, 500*time.Millisecond, "B should have no direct path to A")

	// ===== Phase 2: Validate cap grants in packet filters =====
	t.Log("Phase 2: Validate cap grants in packet filters")

	// --- Positive checks: correct caps on correct nodes ---

	// Relay R should have cap/relay targeting the relay's own IP.
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		pf, err := relayR.PacketFilter()
		assert.NoError(c, err)
		assert.True(c, hasCapMatchForIP(pf, tailcfg.PeerCapabilityRelay, relayIPv4),
			"Relay R should have cap/relay with Dst matching relay's IP %s", relayIPv4)
	}, assertTimeout, 500*time.Millisecond, "R should have cap/relay targeting its own IP")

	// Client A should have cap/relay-target targeting client A's IP.
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		pf, err := clientA.PacketFilter()
		assert.NoError(c, err)
		assert.True(c, hasCapMatchForIP(pf, tailcfg.PeerCapabilityRelayTarget, clientAIPv4),
			"Client A should have cap/relay-target with Dst matching A's IP %s", clientAIPv4)
	}, assertTimeout, 500*time.Millisecond, "A should have cap/relay-target targeting its own IP")

	// Client B should have cap/relay-target targeting client B's IP.
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		pf, err := clientB.PacketFilter()
		assert.NoError(c, err)
		assert.True(c, hasCapMatchForIP(pf, tailcfg.PeerCapabilityRelayTarget, clientBIPv4),
			"Client B should have cap/relay-target with Dst matching B's IP %s", clientBIPv4)
	}, assertTimeout, 500*time.Millisecond, "B should have cap/relay-target targeting its own IP")

	// --- Negative checks: wrong caps must NOT be present ---

	// Relay R should NOT have cap/relay-target (it's a relay server, not a target).
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		pf, err := relayR.PacketFilter()
		assert.NoError(c, err)
		assert.False(c, hasCapMatchInPacketFilter(pf, tailcfg.PeerCapabilityRelayTarget),
			"Relay R should NOT have cap/relay-target")
	}, 10*time.Second, 500*time.Millisecond, "R should not have cap/relay-target")

	// Client A should NOT have cap/relay (it's a client, not a relay server).
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		pf, err := clientA.PacketFilter()
		assert.NoError(c, err)
		assert.False(c, hasCapMatchInPacketFilter(pf, tailcfg.PeerCapabilityRelay),
			"Client A should NOT have cap/relay")
	}, 10*time.Second, 500*time.Millisecond, "A should not have cap/relay")

	// Client B should NOT have cap/relay (it's a client, not a relay server).
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		pf, err := clientB.PacketFilter()
		assert.NoError(c, err)
		assert.False(c, hasCapMatchInPacketFilter(pf, tailcfg.PeerCapabilityRelay),
			"Client B should NOT have cap/relay")
	}, 10*time.Second, 500*time.Millisecond, "B should not have cap/relay")

	// ===== Phase 3: Validate peer relay active (not DERP) =====
	t.Log("Phase 3: Validate peer relay active (not DERP)")

	// Verify PeerRelay is set with valid format and correct relay IPs.
	// Relay endpoint allocation is triggered by traffic between peers,
	// so we send pings in the check loop to initiate relay discovery.
	var peerRelayAtoB, peerRelayBtoA string

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		// Fire a ping to trigger relay path discovery (ignore output).
		clientA.Execute([]string{"tailscale", "ping", "--c=1", "--timeout=1s", clientBIPv4.String()}) //nolint:errcheck

		status, err := clientA.Status()
		assert.NoError(c, err)

		peerB := status.Peer[clientBKey]
		assert.NotNil(c, peerB, "A should see B as a peer")

		if peerB != nil {
			assert.NotEmpty(c, peerB.PeerRelay,
				"A->B should use peer relay, not DERP")
			assert.Empty(c, peerB.CurAddr,
				"A->B should not have direct connection")

			if peerB.PeerRelay != "" {
				peerRelayAtoB = peerB.PeerRelay

				// Validate PeerRelay format: ip:port:vni:N
				ap, vni, ok := parsePeerRelay(peerB.PeerRelay)
				assert.True(c, ok,
					"PeerRelay %q should be parseable as ip:port:vni:N", peerB.PeerRelay)

				if ok {
					assert.NotZero(c, ap.Port(),
						"PeerRelay port should be non-zero")
					assert.NotEmpty(c, vni,
						"PeerRelay VNI should be non-empty")
				}
			}

			t.Logf("Phase 3 - A->B: PeerRelay=%q Relay=%q CurAddr=%q Active=%v",
				peerB.PeerRelay, peerB.Relay, peerB.CurAddr, peerB.Active)
		}
	}, assertTimeout, 2*time.Second, "A should show peer relay to B")

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		clientB.Execute([]string{"tailscale", "ping", "--c=1", "--timeout=1s", clientAIPv4.String()}) //nolint:errcheck

		status, err := clientB.Status()
		assert.NoError(c, err)

		peerA := status.Peer[clientAKey]
		assert.NotNil(c, peerA, "B should see A as a peer")

		if peerA != nil {
			assert.NotEmpty(c, peerA.PeerRelay,
				"B->A should use peer relay, not DERP")

			if peerA.PeerRelay != "" {
				peerRelayBtoA = peerA.PeerRelay

				ap, vni, ok := parsePeerRelay(peerA.PeerRelay)
				assert.True(c, ok,
					"PeerRelay %q should be parseable as ip:port:vni:N", peerA.PeerRelay)

				if ok {
					assert.NotZero(c, ap.Port(),
						"PeerRelay port should be non-zero")
					assert.NotEmpty(c, vni,
						"PeerRelay VNI should be non-empty")
				}
			}

			t.Logf("Phase 3 - B->A: PeerRelay=%q Relay=%q CurAddr=%q Active=%v",
				peerA.PeerRelay, peerA.Relay, peerA.CurAddr, peerA.Active)
		}
	}, assertTimeout, 2*time.Second, "B should show peer relay to A")

	// Cross-validate: both directions should use the same VNI
	// (same relay allocation) but different IPs (dual-homed relay).
	if peerRelayAtoB != "" && peerRelayBtoA != "" {
		apA, vniA, okA := parsePeerRelay(peerRelayAtoB)

		apB, vniB, okB := parsePeerRelay(peerRelayBtoA)
		if okA && okB {
			assert.Equal(t, vniA, vniB,
				"A->B and B->A should share the same VNI (same relay allocation)")
			assert.Equal(t, apA.Port(), apB.Port(),
				"A->B and B->A should use the same relay port")
			assert.NotEqual(t, apA.Addr(), apB.Addr(),
				"A->B and B->A relay IPs should differ (dual-homed relay)")
		}
	}

	// ===== Phase 4: Bring relay down -> DERP fallback =====
	t.Log("Phase 4: Bring relay down, expect DERP fallback")

	require.NoError(t, relayR.Down())

	// Verify PeerRelay is gone and DERP is used.
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		status, err := clientA.Status()
		assert.NoError(c, err)

		peerB := status.Peer[clientBKey]
		assert.NotNil(c, peerB, "A should still see B as a peer")

		if peerB != nil {
			assert.Empty(c, peerB.PeerRelay,
				"A->B peer relay should be gone")
			assert.NotEmpty(c, peerB.Relay,
				"A->B should fall back to DERP")
			t.Logf("Phase 4 - A->B: PeerRelay=%q Relay=%q CurAddr=%q Active=%v",
				peerB.PeerRelay, peerB.Relay, peerB.CurAddr, peerB.Active)
		}
	}, assertTimeout, 500*time.Millisecond, "A should fall back to DERP for B")

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		status, err := clientB.Status()
		assert.NoError(c, err)

		peerA := status.Peer[clientAKey]
		assert.NotNil(c, peerA, "B should still see A as a peer")

		if peerA != nil {
			assert.Empty(c, peerA.PeerRelay,
				"B->A peer relay should be gone")
			t.Logf("Phase 4 - B->A: PeerRelay=%q Relay=%q CurAddr=%q Active=%v",
				peerA.PeerRelay, peerA.Relay, peerA.CurAddr, peerA.Active)
		}
	}, assertTimeout, 500*time.Millisecond, "B should fall back to DERP for A")

	// Verify data plane works via DERP after relay is down.
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		err := clientA.Ping(
			clientBIPv4.String(),
			tsic.WithPingUntilDirect(false),
			tsic.WithPingTimeout(2*time.Second),
			tsic.WithPingCount(1),
		)
		assert.NoError(c, err)
	}, assertTimeout, 1*time.Second, "A should reach B via DERP after relay down")

	// ===== Phase 5: Bring relay back up -> peer relay resumes =====
	t.Log("Phase 5: Bring relay back up, expect peer relay to resume")

	require.NoError(t, relayR.Up())

	err = relayR.WaitForRunning(30 * time.Second)
	require.NoError(t, err)

	// Verify peer relay resumes. Ping to trigger relay re-discovery.
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		clientA.Execute([]string{"tailscale", "ping", "--c=1", "--timeout=1s", clientBIPv4.String()}) //nolint:errcheck

		status, err := clientA.Status()
		assert.NoError(c, err)

		peerB := status.Peer[clientBKey]
		assert.NotNil(c, peerB, "A should see B as a peer")

		if peerB != nil {
			assert.NotEmpty(c, peerB.PeerRelay,
				"A->B peer relay should resume after R comes back")
			t.Logf("Phase 5 - A->B: PeerRelay=%q Relay=%q CurAddr=%q Active=%v",
				peerB.PeerRelay, peerB.Relay, peerB.CurAddr, peerB.Active)
		}
	}, assertTimeout, 2*time.Second, "A should resume peer relay to B")

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		clientB.Execute([]string{"tailscale", "ping", "--c=1", "--timeout=1s", clientAIPv4.String()}) //nolint:errcheck

		status, err := clientB.Status()
		assert.NoError(c, err)

		peerA := status.Peer[clientAKey]
		assert.NotNil(c, peerA, "B should see A as a peer")

		if peerA != nil {
			assert.NotEmpty(c, peerA.PeerRelay,
				"B->A peer relay should resume after R comes back")
			t.Logf("Phase 5 - B->A: PeerRelay=%q Relay=%q CurAddr=%q Active=%v",
				peerA.PeerRelay, peerA.Relay, peerA.CurAddr, peerA.Active)
		}
	}, assertTimeout, 2*time.Second, "B should resume peer relay to A")
}

// driveURL constructs a Taildrive WebDAV URL via the local proxy.
func driveURL(domain, sharerName, path string) string {
	return fmt.Sprintf(
		"http://100.100.100.100:8080/%s/%s/%s",
		domain, sharerName, path,
	)
}

// TestGrantCapDrive validates Taildrive (cap/drive) grant-based access control:
//  1. Node attributes (drive:share, drive:access) are set in all nodes' CapMap
//  2. Cap grants compile correctly (cap/drive + cap/drive-sharer in packet filters)
//  3. RW client can read, write, and delete files on the sharer
//  4. RO client can read but NOT write or delete files on the sharer
//  5. No-access node (no cap/drive grant) cannot read or write files
func TestGrantCapDrive(t *testing.T) {
	IntegrationSkip(t)

	assertTimeout := 120 * time.Second

	spec := ScenarioSpec{
		NodesPerUser: 0,
		Users:        []string{"sharer", "rwclient", "roclient", "noaccess"},
		Networks: map[string]NetworkSpec{
			"usernet1": {Users: []string{"sharer", "rwclient", "roclient", "noaccess"}},
		},
		Versions: []string{"head"},
	}

	scenario, err := NewScenario(spec)

	require.NoErrorf(t, err, "failed to create scenario: %s", err)
	defer scenario.ShutdownAssertNoPanics(t)

	pol := &policyv2.Policy{
		TagOwners: policyv2.TagOwners{
			policyv2.Tag("tag:sharer"):    policyv2.Owners{usernameOwner("sharer@")},
			policyv2.Tag("tag:rw-client"): policyv2.Owners{usernameOwner("rwclient@")},
			policyv2.Tag("tag:ro-client"): policyv2.Owners{usernameOwner("roclient@")},
			policyv2.Tag("tag:no-access"): policyv2.Owners{usernameOwner("noaccess@")},
		},
		// Taildrive caps (drive:share / drive:access) are policy-driven
		// per https://tailscale.com/docs/features/taildrive; no longer
		// emitted as TailNode baseline. Stamp on every node so the
		// SelfNode.CapMap assertions below remain meaningful.
		NodeAttrs: []policyv2.NodeAttrGrant{
			{
				Targets: policyv2.Aliases{policyv2.Wildcard},
				Attrs: []tailcfg.NodeCapability{
					tailcfg.NodeAttrsTaildriveShare,
					tailcfg.NodeAttrsTaildriveAccess,
				},
			},
		},
		Grants: []policyv2.Grant{
			// Grant 1: IP connectivity between ALL nodes.
			{
				Sources: policyv2.Aliases{
					tagp("tag:sharer"), tagp("tag:rw-client"),
					tagp("tag:ro-client"), tagp("tag:no-access"),
				},
				Destinations: policyv2.Aliases{
					tagp("tag:sharer"), tagp("tag:rw-client"),
					tagp("tag:ro-client"), tagp("tag:no-access"),
				},
				InternetProtocols: []policyv2.ProtocolPort{
					{Protocol: "*", Ports: []tailcfg.PortRange{tailcfg.PortRangeAny}},
				},
			},
			// Grant 2: cap/drive RW - rw-client can read+write sharer's drives.
			{
				Sources:      policyv2.Aliases{tagp("tag:rw-client")},
				Destinations: policyv2.Aliases{tagp("tag:sharer")},
				App: tailcfg.PeerCapMap{
					tailcfg.PeerCapabilityTaildrive: {
						tailcfg.RawMessage(`{"shares":["*"],"access":"rw"}`),
					},
				},
			},
			// Grant 3: cap/drive RO - ro-client can only read sharer's drives.
			{
				Sources:      policyv2.Aliases{tagp("tag:ro-client")},
				Destinations: policyv2.Aliases{tagp("tag:sharer")},
				App: tailcfg.PeerCapMap{
					tailcfg.PeerCapabilityTaildrive: {
						tailcfg.RawMessage(`{"shares":["*"],"access":"ro"}`),
					},
				},
			},
			// NO cap/drive grant for tag:no-access (intentional).
		},
	}

	headscale, err := scenario.Headscale(
		hsic.WithTestName("grant-cap-drive"),
		hsic.WithACLPolicy(pol),
		hsic.WithPolicyMode(types.PolicyModeDB),
	)
	requireNoErrGetHeadscale(t, err)

	usernet1, err := scenario.Network("usernet1")
	require.NoError(t, err)

	// Create users on headscale server.
	_, err = scenario.CreateUser("sharer")
	require.NoError(t, err)
	_, err = scenario.CreateUser("rwclient")
	require.NoError(t, err)
	_, err = scenario.CreateUser("roclient")
	require.NoError(t, err)
	_, err = scenario.CreateUser("noaccess")
	require.NoError(t, err)

	userMap, err := headscale.MapUsers()
	require.NoError(t, err)

	// --- Create Sharer node ---
	sharer, err := scenario.CreateTailscaleNode("head",
		tsic.WithNetwork(usernet1),
	)
	require.NoError(t, err)

	defer func() { _, _, _ = sharer.Shutdown() }()

	pakSharer, err := scenario.CreatePreAuthKeyWithTags(
		userMap["sharer"].GetId(), false, false, []string{"tag:sharer"},
	)
	require.NoError(t, err)
	err = sharer.Login(headscale.GetEndpoint(), pakSharer.GetKey())
	require.NoError(t, err)
	err = sharer.WaitForRunning(30 * time.Second)
	require.NoError(t, err)

	// --- Create RW client node ---
	rwClient, err := scenario.CreateTailscaleNode("head",
		tsic.WithNetwork(usernet1),
	)
	require.NoError(t, err)

	defer func() { _, _, _ = rwClient.Shutdown() }()

	pakRW, err := scenario.CreatePreAuthKeyWithTags(
		userMap["rwclient"].GetId(), false, false, []string{"tag:rw-client"},
	)
	require.NoError(t, err)
	err = rwClient.Login(headscale.GetEndpoint(), pakRW.GetKey())
	require.NoError(t, err)
	err = rwClient.WaitForRunning(30 * time.Second)
	require.NoError(t, err)

	// --- Create RO client node ---
	roClient, err := scenario.CreateTailscaleNode("head",
		tsic.WithNetwork(usernet1),
	)
	require.NoError(t, err)

	defer func() { _, _, _ = roClient.Shutdown() }()

	pakRO, err := scenario.CreatePreAuthKeyWithTags(
		userMap["roclient"].GetId(), false, false, []string{"tag:ro-client"},
	)
	require.NoError(t, err)
	err = roClient.Login(headscale.GetEndpoint(), pakRO.GetKey())
	require.NoError(t, err)
	err = roClient.WaitForRunning(30 * time.Second)
	require.NoError(t, err)

	// --- Create No-access node ---
	noAccess, err := scenario.CreateTailscaleNode("head",
		tsic.WithNetwork(usernet1),
	)
	require.NoError(t, err)

	defer func() { _, _, _ = noAccess.Shutdown() }()

	pakNA, err := scenario.CreatePreAuthKeyWithTags(
		userMap["noaccess"].GetId(), false, false, []string{"tag:no-access"},
	)
	require.NoError(t, err)
	err = noAccess.Login(headscale.GetEndpoint(), pakNA.GetKey())
	require.NoError(t, err)
	err = noAccess.WaitForRunning(30 * time.Second)
	require.NoError(t, err)

	// ===== Phase 1: Wait for all peers =====
	t.Log("Phase 1: Wait for all peers to be visible")

	allNodes := []TailscaleClient{sharer, rwClient, roClient, noAccess}
	for _, node := range allNodes {
		err = node.WaitForPeers(len(allNodes)-1, 60*time.Second, 1*time.Second)
		require.NoErrorf(t, err, "node %s failed to see all peers", node.Hostname())
	}

	sharerIPv4 := sharer.MustIPv4()

	// ===== Phase 2: Validate node attributes (self CapMap) =====
	t.Log("Phase 2: Validate Taildrive node attributes in CapMap")

	for _, node := range allNodes {
		assert.EventuallyWithT(t, func(c *assert.CollectT) {
			nm, err := node.Netmap()
			assert.NoError(c, err)

			if nm == nil {
				return
			}

			assert.True(c, nm.SelfNode.Valid(),
				"%s: SelfNode should be valid", node.Hostname())

			if nm.SelfNode.Valid() {
				assert.True(c, nm.SelfNode.HasCap(tailcfg.NodeAttrsTaildriveShare),
					"%s: should have drive:share cap", node.Hostname())
				assert.True(c, nm.SelfNode.HasCap(tailcfg.NodeAttrsTaildriveAccess),
					"%s: should have drive:access cap", node.Hostname())
			}
		}, assertTimeout, 500*time.Millisecond,
			"all nodes should have Taildrive node attributes")
	}

	// ===== Phase 3: Validate cap grants in packet filters =====
	t.Log("Phase 3: Validate cap/drive grants in packet filters")

	// --- Positive checks ---

	// Sharer should have cap/drive targeting its own IP (it's the drive destination).
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		pf, err := sharer.PacketFilter()
		assert.NoError(c, err)
		assert.True(c, hasCapMatchForIP(pf, tailcfg.PeerCapabilityTaildrive, sharerIPv4),
			"Sharer should have cap/drive with Dst matching sharer's IP %s", sharerIPv4)
	}, assertTimeout, 500*time.Millisecond, "sharer should have cap/drive targeting its own IP")

	// RW client should have cap/drive-sharer (companion) targeting rw-client's IP.
	rwClientIPv4 := rwClient.MustIPv4()

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		pf, err := rwClient.PacketFilter()
		assert.NoError(c, err)
		assert.True(c, hasCapMatchForIP(pf, tailcfg.PeerCapabilityTaildriveSharer, rwClientIPv4),
			"RW client should have cap/drive-sharer with Dst matching rw-client's IP %s", rwClientIPv4)
	}, assertTimeout, 500*time.Millisecond, "rw-client should have cap/drive-sharer")

	// RO client should have cap/drive-sharer (companion) targeting ro-client's IP.
	roClientIPv4 := roClient.MustIPv4()

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		pf, err := roClient.PacketFilter()
		assert.NoError(c, err)
		assert.True(c, hasCapMatchForIP(pf, tailcfg.PeerCapabilityTaildriveSharer, roClientIPv4),
			"RO client should have cap/drive-sharer with Dst matching ro-client's IP %s", roClientIPv4)
	}, assertTimeout, 500*time.Millisecond, "ro-client should have cap/drive-sharer")

	// --- Negative checks ---

	// No-access node should NOT have cap/drive or cap/drive-sharer.
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		pf, err := noAccess.PacketFilter()
		assert.NoError(c, err)
		assert.False(c, hasCapMatchInPacketFilter(pf, tailcfg.PeerCapabilityTaildrive),
			"no-access should NOT have cap/drive")
		assert.False(c, hasCapMatchInPacketFilter(pf, tailcfg.PeerCapabilityTaildriveSharer),
			"no-access should NOT have cap/drive-sharer")
	}, 10*time.Second, 500*time.Millisecond, "no-access should have no drive caps")

	// Sharer should NOT have cap/drive-sharer (it's a destination, not a source).
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		pf, err := sharer.PacketFilter()
		assert.NoError(c, err)
		assert.False(c, hasCapMatchInPacketFilter(pf, tailcfg.PeerCapabilityTaildriveSharer),
			"sharer should NOT have cap/drive-sharer")
	}, 10*time.Second, 500*time.Millisecond, "sharer should not have cap/drive-sharer")

	// RW client should NOT have cap/drive (it's a source, not a destination).
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		pf, err := rwClient.PacketFilter()
		assert.NoError(c, err)
		assert.False(c, hasCapMatchInPacketFilter(pf, tailcfg.PeerCapabilityTaildrive),
			"rw-client should NOT have cap/drive")
	}, 10*time.Second, 500*time.Millisecond, "rw-client should not have cap/drive")

	// RO client should NOT have cap/drive (it's a source, not a destination).
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		pf, err := roClient.PacketFilter()
		assert.NoError(c, err)
		assert.False(c, hasCapMatchInPacketFilter(pf, tailcfg.PeerCapabilityTaildrive),
			"ro-client should NOT have cap/drive")
	}, 10*time.Second, 500*time.Millisecond, "ro-client should not have cap/drive")

	// ===== Phase 4: Create share on sharer =====
	t.Log("Phase 4: Create share on sharer node")

	_, _, err = sharer.Execute([]string{"mkdir", "-p", "/tmp/testshare"})
	require.NoError(t, err)
	_, _, err = sharer.Execute([]string{
		"sh", "-c", `echo "hello-taildrive" > /tmp/testshare/testfile.txt`,
	})
	require.NoError(t, err)
	_, _, err = sharer.Execute([]string{
		"tailscale", "drive", "share", "testshare", "/tmp/testshare",
	})
	require.NoError(t, err)

	// Verify share is listed.
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		result, _, err := sharer.Execute([]string{"tailscale", "drive", "list"})
		assert.NoError(c, err)
		assert.Contains(c, result, "testshare",
			"sharer should list 'testshare' in drive list")
	}, 10*time.Second, 500*time.Millisecond, "sharer should have testshare listed")

	// Build the drive URL components from the sharer's FQDN.
	fqdn := strings.TrimSuffix(sharer.MustFQDN(), ".")
	parts := strings.SplitN(fqdn, ".", 2)
	sharerName := parts[0]
	domain := parts[1]

	// ===== Phase 5: RW client - read file (positive) =====
	t.Log("Phase 5: RW client reads file from sharer")

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		result, _, err := rwClient.Execute([]string{
			"curl", "-s", "--max-time", "5",
			driveURL(domain, sharerName, "testshare/testfile.txt"),
		})
		assert.NoError(c, err)
		assert.Equal(c, "hello-taildrive", strings.TrimSpace(result),
			"rw-client should read testfile.txt content")
	}, 60*time.Second, 2*time.Second, "rw-client should read file from sharer")

	// ===== Phase 6: RW client - write file (positive) =====
	t.Log("Phase 6: RW client writes file to sharer")

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		result, _, err := rwClient.Execute([]string{
			"curl", "-s", "--max-time", "5",
			"-o", "/dev/null", "-w", "%{http_code}",
			"-X", "PUT", "--data-binary", "written-by-rw",
			driveURL(domain, sharerName, "testshare/rw-wrote.txt"),
		})
		assert.NoError(c, err)
		assert.Contains(c, result, "20",
			"rw-client PUT should return 2xx status")
	}, 30*time.Second, 2*time.Second, "rw-client should write file to sharer")

	// Verify the file exists on sharer.
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		content, err := sharer.ReadFile("/tmp/testshare/rw-wrote.txt")
		assert.NoError(c, err)
		assert.Equal(c, "written-by-rw", strings.TrimSpace(string(content)))
	}, 10*time.Second, 500*time.Millisecond, "rw-wrote.txt should exist on sharer")

	// ===== Phase 7: RO client - read file (positive) =====
	t.Log("Phase 7: RO client reads file from sharer")

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		result, _, err := roClient.Execute([]string{
			"curl", "-s", "--max-time", "5",
			driveURL(domain, sharerName, "testshare/testfile.txt"),
		})
		assert.NoError(c, err)
		assert.Equal(c, "hello-taildrive", strings.TrimSpace(result),
			"ro-client should read testfile.txt content")
	}, 60*time.Second, 2*time.Second, "ro-client should read file from sharer")

	// ===== Phase 8: RO client - write file (NEGATIVE - expect 403) =====
	t.Log("Phase 8: RO client write attempt (should be denied)")

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		result, _, err := roClient.Execute([]string{
			"curl", "-s", "--max-time", "5",
			"-o", "/dev/null", "-w", "%{http_code}",
			"-X", "PUT", "--data-binary", "should-not-work",
			driveURL(domain, sharerName, "testshare/ro-wrote.txt"),
		})
		assert.NoError(c, err)
		assert.Equal(c, "403", strings.TrimSpace(result),
			"ro-client PUT should return 403 Forbidden")
	}, 30*time.Second, 2*time.Second, "ro-client write should be 403 Forbidden")

	// Verify file was NOT created on sharer.
	_, err = sharer.ReadFile("/tmp/testshare/ro-wrote.txt")
	require.Error(t, err, "ro-wrote.txt should not exist on sharer")

	// ===== Phase 9: No-access node - read file (NEGATIVE) =====
	t.Log("Phase 9: No-access node read attempt (should fail)")

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		result, _, err := noAccess.Execute([]string{
			"curl", "-s", "--max-time", "5",
			"-o", "/dev/null", "-w", "%{http_code}",
			driveURL(domain, sharerName, "testshare/testfile.txt"),
		})
		// Either error (connection refused) or non-200 status.
		if err == nil {
			assert.NotEqual(c, "200", strings.TrimSpace(result),
				"no-access node should NOT get 200 from sharer's drive")
		}
	}, 30*time.Second, 2*time.Second, "no-access node should not read sharer's files")

	// ===== Phase 10: No-access node - write file (NEGATIVE) =====
	t.Log("Phase 10: No-access node write attempt (should fail)")

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		result, _, err := noAccess.Execute([]string{
			"curl", "-s", "--max-time", "5",
			"-o", "/dev/null", "-w", "%{http_code}",
			"-X", "PUT", "--data-binary", "should-not-work",
			driveURL(domain, sharerName, "testshare/no-access-wrote.txt"),
		})
		if err == nil {
			assert.NotEqual(c, "200", strings.TrimSpace(result),
				"no-access node should not get 200 on PUT")
			assert.NotEqual(c, "201", strings.TrimSpace(result),
				"no-access node should not get 201 on PUT")
		}
	}, 30*time.Second, 2*time.Second, "no-access node should not write sharer's files")

	// Verify file NOT created on sharer.
	_, err = sharer.ReadFile("/tmp/testshare/no-access-wrote.txt")
	require.Error(t, err, "no-access-wrote.txt should not exist on sharer")

	// ===== Phase 11: RW client - list directory via PROPFIND (positive) =====
	t.Log("Phase 11: RW client lists directory via PROPFIND")

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		result, _, err := rwClient.Execute([]string{
			"curl", "-s", "--max-time", "5",
			"-X", "PROPFIND", "-H", "Depth: 1",
			driveURL(domain, sharerName, "testshare/"),
		})
		assert.NoError(c, err)
		assert.Contains(c, result, "testfile.txt",
			"PROPFIND should list testfile.txt")
		assert.Contains(c, result, "rw-wrote.txt",
			"PROPFIND should list rw-wrote.txt")
	}, 30*time.Second, 2*time.Second, "rw-client PROPFIND should list files")

	// ===== Phase 12: RW client - delete file (positive) =====
	t.Log("Phase 12: RW client deletes file from sharer")

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		result, _, err := rwClient.Execute([]string{
			"curl", "-s", "--max-time", "5",
			"-o", "/dev/null", "-w", "%{http_code}",
			"-X", "DELETE",
			driveURL(domain, sharerName, "testshare/rw-wrote.txt"),
		})
		assert.NoError(c, err)
		assert.Contains(c, result, "20",
			"rw-client DELETE should return 2xx status")
	}, 30*time.Second, 2*time.Second, "rw-client should delete file from sharer")

	// Verify deleted on sharer.
	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		_, err := sharer.ReadFile("/tmp/testshare/rw-wrote.txt")
		assert.Error(c, err, "rw-wrote.txt should be deleted from sharer")
	}, 10*time.Second, 500*time.Millisecond, "rw-wrote.txt should be gone")

	// ===== Phase 13: RO client - delete file (NEGATIVE - expect 403) =====
	t.Log("Phase 13: RO client delete attempt (should be denied)")

	assert.EventuallyWithT(t, func(c *assert.CollectT) {
		result, _, err := roClient.Execute([]string{
			"curl", "-s", "--max-time", "5",
			"-o", "/dev/null", "-w", "%{http_code}",
			"-X", "DELETE",
			driveURL(domain, sharerName, "testshare/testfile.txt"),
		})
		assert.NoError(c, err)
		assert.Equal(c, "403", strings.TrimSpace(result),
			"ro-client DELETE should return 403 Forbidden")
	}, 30*time.Second, 2*time.Second, "ro-client delete should be 403 Forbidden")

	// Verify file still exists on sharer.
	content, err := sharer.ReadFile("/tmp/testshare/testfile.txt")
	require.NoError(t, err)
	assert.Equal(t, "hello-taildrive", strings.TrimSpace(string(content)),
		"testfile.txt should still exist after RO delete attempt")
}
