package main

import (
	"bytes"
	"fmt"
	"io"
	"math"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"metis/pkg/service"
)

func TestRemoteLocalDeviceAndProgressEdges(t *testing.T) {
	dir := fakeCommandDir(t, map[string]string{
		"lsblk": `cat <<'JSON'
{"blockdevices":[
  {"name":"loop0","path":"/dev/loop0","rm":false,"hotplug":false,"size":"100","model":"loop","tran":"","type":"loop"},
  {"name":"huge","path":"/dev/sdx","rm":true,"hotplug":true,"size":"999999999999","model":"huge","tran":"usb","type":"disk"},
  {"name":"nvme0n1","path":"/dev/nvme0n1","rm":false,"hotplug":false,"size":32000000000,"model":"nvme","tran":"nvme","type":"disk"},
  {"name":"mounted","path":"/dev/sdy","rm":true,"hotplug":true,"size":"32000000000","model":"SD","tran":"usb","type":"disk","mountpoint":"/mnt"},
  {"name":"child-mounted","path":"/dev/sdz","rm":true,"hotplug":true,"size":"32000000000","model":"SD","tran":"usb","type":"disk","children":[{"mountpoint":"/boot"}]},
  {"name":"bad-size","path":"/dev/sdw","rm":true,"hotplug":true,"size":"bad","model":"SD","tran":"usb","type":"disk"},
  {"name":"good","path":"/dev/sdv","rm":true,"hotplug":true,"size":16000000000,"model":"SD Card","tran":"usb","type":"disk"}
]}
JSON`,
	})
	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
	devices, err := localFlashDevices(40000000000, "/host-tmp/")
	if err != nil {
		t.Fatalf("localFlashDevices: %v", err)
	}
	if len(devices) != 2 || devices[0].Path != "/dev/sdv" || !strings.Contains(devices[1].Note, "/tmp") {
		t.Fatalf("unexpected filtered devices: %#v", devices)
	}
	if got := localDeviceScore(service.Device{Path: "hosttmp:///tmp"}); got != -100 {
		t.Fatalf("hosttmp localDeviceScore = %d", got)
	}
	if got := humanHostPath("/host-tmp/"); got != "/tmp" {
		t.Fatalf("humanHostPath root = %q", got)
	}
	if got := humanHostPath("/host-tmp"); got != "/tmp" {
		t.Fatalf("humanHostPath mount root = %q", got)
	}
	if got := getenvOr("METIS_REMOTE_UNSET", "fallback"); got != "fallback" {
		t.Fatalf("getenvOr fallback = %q", got)
	}

	stdout, _ := captureStreams(t, func() {
		emitProgress(service.RemoteProgressUpdate{ProgressPct: math.NaN()})
		emitter := newProgressEmitter("flash", 92, 98, "Writing", true)
		emitter(1, 0)
		emitter(20, 10)
		emitter(20, 10)
	})
	if strings.Count(stdout, "METIS_PROGRESS ") != 1 || !strings.Contains(stdout, `"progress_pct":98`) {
		t.Fatalf("unexpected progress output: %q", stdout)
	}
}

func TestRemoteDevicesSortAndDecodeEdges(t *testing.T) {
	t.Run("invalid lsblk json", func(t *testing.T) {
		dir := fakeCommandDir(t, map[string]string{"lsblk": `printf '{'`})
		t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
		if _, err := localFlashDevices(40000000000, "/tmp"); err == nil {
			t.Fatal("expected invalid lsblk JSON error")
		}
	})

	t.Run("remote devices tie-break by size and path", func(t *testing.T) {
		dir := fakeCommandDir(t, map[string]string{
			"lsblk": `cat <<'JSON'
{"blockdevices":[
  {"name":"sdc","path":"/dev/sdc","rm":true,"hotplug":true,"size":"64000000000","model":"Micro SD","tran":"usb","type":"disk"},
  {"name":"sdb","path":"/dev/sdb","rm":true,"hotplug":true,"size":"32000000000","model":"Micro SD","tran":"usb","type":"disk"},
  {"name":"sda","path":"/dev/sda","rm":true,"hotplug":true,"size":"32000000000","model":"Micro SD","tran":"usb","type":"disk"}
]}
JSON`,
		})
		t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
		stdout, _ := captureStreams(t, func() {
			remoteDevicesCmd([]string{"--max-device-bytes", "100000000000"})
		})
		if strings.Index(stdout, "/dev/sda") > strings.Index(stdout, "/dev/sdb") || strings.Index(stdout, "/dev/sdb") > strings.Index(stdout, "/dev/sdc") {
			t.Fatalf("devices not sorted by size/path: %s", stdout)
		}
	})
}

func TestRemoteArtifactFailureEdges(t *testing.T) {
	dir := fakeCommandDir(t, map[string]string{
		"oras": `printf 'oras failed' >&2; exit 7`,
	})
	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
	artifactDir := t.TempDir()
	imagePath := filepath.Join(artifactDir, "image.img")
	metadataPath := filepath.Join(artifactDir, "metadata.json")
	if err := os.WriteFile(imagePath, []byte("image"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(metadataPath, []byte("{}"), 0o644); err != nil {
		t.Fatal(err)
	}

	for name, fn := range map[string]func() error{
		"login": func() error { return orasLogin("registry.example", "u", "p") },
		"push":  func() error { return orasPush("registry.example/metis/node:tag", imagePath, metadataPath) },
		"tag":   func() error { return orasTag("registry.example/metis/node:tag", "latest") },
		"pull":  func() error { return orasPull("registry.example/metis/node:latest", artifactDir) },
	} {
		t.Run(name, func(t *testing.T) {
			if err := fn(); err == nil || !strings.Contains(err.Error(), "oras failed") {
				t.Fatalf("expected oras failure, got %v", err)
			}
		})
	}

	emptyDir := t.TempDir()
	if _, _, err := resolvePulledArtifact(emptyDir); err == nil || !strings.Contains(err.Error(), "no .img") {
		t.Fatalf("expected empty artifact dir error, got %v", err)
	}
	if err := orasPush("ref", filepath.Join(artifactDir, "one", "a.img"), filepath.Join(artifactDir, "two", "metadata.json")); err == nil || !strings.Contains(err.Error(), "one directory") {
		t.Fatalf("expected push invocation error, got %v", err)
	}
}

func TestRemoteCommandFatalEdges(t *testing.T) {
	expectFatal := func(t *testing.T, want string, fn func()) {
		t.Helper()
		oldFatalf := fatalf
		fatalf = func(format string, args ...any) {
			panic(fmt.Sprintf(format, args...))
		}
		defer func() { fatalf = oldFatalf }()
		defer func() {
			got := recover()
			if got == nil {
				t.Fatalf("expected fatal containing %q", want)
			}
			if !strings.Contains(fmt.Sprint(got), want) {
				t.Fatalf("fatal = %v, want %q", got, want)
			}
		}()
		fn()
	}

	t.Run("remote devices lsblk failure", func(t *testing.T) {
		dir := fakeCommandDir(t, map[string]string{"lsblk": `exit 2`})
		t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
		expectFatal(t, "remote devices", func() { remoteDevicesCmd(nil) })
	})

	t.Run("remote build mkdir failure", func(t *testing.T) {
		root := t.TempDir()
		blocker := filepath.Join(root, "blocker")
		if err := os.WriteFile(blocker, []byte("x"), 0o644); err != nil {
			t.Fatal(err)
		}
		expectFatal(t, "mkdir workdir", func() {
			remoteBuildCmd([]string{"--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(blocker, "child")})
		})
	})

	t.Run("remote build plan failure", func(t *testing.T) {
		root := t.TempDir()
		invPath, _ := writeTestInventory(t, root)
		expectFatal(t, "build plan", func() {
			remoteBuildCmd([]string{"--inventory", invPath, "--node", "missing", "--artifact-ref", "registry.example/metis/missing", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "build")})
		})
	})

	t.Run("remote flash mkdir failure", func(t *testing.T) {
		root := t.TempDir()
		blocker := filepath.Join(root, "blocker")
		if err := os.WriteFile(blocker, []byte("x"), 0o644); err != nil {
			t.Fatal(err)
		}
		expectFatal(t, "mkdir workdir", func() {
			remoteFlashCmd([]string{"--node", "titan-15", "--device", "/dev/sdz", "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(blocker, "child")})
		})
	})

	t.Run("remote flash missing credentials", func(t *testing.T) {
		expectFatal(t, "oras login", func() {
			remoteFlashCmd([]string{"--node", "titan-15", "--device", "/dev/sdz", "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", t.TempDir()})
		})
	})
}

func TestPlanAndBurnCommandFlagBranches(t *testing.T) {
	root := t.TempDir()
	invPath, _ := writeTestInventory(t, root)
	boot := filepath.Join(root, "boot")
	rootfs := filepath.Join(root, "root")
	stdout, _ := captureStreams(t, func() {
		planCmd([]string{"--inventory", invPath, "--node", "titan-15", "--boot", boot, "--root", rootfs})
	})
	if !strings.Contains(stdout, `"node": "titan-15"`) || os.Getenv("METIS_BOOT_PATH") != boot || os.Getenv("METIS_ROOT_PATH") != rootfs {
		t.Fatalf("plan command did not apply boot/root flags: stdout=%s", stdout)
	}

	fakeTools := fakeCommandDir(t, map[string]string{
		"sfdisk": `cat <<'JSON'
{"partitiontable":{"sectorsize":512,"partitions":[{"start":3,"size":1,"type":"ef"},{"start":1,"size":2,"type":"83"}]}}
JSON`,
		"debugfs": `exit 0`,
	})
	t.Setenv("PATH", fakeTools+string(os.PathListSeparator)+os.Getenv("PATH"))
	stdout, _ = captureStreams(t, func() {
		burnCmd([]string{"--inventory", invPath, "--node", "titan-15", "--device", filepath.Join(root, "out.img"), "--boot", boot, "--root", rootfs, "--auto-mount", "--yes"})
	})
	if !strings.Contains(stdout, "Burn complete") || os.Getenv("METIS_AUTO_MOUNT") != "1" {
		t.Fatalf("burn command did not complete confirmed path: stdout=%s", stdout)
	}
}

func TestPlanBurnAndRemoteBuildFatalStageEdges(t *testing.T) {
	root := t.TempDir()
	invPath, baseImage := writeTestInventory(t, root)
	expectCommandFatal(t, "--node is required", func() { planCmd(nil) })
	expectCommandFatal(t, "build plan", func() {
		planCmd([]string{"--inventory", invPath, "--node", "missing"})
	})
	expectCommandFatal(t, "--node and --device are required", func() { burnCmd(nil) })
	expectCommandFatal(t, "burn", func() {
		burnCmd([]string{"--inventory", invPath, "--node", "missing", "--device", filepath.Join(root, "out.img")})
	})

	t.Run("remote build download checksum failure", func(t *testing.T) {
		badInv := filepath.Join(root, "bad-inventory.yaml")
		data, err := os.ReadFile(invPath)
		if err != nil {
			t.Fatal(err)
		}
		data = []byte(strings.Replace(string(data), "checksum: sha256:"+sha256SumHex(t, make([]byte, 4096)), "checksum: sha256:0000", 1))
		if err := os.WriteFile(badInv, data, 0o644); err != nil {
			t.Fatal(err)
		}
		expectCommandFatal(t, "download image", func() {
			remoteBuildCmd([]string{"--inventory", badInv, "--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "download-fail"), "--cache", filepath.Join(root, "cache-download")})
		})
	})

	t.Run("remote build xz failure", func(t *testing.T) {
		tools := remoteBuildToolDir(t, baseImage, `exit 9`, `exit 0`)
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))
		expectCommandFatal(t, "xz compress", func() {
			remoteBuildCmd([]string{"--inventory", invPath, "--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "xz-fail"), "--cache", filepath.Join(root, "cache-xz"), "--harbor-username", "admin", "--harbor-password", "pw"})
		})
	})

	t.Run("remote build missing compressed artifact", func(t *testing.T) {
		tools := remoteBuildToolDir(t, baseImage, `exit 0`, `exit 0`)
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))
		expectCommandFatal(t, "stat compressed image", func() {
			remoteBuildCmd([]string{"--inventory", invPath, "--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "stat-fail"), "--cache", filepath.Join(root, "cache-stat"), "--harbor-username", "admin", "--harbor-password", "pw"})
		})
	})

	t.Run("remote build missing harbor credentials", func(t *testing.T) {
		tools := remoteBuildToolDir(t, baseImage, `cp "${@: -1}" "${@: -1}.xz"`, `exit 0`)
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))
		t.Setenv("METIS_HARBOR_USERNAME", "")
		t.Setenv("METIS_HARBOR_PASSWORD", "")
		expectCommandFatal(t, "oras login", func() {
			remoteBuildCmd([]string{"--inventory", invPath, "--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "login-fail"), "--cache", filepath.Join(root, "cache-login")})
		})
	})
	t.Run("remote build oras push failure", func(t *testing.T) {
		tools := remoteBuildToolDir(t, baseImage, `cp "${@: -1}" "${@: -1}.xz"`, `case "${1:-}" in
  login|pull) exit 0 ;;
  push) printf 'push failed' >&2; exit 6 ;;
  tag) exit 0 ;;
esac
exit 0`)
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))
		expectCommandFatal(t, "oras push", func() {
			remoteBuildCmd([]string{"--inventory", invPath, "--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "push-fail"), "--cache", filepath.Join(root, "cache-push"), "--harbor-username", "admin", "--harbor-password", "pw"})
		})
	})

	t.Run("remote build oras tag failure", func(t *testing.T) {
		tools := remoteBuildToolDir(t, baseImage, `cp "${@: -1}" "${@: -1}.xz"`, `case "${1:-}" in
  login|pull|push) exit 0 ;;
  tag) printf 'tag failed' >&2; exit 7 ;;
esac
exit 0`)
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))
		expectCommandFatal(t, "oras tag latest", func() {
			remoteBuildCmd([]string{"--inventory", invPath, "--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "tag-fail"), "--cache", filepath.Join(root, "cache-tag"), "--harbor-username", "admin", "--harbor-password", "pw"})
		})
	})
}

func TestRemoteFlashFatalAndDeviceFlushEdges(t *testing.T) {
	root := t.TempDir()
	rawTools := fakeCommandDir(t, map[string]string{
		"oras": `case "${1:-}" in
  login) exit 0 ;;
  pull) outdir="${@: -1}"; printf 'image' > "${outdir}/artifact.img"; exit 0 ;;
esac
exit 0`,
		"sync":     `exit 0`,
		"blockdev": `exit 0`,
	})
	t.Setenv("PATH", rawTools+string(os.PathListSeparator)+os.Getenv("PATH"))

	blocker := filepath.Join(root, "blocker")
	if err := os.WriteFile(blocker, []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	expectCommandFatal(t, "mkdir host tmp dir", func() {
		remoteFlashCmd([]string{"--node", "titan-15", "--device", "hosttmp:///tmp", "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(root, "flash-hosttmp"), "--host-tmp-dir", filepath.Join(blocker, "child"), "--harbor-username", "admin", "--harbor-password", "pw"})
	})
	expectCommandFatal(t, "write image", func() {
		remoteFlashCmd([]string{"--node", "titan-15", "--device", filepath.Join(blocker, "child"), "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(root, "flash-write"), "--harbor-username", "admin", "--harbor-password", "pw"})
	})

	pullFailTools := fakeCommandDir(t, map[string]string{
		"oras": `case "${1:-}" in
  login) exit 0 ;;
  pull) printf 'pull failed' >&2; exit 4 ;;
esac
exit 0`,
	})
	t.Setenv("PATH", pullFailTools+string(os.PathListSeparator)+os.Getenv("PATH"))
	expectCommandFatal(t, "oras pull", func() {
		remoteFlashCmd([]string{"--node", "titan-15", "--device", filepath.Join(root, "out.img"), "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(root, "flash-pull"), "--harbor-username", "admin", "--harbor-password", "pw"})
	})

	emptyPullTools := fakeCommandDir(t, map[string]string{
		"oras": `case "${1:-}" in
  login) exit 0 ;;
  pull) exit 0 ;;
esac
exit 0`,
	})
	t.Setenv("PATH", emptyPullTools+string(os.PathListSeparator)+os.Getenv("PATH"))
	expectCommandFatal(t, "resolve artifact", func() {
		remoteFlashCmd([]string{"--node", "titan-15", "--device", filepath.Join(root, "out.img"), "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(root, "flash-empty"), "--harbor-username", "admin", "--harbor-password", "pw"})
	})

	compressedTools := fakeCommandDir(t, map[string]string{
		"oras": `case "${1:-}" in
  login) exit 0 ;;
  pull) outdir="${@: -1}"; printf 'compressed' > "${outdir}/artifact.img.xz"; exit 0 ;;
esac
exit 0`,
		"xz": `exit 8`,
	})
	t.Setenv("PATH", compressedTools+string(os.PathListSeparator)+os.Getenv("PATH"))
	expectCommandFatal(t, "xz stream decompress", func() {
		remoteFlashCmd([]string{"--node", "titan-15", "--device", filepath.Join(root, "out.img"), "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(root, "flash-xz"), "--harbor-username", "admin", "--harbor-password", "pw"})
	})
}

func TestCaptureStreamsAllowsBinaryOutput(t *testing.T) {
	out, _ := captureStreams(t, func() {
		_, _ = io.Copy(os.Stdout, bytes.NewBuffer([]byte("ok")))
	})
	if out != "ok" {
		t.Fatalf("captureStreams = %q", out)
	}
	expectCommandFatal(t, "encode result", func() {
		writeStructuredResult(map[string]any{"bad": func() {}})
	})
}

func expectCommandFatal(t *testing.T, want string, fn func()) {
	t.Helper()
	oldFatalf := fatalf
	fatalf = func(format string, args ...any) {
		panic(fmt.Sprintf(format, args...))
	}
	defer func() { fatalf = oldFatalf }()
	defer func() {
		got := recover()
		if got == nil {
			t.Fatalf("expected fatal containing %q", want)
		}
		if !strings.Contains(fmt.Sprint(got), want) {
			t.Fatalf("fatal = %v, want %q", got, want)
		}
	}()
	fn()
}

func remoteBuildToolDir(t *testing.T, baseImage, xzBody, orasBody string) string {
	t.Helper()
	return fakeCommandDir(t, map[string]string{
		"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":   xzBody,
		"oras": orasBody,
		"losetup": `if [[ "${1:-}" == "-Pf" && "${2:-}" == "--show" ]]; then
  printf '/dev/loop9
'
  exit 0
fi
exit 0`,
		"mount": `mkdir -p "${2:-}"
exit 0`,
		"umount":   `exit 0`,
		"sync":     `exit 0`,
		"blockdev": `exit 0`,
		"cp-base":  `cp "` + baseImage + `" "$1"`,
	})
}
