package plan

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

	"metis/pkg/inventory"
)

func TestExecuteAndBuildImageFileWithFakes(t *testing.T) {
	rootTools := fakeRootfsTools(t)
	mountTools := fakeMountTools(t)
	t.Setenv("PATH", rootTools+string(os.PathListSeparator)+mountTools+string(os.PathListSeparator)+os.Getenv("PATH"))

	dir := t.TempDir()
	rawImage := filepath.Join(dir, "base.img")
	if err := os.WriteFile(rawImage, make([]byte, 4096), 0o644); err != nil {
		t.Fatal(err)
	}
	sum := imageChecksum(t, rawImage)
	inv := &inventory.Inventory{
		Classes: []inventory.NodeClass{{
			Name:     "c1",
			Arch:     "arm64",
			OS:       "linux",
			Image:    "file://" + rawImage,
			Checksum: sum,
		}},
		Nodes: []inventory.NodeSpec{{
			Name:          "n1",
			Class:         "c1",
			Hostname:      "n1",
			IP:            "10.0.0.1",
			K3sRole:       "agent",
			SSHUser:       "atlas",
			SSHAuthorized: []string{"ssh-ed25519 AAA"},
		}},
	}

	planDry, err := Execute(inv, "n1", filepath.Join(dir, "disk.img"), filepath.Join(dir, "cache"), false)
	if err != nil {
		t.Fatalf("Execute dry-run: %v", err)
	}
	if planDry.Node != "n1" || len(planDry.Actions) == 0 {
		t.Fatalf("unexpected dry-run plan: %#v", planDry)
	}

	bootDir := filepath.Join(dir, "boot")
	rootDir := filepath.Join(dir, "root")
	if err := os.MkdirAll(bootDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(rootDir, 0o755); err != nil {
		t.Fatal(err)
	}
	t.Setenv("METIS_BOOT_PATH", bootDir)
	t.Setenv("METIS_ROOT_PATH", rootDir)
	t.Setenv("METIS_AUTO_MOUNT", "1")
	written := filepath.Join(dir, "written.img")
	planRun, err := Execute(inv, "n1", written, filepath.Join(dir, "cache2"), true)
	if err != nil {
		t.Fatalf("Execute confirm: %v", err)
	}
	if planRun.Image != "file://"+rawImage {
		t.Fatalf("unexpected plan image: %#v", planRun)
	}
	if _, err := os.Stat(written); err != nil {
		t.Fatalf("expected written image: %v", err)
	}
	if _, err := os.Stat(filepath.Join(rootDir, "etc/metis/firstboot.env")); err != nil {
		t.Fatalf("expected injected rootfs file: %v", err)
	}
}

func TestBuildImageFileMaterializesRootFS(t *testing.T) {
	rootTools := fakeRootfsTools(t)
	mountTools := fakeMountTools(t)
	t.Setenv("PATH", rootTools+string(os.PathListSeparator)+mountTools+string(os.PathListSeparator)+os.Getenv("PATH"))

	dir := t.TempDir()
	rawImage := filepath.Join(dir, "base.img")
	if err := os.WriteFile(rawImage, make([]byte, 4096), 0o644); err != nil {
		t.Fatal(err)
	}
	sum := imageChecksum(t, rawImage)
	inv := &inventory.Inventory{
		Classes: []inventory.NodeClass{{
			Name:     "c1",
			Arch:     "arm64",
			OS:       "linux",
			Image:    "file://" + rawImage,
			Checksum: sum,
		}},
		Nodes: []inventory.NodeSpec{{
			Name:          "n1",
			Class:         "c1",
			Hostname:      "n1",
			IP:            "10.0.0.1",
			K3sRole:       "agent",
			SSHUser:       "atlas",
			SSHAuthorized: []string{"ssh-ed25519 AAA"},
		}},
	}
	dumpDir := filepath.Join(dir, "dump")
	t.Setenv("METIS_FAKE_UMOUNT_DUMP_DIR", dumpDir)
	out := filepath.Join(dir, "output.img")
	if err := BuildImageFile(context.Background(), inv, "n1", filepath.Join(dir, "cache"), out); err != nil {
		t.Fatalf("BuildImageFile: %v", err)
	}
	if _, err := os.Stat(out); err != nil {
		t.Fatalf("expected output image: %v", err)
	}
	userData := findDumpedFileContent(t, dumpDir, "user-data")
	if !strings.Contains(userData, "metis-apply-node-identity.sh") {
		t.Fatalf("expected boot cloud-init hook in user-data, got: %s", userData)
	}
}

func TestMaybeInjectNoopsWhenEnvUnset(t *testing.T) {
	if err := maybeInject(&inventory.Inventory{}, "n1"); err != nil {
		t.Fatalf("maybeInject without env: %v", err)
	}
}

func TestChecksumFromInventoryAndCacheName(t *testing.T) {
	inv := &inventory.Inventory{
		Classes: []inventory.NodeClass{{Name: "c1", Checksum: "sha256:deadbeef"}},
		Nodes:   []inventory.NodeSpec{{Name: "n1", Class: "c1"}},
	}
	if got := checksumFromInventory(inv, "n1"); got != "sha256:deadbeef" {
		t.Fatalf("checksumFromInventory = %q", got)
	}
	if got := cacheName("/tmp/archive/base.img.xz"); got != "base.img" {
		t.Fatalf("cacheName = %q", got)
	}
}

func fakeRootfsTools(t *testing.T) string {
	t.Helper()
	dir := t.TempDir()
	write := func(name, body string) {
		path := filepath.Join(dir, name)
		if err := os.WriteFile(path, []byte("#!/usr/bin/env bash\nset -eu\n"+body+"\n"), 0o755); err != nil {
			t.Fatalf("write %s: %v", name, err)
		}
	}
	write("sfdisk", `cat <<'JSON'
{"partitiontable":{"sectorsize":512,"partitions":[{"start":3,"size":1,"type":"ef"},{"start":1,"size":2,"type":"83"}]}}
JSON`)
	write("debugfs", `if [[ "${1:-}" == "-w" ]]; then
  cp "${3:-}" "${4:-}.commands"
  exit 0
fi
if [[ "${1:-}" == "-R" ]]; then
  state="${3:-}.commands"
  set -- $2
  case "${1:-}" in
    stat)
      mode="$(awk -v path="${2:-}" '$1=="sif" && $2==path {print $4}' "${state}" | tail -n1)"
      mode="${mode: -4}"
      printf 'Mode:  %s\n' "${mode}"
      exit 0
      ;;
    dump)
      local_path="$(awk -v path="${2:-}" '$1=="write" && $3==path {print $2}' "${state}" | tail -n1)"
      cat "${local_path}" > "${3:-}"
      exit 0
      ;;
  esac
fi
exit 0`)
	return dir
}

func fakeMountTools(t *testing.T) string {
	t.Helper()
	dir := t.TempDir()
	write := func(name, body string) {
		path := filepath.Join(dir, name)
		if err := os.WriteFile(path, []byte("#!/usr/bin/env bash\nset -eu\n"+body+"\n"), 0o755); err != nil {
			t.Fatalf("write %s: %v", name, err)
		}
	}
	write("losetup", `if [[ "${1:-}" == "-Pf" && "${2:-}" == "--show" ]]; then
  printf '/dev/loop9\n'
  exit 0
fi
exit 0`)
	write("mount", `mkdir -p "${2:-}"
exit 0`)
	write("umount", `target="${1:-}"
dump="${METIS_FAKE_UMOUNT_DUMP_DIR:-}"
if [[ -n "$dump" && -d "$target" ]]; then
  base="$(basename "$target")"
  mkdir -p "$dump/$base"
  cp -a "$target"/. "$dump/$base/" 2>/dev/null || true
fi
exit 0`)
	return dir
}

func findDumpedFileContent(t *testing.T, dumpDir, fileName string) string {
	t.Helper()
	var content string
	err := filepath.Walk(dumpDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() || filepath.Base(path) != fileName {
			return nil
		}
		data, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		content = string(data)
		return filepath.SkipAll
	})
	if err != nil {
		t.Fatalf("walk dumped files: %v", err)
	}
	if content == "" {
		t.Fatalf("did not find dumped %s under %s", fileName, dumpDir)
	}
	return content
}

func imageChecksum(t *testing.T, path string) string {
	t.Helper()
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	sum := sha256.Sum256(data)
	return "sha256:" + hex.EncodeToString(sum[:])
}
