package main

import (
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestMainDispatchAllCommands(t *testing.T) {
	root := t.TempDir()
	invPath, baseImage := writeTestInventory(t, root)
	snapDir := filepath.Join(root, "snapshots")
	if err := os.MkdirAll(snapDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(snapDir, "snap.json"), []byte(`{"hostname":"titan-15","kernel":"6.6.63","package_sample":{"containerd":"1.7"}}`), 0o644); err != nil {
		t.Fatal(err)
	}

	fakeTools := fakeCommandDir(t, map[string]string{
		"lsblk": `cat <<'JSON'
{"blockdevices":[{"name":"sdz","path":"/dev/sdz","rm":true,"hotplug":true,"size":"32000000000","model":"Micro SD","tran":"usb","type":"disk"}]}
JSON`,
		"sfdisk": `cat <<'JSON'
{"partitiontable":{"sectorsize":512,"partitions":[{"start":3,"size":1,"type":"ef"},{"start":1,"size":2,"type":"83"}]}}
JSON`,
		"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`,
		"xz": `case "${1:-}" in
  -T0) cp "${@: -1}" "${@: -1}.xz" ;;
  -dc) cat "${2:-}" ;;
esac
exit 0`,
		"oras": `case "${1:-}" in
  login|tag) exit 0 ;;
  push) exit 0 ;;
  pull)
    outdir="${@: -1}"
    cp "` + baseImage + `" "${outdir}/titan-15.img"
    exit 0
    ;;
esac
exit 0`,
		"losetup": `if [[ "${1:-}" == "-Pf" && "${2:-}" == "--show" ]]; then
  printf '/dev/loop9\n'
  exit 0
fi
exit 0`,
		"mount": `mkdir -p "${2:-}"
exit 0`,
		"umount": `exit 0`,
	})
	t.Setenv("PATH", fakeTools+string(os.PathListSeparator)+os.Getenv("PATH"))
	t.Setenv("METIS_INVENTORY_PATH", invPath)
	t.Setenv("METIS_DATA_DIR", filepath.Join(root, "data"))
	listenAndServe = func(addr string, _ http.Handler) error { return nil }
	t.Cleanup(func() { listenAndServe = httpListenAndServe })

	callMain := func(args ...string) {
		oldArgs := os.Args
		os.Args = append([]string{"metis"}, args...)
		defer func() { os.Args = oldArgs }()
		main()
	}

	callMain("config", "--inventory", invPath, "--node", "titan-15")
	callMain("facts", "--inventory", invPath, "--snapshots", snapDir)
	callMain("plan", "--inventory", invPath, "--node", "titan-15")
	callMain("burn", "--inventory", invPath, "--node", "titan-15", "--device", "/dev/sdz")
	callMain("image", "--inventory", invPath, "--node", "titan-15", "--output", filepath.Join(root, "out.img"))
	callMain("inject", "--inventory", invPath, "--node", "titan-15", "--boot", filepath.Join(root, "boot"), "--root", filepath.Join(root, "root"))
	callMain("serve", "--bind", ":0")
	callMain("remote-devices", "--host-tmp-dir", filepath.Join(root, "host-tmp"))
	callMain("remote-build", "--inventory", invPath, "--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "build"), "--cache", filepath.Join(root, "cache"), "--harbor-registry", "registry.example", "--harbor-username", "admin", "--harbor-password", "pw")
	callMain("remote-flash", "--node", "titan-15", "--device", filepath.Join(root, "flash.img"), "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(root, "flash"), "--host-tmp-dir", filepath.Join(root, "host-tmp"), "--harbor-registry", "registry.example", "--harbor-username", "admin", "--harbor-password", "pw")
}

func TestMainAndCommandFatalBranches(t *testing.T) {
	trap := func() {
		fatalf = func(format string, args ...any) {
			panic("fatal: " + format)
		}
		exit = func(code int) {
			panic("exit")
		}
		t.Cleanup(func() {
			fatalf = httpLogFatalf
			exit = httpExit
		})
	}
	trap()

	mustPanic := func(fn func()) {
		t.Helper()
		defer func() {
			if r := recover(); r == nil {
				t.Fatal("expected panic")
			}
		}()
		fn()
	}

	mustPanic(func() {
		oldArgs := os.Args
		os.Args = []string{"metis"}
		defer func() { os.Args = oldArgs }()
		main()
	})
	mustPanic(func() {
		oldArgs := os.Args
		os.Args = []string{"metis", "bogus"}
		defer func() { os.Args = oldArgs }()
		main()
	})
	mustPanic(func() { configCmd(nil) })
	root := t.TempDir()
	invPath, _ := writeTestInventory(t, root)
	mustPanic(func() { configCmd([]string{"--inventory", invPath, "--node", "missing"}) })
	mustPanic(func() { planCmd([]string{"--inventory", "/nope", "--node", "titan-15"}) })
	mustPanic(func() { burnCmd([]string{"--inventory", "/nope", "--node", "titan-15", "--device", "/dev/sdz"}) })
	mustPanic(func() { imageCmd(nil) })
	mustPanic(func() {
		imageCmd([]string{"--inventory", invPath, "--node", "titan-15", "--cache", filepath.Join(root, "cache")})
	})
	mustPanic(func() { injectCmd(nil) })
	mustPanic(func() { injectCmd([]string{"--node", "titan-15"}) })
	mustPanic(func() {
		injectCmd([]string{"--inventory", invPath, "--node", "missing", "--boot", filepath.Join(root, "boot")})
	})
	mustPanic(func() { factsCmd([]string{"--inventory", "/nope", "--snapshots", "/nope"}) })
	mustPanic(func() {
		factsCmd([]string{"--inventory", invPath, "--snapshots", filepath.Join(root, "missing-snaps")})
	})
	mustPanic(func() { serveCmd([]string{"--bind", ":0"}) })
	t.Setenv("METIS_INVENTORY_PATH", invPath)
	t.Setenv("METIS_DATA_DIR", filepath.Join(root, "data"))
	listenAndServe = func(string, http.Handler) error { return http.ErrServerClosed }
	t.Cleanup(func() { listenAndServe = httpListenAndServe })
	mustPanic(func() { serveCmd([]string{"--bind", ":0"}) })
	mustPanic(func() { remoteBuildCmd([]string{"--node", "n1"}) })
	mustPanic(func() { remoteFlashCmd([]string{"--node", "n1"}) })
}

func TestFactsCmdReportsEncodeErrors(t *testing.T) {
	root := t.TempDir()
	invPath, _ := writeTestInventory(t, root)
	snapDir := filepath.Join(root, "snapshots")
	if err := os.MkdirAll(snapDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(snapDir, "snap.json"), []byte(`{"hostname":"titan-15","kernel":"6.6.63"}`), 0o644); err != nil {
		t.Fatal(err)
	}

	oldStdout := os.Stdout
	oldStderr := os.Stderr
	badStdout, stdoutWriter, err := os.Pipe()
	if err != nil {
		t.Fatal(err)
	}
	stderrReader, stderrWriter, err := os.Pipe()
	if err != nil {
		t.Fatal(err)
	}
	os.Stdout = badStdout
	os.Stderr = stderrWriter
	t.Cleanup(func() {
		os.Stdout = oldStdout
		os.Stderr = oldStderr
		_ = badStdout.Close()
		_ = stdoutWriter.Close()
		_ = stderrReader.Close()
		_ = stderrWriter.Close()
	})

	factsCmd([]string{"--inventory", invPath, "--snapshots", snapDir})
	_ = stderrWriter.Close()
	buf := make([]byte, 1024)
	n, _ := stderrReader.Read(buf)
	if !strings.Contains(string(buf[:n]), "encode:") {
		t.Fatalf("expected encode error on stderr, got %q", string(buf[:n]))
	}
}

func TestRemoteCommandHelpers(t *testing.T) {
	if !hasMountedChildren([]struct {
		Mountpoint string `json:"mountpoint"`
	}{{Mountpoint: "/mnt"}}) {
		t.Fatal("hasMountedChildren should detect a mount point")
	}
	if got := humanHostPath("/host-tmp/metis-flash"); got != "/tmp/metis-flash" {
		t.Fatalf("humanHostPath = %q", got)
	}
	t.Setenv("METIS_REMOTE_SAMPLE", "value")
	if got := getenvOr("METIS_REMOTE_SAMPLE", "fallback"); got != "value" {
		t.Fatalf("getenvOr = %q", got)
	}

	dir := t.TempDir()
	base := filepath.Join(dir, "base.img")
	if err := os.WriteFile(base, []byte("artifact"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(base+".meta", []byte(`{"meta":true}`), 0o644); err != nil {
		t.Fatal(err)
	}
	fakeTools := fakeCommandDir(t, map[string]string{
		"oras": `case "${1:-}" in
  login|tag) exit 0 ;;
  push) exit 0 ;;
  pull)
    outdir="${@: -1}"
    cp "` + base + `" "${outdir}/artifact.img"
    exit 0
    ;;
esac
exit 0`,
		"lsblk": `cat <<'JSON'
{"blockdevices":[{"name":"sdz","path":"/dev/sdz","rm":true,"hotplug":true,"size":"32000000000","model":"Micro SD","tran":"usb","type":"disk","children":[{"mountpoint":""}]},{"name":"sdy","path":"/dev/sdy","rm":true,"hotplug":true,"size":"64000000000","model":"SSD","tran":"usb","type":"disk","children":[{"mountpoint":"/mnt"}]}]}
JSON`,
	})
	t.Setenv("PATH", fakeTools+string(os.PathListSeparator)+os.Getenv("PATH"))

	if err := orasLogin("registry.example", "", ""); err == nil {
		t.Fatal("expected orasLogin to reject missing creds")
	}
	if err := orasLogin("registry.example", "u", "p"); err != nil {
		t.Fatalf("orasLogin: %v", err)
	}
	if _, _, err := orasPushInvocation("r", filepath.Join(dir, "one", "a.img"), filepath.Join(dir, "two", "b.meta")); err == nil {
		t.Fatal("expected orasPushInvocation mismatch error")
	}
	pushDir := filepath.Join(dir, "push")
	if err := os.MkdirAll(pushDir, 0o755); err != nil {
		t.Fatal(err)
	}
	img := filepath.Join(pushDir, "a.img")
	meta := filepath.Join(pushDir, "a.meta")
	if err := os.WriteFile(img, []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(meta, []byte(`{}`), 0o644); err != nil {
		t.Fatal(err)
	}
	if _, args, err := orasPushInvocation("ref", img, meta); err != nil || len(args) == 0 {
		t.Fatalf("orasPushInvocation success = %#v %v", args, err)
	}
	if err := orasPush("ref", img, meta); err != nil {
		t.Fatalf("orasPush: %v", err)
	}
	if err := orasTag("ref", "latest"); err != nil {
		t.Fatalf("orasTag: %v", err)
	}
	pullDir := filepath.Join(dir, "pull")
	if err := os.MkdirAll(pullDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := orasPull("ref", pullDir); err != nil {
		t.Fatalf("orasPull: %v", err)
	}
	artifact, compressed, err := resolvePulledArtifact(pullDir)
	if err != nil || compressed || !strings.HasSuffix(artifact, ".img") {
		t.Fatalf("resolvePulledArtifact raw = %q compressed=%v err=%v", artifact, compressed, err)
	}
	if err := os.WriteFile(filepath.Join(pullDir, "artifact.img.xz"), []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	artifact, compressed, err = resolvePulledArtifact(pullDir)
	if err != nil || !compressed || !strings.HasSuffix(artifact, ".img.xz") {
		t.Fatalf("resolvePulledArtifact xz = %q compressed=%v err=%v", artifact, compressed, err)
	}
	if _, _, err := resolvePulledArtifact(filepath.Join(dir, "missing")); err == nil {
		t.Fatal("expected resolvePulledArtifact error")
	}
	devices, err := localFlashDevices(40000000000, filepath.Join(dir, "host-tmp"))
	if err != nil {
		t.Fatalf("localFlashDevices: %v", err)
	}
	if len(devices) == 0 || devices[0].Path != "/dev/sdz" {
		t.Fatalf("localFlashDevices = %#v", devices)
	}
	writeStructuredResult(map[string]any{"ok": true})
}

var (
	httpLogFatalf      = fatalf
	httpExit           = exit
	httpListenAndServe = listenAndServe
)
