// This file implements data-driven via grant compatibility tests using
// golden data captured from Tailscale SaaS (v29, v30, v31, v33, v35,
// v36). These scenarios exercise via grant steering with peer
// connectivity and cross-subnet forwarding.
//
// Test data source: ../policy/v2/testdata/grant_results/via-grant-v{29,30,31,33,35,36}.hujson
// Source format:    github.com/juanfont/headscale/hscontrol/types/testcapture
package servertest_test

import (
	"context"
	"net/netip"
	"path/filepath"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/juanfont/headscale/hscontrol/servertest"
	"github.com/juanfont/headscale/hscontrol/types/testcapture"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"tailscale.com/tailcfg"
	"tailscale.com/types/netmap"
)

// viaCompatTests lists golden captures that exercise via grant steering.
var viaCompatTests = []struct {
	id   string
	desc string
}{
	{"via-grant-v29", "crossed subnet steering: group-a via router-a, group-b via router-b"},
	{"via-grant-v30", "crossed mixed: subnet via router-a/b, exit via exit-b/a"},
	{"via-grant-v31", "peer connectivity + via exit A/B steering"},
	{"via-grant-v33", "single via grant + HA primary election"},
	{"via-grant-v35", "via grant with unadvertised destination"},
	{"via-grant-v36", "full complex: peer connectivity + crossed subnet + crossed exit"},
}

// TestViaGrantMapCompat loads golden captures from Tailscale SaaS and
// compares headscale's [tailcfg.MapResponse] structure against the captured [netmap.NetworkMap].
//
// The comparison is IP-independent: it validates peer visibility, route
// prefixes in AllowedIPs, and PrimaryRoutes — not literal Tailscale IP
// addresses which differ between Tailscale SaaS and headscale allocation.
//
// CROSS-DEPENDENCY WARNING:
// This test reads golden files from ../policy/v2/testdata/grant_results/
// (specifically via-grant-v29, v30, v31, v33, v35, v36). These files are shared
// with TestGrantsCompat in the policy/v2 package. Any changes to the
// file format, field structure, or naming must be coordinated with
// BOTH tests.
//
// Fields consumed by this test (but NOT by TestGrantsCompat):
//   - captures[name].netmap (Peers, AllowedIPs, PrimaryRoutes, PacketFilterRules)
//   - topology.nodes[name].tags (used for servertest node creation)
//
// Fields consumed by TestGrantsCompat (but NOT by this test):
//   - captures[name].packet_filter_rules (golden filter rule comparison)
//   - input.api_response_code/body (error case handling)
func TestViaGrantMapCompat(t *testing.T) {
	t.Parallel()

	for _, tc := range viaCompatTests {
		t.Run(tc.id, func(t *testing.T) {
			t.Parallel()

			path := filepath.Join(
				"..", "policy", "v2", "testdata", "grant_results", tc.id+".hujson",
			)

			c, err := testcapture.Read(path)
			require.NoError(t, err, "failed to read %s", path)

			if c.Error {
				t.Skipf("test %s is an error case", tc.id)

				return
			}

			runViaMapCompat(t, c)
		})
	}
}

func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
	t.Helper()

	srv := servertest.NewServer(t)
	tagUser := srv.CreateUser(t, "tag-user")

	policyJSON := convertCapturePolicy(t, c)

	changed, err := srv.State().SetPolicy(policyJSON)
	require.NoError(t, err, "failed to set policy")

	if changed {
		changes, err := srv.State().ReloadPolicy()
		require.NoError(t, err)
		srv.App.Change(changes...)
	}

	// Create tagged clients matching the golden topology.
	// Nodes are created in SaaS registration order so headscale assigns
	// sequential DB IDs in the same relative order. This matters for
	// PrimaryRoutes election which uses lowest-node-ID-wins — the
	// tiebreaker must pick the same node as SaaS.
	clients := map[string]*servertest.TestClient{}
	order := captureNodeOrder(t, c)

	for _, name := range order {
		topoNode, exists := c.Topology.Nodes[name]
		if !exists || len(topoNode.Tags) == 0 {
			continue
		}

		if _, inCaptures := c.Captures[name]; !inCaptures {
			continue
		}

		clients[name] = servertest.NewClient(t, srv, name,
			servertest.WithUser(tagUser),
			servertest.WithTags(topoNode.Tags...),
		)
	}

	require.NotEmpty(t, clients, "no relevant nodes created")

	// Determine which routes each node should advertise. If the golden
	// topology has explicit routable_ips, use those. Otherwise infer
	// from the [netmap.NetworkMap] peer AllowedIPs and packet filter dst prefixes.
	nodeRoutes := inferNodeRoutes(t, c)

	// Build approved routes from topology. The topology's approved_routes
	// field records what SaaS actually approved (which may be a subset of
	// routable_ips). Using this instead of approving all advertised routes
	// ensures exit routes are only approved when SaaS approved them.
	nodeApproved := map[string][]netip.Prefix{}

	for name, node := range c.Topology.Nodes {
		for _, r := range node.ApprovedRoutes {
			nodeApproved[name] = append(
				nodeApproved[name], netip.MustParsePrefix(r),
			)
		}
	}

	// Advertise and approve routes in SaaS registration order. Via
	// grants depend on routes being advertised for compileViaGrant to
	// produce filter rules. The order matters because PrimaryRoutes
	// election is sticky — the first node to register a prefix becomes
	// primary.
	for _, name := range order {
		cl, ok := clients[name]
		if !ok {
			continue
		}

		routes := nodeRoutes[name]
		if len(routes) == 0 {
			continue
		}

		cl.Direct().SetHostinfo(&tailcfg.Hostinfo{
			BackendLogID: "servertest-" + name,
			Hostname:     name,
			RoutableIPs:  routes,
		})

		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		require.NoError(t, cl.Direct().SendUpdate(ctx),
			"route advertisement for %s should succeed", name)

		cancel()

		nodeID := findNodeID(t, srv, name)
		_, routeChange, err := srv.State().SetApprovedRoutes(
			nodeID, nodeApproved[name],
		)
		require.NoError(t, err)
		srv.App.Change(routeChange)
	}

	// Wait for peers based on golden [netmap.NetworkMap] expected counts.
	for viewerName, cl := range clients {
		capture := c.Captures[viewerName]
		if capture.Netmap == nil {
			continue
		}

		expected := 0

		for _, peer := range capture.Netmap.Peers {
			peerName := extractHostname(peer.Name())
			if _, isOurs := clients[peerName]; isOurs {
				expected++
			}
		}

		if expected > 0 {
			cl.WaitForPeers(t, expected, 30*time.Second)
		}
	}

	// Ensure all nodes have received at least one [tailcfg.MapResponse],
	// including nodes with 0 expected peers that skipped [TestClient.WaitForPeers].
	for name, cl := range clients {
		cl.WaitForCondition(t, name+" initial netmap", 15*time.Second,
			func(nm *netmap.NetworkMap) bool {
				return nm != nil
			})
	}

	// Compare each viewer's [tailcfg.MapResponse] against the golden [netmap.NetworkMap].
	for viewerName, cl := range clients {
		capture := c.Captures[viewerName]
		if capture.Netmap == nil {
			continue
		}

		t.Run(viewerName, func(t *testing.T) {
			nm := cl.Netmap()
			require.NotNil(t, nm, "netmap is nil")

			compareNetmap(t, nm, capture, clients)
		})
	}
}

// compareNetmap compares the headscale [tailcfg.MapResponse] against the
// captured [netmap.NetworkMap] data in an IP-independent way. It validates:
//   - Peer visibility (which peers are present, by hostname)
//   - Route prefixes in AllowedIPs (non-Tailscale-IP entries like 10.44.0.0/16)
//   - Number of Tailscale IPs per peer (should be 2: one v4 + one v6)
//   - PrimaryRoutes per peer
//   - PacketFilter rule count and non-Tailscale dst prefixes
func compareNetmap(
	t *testing.T,
	got *netmap.NetworkMap,
	want testcapture.Node,
	clients map[string]*servertest.TestClient,
) {
	t.Helper()

	require.NotNil(t, want.Netmap, "golden Netmap is nil")

	// Build golden peer map (only peers in our client set).
	wantPeers := map[string]tailcfg.NodeView{}

	for _, p := range want.Netmap.Peers {
		name := extractHostname(p.Name())
		if _, isOurs := clients[name]; isOurs {
			wantPeers[name] = p
		}
	}

	// Build headscale peer map.
	gotPeers := map[string]peerSummary{}

	for _, peer := range got.Peers {
		name := ""

		if peer.Hostinfo().Valid() {
			name = peer.Hostinfo().Hostname()
		}

		if name == "" {
			for n := range clients {
				if strings.Contains(peer.Name(), n+".") {
					name = n

					break
				}
			}
		}

		if name == "" {
			continue
		}

		// Separate AllowedIPs into Tailscale IPs (node addresses)
		// and route prefixes (subnets, exit routes).
		var tsIPs []netip.Prefix

		var routePrefixes []string

		for i := range peer.AllowedIPs().Len() {
			prefix := peer.AllowedIPs().At(i)
			if isTailscaleIP(prefix) {
				tsIPs = append(tsIPs, prefix)
			} else {
				routePrefixes = append(routePrefixes, prefix.String())
			}
		}

		slices.Sort(routePrefixes)

		var proutes []string
		for i := range peer.PrimaryRoutes().Len() {
			proutes = append(proutes, peer.PrimaryRoutes().At(i).String())
		}

		slices.Sort(proutes)

		gotPeers[name] = peerSummary{
			TailscaleIPs:  tsIPs,
			RoutePrefixes: routePrefixes,
			PrimaryRoutes: proutes,
		}
	}

	// Compare peer visibility: golden peers must be present.
	for name, wantPeer := range wantPeers {
		gotPeer, visible := gotPeers[name]
		if !visible {
			wantRoutes := extractRoutePrefixesView(wantPeer.AllowedIPs())
			t.Errorf("peer %s: visible in Tailscale SaaS (routes=%v), missing in headscale",
				name, wantRoutes)

			continue
		}

		// Compare route prefixes in AllowedIPs (IP-independent).
		wantRoutes := extractRoutePrefixesView(wantPeer.AllowedIPs())
		slices.Sort(wantRoutes)

		assert.Equalf(t, wantRoutes, gotPeer.RoutePrefixes,
			"peer %s: route prefixes in AllowedIPs mismatch", name)

		// Tailscale IPs: count should match, and they must belong to
		// this peer (not some other node's IPs).
		wantTSIPCount := countTailscaleIPsView(wantPeer.AllowedIPs())

		assert.Lenf(t, gotPeer.TailscaleIPs, wantTSIPCount,
			"peer %s: Tailscale IP count mismatch", name)

		// Verify the Tailscale IPs are actually this peer's addresses.
		if peerClient, ok := clients[name]; ok {
			peerNM := peerClient.Netmap()
			if peerNM != nil && peerNM.SelfNode.Valid() {
				peerAddrs := map[netip.Prefix]bool{}

				addrs := peerNM.SelfNode.Addresses()
				for i := range addrs.Len() {
					peerAddrs[addrs.At(i)] = true
				}

				for _, tsIP := range gotPeer.TailscaleIPs {
					assert.Truef(t, peerAddrs[tsIP],
						"peer %s: AllowedIPs contains Tailscale IP %s which is NOT this peer's address (peer has %v)",
						name, tsIP, peerAddrs)
				}
			}
		}

		// Compare PrimaryRoutes.
		var wantPRoutes []string
		for i := range wantPeer.PrimaryRoutes().Len() {
			wantPRoutes = append(wantPRoutes, wantPeer.PrimaryRoutes().At(i).String())
		}

		assert.ElementsMatchf(t, wantPRoutes, gotPeer.PrimaryRoutes,
			"peer %s: PrimaryRoutes mismatch", name)
	}

	// Check for extra peers headscale shows that Tailscale SaaS doesn't.
	for name := range gotPeers {
		if _, expected := wantPeers[name]; !expected {
			t.Errorf("peer %s: visible in headscale but NOT in Tailscale SaaS", name)
		}
	}

	// Compare PacketFilter rules (IP-independent).
	wantFilterRules := want.PacketFilterRules

	if !assert.Lenf(t, got.PacketFilter, len(wantFilterRules),
		"PacketFilter rule count mismatch") {
		return
	}

	// Resolve SaaS IPs → peer name and HS IPs → peer name so we can
	// compare rule sources structurally. Tailscale IPs in SaaS vs HS
	// allocations never match literally, but each IP belongs to a
	// peer with a stable hostname.
	saasAddrs := saasAddrsByPeer(want, clients)
	hsAddrs := hsAddrsByPeer(clients)

	// Compare destination prefixes per rule — subnet CIDRs like
	// 10.44.0.0/16 are stable between Tailscale SaaS and headscale.
	// Source IPs are re-keyed per peer identity before comparison.
	for i := range wantFilterRules {
		wantRule := wantFilterRules[i]
		gotMatch := got.PacketFilter[i]

		wantSrcIdents := canonicaliseSrcStrings(t, wantRule.SrcIPs, saasAddrs, i)
		gotSrcIdents := canonicaliseSrcPrefixes(t, gotMatch.Srcs, hsAddrs, i)

		assert.Equalf(t, wantSrcIdents, gotSrcIdents,
			"PacketFilter[%d]: source peer identities mismatch", i)

		// Destination prefixes: extract non-Tailscale-IP CIDRs
		// from both golden and headscale rules and compare.
		var wantDstPrefixes []string

		for _, dp := range wantRule.DstPorts {
			pfx, err := parsePrefixOrAddr(dp.IP)
			require.NoErrorf(t, err,
				"golden DstPorts[%d].IP %q should parse as prefix or addr", i, dp.IP)

			if !isTailscaleIP(pfx) {
				wantDstPrefixes = append(wantDstPrefixes, pfx.String())
			}
		}

		var gotDstPrefixes []string

		for _, dst := range gotMatch.Dsts {
			pfx := dst.Net
			if !isTailscaleIP(pfx) {
				gotDstPrefixes = append(gotDstPrefixes, pfx.String())
			}
		}

		slices.Sort(wantDstPrefixes)
		slices.Sort(gotDstPrefixes)

		assert.Equalf(t, wantDstPrefixes, gotDstPrefixes,
			"PacketFilter[%d]: non-Tailscale destination prefixes mismatch", i)
	}
}

// saasAddrsByPeer builds a map from SaaS Tailscale address to peer
// hostname using each capture's [tailcfg.NodeView.Addresses]. Peers not in
// clients are skipped.
func saasAddrsByPeer(
	want testcapture.Node,
	clients map[string]*servertest.TestClient,
) map[netip.Addr]string {
	out := map[netip.Addr]string{}

	if want.Netmap == nil {
		return out
	}

	// Walk peers listed in this [netmap.NetworkMap].
	for _, peer := range want.Netmap.Peers {
		name := extractHostname(peer.Name())
		if _, isOurs := clients[name]; !isOurs {
			continue
		}

		for i := range peer.Addresses().Len() {
			pfx := peer.Addresses().At(i)
			if isTailscaleIP(pfx) {
				out[pfx.Addr()] = name
			}
		}
	}

	// The viewer's own [tailcfg.NodeView] addresses also appear as possible src.
	if want.Netmap.SelfNode.Valid() {
		name := extractHostname(want.Netmap.SelfNode.Name())

		if _, isOurs := clients[name]; isOurs {
			addrs := want.Netmap.SelfNode.Addresses()
			for i := range addrs.Len() {
				pfx := addrs.At(i)
				if isTailscaleIP(pfx) {
					out[pfx.Addr()] = name
				}
			}
		}
	}

	return out
}

// hsAddrsByPeer builds a map from headscale Tailscale address to peer
// hostname by walking each live client's self addresses.
func hsAddrsByPeer(clients map[string]*servertest.TestClient) map[netip.Addr]string {
	out := map[netip.Addr]string{}

	for name, cl := range clients {
		nm := cl.Netmap()
		if nm == nil || !nm.SelfNode.Valid() {
			continue
		}

		addrs := nm.SelfNode.Addresses()
		for i := range addrs.Len() {
			pfx := addrs.At(i)
			if isTailscaleIP(pfx) {
				out[pfx.Addr()] = name
			}
		}
	}

	return out
}

// canonicaliseSrcStrings converts a SrcIPs slice (as produced by the
// SaaS wire format) into a sorted list of canonical identifiers: "*"
// for wildcard, "peer:<name>" for each Tailscale address or prefix
// that resolves to a known peer, or the raw CIDR string for
// non-Tailscale prefixes. A Tailscale prefix wider than /32 (IPv4)
// or /128 (IPv6) expands to the union of its contained peers.
// Unresolvable Tailscale-range sources fail the test.
func canonicaliseSrcStrings(
	t *testing.T,
	srcs []string,
	addrToPeer map[netip.Addr]string,
	ruleIndex int,
) []string {
	t.Helper()

	seen := map[string]struct{}{}

	for _, src := range srcs {
		if src == "*" {
			seen["*"] = struct{}{}

			continue
		}

		pfx, err := parsePrefixOrAddr(src)
		require.NoErrorf(t, err,
			"PacketFilter[%d]: unparseable SrcIP %q", ruleIndex, src)

		addIdentsForSrc(t, pfx, addrToPeer, ruleIndex, seen)
	}

	return sortedKeys(seen)
}

// canonicaliseSrcPrefixes is the headscale-side counterpart of
// [canonicaliseSrcStrings], reading already-parsed [netip.Prefix] values
// from [tailcfg.Match.Srcs].
func canonicaliseSrcPrefixes(
	t *testing.T,
	srcs []netip.Prefix,
	addrToPeer map[netip.Addr]string,
	ruleIndex int,
) []string {
	t.Helper()

	seen := map[string]struct{}{}

	for _, pfx := range srcs {
		if pfx.Bits() == 0 && pfx.Addr().IsUnspecified() {
			seen["*"] = struct{}{}

			continue
		}

		addIdentsForSrc(t, pfx, addrToPeer, ruleIndex, seen)
	}

	return sortedKeys(seen)
}

// addIdentsForSrc resolves one source prefix into canonical identity
// tokens and inserts them into seen. A non-Tailscale prefix passes
// through literally; a Tailscale-range prefix expands to the union
// of peer names whose addresses fall within it.
func addIdentsForSrc(
	t *testing.T,
	pfx netip.Prefix,
	addrToPeer map[netip.Addr]string,
	ruleIndex int,
	seen map[string]struct{},
) {
	t.Helper()

	if !prefixInTailscaleRange(pfx) {
		seen[pfx.String()] = struct{}{}

		return
	}

	matched := false

	for addr, name := range addrToPeer {
		if pfx.Contains(addr) {
			seen["peer:"+name] = struct{}{}
			matched = true
		}
	}

	require.Truef(t, matched,
		"PacketFilter[%d]: Tailscale-range SrcIP %s does not cover any known peer; addrToPeer=%v",
		ruleIndex, pfx, addrToPeer)
}

// prefixInTailscaleRange reports whether a prefix lies entirely
// within the Tailscale CGNAT range (100.64.0.0/10) or Tailscale ULA
// range (fd7a:115c:a1e0::/48), regardless of prefix length.
func prefixInTailscaleRange(p netip.Prefix) bool {
	addr := p.Addr()

	if addr.Is4() {
		return addr.As4()[0] == 100 && (addr.As4()[1]&0xC0) == 64
	}

	if addr.Is6() {
		b := addr.As16()

		return b[0] == 0xfd && b[1] == 0x7a && b[2] == 0x11 && b[3] == 0x5c //nolint:gosec // As16 returns [16]byte, indexing [0..3] is safe
	}

	return false
}

func sortedKeys(m map[string]struct{}) []string {
	out := make([]string, 0, len(m))
	for k := range m {
		out = append(out, k)
	}

	slices.Sort(out)

	return out
}

type peerSummary struct {
	TailscaleIPs  []netip.Prefix // Tailscale address entries from AllowedIPs
	RoutePrefixes []string       // non-Tailscale-IP AllowedIPs (sorted)
	PrimaryRoutes []string       // sorted
}

// parsePrefixOrAddr parses a string as a [netip.Prefix]. If the string
// is a bare IP address (no slash), it is converted to a single-host
// prefix (/32 for IPv4, /128 for IPv6). Golden data DstPorts.IP can
// contain either form.
func parsePrefixOrAddr(s string) (netip.Prefix, error) {
	pfx, err := netip.ParsePrefix(s)
	if err == nil {
		return pfx, nil
	}

	addr, addrErr := netip.ParseAddr(s)
	if addrErr != nil {
		return netip.Prefix{}, err // return original prefix error
	}

	return netip.PrefixFrom(addr, addr.BitLen()), nil
}

// isTailscaleIP returns true if the prefix is a single-host Tailscale
// address (/32 for IPv4 in CGNAT range, /128 for IPv6 in Tailscale ULA).
func isTailscaleIP(prefix netip.Prefix) bool {
	addr := prefix.Addr()

	if addr.Is4() && prefix.Bits() == 32 {
		// CGNAT range 100.64.0.0/10
		return addr.As4()[0] == 100 && (addr.As4()[1]&0xC0) == 64
	}

	if addr.Is6() && prefix.Bits() == 128 {
		// Tailscale ULA fd7a:115c:a1e0::/48
		b := addr.As16()

		return b[0] == 0xfd && b[1] == 0x7a && b[2] == 0x11 && b[3] == 0x5c //nolint:gosec // As16 returns [16]byte, indexing [0..3] is safe
	}

	return false
}

// extractRoutePrefixesView returns the non-Tailscale-IP entries from
// a typed AllowedIPs view (subnet routes, exit routes, etc.).
func extractRoutePrefixesView(allowedIPs interface {
	Len() int
	At(i int) netip.Prefix
},
) []string {
	var routes []string

	for i := range allowedIPs.Len() {
		pfx := allowedIPs.At(i)
		if !isTailscaleIP(pfx) {
			routes = append(routes, pfx.String())
		}
	}

	return routes
}

// countTailscaleIPsView returns the number of Tailscale IP entries
// in a typed AllowedIPs view.
func countTailscaleIPsView(allowedIPs interface {
	Len() int
	At(i int) netip.Prefix
},
) int {
	count := 0

	for i := range allowedIPs.Len() {
		if isTailscaleIP(allowedIPs.At(i)) {
			count++
		}
	}

	return count
}

// inferNodeRoutes determines which routes each node should advertise.
// If the topology has explicit routable_ips, those are used. Otherwise
// routes are inferred from the [netmap.NetworkMap] peer AllowedIPs and packet
// filter destination prefixes.
func inferNodeRoutes(t *testing.T, c *testcapture.Capture) map[string][]netip.Prefix {
	t.Helper()

	result := map[string][]netip.Prefix{}

	// First use explicit routable_ips from topology.
	for name, node := range c.Topology.Nodes {
		for _, r := range node.RoutableIPs {
			result[name] = append(result[name], netip.MustParsePrefix(r))
		}
	}

	// If any node already has routes, the topology is populated — use as-is.
	for _, routes := range result {
		if len(routes) > 0 {
			return result
		}
	}

	// Tier 2: infer from each capture's [netmap.NetworkMap] — scan peers with
	// route prefixes in AllowedIPs. If node X appears as a peer with
	// route prefix 10.44.0.0/16, then X should advertise that route.
	for _, node := range c.Captures {
		if node.Netmap == nil {
			continue
		}

		for _, peer := range node.Netmap.Peers {
			peerName := extractHostname(peer.Name())

			for i := range peer.AllowedIPs().Len() {
				pfx := peer.AllowedIPs().At(i)
				if isTailscaleIP(pfx) {
					continue
				}

				if !slices.Contains(result[peerName], pfx) {
					result[peerName] = append(result[peerName], pfx)
				}
			}
		}
	}

	// Tier 3: infer from packet_filter_rules DstPorts — secondary HA
	// routers whose routes don't appear in AllowedIPs (only the
	// primary gets the route in AllowedIPs) DO receive filter rules
	// for those routes.
	for nodeName, node := range c.Captures {
		for _, rule := range node.PacketFilterRules {
			for _, dp := range rule.DstPorts {
				if dp.IP == "*" {
					continue
				}

				prefix, err := parsePrefixOrAddr(dp.IP)
				require.NoErrorf(t, err,
					"golden DstPorts.IP %q for %s unparseable", dp.IP, nodeName)

				if isTailscaleIP(prefix) {
					continue
				}

				if !slices.Contains(result[nodeName], prefix) {
					result[nodeName] = append(
						result[nodeName], prefix,
					)
				}
			}
		}
	}

	return result
}

// extractHostname extracts the hostname from a Tailscale FQDN like
// "router-a.tail78f774.ts.net.".
func extractHostname(fqdn string) string {
	if before, _, ok := strings.Cut(fqdn, "."); ok {
		return before
	}

	return fqdn
}
