package plan

import (
	"os"
	"path/filepath"
	"strings"
	"testing"

	"metis/pkg/config"
	"metis/pkg/inventory"
	"metis/pkg/secrets"
)

func TestBuildFilesProducesK3sConfig(t *testing.T) {
	cfg := &config.NodeConfig{
		Hostname: "n1",
		IP:       "10.0.0.10",
		SSHUser:  "pi",
		SSHKeys:  []string{"ssh-rsa AAA"},
		K3s: config.K3sConfig{
			Role:    "agent",
			URL:     "https://server:6443",
			Token:   "secret",
			Version: "v1.31.5+k3s1",
		},
		Fstab: []config.FstabEntry{
			{
				UUID:       "disk-uuid",
				Mountpoint: "/mnt/astreae",
				FS:         "ext4",
				Options:    "defaults,nofail",
			},
			{
				UUID:       "usb-uuid",
				Mountpoint: "/mnt/scratch",
				FS:         "ext4",
				Options:    "defaults,nofail",
			},
			{
				Source:     "/mnt/scratch",
				Mountpoint: "/var/lib/rancher",
				FS:         "none",
				Options:    "bind,nofail",
			},
		},
		Labels: map[string]string{"role": "worker", "zone": "a", "node-role.kubernetes.io/worker": "true"},
		Taints: []string{"gpu=true:NoSchedule"},
	}
	files, err := buildFiles(cfg, nil)
	if err != nil {
		t.Fatalf("buildFiles: %v", err)
	}
	pathMap := map[string]string{}
	for _, f := range files {
		pathMap[f.Path] = string(f.Content)
	}
	k3s, ok := pathMap["etc/rancher/k3s/config.yaml"]
	if !ok {
		t.Fatalf("missing k3s config")
	}
	if !strings.Contains(k3s, "server: https://server:6443") || !strings.Contains(k3s, "node-name: n1") {
		t.Fatalf("unexpected k3s config: %s", k3s)
	}
	if strings.Contains(k3s, "write-kubeconfig-mode") {
		t.Fatalf("agent config should not include write-kubeconfig-mode: %s", k3s)
	}
	if strings.Contains(k3s, "node-role.kubernetes.io/worker") {
		t.Fatalf("agent config should skip reserved node-role label: %s", k3s)
	}
	hostFile, ok := pathMap["etc/hostname"]
	if !ok || strings.TrimSpace(hostFile) != "n1" {
		t.Fatalf("hostname file missing/incorrect: %q", hostFile)
	}
	auth, ok := pathMap["home/pi/.ssh/authorized_keys"]
	if !ok || !strings.Contains(auth, "ssh-rsa AAA") {
		t.Fatalf("authorized_keys missing/incorrect: %s", auth)
	}
	firstboot, ok := pathMap["etc/metis/firstboot.env"]
	if !ok || !strings.Contains(firstboot, "METIS_K3S_VERSION='v1.31.5+k3s1'") {
		t.Fatalf("firstboot env missing/incorrect: %s", firstboot)
	}
	network, ok := pathMap["etc/NetworkManager/system-connections/end0-static.nmconnection"]
	if !ok || !strings.Contains(network, "address1=10.0.0.10/24,10.0.0.1") {
		t.Fatalf("networkmanager config missing/incorrect: %s", network)
	}
	networkEth0, ok := pathMap["etc/NetworkManager/system-connections/eth0-static.nmconnection"]
	if !ok || !strings.Contains(networkEth0, "interface-name=eth0") {
		t.Fatalf("eth0 networkmanager config missing/incorrect: %s", networkEth0)
	}
	networkd, ok := pathMap["etc/systemd/network/10-end0-static.network"]
	if !ok || !strings.Contains(networkd, "Name=end0 eth0") || !strings.Contains(networkd, "Address=10.0.0.10/24") || !strings.Contains(networkd, "Gateway=10.0.0.1") {
		t.Fatalf("systemd-networkd config missing/incorrect: %s", networkd)
	}
	fstab, ok := pathMap["etc/metis/fstab.append"]
	if !ok || !strings.Contains(fstab, "UUID=disk-uuid /mnt/astreae ext4 defaults,nofail 0 0") || !strings.Contains(fstab, "UUID=usb-uuid /mnt/scratch ext4 defaults,nofail 0 0") || !strings.Contains(fstab, "/mnt/scratch /var/lib/rancher none bind,nofail 0 0") {
		t.Fatalf("fstab append missing/incorrect: %s", fstab)
	}
}

func TestOverlayFiles(t *testing.T) {
	dir := t.TempDir()
	bootDir := filepath.Join(dir, "boot")
	rootDir := filepath.Join(dir, "root")
	if err := os.MkdirAll(filepath.Join(bootDir, "over"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(filepath.Join(rootDir, "etc"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(bootDir, "over", "cmdline.txt"), []byte("console=tty1"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(rootDir, "etc", "issue"), []byte("hello"), 0o644); err != nil {
		t.Fatal(err)
	}
	class := &inventory.NodeClass{
		BootOverlay: bootDir,
		RootOverlay: rootDir,
	}
	files, err := collectOverlays(class)
	if err != nil {
		t.Fatalf("collectOverlays: %v", err)
	}
	if len(files) != 2 {
		t.Fatalf("expected 2 files, got %d", len(files))
	}
}

func TestSecretsWrite(t *testing.T) {
	cfg := &config.NodeConfig{
		Hostname: "n1",
		IP:       "10.0.0.1",
	}
	sec := &secrets.NodeSecrets{K3sToken: "tok", AtlasPassword: "pw", Extra: map[string]string{"foo": "bar"}}
	files, err := buildFiles(cfg, sec)
	if err != nil {
		t.Fatalf("buildFiles: %v", err)
	}
	found := false
	for _, f := range files {
		if f.Path == "etc/metis/secrets.json" && f.RootFS {
			found = true
		}
	}
	if !found {
		t.Fatalf("secrets file not written")
	}
}

func TestBuildFilesAddsHecateSudoersForAtlas(t *testing.T) {
	cfg := &config.NodeConfig{
		Hostname: "n1",
		IP:       "10.0.0.10",
		SSHUser:  "atlas",
		SSHKeys:  []string{"ssh-ed25519 AAA test"},
		K3s: config.K3sConfig{
			Role: "agent",
		},
	}
	files, err := buildFiles(cfg, nil)
	if err != nil {
		t.Fatalf("buildFiles: %v", err)
	}
	pathMap := map[string]string{}
	for _, f := range files {
		pathMap[f.Path] = string(f.Content)
	}
	sudoers, ok := pathMap["etc/sudoers.d/90-hecate-atlas"]
	if !ok || !strings.Contains(sudoers, "atlas ALL=(ALL) NOPASSWD: /usr/bin/systemctl") {
		t.Fatalf("sudoers file missing/incorrect: %s", sudoers)
	}
	backup, ok := pathMap["etc/metis/sudoers-hecate"]
	if !ok || backup != sudoers {
		t.Fatalf("metis sudoers backup missing/incorrect: %s", backup)
	}
}

func TestBuildFilesAddsPasswordArtifactsAndRedactsSecrets(t *testing.T) {
	cfg := &config.NodeConfig{
		Hostname: "titan-15",
		IP:       "192.168.22.43",
		SSHUser:  "atlas",
		SSHKeys:  []string{"ssh-ed25519 AAA test"},
		K3s: config.K3sConfig{
			Role:    "agent",
			Version: "v1.31.5+k3s1",
		},
	}
	sec := &secrets.NodeSecrets{
		AtlasPassword: "atlas-pass",
		RootPassword:  "root-pass",
		K3sToken:      "super-secret-token",
		Extra:         map[string]string{"api_key": "secret"},
	}
	files, err := buildFiles(cfg, sec)
	if err != nil {
		t.Fatalf("buildFiles: %v", err)
	}
	pathMap := map[string]string{}
	for _, file := range files {
		pathMap[file.Path] = string(file.Content)
	}
	firstboot := pathMap["etc/metis/firstboot.env"]
	if !strings.Contains(firstboot, "METIS_ATLAS_PASSWORD='atlas-pass'") || !strings.Contains(firstboot, "METIS_ROOT_PASSWORD='root-pass'") {
		t.Fatalf("firstboot env missing password material: %s", firstboot)
	}
	if sshd := pathMap["etc/ssh/sshd_config.d/90-metis-password-auth.conf"]; !strings.Contains(sshd, "PasswordAuthentication yes") || !strings.Contains(sshd, "PermitRootLogin yes") {
		t.Fatalf("password auth config missing: %s", sshd)
	}
	if script := pathMap["usr/local/sbin/metis-apply-node-identity.sh"]; !strings.Contains(script, "apply_password root") || !strings.Contains(script, "METIS_ATLAS_PASSWORD") {
		t.Fatalf("node identity script missing password application: %s", script)
	}
	if cloudCfg := pathMap["etc/cloud/cloud.cfg.d/90-metis-recovery.cfg"]; !strings.Contains(cloudCfg, "ssh_pwauth: true") {
		t.Fatalf("cloud recovery config missing ssh_pwauth: %s", cloudCfg)
	}
	if userData := pathMap["user-data"]; !strings.Contains(userData, "ssh_pwauth: true") || !strings.Contains(userData, "metis-apply-node-identity.sh") {
		t.Fatalf("cloud-init user-data missing recovery hooks: %s", userData)
	}
	secretsJSON := pathMap["etc/metis/secrets.json"]
	if strings.Contains(secretsJSON, "atlas-pass") || strings.Contains(secretsJSON, "root-pass") || strings.Contains(secretsJSON, "super-secret-token") {
		t.Fatalf("secrets.json should be redacted: %s", secretsJSON)
	}
	if !strings.Contains(secretsJSON, `"has_atlas_password": true`) || !strings.Contains(secretsJSON, `"extra_keys": [`) {
		t.Fatalf("secrets.json should keep redacted debug metadata: %s", secretsJSON)
	}
}

func TestApplyNodeMetadataEnv(t *testing.T) {
	cfg := &config.NodeConfig{
		Labels: map[string]string{"hardware": "rpi4"},
		Taints: []string{"flash=true:NoSchedule"},
		K3s: config.K3sConfig{
			Labels: map[string]string{"hardware": "rpi4"},
			Taints: []string{"flash=true:NoSchedule"},
		},
	}
	t.Setenv("METIS_NODE_LABELS_JSON", `{"hardware":"rpi5","maintenance.bstein.dev/role":"recovery"}`)
	t.Setenv("METIS_NODE_TAINTS_JSON", `["dedicated=recovery:NoSchedule","flash=true:NoSchedule"]`)
	applyNodeMetadataEnv(cfg)
	if cfg.Labels["hardware"] != "rpi5" || cfg.Labels["maintenance.bstein.dev/role"] != "recovery" {
		t.Fatalf("applyNodeMetadataEnv labels = %#v", cfg.Labels)
	}
	if !strings.Contains(strings.Join(cfg.Taints, ","), "dedicated=recovery:NoSchedule") {
		t.Fatalf("applyNodeMetadataEnv taints = %#v", cfg.Taints)
	}
	cfg = &config.NodeConfig{}
	t.Setenv("METIS_NODE_LABELS_JSON", `{bad-json`)
	t.Setenv("METIS_NODE_TAINTS_JSON", `{bad-json`)
	applyNodeMetadataEnv(cfg)
	if cfg.Labels != nil || cfg.Taints != nil {
		t.Fatalf("invalid env JSON should be ignored: %#v", cfg)
	}
}
