package plan

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"metis/pkg/inventory"
)

func TestPlanBuildFilesAndExecuteBranches(t *testing.T) {
	dir := t.TempDir()
	base := filepath.Join(dir, "base.img")
	baseContent := []byte("image")
	if err := os.WriteFile(base, baseContent, 0o644); err != nil {
		t.Fatal(err)
	}
	sum := sha256.Sum256(baseContent)
	inv := &inventory.Inventory{
		Classes: []inventory.NodeClass{{
			Name:     "rpi4",
			Arch:     "arm64",
			OS:       "armbian",
			Image:    "file://" + base,
			Checksum: "sha256:" + hex.EncodeToString(sum[:]),
			DefaultLabels: map[string]string{
				"node-role.kubernetes.io/worker": "true",
			},
			BootOverlay: filepath.Join(dir, "boot-overlay"),
			RootOverlay: filepath.Join(dir, "root-overlay"),
		}},
		Nodes: []inventory.NodeSpec{{
			Name:          "titan-15",
			Class:         "rpi4",
			Hostname:      "titan-15",
			IP:            "192.168.22.43",
			K3sRole:       "agent",
			K3sURL:        "https://192.168.22.7:6443",
			K3sToken:      "token",
			SSHUser:       "atlas",
			SSHAuthorized: []string{"ssh-ed25519 AAA"},
			LonghornDisks: []inventory.LonghornDisk{{Mountpoint: "/var/lib/longhorn", UUID: "u1"}},
		}},
	}
	if err := os.MkdirAll(inv.Classes[0].BootOverlay, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(inv.Classes[0].RootOverlay, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(inv.Classes[0].BootOverlay, "boot.txt"), []byte("boot"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(inv.Classes[0].RootOverlay, "root.txt"), []byte("root"), 0o600); err != nil {
		t.Fatal(err)
	}

	if _, err := Build(inv, "missing", "", dir); err == nil {
		t.Fatal("expected Build to fail for missing node")
	}
	p, err := Build(inv, "titan-15", "", dir)
	if err != nil {
		t.Fatalf("Build: %v", err)
	}
	if p.Device != "/dev/sdX" || !strings.Contains(strings.Join(actionDetails(p.Actions), " "), "Inject hostname/network/k3s config") {
		t.Fatalf("unexpected plan: %#v", p)
	}
	if got := cacheName("foo.img.xz"); got != "foo.img" {
		t.Fatalf("cacheName = %q", got)
	}

	files, err := Files(inv, "titan-15")
	if err != nil {
		t.Fatalf("Files: %v", err)
	}
	if len(files) == 0 {
		t.Fatal("expected files")
	}
	if got := cloudInitUserData(nil, nil); got != "" {
		t.Fatalf("cloudInitUserData nil = %q", got)
	}
	if got := allowK3sNodeLabel("agent", "node-role.kubernetes.io/master"); got {
		t.Fatal("agent should reject node-role labels")
	}

	// Inject is a thin wrapper around maybeInject; exercise both the no-op and
	// path-setting branches.
	if err := Inject(inv, "titan-15", "", ""); err != nil {
		t.Fatalf("Inject noop: %v", err)
	}
	boot := filepath.Join(dir, "boot")
	root := filepath.Join(dir, "root")
	if err := os.MkdirAll(boot, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(root, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := Inject(inv, "titan-15", boot, root); err != nil {
		t.Fatalf("Inject with paths: %v", err)
	}

	cacheDir := filepath.Join(dir, "cache")
	output := filepath.Join(dir, "output.img")
	t.Setenv("PATH", dir)
	if err := BuildImageFile(context.Background(), inv, "titan-15", cacheDir, output); err == nil {
		t.Fatal("expected BuildImageFile to fail without xz/debugfs setup")
	}
	if _, err := Execute(inv, "titan-15", "/dev/sdX", cacheDir, true); err == nil {
		t.Fatal("expected Execute to reject placeholder device")
	}
	if _, err := Execute(inv, "titan-15", "/dev/sdz", cacheDir, false); err != nil {
		t.Fatalf("Execute dry-run: %v", err)
	}
}

func actionDetails(actions []Action) []string {
	out := make([]string, 0, len(actions))
	for _, action := range actions {
		out = append(out, action.Detail)
	}
	return out
}

func TestPlanMiscBranches(t *testing.T) {
	if !NextRunStale(timeNow().Add(-time.Hour), time.Minute) {
		t.Fatal("expected NextRunStale")
	}
}

func timeNow() time.Time { return time.Now() }
