package main

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

func TestVerifyFlashDestinationForImageFilesAndBlockDevices(t *testing.T) {
	t.Run("image file success and helpers", func(t *testing.T) {
		tools := fakeCommandDir(t, map[string]string{
			"sfdisk": `cat <<'JSON'
{"partitiontable":{"partitions":[{"type":"ef00"},{"type":"8300"}]}}
JSON`,
		})
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))

		result, err := verifyFlashDestination(filepath.Join(t.TempDir(), "titan-15.img"))
		if err != nil {
			t.Fatalf("verifyFlashDestination(image): %v", err)
		}
		if !result.Verified || result.VerificationKind != "image-file" {
			t.Fatalf("unexpected image verification result: %#v", result)
		}
		if !isBootPartitionType("ef00") || !isLinuxPartitionType("8300") {
			t.Fatal("expected helper partition type recognizers to accept GPT aliases")
		}
		if got := firstNonEmpty("", " writable ", "ignored"); got != "writable" {
			t.Fatalf("firstNonEmpty = %q", got)
		}
	})

	t.Run("image file missing writable partition", func(t *testing.T) {
		tools := fakeCommandDir(t, map[string]string{
			"sfdisk": `cat <<'JSON'
{"partitiontable":{"partitions":[{"type":"ef00"}]}}
JSON`,
		})
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))

		if _, err := verifyImageFileFlash(filepath.Join(t.TempDir(), "broken.img")); err == nil || !strings.Contains(err.Error(), "boot and writable partitions") {
			t.Fatalf("expected missing partition error, got %v", err)
		}
	})

	t.Run("image file missing boot partition", func(t *testing.T) {
		tools := fakeCommandDir(t, map[string]string{
			"sfdisk": `cat <<'JSON'
{"partitiontable":{"partitions":[{"type":"8300"}]}}
JSON`,
		})
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))

		if _, err := verifyImageFileFlash(filepath.Join(t.TempDir(), "broken-boot.img")); err == nil || !strings.Contains(err.Error(), "boot and writable partitions") {
			t.Fatalf("expected missing boot partition error, got %v", err)
		}
	})

	t.Run("block device success", func(t *testing.T) {
		tools := fakeCommandDir(t, map[string]string{
			"lsblk": `cat <<'JSON'
{"blockdevices":[{"path":"/dev/sdk","children":[{"path":"/dev/sdk1","type":"part","fstype":"vfat","label":"system-boot"},{"path":"/dev/sdk2","type":"part","fstype":"ext4","label":"writable"}]}]}
JSON`,
			"mount": `mount_dir="${4:-}"
mkdir -p "${mount_dir}"
printf 'ok' > "${mount_dir}/config.txt"
printf 'console=ttyAMA0' > "${mount_dir}/cmdline.txt"
printf 'boot script' > "${mount_dir}/boot.scr"`,
			"umount":    `exit 0`,
			"blockdev":  `exit 0`,
			"partprobe": `exit 0`,
			"udevadm":   `exit 0`,
		})
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))

		result, err := verifyFlashDestination("/dev/sdk")
		if err != nil {
			t.Fatalf("verifyFlashDestination(block): %v", err)
		}
		if !result.Verified || result.VerificationKind != "block-device" {
			t.Fatalf("unexpected block-device verification result: %#v", result)
		}
		if result.BootPartition != "/dev/sdk1" || result.RootPartition != "/dev/sdk2" {
			t.Fatalf("unexpected partition details: %#v", result)
		}
		if len(result.CheckedFiles) != 3 {
			t.Fatalf("expected checked boot files, got %#v", result.CheckedFiles)
		}
	})

	t.Run("block device missing boot file", func(t *testing.T) {
		tools := fakeCommandDir(t, map[string]string{
			"lsblk": `cat <<'JSON'
{"blockdevices":[{"path":"/dev/sdk","children":[{"path":"/dev/sdk1","type":"part","fstype":"vfat","label":"system-boot"},{"path":"/dev/sdk2","type":"part","fstype":"ext4","label":"writable"}]}]}
JSON`,
			"mount": `mount_dir="${4:-}"
mkdir -p "${mount_dir}"
printf 'ok' > "${mount_dir}/config.txt"
printf 'console=ttyAMA0' > "${mount_dir}/cmdline.txt"`,
			"umount":    `exit 0`,
			"blockdev":  `exit 0`,
			"partprobe": `exit 0`,
			"udevadm":   `exit 0`,
		})
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))

		if _, err := verifyBlockDeviceFlash("/dev/sdk"); err == nil || !strings.Contains(err.Error(), "missing boot.scr") {
			t.Fatalf("expected missing boot file error, got %v", err)
		}
	})
}

func TestFlushFlashTargetAndClassifyHelpers(t *testing.T) {
	logPath := filepath.Join(t.TempDir(), "commands.log")
	tools := fakeCommandDir(t, map[string]string{
		"sync":      `printf 'sync %s\n' "$*" >> "` + logPath + `"`,
		"blockdev":  `printf 'blockdev %s\n' "$*" >> "` + logPath + `"`,
		"partprobe": `printf 'partprobe %s\n' "$*" >> "` + logPath + `"`,
		"udevadm":   `printf 'udevadm %s\n' "$*" >> "` + logPath + `"`,
	})
	t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))

	flushFlashTarget("/tmp/test.img", "titan-15")
	flushFlashTarget("/dev/sdk", "titan-15")

	logged, err := os.ReadFile(logPath)
	if err != nil {
		t.Fatalf("read flush log: %v", err)
	}
	text := string(logged)
	if strings.Count(text, "sync ") != 2 {
		t.Fatalf("expected sync for file and block targets, got %q", text)
	}
	for _, want := range []string{"blockdev --flushbufs /dev/sdk", "blockdev --rereadpt /dev/sdk", "partprobe /dev/sdk", "udevadm settle --timeout=10"} {
		if !strings.Contains(text, want) {
			t.Fatalf("expected %q in %q", want, text)
		}
	}

	if _, _, err := classifyFlashPartitions([]flashBlockDevice{{Path: "/dev/sdk1", Type: "part", FSType: "vfat", Label: "system-boot"}}); err == nil || !strings.Contains(err.Error(), "writable ext4") {
		t.Fatalf("expected classify failure, got %v", err)
	}
}

func TestReadFlashPartitionTableAndBlockPartitionErrors(t *testing.T) {
	t.Run("partition table decode error", func(t *testing.T) {
		tools := fakeCommandDir(t, map[string]string{"sfdisk": `printf '{'`})
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))
		if _, err := readFlashPartitionTable(filepath.Join(t.TempDir(), "broken.img")); err == nil || !strings.Contains(err.Error(), "decode partition table") {
			t.Fatalf("expected partition table decode error, got %v", err)
		}
	})

	t.Run("lsblk decode error", func(t *testing.T) {
		tools := fakeCommandDir(t, map[string]string{"lsblk": `printf '{'`})
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))
		if _, err := readBlockDevicePartitions("/dev/sdk"); err == nil || !strings.Contains(err.Error(), "decode lsblk output") {
			t.Fatalf("expected lsblk decode error, got %v", err)
		}
	})

	t.Run("lsblk finds explicit device match", func(t *testing.T) {
		tools := fakeCommandDir(t, map[string]string{
			"lsblk": `cat <<'JSON'
{"blockdevices":[{"path":"/dev/other","children":[{"path":"/dev/other1","type":"part"}]},{"path":"/dev/sdk","children":[{"path":"/dev/sdk1","type":"part","fstype":"vfat","label":"system-boot"}]}]}
JSON`,
		})
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))
		parts, err := readBlockDevicePartitions("/dev/sdk")
		if err != nil {
			t.Fatalf("readBlockDevicePartitions match: %v", err)
		}
		if len(parts) != 1 || parts[0].Path != "/dev/sdk1" {
			t.Fatalf("unexpected matching partitions: %#v", parts)
		}
	})

	t.Run("lsblk missing device match", func(t *testing.T) {
		tools := fakeCommandDir(t, map[string]string{
			"lsblk": `cat <<'JSON'
{"blockdevices":[{"path":"/dev/other","children":[]},{"path":"/dev/another","children":[]}]}
JSON`,
		})
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))
		if _, err := readBlockDevicePartitions("/dev/sdk"); err == nil || !strings.Contains(err.Error(), "did not report partitions") {
			t.Fatalf("expected missing block-device match error, got %v", err)
		}
	})
}

func TestVerifyBootPartitionFilesMountFailure(t *testing.T) {
	tools := fakeCommandDir(t, map[string]string{
		"mount": `printf 'mount failed' >&2; exit 9`,
	})
	t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))
	if _, err := verifyBootPartitionFiles("/dev/sdk1"); err == nil || !strings.Contains(err.Error(), "mount /dev/sdk1 read-only") {
		t.Fatalf("expected mount failure, got %v", err)
	}
}

func TestRemoteFlashHelperAdditionalFailureBranches(t *testing.T) {
	t.Run("partition table command failure", func(t *testing.T) {
		tools := fakeCommandDir(t, map[string]string{"sfdisk": `printf 'bad disk' >&2; exit 7`})
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))
		if _, err := readFlashPartitionTable(filepath.Join(t.TempDir(), "broken.img")); err == nil || !strings.Contains(err.Error(), "sfdisk -J") {
			t.Fatalf("expected sfdisk failure, got %v", err)
		}
	})

	t.Run("lsblk command failure", func(t *testing.T) {
		tools := fakeCommandDir(t, map[string]string{"lsblk": `printf 'lsblk failed' >&2; exit 4`})
		t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))
		if _, err := readBlockDevicePartitions("/dev/sdk"); err == nil || !strings.Contains(err.Error(), "lsblk /dev/sdk") {
			t.Fatalf("expected lsblk command failure, got %v", err)
		}
	})

	t.Run("boot partition classification failure", func(t *testing.T) {
		if _, _, err := classifyFlashPartitions([]flashBlockDevice{{Path: "/dev/sdk2", Type: "part", FSType: "ext4", Label: "writable"}}); err == nil || !strings.Contains(err.Error(), "boot partition") {
			t.Fatalf("expected missing boot partition error, got %v", err)
		}
	})

	t.Run("firstNonEmpty returns empty when every value is blank", func(t *testing.T) {
		if got := firstNonEmpty("", "   "); got != "" {
			t.Fatalf("expected empty firstNonEmpty result, got %q", got)
		}
	})
}

func TestReadBlockDevicePartitionsFallsBackToSingleReturnedDevice(t *testing.T) {
	tools := fakeCommandDir(t, map[string]string{
		"lsblk": `cat <<'JSON'
{"blockdevices":[{"path":"/dev/mmcblk0","children":[{"path":"/dev/mmcblk0p1","type":"part","fstype":"vfat","label":"system-boot"}]}]}
JSON`,
	})
	t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))
	parts, err := readBlockDevicePartitions("/dev/sdk")
	if err != nil {
		t.Fatalf("readBlockDevicePartitions fallback: %v", err)
	}
	if len(parts) != 1 || parts[0].Path != "/dev/mmcblk0p1" {
		t.Fatalf("unexpected fallback partitions: %#v", parts)
	}
}
