package debug

import (
	"archive/zip"
	"bytes"
	"encoding/json"
	"net"
	"net/netip"
	"net/url"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"
	"time"

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

	"github.com/netbirdio/netbird/client/anonymize"
	"github.com/netbirdio/netbird/client/configs"
	"github.com/netbirdio/netbird/client/internal/profilemanager"
	"github.com/netbirdio/netbird/shared/management/domain"
	mgmProto "github.com/netbirdio/netbird/shared/management/proto"
	"github.com/netbirdio/netbird/shared/netiputil"
)

func mustEncodePrefix(t *testing.T, p netip.Prefix) []byte {
	t.Helper()
	b, err := netiputil.EncodePrefix(p)
	require.NoError(t, err)
	return b
}

func TestAnonymizeStateFile(t *testing.T) {
	testState := map[string]json.RawMessage{
		"null_state": json.RawMessage("null"),
		"test_state": mustMarshal(map[string]any{
			// Test simple fields
			"public_ip":      "203.0.113.1",
			"private_ip":     "192.168.1.1",
			"protected_ip":   "100.64.0.1",
			"well_known_ip":  "8.8.8.8",
			"ipv6_addr":      "2001:db8::1",
			"private_ipv6":   "fd00::1",
			"domain":         "test.example.com",
			"uri":            "stun:stun.example.com:3478",
			"uri_with_ip":    "turn:203.0.113.1:3478",
			"netbird_domain": "device.netbird.cloud",

			// Test CIDR ranges
			"public_cidr":       "203.0.113.0/24",
			"private_cidr":      "192.168.0.0/16",
			"protected_cidr":    "100.64.0.0/10",
			"ipv6_cidr":         "2001:db8::/32",
			"private_ipv6_cidr": "fd00::/8",

			// Test nested structures
			"nested": map[string]any{
				"ip":     "203.0.113.2",
				"domain": "nested.example.com",
				"more_nest": map[string]any{
					"ip":     "203.0.113.3",
					"domain": "deep.example.com",
				},
			},

			// Test arrays
			"string_array": []any{
				"203.0.113.4",
				"test1.example.com",
				"test2.example.com",
			},
			"object_array": []any{
				map[string]any{
					"ip":     "203.0.113.5",
					"domain": "array1.example.com",
				},
				map[string]any{
					"ip":     "203.0.113.6",
					"domain": "array2.example.com",
				},
			},

			// Test multiple occurrences of same value
			"duplicate_ip":     "203.0.113.1",      // Same as public_ip
			"duplicate_domain": "test.example.com", // Same as domain

			// Test URIs with various schemes
			"stun_uri":  "stun:stun.example.com:3478",
			"turns_uri": "turns:turns.example.com:5349",
			"http_uri":  "http://web.example.com:80",
			"https_uri": "https://secure.example.com:443",

			// Test strings that might look like IPs but aren't
			"not_ip":         "300.300.300.300",
			"partial_ip":     "192.168",
			"ip_like_string": "1234.5678",

			// Test mixed content strings
			"mixed_content": "Server at 203.0.113.1 (test.example.com) on port 80",

			// Test empty and special values
			"empty_string":  "",
			"null_value":    nil,
			"numeric_value": 42,
			"boolean_value": true,
		}),
		"route_state": mustMarshal(map[string]any{
			"routes": []any{
				map[string]any{
					"network": "203.0.113.0/24",
					"gateway": "203.0.113.1",
					"domains": []any{
						"route1.example.com",
						"route2.example.com",
					},
				},
				map[string]any{
					"network": "2001:db8::/32",
					"gateway": "2001:db8::1",
					"domains": []any{
						"route3.example.com",
						"route4.example.com",
					},
				},
			},
			// Test map with IP/CIDR keys
			"refCountMap": map[string]any{
				"203.0.113.1/32": map[string]any{
					"Count": 1,
					"Out": map[string]any{
						"IP": "192.168.0.1",
						"Intf": map[string]any{
							"Name":  "eth0",
							"Index": 1,
						},
					},
				},
				"2001:db8::1/128": map[string]any{
					"Count": 1,
					"Out": map[string]any{
						"IP": "fe80::1",
						"Intf": map[string]any{
							"Name":  "eth0",
							"Index": 1,
						},
					},
				},
				"10.0.0.1/32": map[string]any{ // private IP should remain unchanged
					"Count": 1,
					"Out": map[string]any{
						"IP": "192.168.0.1",
					},
				},
			},
		}),
	}

	anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())

	// Pre-seed the domains we need to verify in the test assertions
	anonymizer.AnonymizeDomain("test.example.com")
	anonymizer.AnonymizeDomain("nested.example.com")
	anonymizer.AnonymizeDomain("deep.example.com")
	anonymizer.AnonymizeDomain("array1.example.com")

	err := anonymizeStateFile(&testState, anonymizer)
	require.NoError(t, err)

	// Helper function to unmarshal and get nested values
	var state map[string]any
	err = json.Unmarshal(testState["test_state"], &state)
	require.NoError(t, err)

	// Test null state remains unchanged
	require.Equal(t, "null", string(testState["null_state"]))

	// Basic assertions
	assert.NotEqual(t, "203.0.113.1", state["public_ip"])
	assert.Equal(t, "192.168.1.1", state["private_ip"])  // Private IP unchanged
	assert.Equal(t, "100.64.0.1", state["protected_ip"]) // Protected IP unchanged
	assert.Equal(t, "8.8.8.8", state["well_known_ip"])   // Well-known IP unchanged
	assert.NotEqual(t, "2001:db8::1", state["ipv6_addr"])
	assert.NotEqual(t, "fd00::1", state["private_ipv6"]) // ULA IPv6 anonymized (global ID is a fingerprint)
	assert.NotEqual(t, "test.example.com", state["domain"])
	assert.True(t, strings.HasSuffix(state["domain"].(string), ".domain"))
	assert.Equal(t, "device.netbird.cloud", state["netbird_domain"]) // Netbird domain unchanged

	// CIDR ranges
	assert.NotEqual(t, "203.0.113.0/24", state["public_cidr"])
	assert.Contains(t, state["public_cidr"], "/24")           // Prefix preserved
	assert.Equal(t, "192.168.0.0/16", state["private_cidr"])  // Private CIDR unchanged
	assert.Equal(t, "100.64.0.0/10", state["protected_cidr"]) // Protected CIDR unchanged
	assert.NotEqual(t, "2001:db8::/32", state["ipv6_cidr"])
	assert.Contains(t, state["ipv6_cidr"], "/32") // IPv6 prefix preserved

	// Nested structures
	nested := state["nested"].(map[string]any)
	assert.NotEqual(t, "203.0.113.2", nested["ip"])
	assert.NotEqual(t, "nested.example.com", nested["domain"])
	moreNest := nested["more_nest"].(map[string]any)
	assert.NotEqual(t, "203.0.113.3", moreNest["ip"])
	assert.NotEqual(t, "deep.example.com", moreNest["domain"])

	// Arrays
	strArray := state["string_array"].([]any)
	assert.NotEqual(t, "203.0.113.4", strArray[0])
	assert.NotEqual(t, "test1.example.com", strArray[1])
	assert.True(t, strings.HasSuffix(strArray[1].(string), ".domain"))

	objArray := state["object_array"].([]any)
	firstObj := objArray[0].(map[string]any)
	assert.NotEqual(t, "203.0.113.5", firstObj["ip"])
	assert.NotEqual(t, "array1.example.com", firstObj["domain"])

	// Duplicate values should be anonymized consistently
	assert.Equal(t, state["public_ip"], state["duplicate_ip"])
	assert.Equal(t, state["domain"], state["duplicate_domain"])

	// URIs
	assert.NotContains(t, state["stun_uri"], "stun.example.com")
	assert.NotContains(t, state["turns_uri"], "turns.example.com")
	assert.NotContains(t, state["http_uri"], "web.example.com")
	assert.NotContains(t, state["https_uri"], "secure.example.com")

	// Non-IP strings should remain unchanged
	assert.Equal(t, "300.300.300.300", state["not_ip"])
	assert.Equal(t, "192.168", state["partial_ip"])
	assert.Equal(t, "1234.5678", state["ip_like_string"])

	// Mixed content should have IPs and domains replaced
	mixedContent := state["mixed_content"].(string)
	assert.NotContains(t, mixedContent, "203.0.113.1")
	assert.NotContains(t, mixedContent, "test.example.com")
	assert.Contains(t, mixedContent, "Server at ")
	assert.Contains(t, mixedContent, " on port 80")

	// Special values should remain unchanged
	assert.Equal(t, "", state["empty_string"])
	assert.Nil(t, state["null_value"])
	assert.Equal(t, float64(42), state["numeric_value"])
	assert.Equal(t, true, state["boolean_value"])

	// Check route state
	var routeState map[string]any
	err = json.Unmarshal(testState["route_state"], &routeState)
	require.NoError(t, err)

	routes := routeState["routes"].([]any)
	route1 := routes[0].(map[string]any)
	assert.NotEqual(t, "203.0.113.0/24", route1["network"])
	assert.Contains(t, route1["network"], "/24")
	assert.NotEqual(t, "203.0.113.1", route1["gateway"])
	domains := route1["domains"].([]any)
	assert.True(t, strings.HasSuffix(domains[0].(string), ".domain"))
	assert.True(t, strings.HasSuffix(domains[1].(string), ".domain"))

	// Check map keys are anonymized
	refCountMap := routeState["refCountMap"].(map[string]any)
	hasPublicIPKey := false
	hasIPv6Key := false
	hasPrivateIPKey := false
	for key := range refCountMap {
		if strings.Contains(key, "203.0.113.1") {
			hasPublicIPKey = true
		}
		if strings.Contains(key, "2001:db8::1") {
			hasIPv6Key = true
		}
		if key == "10.0.0.1/32" {
			hasPrivateIPKey = true
		}
	}
	assert.False(t, hasPublicIPKey, "public IP in key should be anonymized")
	assert.False(t, hasIPv6Key, "IPv6 in key should be anonymized")
	assert.True(t, hasPrivateIPKey, "private IP in key should remain unchanged")
}

func mustMarshal(v any) json.RawMessage {
	data, err := json.Marshal(v)
	if err != nil {
		panic(err)
	}
	return data
}

func TestAnonymizeNetworkMap(t *testing.T) {
	origV6Prefix := netip.MustParsePrefix("2001:db8:abcd::5/64")
	networkMap := &mgmProto.NetworkMap{
		PeerConfig: &mgmProto.PeerConfig{
			Address:   "203.0.113.5",
			AddressV6: mustEncodePrefix(t, origV6Prefix),
			Dns:       "1.2.3.4",
			Fqdn:      "peer1.corp.example.com",
			SshConfig: &mgmProto.SSHConfig{
				SshPubKey: []byte("ssh-rsa AAAAB3NzaC1..."),
			},
		},
		RemotePeers: []*mgmProto.RemotePeerConfig{
			{
				AllowedIps: []string{
					"203.0.113.1/32",
					"2001:db8:1234::1/128",
					"192.168.1.1/32",
					"100.64.0.1/32",
					"10.0.0.1/32",
				},
				Fqdn: "peer2.corp.example.com",
				SshConfig: &mgmProto.SSHConfig{
					SshPubKey: []byte("ssh-rsa AAAAB3NzaC2..."),
				},
			},
		},
		Routes: []*mgmProto.Route{
			{
				Network: "197.51.100.0/24",
				Domains: []string{"prod.example.com", "staging.example.com"},
				NetID:   "net-123abc",
			},
		},
		DNSConfig: &mgmProto.DNSConfig{
			NameServerGroups: []*mgmProto.NameServerGroup{
				{
					NameServers: []*mgmProto.NameServer{
						{IP: "8.8.8.8"},
						{IP: "1.1.1.1"},
						{IP: "203.0.113.53"},
					},
					Domains: []string{"example.com", "internal.example.com"},
				},
			},
			CustomZones: []*mgmProto.CustomZone{
				{
					Domain: "custom.example.com",
					Records: []*mgmProto.SimpleRecord{
						{
							Name:  "www.custom.example.com",
							Type:  1,
							RData: "203.0.113.10",
						},
						{
							Name:  "internal.custom.example.com",
							Type:  1,
							RData: "192.168.1.10",
						},
					},
				},
			},
		},
	}

	// Create anonymizer with test addresses
	anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())

	// Anonymize the network map
	err := anonymizeNetworkMap(networkMap, anonymizer)
	require.NoError(t, err)

	// Test PeerConfig anonymization
	peerCfg := networkMap.PeerConfig
	require.NotEqual(t, "203.0.113.5", peerCfg.Address)

	// Verify DNS and FQDN are properly anonymized
	require.NotEqual(t, "1.2.3.4", peerCfg.Dns)
	require.NotEqual(t, "peer1.corp.example.com", peerCfg.Fqdn)
	require.True(t, strings.HasSuffix(peerCfg.Fqdn, ".domain"))

	// Verify AddressV6 is anonymized but preserves prefix length
	anonV6Prefix, err := netiputil.DecodePrefix(peerCfg.AddressV6)
	require.NoError(t, err)
	assert.Equal(t, origV6Prefix.Bits(), anonV6Prefix.Bits(), "prefix length must be preserved")
	assert.NotEqual(t, origV6Prefix.Addr(), anonV6Prefix.Addr(), "IPv6 address must be anonymized")

	// Verify SSH key is replaced
	require.Equal(t, []byte("ssh-placeholder-key"), peerCfg.SshConfig.SshPubKey)

	// Test RemotePeers anonymization
	remotePeer := networkMap.RemotePeers[0]

	// Verify FQDN is anonymized
	require.NotEqual(t, "peer2.corp.example.com", remotePeer.Fqdn)
	require.True(t, strings.HasSuffix(remotePeer.Fqdn, ".domain"))

	// Check that public IPs are anonymized but private IPs are preserved
	for _, allowedIP := range remotePeer.AllowedIps {
		ip, _, err := net.ParseCIDR(allowedIP)
		require.NoError(t, err)

		if ip.IsPrivate() || isInCGNATRange(ip) {
			require.Contains(t, []string{
				"192.168.1.1/32",
				"100.64.0.1/32",
				"10.0.0.1/32",
			}, allowedIP)
		} else {
			require.NotContains(t, []string{
				"203.0.113.1/32",
				"2001:db8:1234::1/128",
			}, allowedIP)
		}
	}

	// Test Routes anonymization
	route := networkMap.Routes[0]
	require.NotEqual(t, "197.51.100.0/24", route.Network)
	for _, domain := range route.Domains {
		require.True(t, strings.HasSuffix(domain, ".domain"))
		require.NotContains(t, domain, "example.com")
	}

	// Test DNS config anonymization
	dnsConfig := networkMap.DNSConfig
	nameServerGroup := dnsConfig.NameServerGroups[0]

	// Verify well-known DNS servers are preserved
	require.Equal(t, "8.8.8.8", nameServerGroup.NameServers[0].IP)
	require.Equal(t, "1.1.1.1", nameServerGroup.NameServers[1].IP)

	// Verify public DNS server is anonymized
	require.NotEqual(t, "203.0.113.53", nameServerGroup.NameServers[2].IP)

	// Verify domains are anonymized
	for _, domain := range nameServerGroup.Domains {
		require.True(t, strings.HasSuffix(domain, ".domain"))
		require.NotContains(t, domain, "example.com")
	}

	// Test CustomZones anonymization
	customZone := dnsConfig.CustomZones[0]
	require.True(t, strings.HasSuffix(customZone.Domain, ".domain"))
	require.NotContains(t, customZone.Domain, "example.com")

	// Verify records are properly anonymized
	for _, record := range customZone.Records {
		require.True(t, strings.HasSuffix(record.Name, ".domain"))
		require.NotContains(t, record.Name, "example.com")

		ip := net.ParseIP(record.RData)
		if ip != nil {
			if !ip.IsPrivate() {
				require.NotEqual(t, "203.0.113.10", record.RData)
			} else {
				require.Equal(t, "192.168.1.10", record.RData)
			}
		}
	}
}

func TestIsSensitiveEnvVar(t *testing.T) {
	tests := []struct {
		key       string
		sensitive bool
	}{
		{"NB_SETUP_KEY", true},
		{"NB_API_TOKEN", true},
		{"NB_CLIENT_SECRET", true},
		{"NB_PASSWORD", true},
		{"NB_CREDENTIAL", true},
		{"NB_LOG_LEVEL", false},
		{"NB_MANAGEMENT_URL", false},
		{"NB_HOSTNAME", false},
		{"HOME", false},
		{"PATH", false},
	}
	for _, tt := range tests {
		t.Run(tt.key, func(t *testing.T) {
			assert.Equal(t, tt.sensitive, isSensitiveEnvVar(tt.key))
		})
	}
}

func TestSanitizeServiceEnvVars(t *testing.T) {
	tests := []struct {
		name      string
		anonymize bool
		input     map[string]any
		check     func(t *testing.T, params map[string]any)
	}{
		{
			name:      "no env vars key",
			anonymize: false,
			input:     map[string]any{"management_url": "https://mgmt.example.com"},
			check: func(t *testing.T, params map[string]any) {
				t.Helper()
				assert.Equal(t, "https://mgmt.example.com", params["management_url"], "non-env fields should be untouched")
				_, ok := params[jsonKeyServiceEnv]
				assert.False(t, ok, "service_env_vars should not be added")
			},
		},
		{
			name:      "non-NB vars are masked",
			anonymize: false,
			input: map[string]any{
				jsonKeyServiceEnv: map[string]any{
					"HOME":         "/root",
					"PATH":         "/usr/bin",
					"NB_LOG_LEVEL": "debug",
				},
			},
			check: func(t *testing.T, params map[string]any) {
				t.Helper()
				env := params[jsonKeyServiceEnv].(map[string]any)
				assert.Equal(t, maskedValue, env["HOME"], "non-NB_ var should be masked")
				assert.Equal(t, maskedValue, env["PATH"], "non-NB_ var should be masked")
				assert.Equal(t, "debug", env["NB_LOG_LEVEL"], "safe NB_ var should pass through")
			},
		},
		{
			name:      "sensitive NB vars are masked",
			anonymize: false,
			input: map[string]any{
				jsonKeyServiceEnv: map[string]any{
					"NB_SETUP_KEY": "abc123",
					"NB_API_TOKEN": "tok_xyz",
					"NB_LOG_LEVEL": "info",
				},
			},
			check: func(t *testing.T, params map[string]any) {
				t.Helper()
				env := params[jsonKeyServiceEnv].(map[string]any)
				assert.Equal(t, maskedValue, env["NB_SETUP_KEY"], "sensitive NB_ var should be masked")
				assert.Equal(t, maskedValue, env["NB_API_TOKEN"], "sensitive NB_ var should be masked")
				assert.Equal(t, "info", env["NB_LOG_LEVEL"], "safe NB_ var should pass through")
			},
		},
		{
			name:      "safe NB vars anonymized when anonymize is true",
			anonymize: true,
			input: map[string]any{
				jsonKeyServiceEnv: map[string]any{
					"NB_MANAGEMENT_URL": "https://mgmt.example.com:443",
					"NB_LOG_LEVEL":      "debug",
					"NB_SETUP_KEY":      "secret",
					"SOME_OTHER":        "val",
				},
			},
			check: func(t *testing.T, params map[string]any) {
				t.Helper()
				env := params[jsonKeyServiceEnv].(map[string]any)
				// Safe NB_ values should be anonymized (not the original, not masked)
				mgmtVal := env["NB_MANAGEMENT_URL"].(string)
				assert.NotEqual(t, "https://mgmt.example.com:443", mgmtVal, "should be anonymized")
				assert.NotEqual(t, maskedValue, mgmtVal, "should not be masked")

				logVal := env["NB_LOG_LEVEL"].(string)
				assert.NotEqual(t, maskedValue, logVal, "safe NB_ var should not be masked")

				// Sensitive and non-NB_ still masked
				assert.Equal(t, maskedValue, env["NB_SETUP_KEY"])
				assert.Equal(t, maskedValue, env["SOME_OTHER"])
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
			g := &BundleGenerator{
				anonymize:  tt.anonymize,
				anonymizer: anonymizer,
			}
			g.sanitizeServiceEnvVars(tt.input)
			tt.check(t, tt.input)
		})
	}
}

func TestAddServiceParams(t *testing.T) {
	t.Run("missing service.json returns nil", func(t *testing.T) {
		g := &BundleGenerator{
			anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()),
		}

		origStateDir := configs.StateDir
		configs.StateDir = t.TempDir()
		t.Cleanup(func() { configs.StateDir = origStateDir })

		err := g.addServiceParams()
		assert.NoError(t, err)
	})

	t.Run("management_url anonymized when anonymize is true", func(t *testing.T) {
		dir := t.TempDir()
		origStateDir := configs.StateDir
		configs.StateDir = dir
		t.Cleanup(func() { configs.StateDir = origStateDir })

		input := map[string]any{
			jsonKeyManagementURL: "https://api.example.com:443",
			jsonKeyServiceEnv: map[string]any{
				"NB_LOG_LEVEL": "trace",
			},
		}
		data, err := json.Marshal(input)
		require.NoError(t, err)
		require.NoError(t, os.WriteFile(filepath.Join(dir, serviceParamsFile), data, 0600))

		var buf bytes.Buffer
		zw := zip.NewWriter(&buf)

		g := &BundleGenerator{
			anonymize:  true,
			anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()),
			archive:    zw,
		}

		require.NoError(t, g.addServiceParams())
		require.NoError(t, zw.Close())

		zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
		require.NoError(t, err)
		require.Len(t, zr.File, 1)
		assert.Equal(t, serviceParamsBundle, zr.File[0].Name)

		rc, err := zr.File[0].Open()
		require.NoError(t, err)
		defer rc.Close()

		var result map[string]any
		require.NoError(t, json.NewDecoder(rc).Decode(&result))

		mgmt := result[jsonKeyManagementURL].(string)
		assert.NotEqual(t, "https://api.example.com:443", mgmt, "management_url should be anonymized")
		assert.NotEmpty(t, mgmt)

		env := result[jsonKeyServiceEnv].(map[string]any)
		assert.NotEqual(t, maskedValue, env["NB_LOG_LEVEL"], "safe NB_ var should not be masked")
	})

	t.Run("management_url preserved when anonymize is false", func(t *testing.T) {
		dir := t.TempDir()
		origStateDir := configs.StateDir
		configs.StateDir = dir
		t.Cleanup(func() { configs.StateDir = origStateDir })

		input := map[string]any{
			jsonKeyManagementURL: "https://api.example.com:443",
		}
		data, err := json.Marshal(input)
		require.NoError(t, err)
		require.NoError(t, os.WriteFile(filepath.Join(dir, serviceParamsFile), data, 0600))

		var buf bytes.Buffer
		zw := zip.NewWriter(&buf)

		g := &BundleGenerator{
			anonymize:  false,
			anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()),
			archive:    zw,
		}

		require.NoError(t, g.addServiceParams())
		require.NoError(t, zw.Close())

		zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
		require.NoError(t, err)

		rc, err := zr.File[0].Open()
		require.NoError(t, err)
		defer rc.Close()

		var result map[string]any
		require.NoError(t, json.NewDecoder(rc).Decode(&result))

		assert.Equal(t, "https://api.example.com:443", result[jsonKeyManagementURL], "management_url should be preserved")
	})
}

// Helper function to check if IP is in CGNAT range
func isInCGNATRange(ip net.IP) bool {
	cgnat := net.IPNet{
		IP:   net.ParseIP("100.64.0.0"),
		Mask: net.CIDRMask(10, 32),
	}
	return cgnat.Contains(ip)
}

func TestAnonymizeFirewallRules(t *testing.T) {
	// Example iptables-save output
	iptablesSave := `# Generated by iptables-save v1.8.7 on Thu Dec 19 10:00:00 2024
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -s 192.168.1.0/24 -j ACCEPT
-A INPUT -s 44.192.140.1/32 -j DROP
-A FORWARD -s 10.0.0.0/8 -j DROP
-A FORWARD -s 44.192.140.0/24 -d 52.84.12.34/24 -j ACCEPT
COMMIT

*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 192.168.100.0/24 -j MASQUERADE
-A PREROUTING -d 44.192.140.10/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 192.168.1.10:80
COMMIT`

	// Example iptables -v -n -L output
	iptablesVerbose := `Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
    pkts bytes target     prot opt in     out     source               destination         
       0     0 ACCEPT     all  --  *      *       192.168.1.0/24       0.0.0.0/0           
     100  1024 DROP       all  --  *      *       44.192.140.1         0.0.0.0/0

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
    pkts bytes target     prot opt in     out     source               destination         
       0     0 DROP       all  --  *      *       10.0.0.0/8           0.0.0.0/0           
      25   256 ACCEPT     all  --  *      *       44.192.140.0/24      52.84.12.34/24

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
    pkts bytes target     prot opt in     out     source               destination`

	// Example ip6tables-save output
	ip6tablesSave := `# Generated by ip6tables-save v1.8.7 on Thu Dec 19 10:00:00 2024
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -s fd00:1234::1/128 -j ACCEPT
-A INPUT -s 2607:f8b0:4005::1/128 -j DROP
-A FORWARD -s 2001:db8::/32 -d 2607:f8b0:4005::200e/128 -j ACCEPT
COMMIT`

	// Example nftables output with IPv6
	nftablesRules := `table inet filter {
        chain input {
                type filter hook input priority filter; policy accept;
                ip saddr 192.168.1.1 accept
                ip saddr 44.192.140.1 drop
                ip6 saddr 2607:f8b0:4005::1 drop
                ip6 saddr fd00:1234::1 accept
        }
        chain forward {
                type filter hook forward priority filter; policy accept;
                ip saddr 10.0.0.0/8 drop
                ip saddr 44.192.140.0/24 ip daddr 52.84.12.34/24 accept
                ip6 saddr 2001:db8::/32 ip6 daddr 2607:f8b0:4005::200e/128 accept
        }
    }`

	anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())

	// Test iptables-save anonymization
	anonIptablesSave := anonymizer.AnonymizeString(iptablesSave)

	// Private IP addresses should remain unchanged
	assert.Contains(t, anonIptablesSave, "192.168.1.0/24")
	assert.Contains(t, anonIptablesSave, "10.0.0.0/8")
	assert.Contains(t, anonIptablesSave, "192.168.100.0/24")
	assert.Contains(t, anonIptablesSave, "192.168.1.10")

	// Public IP addresses should be anonymized to the default range
	assert.NotContains(t, anonIptablesSave, "44.192.140.1")
	assert.NotContains(t, anonIptablesSave, "44.192.140.0/24")
	assert.NotContains(t, anonIptablesSave, "52.84.12.34")
	assert.Contains(t, anonIptablesSave, "198.51.100.") // Default anonymous range

	// Structure should be preserved
	assert.Contains(t, anonIptablesSave, "*filter")
	assert.Contains(t, anonIptablesSave, ":INPUT ACCEPT [0:0]")
	assert.Contains(t, anonIptablesSave, "COMMIT")
	assert.Contains(t, anonIptablesSave, "-j MASQUERADE")
	assert.Contains(t, anonIptablesSave, "--dport 80")

	// Test iptables verbose output anonymization
	anonIptablesVerbose := anonymizer.AnonymizeString(iptablesVerbose)

	// Private IP addresses should remain unchanged
	assert.Contains(t, anonIptablesVerbose, "192.168.1.0/24")
	assert.Contains(t, anonIptablesVerbose, "10.0.0.0/8")

	// Public IP addresses should be anonymized to the default range
	assert.NotContains(t, anonIptablesVerbose, "44.192.140.1")
	assert.NotContains(t, anonIptablesVerbose, "44.192.140.0/24")
	assert.NotContains(t, anonIptablesVerbose, "52.84.12.34")
	assert.Contains(t, anonIptablesVerbose, "198.51.100.") // Default anonymous range

	// Structure and counters should be preserved
	assert.Contains(t, anonIptablesVerbose, "Chain INPUT (policy ACCEPT 0 packets, 0 bytes)")
	assert.Contains(t, anonIptablesVerbose, "100  1024 DROP")
	assert.Contains(t, anonIptablesVerbose, "pkts bytes target")

	// Test nftables anonymization
	anonNftables := anonymizer.AnonymizeString(nftablesRules)

	// Private IP addresses should remain unchanged
	assert.Contains(t, anonNftables, "192.168.1.1")
	assert.Contains(t, anonNftables, "10.0.0.0/8")

	// Public IP addresses should be anonymized to the default range
	assert.NotContains(t, anonNftables, "44.192.140.1")
	assert.NotContains(t, anonNftables, "44.192.140.0/24")
	assert.NotContains(t, anonNftables, "52.84.12.34")
	assert.Contains(t, anonNftables, "198.51.100.") // Default anonymous range

	// Structure should be preserved
	assert.Contains(t, anonNftables, "table inet filter {")
	assert.Contains(t, anonNftables, "chain input {")
	assert.Contains(t, anonNftables, "type filter hook input priority filter; policy accept;")

	// IPv6 public addresses in nftables should be anonymized
	assert.NotContains(t, anonNftables, "2607:f8b0:4005::1")
	assert.NotContains(t, anonNftables, "2607:f8b0:4005::200e")
	assert.NotContains(t, anonNftables, "2001:db8::")
	assert.Contains(t, anonNftables, "2001:db8:ffff::") // Default anonymous v6 range

	// ULA addresses in nftables should be anonymized (global ID is a fingerprint)
	assert.NotContains(t, anonNftables, "fd00:1234::1")

	// IPv6 nftables structure preserved
	assert.Contains(t, anonNftables, "ip6 saddr")
	assert.Contains(t, anonNftables, "ip6 daddr")

	// Test ip6tables-save anonymization
	anonIp6tablesSave := anonymizer.AnonymizeString(ip6tablesSave)

	// ULA IPv6 should be anonymized (global ID is a fingerprint)
	assert.NotContains(t, anonIp6tablesSave, "fd00:1234::1/128")

	// Public IPv6 addresses should be anonymized
	assert.NotContains(t, anonIp6tablesSave, "2607:f8b0:4005::1")
	assert.NotContains(t, anonIp6tablesSave, "2607:f8b0:4005::200e")
	assert.NotContains(t, anonIp6tablesSave, "2001:db8::")
	assert.Contains(t, anonIp6tablesSave, "2001:db8:ffff::") // Default anonymous v6 range

	// Structure should be preserved
	assert.Contains(t, anonIp6tablesSave, "*filter")
	assert.Contains(t, anonIp6tablesSave, "COMMIT")
	assert.Contains(t, anonIp6tablesSave, "-j DROP")
	assert.Contains(t, anonIp6tablesSave, "-j ACCEPT")
}

// TestAddConfig_AllFieldsCovered uses reflection to ensure every field in
// profilemanager.Config is either rendered in the debug bundle or explicitly
// excluded. When a new field is added to Config, this test fails until the
// developer either dumps it in addConfig/addCommonConfigFields or adds it to
// the excluded set with a justification.
func TestAddConfig_AllFieldsCovered(t *testing.T) {
	excluded := map[string]string{
		"PrivateKey":        "sensitive: WireGuard private key",
		"PreSharedKey":      "sensitive: WireGuard pre-shared key",
		"SSHKey":            "sensitive: SSH private key",
		"ClientCertKeyPair": "non-config: parsed cert pair, not serialized",
		"Name":              "non-config: profile name is not needed for debug purposes",
		"policy":            "non-config: in-memory MDM policy snapshot, surfaced via Config.Policy() / GetConfigResponse.MDMManagedFields",
	}

	mURL, _ := url.Parse("https://api.example.com:443")
	aURL, _ := url.Parse("https://admin.example.com:443")
	bTrue := true
	iVal := 42
	cfg := &profilemanager.Config{
		PrivateKey:                    "priv",
		PreSharedKey:                  "psk",
		ManagementURL:                 mURL,
		AdminURL:                      aURL,
		WgIface:                       "wt0",
		WgPort:                        51820,
		NetworkMonitor:                &bTrue,
		IFaceBlackList:                []string{"eth0"},
		DisableIPv6Discovery:          true,
		RosenpassEnabled:              true,
		RosenpassPermissive:           true,
		ServerSSHAllowed:              &bTrue,
		EnableSSHRoot:                 &bTrue,
		EnableSSHSFTP:                 &bTrue,
		EnableSSHLocalPortForwarding:  &bTrue,
		EnableSSHRemotePortForwarding: &bTrue,
		DisableSSHAuth:                &bTrue,
		SSHJWTCacheTTL:                &iVal,
		DisableClientRoutes:           true,
		DisableServerRoutes:           true,
		DisableDNS:                    true,
		DisableFirewall:               true,
		BlockLANAccess:                true,
		BlockInbound:                  true,
		DisableNotifications:          &bTrue,
		DNSLabels:                     domain.List{},
		SSHKey:                        "sshkey",
		NATExternalIPs:                []string{"1.2.3.4"},
		CustomDNSAddress:              "1.1.1.1:53",
		DisableAutoConnect:            true,
		DNSRouteInterval:              5 * time.Second,
		ClientCertPath:                "/tmp/cert",
		ClientCertKeyPath:             "/tmp/key",
		LazyConnection:                "on",
		MTU:                           1280,
	}

	for _, anonymize := range []bool{false, true} {
		t.Run("anonymize="+map[bool]string{true: "true", false: "false"}[anonymize], func(t *testing.T) {
			g := &BundleGenerator{
				anonymizer:     newAnonymizerForTest(),
				internalConfig: cfg,
				anonymize:      anonymize,
			}

			var sb strings.Builder
			g.addCommonConfigFields(&sb)
			rendered := sb.String() + renderAddConfigSpecific(g)

			val := reflect.ValueOf(cfg).Elem()
			typ := val.Type()
			var missing []string
			for i := 0; i < typ.NumField(); i++ {
				name := typ.Field(i).Name
				if _, ok := excluded[name]; ok {
					continue
				}
				if !strings.Contains(rendered, name+":") {
					missing = append(missing, name)
				}
			}
			if len(missing) > 0 {
				t.Fatalf("Config field(s) not present in debug bundle output: %v\n"+
					"Either render the field in addCommonConfigFields/addConfig, "+
					"or add it to the excluded map with a justification.", missing)
			}
		})
	}
}

// renderAddConfigSpecific renders the fields handled by the anonymize/non-anonymize
// branches in addConfig (ManagementURL, AdminURL, NATExternalIPs, CustomDNSAddress).
// addCommonConfigFields covers the rest. Keeping this in the test mirrors the
// production shape without needing to write an actual zip.
func renderAddConfigSpecific(g *BundleGenerator) string {
	var sb strings.Builder
	if g.anonymize {
		if g.internalConfig.ManagementURL != nil {
			sb.WriteString("ManagementURL: " + g.anonymizer.AnonymizeURI(g.internalConfig.ManagementURL.String()) + "\n")
		}
		if g.internalConfig.AdminURL != nil {
			sb.WriteString("AdminURL: " + g.anonymizer.AnonymizeURI(g.internalConfig.AdminURL.String()) + "\n")
		}
		sb.WriteString("NATExternalIPs: x\n")
		if g.internalConfig.CustomDNSAddress != "" {
			sb.WriteString("CustomDNSAddress: " + g.anonymizer.AnonymizeString(g.internalConfig.CustomDNSAddress) + "\n")
		}
	} else {
		if g.internalConfig.ManagementURL != nil {
			sb.WriteString("ManagementURL: " + g.internalConfig.ManagementURL.String() + "\n")
		}
		if g.internalConfig.AdminURL != nil {
			sb.WriteString("AdminURL: " + g.internalConfig.AdminURL.String() + "\n")
		}
		sb.WriteString("NATExternalIPs: x\n")
		if g.internalConfig.CustomDNSAddress != "" {
			sb.WriteString("CustomDNSAddress: " + g.internalConfig.CustomDNSAddress + "\n")
		}
	}
	return sb.String()
}

func newAnonymizerForTest() *anonymize.Anonymizer {
	return anonymize.NewAnonymizer(anonymize.DefaultAddresses())
}
