package types

import (
	"slices"
	"testing"

	"github.com/google/go-cmp/cmp"
)

var topologyTestSet = map[string]struct {
	input *Topology
	want  map[string]*NodeDefinition
}{
	"node_only": {
		input: &Topology{
			Nodes: map[string]*NodeDefinition{
				"node1": {
					Kind:       "nokia_srlinux",
					CPU:        1,
					Memory:     "1G",
					AutoRemove: new(true),
					DNS: &DNSConfig{
						Servers: []string{"1.1.1.1"},
						Search:  []string{"foo.com"},
						Options: []string{"someopt"},
					},
					Certificate: &CertificateConfig{
						Issue: new(true),
					},
				},
			},
		},
		want: map[string]*NodeDefinition{
			"node1": {
				Kind:       "nokia_srlinux",
				CPU:        1,
				Memory:     "1G",
				AutoRemove: new(true),
				DNS: &DNSConfig{
					Servers: []string{"1.1.1.1"},
					Search:  []string{"foo.com"},
					Options: []string{"someopt"},
				},
				Certificate: &CertificateConfig{
					Issue: new(true),
				},
				Env:    map[string]string{},
				Labels: map[string]string{},
			},
		},
	},
	"node_kind": {
		input: &Topology{
			Kinds: map[string]*NodeDefinition{
				"nokia_srlinux": {
					Group:         "grp1",
					Type:          "type1",
					StartupConfig: "test_data/config.cfg",
					Image:         "image:latest",
					License:       "test_data/lic1.key",
					Position:      "pos1",
					Cmd:           "runit",
					Exec: []string{
						"bash test1.sh",
						"bash test2.sh",
					},
					User: "user1",
					Binds: []string{
						"a:b",
						"c:d",
					},
					Ports: []string{
						"80:8080",
					},
					Env: map[string]string{
						"env1": "v1",
						"env2": "v2",
					},
					Labels: map[string]string{
						"label1": "v1",
						"label2": "v2",
					},
					CPU:        1,
					Memory:     "1G",
					AutoRemove: new(true),
					DNS: &DNSConfig{
						Servers: []string{"8.8.8.8"},
						Search:  []string{"bar.com"},
						Options: []string{"someotheropt"},
					},
					Certificate: &CertificateConfig{
						Issue: new(true),
					},
				},
			},
			Nodes: map[string]*NodeDefinition{
				"node1": {
					Kind: "nokia_srlinux",
					Env: map[string]string{
						"env2": "notv2",
					},
					Labels: map[string]string{
						"label2": "notv2",
					},
					Memory:     "2G",
					AutoRemove: new(false),
					DNS: &DNSConfig{
						Servers: []string{"1.1.1.1"},
						Search:  []string{"foo.com"},
						Options: []string{"someopt"},
					},
				},
			},
		},
		want: map[string]*NodeDefinition{
			"node1": {
				Kind:          "nokia_srlinux",
				Group:         "grp1",
				Type:          "type1",
				StartupConfig: "test_data/config.cfg",
				Image:         "image:latest",
				License:       "test_data/lic1.key",
				Position:      "pos1",
				Cmd:           "runit",
				Exec: []string{
					"bash test1.sh",
					"bash test2.sh",
				},
				User: "user1",
				Binds: []string{
					"a:b",
					"c:d",
				},
				Ports: []string{
					"80:8080",
				},
				Env: map[string]string{
					"env1": "v1",
					"env2": "notv2",
				},
				Labels: map[string]string{
					"label1": "v1",
					"label2": "notv2",
				},
				CPU:        1,
				Memory:     "2G",
				AutoRemove: new(false),
				DNS: &DNSConfig{
					Servers: []string{"1.1.1.1"},
					Search:  []string{"foo.com"},
					Options: []string{"someopt"},
				},
				Certificate: &CertificateConfig{
					Issue: new(true),
				},
			},
		},
	},
	"node_kind_default": {
		input: &Topology{
			Defaults: &NodeDefinition{
				Kind: "nokia_srlinux",
				User: "user1",
				CPU:  1,
				Binds: []string{
					"x:z",
					"m:n", // overridden by node
				},
				Volumes: []string{
					"default-vol:/app/default",
					"shared-vol:/app/shared", // overriden by node
				},
			},
			Kinds: map[string]*NodeDefinition{
				"nokia_srlinux": {
					Group:         "grp1",
					Type:          "type1",
					StartupConfig: "test_data/config.cfg",
					Image:         "image:latest",
					License:       "test_data/lic1.key",
					Position:      "pos1",
					Cmd:           "runit",
					Exec: []string{
						"bash test1.sh",
						"bash test2.sh",
					},
					Binds: []string{
						"a:b",
						"c:d",
					},
					Volumes: []string{
						"kind-vol:/app/kind",
						"db-vol:/app/data",
					},
					Ports: []string{
						"80:8080",
					},
					Env: map[string]string{
						"env1": "v1",
						"env2": "v2",
					},
					Labels: map[string]string{
						"label1": "v1",
						"label2": "v2",
					},
					CPU:    2,
					Memory: "2G",
					DNS: &DNSConfig{
						Servers: []string{"1.1.1.1"},
						Search:  []string{"foo.com"},
						Options: []string{"someopt"},
					},
				},
			},
			Nodes: map[string]*NodeDefinition{
				"node1": {
					Binds: []string{
						"e:f",
						"newm:n",
					},
					Volumes: []string{
						"node-vol:/app/node",
						"override-vol:/app/shared", // overrides defaults
					},
				},
			},
		},
		want: map[string]*NodeDefinition{
			"node1": {
				Kind:          "nokia_srlinux",
				Group:         "grp1",
				Type:          "type1",
				StartupConfig: "test_data/config.cfg",
				Image:         "image:latest",
				License:       "test_data/lic1.key",
				Position:      "pos1",
				Cmd:           "runit",
				User:          "user1",
				Exec: []string{
					"bash test1.sh",
					"bash test2.sh",
				},
				Binds: []string{
					"e:f",
					"a:b",
					"c:d",
					"x:z",
					"newm:n",
				},
				Volumes: []string{
					"node-vol:/app/node",
					"kind-vol:/app/kind",
					"db-vol:/app/data",
					"default-vol:/app/default",
					"override-vol:/app/shared",
				},
				Ports: []string{
					"80:8080",
				},
				Env: map[string]string{
					"env1": "v1",
					"env2": "v2",
				},
				Labels: map[string]string{
					"label1": "v1",
					"label2": "v2",
				},
				CPU:    1,
				Memory: "2G",
				DNS: &DNSConfig{
					Servers: []string{"1.1.1.1"},
					Search:  []string{"foo.com"},
					Options: []string{"someopt"},
				},
				Certificate: &CertificateConfig{
					Issue: new(false),
				},
			},
		},
	},
	"node_default": {
		input: &Topology{
			Defaults: &NodeDefinition{
				Kind:          "nokia_srlinux",
				Group:         "grp1",
				Type:          "type1",
				StartupConfig: "test_data/config.cfg",
				Image:         "image:latest",
				License:       "test_data/lic1.key",
				Position:      "pos1",
				Cmd:           "runit",
				Exec: []string{
					"bash test1.sh",
					"bash test2.sh",
				},
				Binds: []string{
					"a:b",
					"c:d",
				},
				Ports: []string{
					"80:8080",
				},
				Env: map[string]string{
					"env1": "v1",
					"env2": "v2",
				},
				Labels: map[string]string{
					"label1": "v1",
					"label2": "v2",
				},
				CPU:    1,
				Memory: "1G",
				DNS: &DNSConfig{
					Servers: []string{"1.1.1.1"},
					Search:  []string{"foo.com"},
					Options: []string{"someopt"},
				},
			},
			Nodes: map[string]*NodeDefinition{
				"node1": {},
			},
		},
		want: map[string]*NodeDefinition{
			"node1": {
				Kind:          "nokia_srlinux",
				Group:         "grp1",
				Type:          "type1",
				StartupConfig: "test_data/config.cfg",
				Image:         "image:latest",
				License:       "test_data/lic1.key",
				Position:      "pos1",
				Cmd:           "runit",
				Exec: []string{
					"bash test1.sh",
					"bash test2.sh",
				},
				Binds: []string{
					"a:b",
					"c:d",
				},
				Ports: []string{
					"80:8080",
				},
				Env: map[string]string{
					"env1": "v1",
					"env2": "v2",
				},
				Labels: map[string]string{
					"label1": "v1",
					"label2": "v2",
				},
				CPU:        1,
				Memory:     "1G",
				AutoRemove: new(false),
				DNS: &DNSConfig{
					Servers: []string{"1.1.1.1"},
					Search:  []string{"foo.com"},
					Options: []string{"someopt"},
				},
				Certificate: &CertificateConfig{
					Issue: new(false),
				},
			},
		},
	},
	"node_group": {
		input: &Topology{
			Groups: map[string]*NodeDefinition{
				"grp1": {
					Kind:          "nokia_srlinux",
					Type:          "type1",
					StartupConfig: "test_data/config.cfg",
					Image:         "image:latest",
					License:       "test_data/lic1.key",
					Position:      "pos1",
					Cmd:           "runit",
					Exec: []string{
						"bash test1.sh",
						"bash test2.sh",
					},
					User: "user1",
					Binds: []string{
						"a:b",
						"c:d",
					},
					Ports: []string{
						"80:8080",
					},
					Env: map[string]string{
						"env1": "v1",
						"env2": "v2",
					},
					Labels: map[string]string{
						"label1": "v1",
						"label2": "v2",
					},
					CPU:        1,
					Memory:     "1G",
					AutoRemove: new(true),
					DNS: &DNSConfig{
						Servers: []string{"8.8.8.8"},
						Search:  []string{"bar.com"},
						Options: []string{"someotheropt"},
					},
					Certificate: &CertificateConfig{
						Issue: new(true),
					},
				},
			},
			Nodes: map[string]*NodeDefinition{
				"node1": {
					Group: "grp1",
					Env: map[string]string{
						"env2": "notv2",
					},
					Labels: map[string]string{
						"label2": "notv2",
					},
					Memory:     "2G",
					AutoRemove: new(false),
					DNS: &DNSConfig{
						Servers: []string{"1.1.1.1"},
						Search:  []string{"foo.com"},
						Options: []string{"someopt"},
					},
				},
			},
		},
		want: map[string]*NodeDefinition{
			"node1": {
				Kind:          "nokia_srlinux",
				Group:         "grp1",
				Type:          "type1",
				StartupConfig: "test_data/config.cfg",
				Image:         "image:latest",
				License:       "test_data/lic1.key",
				Position:      "pos1",
				Cmd:           "runit",
				Exec: []string{
					"bash test1.sh",
					"bash test2.sh",
				},
				User: "user1",
				Binds: []string{
					"a:b",
					"c:d",
				},
				Ports: []string{
					"80:8080",
				},
				Env: map[string]string{
					"env1": "v1",
					"env2": "notv2",
				},
				Labels: map[string]string{
					"label1": "v1",
					"label2": "notv2",
				},
				CPU:        1,
				Memory:     "2G",
				AutoRemove: new(false),
				DNS: &DNSConfig{
					Servers: []string{"1.1.1.1"},
					Search:  []string{"foo.com"},
					Options: []string{"someopt"},
				},
				Certificate: &CertificateConfig{
					Issue: new(true),
				},
			},
		},
	},
	"node_group_kind": {
		input: &Topology{
			Kinds: map[string]*NodeDefinition{
				"nokia_srlinux": {
					User: "user0",
					CPU:  1,
					Binds: []string{
						"x:z",
						"m:n", // overridden by group
					},
				},
			},
			Groups: map[string]*NodeDefinition{
				"grp1": {
					Kind:          "nokia_srlinux",
					User:          "user1",
					Type:          "type1",
					StartupConfig: "test_data/config.cfg",
					Image:         "image:latest",
					License:       "test_data/lic1.key",
					Position:      "pos1",
					Cmd:           "runit",
					Exec: []string{
						"bash test1.sh",
						"bash test2.sh",
					},
					Binds: []string{
						"a:b",
						"c:d",
					},
					Ports: []string{
						"80:8080",
					},
					Env: map[string]string{
						"env1": "v1",
						"env2": "v2",
					},
					Labels: map[string]string{
						"label1": "v1",
						"label2": "v2",
					},
					CPU:    2,
					Memory: "2G",
					DNS: &DNSConfig{
						Servers: []string{"1.1.1.1"},
						Search:  []string{"foo.com"},
						Options: []string{"someopt"},
					},
				},
			},
			Nodes: map[string]*NodeDefinition{
				"node1": {
					Group: "grp1",
					Binds: []string{
						"e:f",
						"newm:n",
					},
				},
			},
		},
		want: map[string]*NodeDefinition{
			"node1": {
				Kind:          "nokia_srlinux",
				Group:         "grp1",
				Type:          "type1",
				StartupConfig: "test_data/config.cfg",
				Image:         "image:latest",
				License:       "test_data/lic1.key",
				Position:      "pos1",
				Cmd:           "runit",
				User:          "user1",
				Exec: []string{
					"bash test1.sh",
					"bash test2.sh",
				},
				Binds: []string{
					"e:f",
					"a:b",
					"c:d",
					"x:z",
					"newm:n",
				},
				Ports: []string{
					"80:8080",
				},
				Env: map[string]string{
					"env1": "v1",
					"env2": "v2",
				},
				Labels: map[string]string{
					"label1": "v1",
					"label2": "v2",
				},
				CPU:    1,
				Memory: "2G",
				DNS: &DNSConfig{
					Servers: []string{"1.1.1.1"},
					Search:  []string{"foo.com"},
					Options: []string{"someopt"},
				},
				Certificate: &CertificateConfig{
					Issue: new(false),
				},
			},
		},
	},
	"node_group_default": {
		input: &Topology{
			Defaults: &NodeDefinition{
				Kind: "linux", // overridden by group
				User: "user1",
				CPU:  1,
				Binds: []string{
					"x:z",
					"m:n", // overridden by node
				},
			},
			Groups: map[string]*NodeDefinition{
				"grp1": {
					Kind:          "nokia_srlinux",
					Type:          "type1",
					StartupConfig: "test_data/config.cfg",
					Image:         "image:latest",
					License:       "test_data/lic1.key",
					Position:      "pos1",
					Cmd:           "runit",
					Exec: []string{
						"bash test1.sh",
						"bash test2.sh",
					},
					Binds: []string{
						"a:b",
						"c:d",
					},
					Ports: []string{
						"80:8080",
					},
					Env: map[string]string{
						"env1": "v1",
						"env2": "v2",
					},
					Labels: map[string]string{
						"label1": "v1",
						"label2": "v2",
					},
					CPU:    2,
					Memory: "2G",
					DNS: &DNSConfig{
						Servers: []string{"1.1.1.1"},
						Search:  []string{"foo.com"},
						Options: []string{"someopt"},
					},
				},
			},
			Nodes: map[string]*NodeDefinition{
				"node1": {
					Group: "grp1",
					Binds: []string{
						"e:f",
						"newm:n",
					},
				},
			},
		},
		want: map[string]*NodeDefinition{
			"node1": {
				Kind:          "nokia_srlinux",
				Group:         "grp1",
				Type:          "type1",
				StartupConfig: "test_data/config.cfg",
				Image:         "image:latest",
				License:       "test_data/lic1.key",
				Position:      "pos1",
				Cmd:           "runit",
				User:          "user1",
				Exec: []string{
					"bash test1.sh",
					"bash test2.sh",
				},
				Binds: []string{
					"e:f",
					"a:b",
					"c:d",
					"x:z",
					"newm:n",
				},
				Ports: []string{
					"80:8080",
				},
				Env: map[string]string{
					"env1": "v1",
					"env2": "v2",
				},
				Labels: map[string]string{
					"label1": "v1",
					"label2": "v2",
				},
				CPU:    1,
				Memory: "2G",
				DNS: &DNSConfig{
					Servers: []string{"1.1.1.1"},
					Search:  []string{"foo.com"},
					Options: []string{"someopt"},
				},
				Certificate: &CertificateConfig{
					Issue: new(false),
				},
			},
		},
	},
	// see #2852
	"type_from_kind_via_defaults": {
		input: &Topology{
			Nodes: map[string]*NodeDefinition{
				"node1": {},
			},
			Kinds: map[string]*NodeDefinition{
				"default_kind": {
					Type: "fooer",
				},
			},
			Defaults: &NodeDefinition{
				Kind: "default_kind",
			},
		},
		want: map[string]*NodeDefinition{
			"node1": {
				Kind:   "default_kind",
				Type:   "fooer",
				Env:    map[string]string{},
				Labels: map[string]string{},
				DNS:    &DNSConfig{},
				Certificate: &CertificateConfig{
					Issue: new(false),
				},
			},
		},
	},
}

func TestGetNodeKind(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		kind := item.input.GetNodeKind("node1")

		if diff := cmp.Diff(item.want["node1"].Kind, kind); diff != "" {
			t.Errorf("item %q failed: (-want +got)\n%s", name, diff)
		}
	}
}

func TestGetNodeGroup(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		group := item.input.GetNodeGroup("node1")

		t.Logf("%q test item result: %v", name, group)

		if diff := cmp.Diff(item.want["node1"].Group, group); diff != "" {
			t.Errorf("item %q failed: (-want +got)\n%s", name, diff)
		}
	}
}

func TestGetNodeType(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		typ := item.input.GetNodeType("node1")

		t.Logf("%q test item result: %v", name, typ)

		if diff := cmp.Diff(item.want["node1"].Type, typ); diff != "" {
			t.Errorf("item %q failed: (-want +got)\n%s", name, diff)
		}
	}
}

func TestGetNodeConfig(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		config := item.input.GetNodeStartupConfig("node1")

		wantedConfig := item.want["node1"].StartupConfig

		t.Logf("%q test item result: %v", name, config)

		if diff := cmp.Diff(wantedConfig, config); diff != "" {
			t.Errorf("item %q failed: (-want +got)\n%s", name, diff)
		}
	}
}

func TestGetNodeImage(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		image := item.input.GetNodeImage("node1")

		t.Logf("%q test item result: %v", name, image)

		if diff := cmp.Diff(item.want["node1"].Image, image); diff != "" {
			t.Errorf("item %q failed: (-want +got)\n%s", name, diff)
		}
	}
}

// TestGetNodeImageOnlyViaDefault tests that the node
// inherits image from kinds via defaults.
func TestGetNodeImageOnlyViaDefault(t *testing.T) {
	// This represents the exact scenario from the GitHub issue
	topology := &Topology{
		Defaults: &NodeDefinition{
			Kind: "nokia_srlinux",
		},
		Kinds: map[string]*NodeDefinition{
			"nokia_srlinux": {
				Image: "ghcr.io/nokia/srlinux:latest",
			},
		},
		Nodes: map[string]*NodeDefinition{
			"sw01": nil, // This simulates YAML parsing where "sw01:" creates a nil entry
			"sw02": nil,
			"host01": {
				Kind:  "linux",
				Image: "debian-host:latest",
			},
		},
	}

	// Test that nil nodes inherit image from kinds via defaults
	tests := []struct {
		nodeName string
		want     string
	}{
		{"sw01", "ghcr.io/nokia/srlinux:latest"},
		{"sw02", "ghcr.io/nokia/srlinux:latest"},
		{"host01", "debian-host:latest"},
	}

	for _, tt := range tests {
		t.Run(tt.nodeName, func(t *testing.T) {
			got := topology.GetNodeImage(tt.nodeName)
			if got != tt.want {
				t.Errorf("GetNodeImage(%s) = %q, want %q", tt.nodeName, got, tt.want)
			}
		})
	}
}

func TestGetNodeLicense(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		lic := item.input.GetNodeLicense("node1")

		wantedLicense := item.want["node1"].License

		t.Logf("%q test item result: %v", name, lic)

		if diff := cmp.Diff(wantedLicense, lic); diff != "" {
			t.Errorf("item %q failed: (-want +got)\n%s", name, diff)
		}
	}
}

func TestGetNodePosition(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		pos := item.input.GetNodePosition("node1")

		t.Logf("%q test item result: %v", name, pos)

		if diff := cmp.Diff(item.want["node1"].Position, pos); diff != "" {
			t.Errorf("item %q failed: (-want +got)\n%s", name, diff)
		}
	}
}

func TestGetNodeHostname(t *testing.T) {
	topology := &Topology{
		Defaults: &NodeDefinition{Hostname: "default-host"},
		Kinds: map[string]*NodeDefinition{
			"linux": {Hostname: "kind-host"},
		},
		Groups: map[string]*NodeDefinition{
			"apps": {Hostname: "group-host"},
		},
		Nodes: map[string]*NodeDefinition{
			"node-default": {},
			"node-kind":    {Kind: "linux"},
			"node-group":   {Kind: "linux", Group: "apps"},
			"node-explicit": {
				Kind: "linux", Group: "apps", Hostname: "node-host",
			},
		},
	}

	tests := []struct {
		name string
		want string
	}{
		{name: "node-default", want: "default-host"},
		{name: "node-kind", want: "kind-host"},
		{name: "node-group", want: "group-host"},
		{name: "node-explicit", want: "node-host"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := topology.GetNodeHostname(tt.name); got != tt.want {
				t.Fatalf("GetNodeHostname(%q) = %q, want %q", tt.name, got, tt.want)
			}
		})
	}
}

func TestGetNodeCmd(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		cmd := item.input.GetNodeCmd("node1")

		t.Logf("%q test item result: %v", name, cmd)

		if diff := cmp.Diff(item.want["node1"].Cmd, cmd); diff != "" {
			t.Errorf("item %q failed: (-want +got)\n%s", name, diff)
		}
	}
}

func TestGetNodeExec(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		exec := item.input.GetNodeExec("node1")

		t.Logf("%q test item result: %v", name, exec)

		if diff := cmp.Diff(item.want["node1"].Exec, exec); diff != "" {
			t.Errorf("item %q failed: (-want +got)\n%s", name, diff)
		}
	}
}

func TestGetNodeUser(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		user := item.input.GetNodeUser("node1")

		t.Logf("%q test item result: %v", name, user)

		if diff := cmp.Diff(item.want["node1"].User, user); diff != "" {
			t.Errorf("item %q failed: (-want +got)\n%s", name, diff)
		}
	}
}

func TestGetNodeBinds(t *testing.T) {
	for _, item := range topologyTestSet {
		binds, _ := item.input.GetNodeBinds("node1")

		// sort the slices so we can compare them
		slices.Sort(binds)
		slices.Sort(item.want["node1"].Binds)

		if diff := cmp.Diff(item.want["node1"].Binds, binds); diff != "" {
			t.Fatalf(
				"Binds resolve failed.\nGot: %q\nWant: %q\nDiff\n%s",
				binds,
				item.want["node1"].Binds,
				diff,
			)
		}
	}
}

func TestGetNodeVolumes(t *testing.T) {
	for _, item := range topologyTestSet {
		volumes, _ := item.input.GetNodeVolumes("node1")

		// sort the slices so we can compare them
		slices.Sort(volumes)
		slices.Sort(item.want["node1"].Volumes)

		if d := cmp.Diff(volumes, item.want["node1"].Volumes); d != "" {
			t.Fatalf(
				"Volumes resolve failed.\nGot: %q\nWant: %q\nDiff\n%s",
				volumes,
				item.want["node1"].Volumes,
				d,
			)
		}
	}
}

func TestGetNodeVolumesInheritance(t *testing.T) {
	topology := &Topology{
		Defaults: &NodeDefinition{
			Volumes: []string{"default:/default"},
		},
		Kinds: map[string]*NodeDefinition{
			"linux": {
				Volumes: []string{"kind:/kind"},
			},
		},
		Groups: map[string]*NodeDefinition{
			"app": {
				Volumes: []string{"group:/group"},
			},
		},
		Nodes: map[string]*NodeDefinition{
			"node1": {
				Kind:  "linux",
				Group: "app",
				Volumes: []string{
					"node:/node",
				},
			},
		},
	}

	volumes, err := topology.GetNodeVolumes("node1")
	if err != nil {
		t.Fatalf("GetNodeVolumes() unexpected error: %v", err)
	}
	wantVolumes := []string{
		"default:/default",
		"kind:/kind",
		"group:/group",
		"node:/node",
	}
	slices.Sort(wantVolumes)
	if diff := cmp.Diff(wantVolumes, volumes); diff != "" {
		t.Fatalf("GetNodeVolumes() mismatch (-want +got):\n%s", diff)
	}
}

func TestGetNodeBindsRejectsAnonymousVolumeSyntax(t *testing.T) {
	topology := &Topology{
		Defaults: &NodeDefinition{},
		Nodes: map[string]*NodeDefinition{
			"node1": {
				Binds: []string{"/data"},
			},
		},
	}

	_, err := topology.GetNodeBinds("node1")
	if err == nil {
		t.Fatal("GetNodeBinds() accepted anonymous volume syntax in binds")
	}
}

func TestGetNodeVolumesHandlesNilNodeDefinition(t *testing.T) {
	topology := &Topology{
		Defaults: &NodeDefinition{},
		Nodes: map[string]*NodeDefinition{
			"node1": nil,
		},
	}

	volumes, err := topology.GetNodeVolumes("node1")
	if err != nil {
		t.Fatalf("GetNodeVolumes() unexpected error: %v", err)
	}
	if volumes != nil {
		t.Fatalf("GetNodeVolumes() = %v, want nil", volumes)
	}
}

func TestGetNodeBindsDeterministicOrder(t *testing.T) {
	topology := &Topology{
		Defaults: &NodeDefinition{},
		Nodes: map[string]*NodeDefinition{
			"node1": {
				Binds: []string{
					"z-source:/etc/z.conf",
					"a-source:/etc/a.conf",
					"m-source:/etc/m.conf",
				},
			},
		},
	}
	want := []string{
		"a-source:/etc/a.conf",
		"m-source:/etc/m.conf",
		"z-source:/etc/z.conf",
	}

	for range 100 {
		got, err := topology.GetNodeBinds("node1")
		if err != nil {
			t.Fatalf("GetNodeBinds() unexpected error: %v", err)
		}
		if diff := cmp.Diff(want, got); diff != "" {
			t.Fatalf(
				"GetNodeBinds() returned binds in a non-deterministic order (-want +got):\n%s",
				diff,
			)
		}
	}
}

func TestGetNodeEnv(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		envs := item.input.GetNodeEnv("node1")

		t.Logf("%q test item result: %v", name, envs)

		if diff := cmp.Diff(item.want["node1"].Env, envs); diff != "" {
			t.Errorf("item %q failed: (-want +got)\n%s", name, diff)
		}
	}
}

func TestGetNodeLabels(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		labels := item.input.GetNodeLabels("node1")

		t.Logf("%q test item result: %v", name, labels)

		if diff := cmp.Diff(item.want["node1"].Labels, labels); diff != "" {
			t.Errorf("item %q failed: (-want +got)\n%s", name, diff)
		}
	}
}

func TestGetNodeAutoRemove(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		autoremove := item.input.GetNodeAutoRemove("node1")

		t.Logf("%q test item result: %v", name, autoremove)

		want := false

		if item.want["node1"].AutoRemove != nil {
			want = *item.want["node1"].AutoRemove
		}

		if diff := cmp.Diff(want, autoremove); diff != "" {
			t.Errorf("item %q failed: (-want +got)\n%s", name, diff)
		}
	}
}

func TestGetNodeDNS(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		dns := item.input.GetNodeDns("node1")

		t.Logf("%q test item result: %v", name, dns)

		if diff := cmp.Diff(item.want["node1"].DNS, dns); diff != "" {
			t.Fatalf(
				"DNS config object doesn't match.\nGot: %+v\nWant: %+v\nDiff\n%s",
				dns,
				item.want["node1"].DNS,
				diff,
			)
		}
	}
}

func TestGetNodeCertificateConfig(t *testing.T) {
	for name, item := range topologyTestSet {
		t.Logf("%q test item", name)

		cert := item.input.GetCertificateConfig("node1")

		t.Logf("%q test item result: %v", name, cert)

		if diff := cmp.Diff(item.want["node1"].Certificate, cert); diff != "" {
			t.Fatalf("Certificate config objects don't match.\nGot: %+v\nWant: %+v\nDiff\n%s",
				cert, item.want["node1"].Certificate, diff)
		}
	}
}

// TestGetNodeCredentials tests the credential resolution hierarchy.
func TestGetNodeCredentials(t *testing.T) {
	tests := map[string]struct {
		topo                *Topology
		nodeName            string
		wantUsername        string
		wantPassword        string
		wantIdentityFile    string
		checkCredentialsSrc bool
		wantCredentialsSrc  CredentialTopologySource
	}{
		"node_overrides_kind": {
			topo: &Topology{
				Kinds: map[string]*NodeDefinition{
					"srl": {
						Credentials: NodeCredentials{Username: "kind-user", Password: "kind-pass"},
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {
						Kind:        "srl",
						Credentials: NodeCredentials{Username: "node-user", Password: "node-pass"},
					},
				},
			},
			nodeName:     "node1",
			wantUsername: "node-user",
			wantPassword: "node-pass",
		},
		"kind_overrides_defaults": {
			topo: &Topology{
				Defaults: &NodeDefinition{
					Credentials: NodeCredentials{
						Username: "default-user",
						Password: "default-pass",
					},
				},
				Kinds: map[string]*NodeDefinition{
					"srl": {
						Credentials: NodeCredentials{Username: "kind-user", Password: "kind-pass"},
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {Kind: "srl"},
				},
			},
			nodeName:     "node1",
			wantUsername: "kind-user",
			wantPassword: "kind-pass",
		},
		"defaults_used_when_no_kind_or_node": {
			topo: &Topology{
				Defaults: &NodeDefinition{
					Credentials: NodeCredentials{
						Username: "default-user",
						Password: "default-pass",
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {Kind: "srl"},
				},
			},
			nodeName:     "node1",
			wantUsername: "default-user",
			wantPassword: "default-pass",
		},
		"empty_when_nothing_set": {
			topo: &Topology{
				Nodes: map[string]*NodeDefinition{
					"node1": {Kind: "srl"},
				},
			},
			nodeName:     "node1",
			wantUsername: "",
			wantPassword: "",
		},
		"group_overrides_defaults": {
			topo: &Topology{
				Defaults: &NodeDefinition{
					Credentials: NodeCredentials{
						Username: "default-user",
						Password: "default-pass",
					},
				},
				Groups: map[string]*NodeDefinition{
					"grp1": {
						Credentials: NodeCredentials{
							Username: "group-user",
							Password: "group-pass",
						},
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {Kind: "srl", Group: "grp1"},
				},
			},
			nodeName:     "node1",
			wantUsername: "group-user",
			wantPassword: "group-pass",
		},
		"node_overrides_group_and_defaults": {
			topo: &Topology{
				Defaults: &NodeDefinition{
					Credentials: NodeCredentials{
						Username: "default-user",
						Password: "default-pass",
					},
				},
				Groups: map[string]*NodeDefinition{
					"grp1": {
						Credentials: NodeCredentials{
							Username: "group-user",
							Password: "group-pass",
						},
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {
						Kind:        "srl",
						Group:       "grp1",
						Credentials: NodeCredentials{Username: "node-user", Password: "node-pass"},
					},
				},
			},
			nodeName:     "node1",
			wantUsername: "node-user",
			wantPassword: "node-pass",
		},
		"node_password_only_does_not_merge_kind_username": {
			topo: &Topology{
				Kinds: map[string]*NodeDefinition{
					"srl": {
						Credentials: NodeCredentials{Username: "kind-user", Password: "kind-pass"},
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {
						Kind:        "srl",
						Credentials: NodeCredentials{Password: "node-pass-only"},
					},
				},
			},
			nodeName:     "node1",
			wantUsername: "",
			wantPassword: "node-pass-only",
		},
		"node_username_only_does_not_merge_kind_password": {
			topo: &Topology{
				Kinds: map[string]*NodeDefinition{
					"srl": {
						Credentials: NodeCredentials{Username: "kind-user", Password: "kind-pass"},
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {
						Kind:        "srl",
						Credentials: NodeCredentials{Username: "node-user-only"},
					},
				},
			},
			nodeName:     "node1",
			wantUsername: "node-user-only",
			wantPassword: "",
		},
		"defaults_identity_file_applies_to_node": {
			topo: &Topology{
				Defaults: &NodeDefinition{
					Credentials: NodeCredentials{
						Username:     "default-user",
						IdentityFile: "/keys/default",
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {Kind: "srl"},
				},
			},
			nodeName:         "node1",
			wantUsername:     "default-user",
			wantIdentityFile: "/keys/default",
		},
		"node_identity_file_overrides_defaults": {
			topo: &Topology{
				Defaults: &NodeDefinition{
					Credentials: NodeCredentials{IdentityFile: "/keys/default"},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {
						Kind:        "srl",
						Credentials: NodeCredentials{IdentityFile: "/keys/node"},
					},
				},
			},
			nodeName:         "node1",
			wantIdentityFile: "/keys/node",
		},
		"node_identity_file_resolves_independently_of_username_password": {
			// A node that sets only identity-file must still pick up its own identity-file, while
			// username/password continue to resolve on their own precedence chain (here: the kind).
			// The identity-file source must not hijack the username/password winning level used by
			// inventory generation.
			topo: &Topology{
				Kinds: map[string]*NodeDefinition{
					"srl": {
						Credentials: NodeCredentials{Username: "kind-user", Password: "kind-pass"},
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {
						Kind:        "srl",
						Credentials: NodeCredentials{IdentityFile: "/keys/node"},
					},
				},
			},
			nodeName:         "node1",
			wantUsername:     "kind-user",
			wantPassword:     "kind-pass",
			wantIdentityFile: "/keys/node",
		},
		"defaults_identity_only_does_not_change_credentials_source": {
			// defaults sets only identity-file; the kind supplies username/password. The
			// credentials
			// source must remain the kind so inventory generation keeps emitting the kind creds.
			topo: &Topology{
				Defaults: &NodeDefinition{
					Credentials: NodeCredentials{IdentityFile: "/keys/default"},
				},
				Kinds: map[string]*NodeDefinition{
					"srl": {
						Credentials: NodeCredentials{Username: "kind-user", Password: "kind-pass"},
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {Kind: "srl"},
				},
			},
			nodeName:            "node1",
			wantUsername:        "kind-user",
			wantPassword:        "kind-pass",
			wantIdentityFile:    "/keys/default",
			checkCredentialsSrc: true,
			wantCredentialsSrc:  CredentialTopologyKind,
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			gotUsername := tc.topo.GetNodeUsername(tc.nodeName)
			gotPassword := tc.topo.GetNodePassword(tc.nodeName)
			gotIdentityFile := tc.topo.GetNodeIdentityFile(tc.nodeName)

			if gotUsername != tc.wantUsername {
				t.Errorf("username: got %q, want %q", gotUsername, tc.wantUsername)
			}

			if gotPassword != tc.wantPassword {
				t.Errorf("password: got %q, want %q", gotPassword, tc.wantPassword)
			}

			if gotIdentityFile != tc.wantIdentityFile {
				t.Errorf("identity-file: got %q, want %q", gotIdentityFile, tc.wantIdentityFile)
			}

			if tc.checkCredentialsSrc {
				if gotSrc := tc.topo.GetNodeCredentialsTopologySource(
					tc.nodeName,
				); gotSrc != tc.wantCredentialsSrc {
					t.Errorf("credentials source: got %v, want %v", gotSrc, tc.wantCredentialsSrc)
				}
			}
		})
	}
}

func TestGetNodeCredentialTopologySource(t *testing.T) {
	tests := map[string]struct {
		topo     *Topology
		nodeName string
		want     CredentialTopologySource
	}{
		"node": {
			topo: &Topology{
				Kinds: map[string]*NodeDefinition{
					"srl": {
						Credentials: NodeCredentials{Username: "kind-user", Password: "kind-pass"},
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {
						Kind:        "srl",
						Credentials: NodeCredentials{Username: "node-user", Password: "node-pass"},
					},
				},
			},
			nodeName: "node1",
			want:     CredentialTopologyNode,
		},
		"group": {
			topo: &Topology{
				Defaults: &NodeDefinition{
					Credentials: NodeCredentials{
						Username: "default-user",
						Password: "default-pass",
					},
				},
				Groups: map[string]*NodeDefinition{
					"grp1": {
						Credentials: NodeCredentials{
							Username: "group-user",
							Password: "group-pass",
						},
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {Kind: "srl", Group: "grp1"},
				},
			},
			nodeName: "node1",
			want:     CredentialTopologyGroup,
		},
		"kind": {
			topo: &Topology{
				Defaults: &NodeDefinition{
					Credentials: NodeCredentials{
						Username: "default-user",
						Password: "default-pass",
					},
				},
				Kinds: map[string]*NodeDefinition{
					"srl": {
						Credentials: NodeCredentials{Username: "kind-user", Password: "kind-pass"},
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {Kind: "srl"},
				},
			},
			nodeName: "node1",
			want:     CredentialTopologyKind,
		},
		"defaults": {
			topo: &Topology{
				Defaults: &NodeDefinition{
					Credentials: NodeCredentials{
						Username: "default-user",
						Password: "default-pass",
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {Kind: "srl"},
				},
			},
			nodeName: "node1",
			want:     CredentialTopologyDefaults,
		},
		"unset": {
			topo: &Topology{
				Nodes: map[string]*NodeDefinition{
					"node1": {Kind: "srl"},
				},
			},
			nodeName: "node1",
			want:     CredentialTopologyUnset,
		},
		"node_partial_credentials_still_node_source": {
			topo: &Topology{
				Kinds: map[string]*NodeDefinition{
					"srl": {
						Credentials: NodeCredentials{Username: "kind-user", Password: "kind-pass"},
					},
				},
				Nodes: map[string]*NodeDefinition{
					"node1": {Kind: "srl", Credentials: NodeCredentials{Password: "only-pass"}},
				},
			},
			nodeName: "node1",
			want:     CredentialTopologyNode,
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			got := tc.topo.GetNodeCredentialsTopologySource(tc.nodeName)
			if got != tc.want {
				t.Errorf("credentials source: got %v, want %v", got, tc.want)
			}
		})
	}
}

func TestGetNodeRuntimeOptions(t *testing.T) {
	defaultPrivileged := false
	kindPrivileged := true
	nodePrivileged := false

	topo := &Topology{
		Defaults: &NodeDefinition{
			Privileged:   &defaultPrivileged,
			CgroupnsMode: "private",
			PidMode:      "host",
			Tmpfs:        map[string]string{"/run": "rw"},
			SecurityOpts: []string{"label=disable"},
		},
		Kinds: map[string]*NodeDefinition{
			"linux": {
				Privileged:   &kindPrivileged,
				CgroupnsMode: "host",
				Tmpfs:        map[string]string{"/run/lock": "rw"},
				SecurityOpts: []string{"seccomp=unconfined"},
			},
		},
		Groups: map[string]*NodeDefinition{
			"systemd": {
				PidMode: "container:infra",
				Tmpfs:   map[string]string{"/tmp": "rw,nosuid"},
			},
		},
		Nodes: map[string]*NodeDefinition{
			"node1": {
				Kind:         "linux",
				Group:        "systemd",
				Privileged:   &nodePrivileged,
				CgroupnsMode: "host",
				Tmpfs:        map[string]string{"/run": "rw,nosuid,nodev"},
				SecurityOpts: []string{"apparmor=unconfined"},
			},
			"node2": {
				Kind: "linux",
			},
			"node3": {},
		},
	}

	if got := topo.GetNodePrivileged("node1", true); got {
		t.Fatalf("node1 privileged = %v, want false", got)
	}

	if got := topo.GetNodePrivileged("node2", true); !got {
		t.Fatalf("node2 privileged = %v, want true", got)
	}

	if got := topo.GetNodePrivileged("node3", true); got {
		t.Fatalf("node3 privileged = %v, want false", got)
	}

	if got := topo.GetNodeCgroupnsMode("node1"); got != "host" {
		t.Fatalf("node1 cgroupns-mode = %q, want host", got)
	}

	if got := topo.GetNodePidMode("node1"); got != "container:infra" {
		t.Fatalf("node1 pid-mode = %q, want container:infra", got)
	}

	wantTmpfs := map[string]string{
		"/run":      "rw,nosuid,nodev",
		"/run/lock": "rw",
		"/tmp":      "rw,nosuid",
	}
	if diff := cmp.Diff(wantTmpfs, topo.GetNodeTmpfs("node1")); diff != "" {
		t.Fatalf("node1 tmpfs mismatch (-want +got):\n%s", diff)
	}

	wantSecurityOpts := []string{
		"label=disable",
		"seccomp=unconfined",
		"apparmor=unconfined",
	}
	if diff := cmp.Diff(wantSecurityOpts, topo.GetNodeSecurityOpts("node1")); diff != "" {
		t.Fatalf("node1 security-opts mismatch (-want +got):\n%s", diff)
	}
}

func TestGetNodeCgroupParent(t *testing.T) {
	topo := &Topology{
		Defaults: &NodeDefinition{CgroupParent: "/defaults"},
		Kinds: map[string]*NodeDefinition{
			"linux": {CgroupParent: "/kind"},
		},
		Groups: map[string]*NodeDefinition{
			"leaves":      {CgroupParent: "/group"},
			"empty-group": {},
		},
		Nodes: map[string]*NodeDefinition{
			"direct":        {Kind: "linux", Group: "leaves", CgroupParent: "/node"},
			"from-group":    {Kind: "linux", Group: "leaves"},
			"from-kind":     {Kind: "linux", Group: "empty-group"},
			"from-defaults": {},
			"omitted":       {},
		},
	}

	tests := map[string]string{
		"direct":        "/node",
		"from-group":    "/group",
		"from-kind":     "/kind",
		"from-defaults": "/defaults",
	}
	for nodeName, want := range tests {
		if got := topo.GetNodeCgroupParent(nodeName); got != want {
			t.Errorf("%s cgroup-parent = %q, want %q", nodeName, got, want)
		}
	}

	// Empty values are treated as omitted and therefore do not mask inherited values.
	topo.Nodes["from-group"].CgroupParent = ""
	if got := topo.GetNodeCgroupParent("from-group"); got != "/group" {
		t.Errorf("empty node cgroup-parent = %q, want inherited /group", got)
	}

	emptyTopo := NewTopology()
	emptyTopo.Nodes["omitted"] = &NodeDefinition{}
	if got := emptyTopo.GetNodeCgroupParent("omitted"); got != "" {
		t.Errorf("omitted cgroup-parent = %q, want empty runtime default", got)
	}
}

func TestGetNodePrivilegedDefault(t *testing.T) {
	topo := &Topology{
		Nodes: map[string]*NodeDefinition{
			"node1": {Kind: "linux"},
		},
	}

	if got := topo.GetNodePrivileged("node1", true); !got {
		t.Fatalf("privileged = %v, want true", got)
	}
	if got := topo.GetNodePrivileged("node1", false); got {
		t.Fatalf("privileged with kind default = %v, want false", got)
	}
}
