package main

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestUsageWritesSupportedCommands(t *testing.T) {
	stdout, stderr := captureStreams(t, func() {
		usage()
	})
	if stdout != "" {
		t.Fatalf("usage wrote stdout: %q", stdout)
	}
	if !strings.Contains(stderr, "remote-flash") || !strings.Contains(stderr, "plan") {
		t.Fatalf("usage output missing commands: %q", stderr)
	}
}

func TestConfigFactsPlanBurnImageInjectAndServeCommands(t *testing.T) {
	root := t.TempDir()
	invPath, baseImage := writeTestInventory(t, root)
	t.Setenv("METIS_INVENTORY_PATH", invPath)
	t.Setenv("METIS_DATA_DIR", filepath.Join(root, "data"))
	t.Setenv("METIS_BOOT_PATH", "")
	t.Setenv("METIS_ROOT_PATH", "")

	stdout, _ := captureStreams(t, func() {
		configCmd([]string{"--inventory", invPath, "--node", "titan-15"})
	})
	if !strings.Contains(stdout, `"hostname": "titan-15"`) {
		t.Fatalf("config output missing hostname: %s", stdout)
	}

	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)
	}
	stdout, _ = captureStreams(t, func() {
		factsCmd([]string{"--inventory", invPath, "--snapshots", snapDir})
	})
	if !strings.Contains(stdout, `"class": "rpi4"`) {
		t.Fatalf("facts output missing class summary: %s", stdout)
	}

	stdout, _ = captureStreams(t, func() {
		planCmd([]string{"--inventory", invPath, "--node", "titan-15", "--device", "/dev/sdz", "--cache", filepath.Join(root, "cache")})
	})
	if !strings.Contains(stdout, `"node": "titan-15"`) || !strings.Contains(stdout, `"actions"`) {
		t.Fatalf("plan output missing plan JSON: %s", stdout)
	}

	rootTools := 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`,
		"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", rootTools+string(os.PathListSeparator)+os.Getenv("PATH"))
	stdout, _ = captureStreams(t, func() {
		imageCmd([]string{"--inventory", invPath, "--node", "titan-15", "--output", filepath.Join(root, "out.img"), "--cache", filepath.Join(root, "cache")})
	})
	if !strings.Contains(stdout, "Wrote ") {
		t.Fatalf("image output missing write confirmation: %s", stdout)
	}

	stdout, _ = captureStreams(t, func() {
		burnCmd([]string{"--inventory", invPath, "--node", "titan-15", "--device", "/dev/sdz", "--cache", filepath.Join(root, "cache")})
	})
	if !strings.Contains(stdout, "Plan for titan-15 to /dev/sdz") {
		t.Fatalf("burn output missing plan header: %s", stdout)
	}

	bootDir := filepath.Join(root, "boot")
	rootDir := filepath.Join(root, "root")
	if err := os.MkdirAll(bootDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(rootDir, 0o755); err != nil {
		t.Fatal(err)
	}
	captureStreams(t, func() {
		injectCmd([]string{"--inventory", invPath, "--node", "titan-15", "--boot", bootDir, "--root", rootDir})
	})
	if _, err := os.Stat(filepath.Join(rootDir, "etc/metis/node.json")); err != nil {
		t.Fatalf("injectCmd did not write root file: %v", err)
	}

	listenAndServe = func(addr string, handler http.Handler) error {
		if addr != ":0" {
			t.Fatalf("unexpected bind addr: %s", addr)
		}
		return nil
	}
	t.Cleanup(func() { listenAndServe = http.ListenAndServe })
	t.Setenv("METIS_BIND_ADDR", ":0")
	t.Setenv("METIS_INVENTORY_PATH", invPath)
	serveCmd([]string{"--bind", ":0"})
	_ = baseImage
}

func TestMainDispatchesConfig(t *testing.T) {
	root := t.TempDir()
	invPath, _ := writeTestInventory(t, root)
	oldArgs := os.Args
	os.Args = []string{"metis", "config", "--inventory", invPath, "--node", "titan-15"}
	t.Cleanup(func() { os.Args = oldArgs })
	main()
}

func TestRemoteCommandsAndHelpers(t *testing.T) {
	root := t.TempDir()
	invPath, baseImage := writeTestInventory(t, root)
	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": `dest="${@: -1}"; cp "$dest" "$dest.xz"`,
		"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"))

	stdout, _ := captureStreams(t, func() {
		remoteDevicesCmd([]string{"--max-device-bytes", "40000000000", "--host-tmp-dir", filepath.Join(root, "host-tmp")})
	})
	if !strings.Contains(stdout, `"path":"/dev/sdz"`) {
		t.Fatalf("remoteDevicesCmd output missing device: %s", stdout)
	}

	stdout, _ = captureStreams(t, func() {
		remoteBuildCmd([]string{"--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"})
	})
	if !strings.Contains(stdout, `"build_tag":"build-1"`) {
		t.Fatalf("remoteBuildCmd output missing build tag: %s", stdout)
	}

	stdout, _ = captureStreams(t, func() {
		remoteFlashCmd([]string{"--node", "titan-15", "--device", "hosttmp:///tmp", "--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"})
	})
	if !strings.Contains(stdout, `"dest_path"`) {
		t.Fatalf("remoteFlashCmd output missing dest_path: %s", stdout)
	}
}

func captureStreams(t *testing.T, fn func()) (string, string) {
	t.Helper()
	oldStdout := os.Stdout
	oldStderr := os.Stderr
	stdoutR, stdoutW, _ := os.Pipe()
	stderrR, stderrW, _ := os.Pipe()
	os.Stdout = stdoutW
	os.Stderr = stderrW
	defer func() {
		os.Stdout = oldStdout
		os.Stderr = oldStderr
	}()
	done := make(chan struct {
		out string
		err string
	}, 1)
	go func() {
		var outBuf bytes.Buffer
		var errBuf bytes.Buffer
		_, _ = io.Copy(&outBuf, stdoutR)
		_, _ = io.Copy(&errBuf, stderrR)
		done <- struct {
			out string
			err string
		}{out: outBuf.String(), err: errBuf.String()}
	}()
	fn()
	_ = stdoutW.Close()
	_ = stderrW.Close()
	captured := <-done
	return captured.out, captured.err
}

func writeTestInventory(t *testing.T, root string) (string, string) {
	t.Helper()
	baseImage := filepath.Join(root, "base.img")
	if err := os.WriteFile(baseImage, make([]byte, 4096), 0o644); err != nil {
		t.Fatal(err)
	}
	invPath := filepath.Join(root, "inventory.yaml")
	inv := `classes:
  - name: rpi4
    arch: arm64
    os: armbian
    image: file://` + baseImage + `
    checksum: sha256:` + sha256SumHex(t, make([]byte, 4096)) + `
    k3s_version: v1.31.5+k3s1
nodes:
  - name: titan-15
    class: rpi4
    hostname: titan-15
    ip: 192.168.22.43
    k3s_role: agent
    k3s_url: https://192.168.22.7:6443
    k3s_token: token
    ssh_user: atlas
    ssh_authorized_keys:
      - ssh-ed25519 AAA
`
	if err := os.WriteFile(invPath, []byte(inv), 0o644); err != nil {
		t.Fatal(err)
	}
	return invPath, baseImage
}

func sha256SumHex(t *testing.T, data []byte) string {
	t.Helper()
	sum := sha256.Sum256(data)
	return hex.EncodeToString(sum[:])
}

func fakeCommandDir(t *testing.T, scripts map[string]string) string {
	t.Helper()
	dir := t.TempDir()
	for name, body := range scripts {
		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)
		}
	}
	return dir
}
