//go:build smoke

package smoketest

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

// testWorkspaceRun ports scenario_workspace_run. Ships the throwaway
// git repo to a fresh VM and reads the marker file from the guest.
func testWorkspaceRun(t *testing.T) {
	out := mustBanger(t, "vm", "run", "--rm", repoDir, "--", "cat", "/root/repo/smoke-file.txt")
	wantContains(t, out, "smoke-workspace-marker", "workspace vm run guest read")
}

// testWorkspaceDryrun ports scenario_workspace_dryrun. `--dry-run`
// lists the tracked files and the resolved transfer mode without
// creating a VM.
func testWorkspaceDryrun(t *testing.T) {
	out := mustBanger(t, "vm", "run", "--dry-run", repoDir)
	wantContains(t, out, "smoke-file.txt", "dry-run file list")
	wantContains(t, out, "mode: tracked only", "dry-run mode line")
}

// testIncludeUntracked ports scenario_include_untracked. Drops an
// untracked file in the fixture and asserts --include-untracked picks
// it up. The cleanup hook removes the file even if the scenario fails
// so downstream repodir scenarios see the original tree.
func testIncludeUntracked(t *testing.T) {
	untracked := filepath.Join(repoDir, "smoke-untracked.txt")
	if err := os.WriteFile(untracked, []byte("untracked-marker\n"), 0o644); err != nil {
		t.Fatalf("write untracked file: %v", err)
	}
	t.Cleanup(func() { _ = os.Remove(untracked) })

	out := mustBanger(t, "vm", "run", "--rm", "--include-untracked", repoDir,
		"--", "cat", "/root/repo/smoke-untracked.txt")
	wantContains(t, out, "untracked-marker", "include-untracked guest read")
}

// testWorkspaceExport ports scenario_workspace_export. Round-trips a
// guest-side edit back out as a patch via `vm workspace export`.
func testWorkspaceExport(t *testing.T) {
	const name = "smoke-export"
	vmCreate(t, name, "--image", "debian-bookworm")
	mustBanger(t, "vm", "workspace", "prepare", name, repoDir)
	mustBanger(t, "vm", "ssh", name, "--", "sh", "-c",
		"echo guest-edit > /root/repo/new-guest-file.txt")

	patch := filepath.Join(runtimeDir, "smoke-export.diff")
	mustBanger(t, "vm", "workspace", "export", name, "--output", patch)

	st, err := os.Stat(patch)
	if err != nil {
		t.Fatalf("export: stat patch %s: %v", patch, err)
	}
	if st.Size() == 0 {
		t.Fatalf("export: patch file empty at %s", patch)
	}
	body, err := os.ReadFile(patch)
	if err != nil {
		t.Fatalf("export: read patch: %v", err)
	}
	wantContains(t, string(body), "new-guest-file.txt", "export: patch must reference new-guest-file.txt")
}

// testWorkspaceFullCopy ports scenario_workspace_full_copy. Verifies
// the alternate transfer path (--mode full_copy) lands the same fixture
// in the guest.
func testWorkspaceFullCopy(t *testing.T) {
	const name = "smoke-fc"
	vmCreate(t, name)
	mustBanger(t, "vm", "workspace", "prepare", name, repoDir, "--mode", "full_copy")

	out := mustBanger(t, "vm", "ssh", name, "--", "cat", "/root/repo/smoke-file.txt")
	wantContains(t, out, "smoke-workspace-marker", "full_copy: marker missing in guest")
}

// testWorkspaceBasecommit ports scenario_workspace_basecommit. Confirms
// that `vm workspace export` without --base-commit captures only the
// working-copy diff, while --base-commit also captures guest-side
// commits made on top of HEAD.
func testWorkspaceBasecommit(t *testing.T) {
	const name = "smoke-basecommit"
	vmCreate(t, name)
	mustBanger(t, "vm", "workspace", "prepare", name, repoDir)

	baseSHA := strings.TrimSpace(mustBanger(t, "vm", "ssh", name, "--",
		"sh", "-c", "cd /root/repo && git rev-parse HEAD"))
	if len(baseSHA) != 40 {
		t.Fatalf("export base: bad base sha: %q", baseSHA)
	}

	mustBanger(t, "vm", "ssh", name, "--", "sh", "-c",
		"cd /root/repo && "+
			"git -c user.email=smoke@smoke -c user.name=smoke checkout -b smoke-branch >/dev/null 2>&1 && "+
			"echo committed-marker > smoke-committed.txt && "+
			"git add smoke-committed.txt && "+
			"git -c user.email=smoke@smoke -c user.name=smoke commit -q -m 'guest side'")

	plain := filepath.Join(runtimeDir, "smoke-plain.diff")
	mustBanger(t, "vm", "workspace", "export", name, "--output", plain)
	if body, err := os.ReadFile(plain); err == nil {
		wantNotContains(t, string(body), "smoke-committed.txt",
			"export base: plain export must NOT capture guest-side commit")
	}

	base := filepath.Join(runtimeDir, "smoke-base.diff")
	mustBanger(t, "vm", "workspace", "export", name, "--base-commit", baseSHA, "--output", base)
	st, err := os.Stat(base)
	if err != nil || st.Size() == 0 {
		t.Fatalf("export base: --base-commit patch empty/missing: stat=%v err=%v", st, err)
	}
	body, _ := os.ReadFile(base)
	wantContains(t, string(body), "smoke-committed.txt",
		"export base: --base-commit patch must include committed marker")
}

// testWorkspaceRestart ports scenario_workspace_restart. Verifies the
// workspace marker survives a stop/start cycle (rootfs persistence).
func testWorkspaceRestart(t *testing.T) {
	const name = "smoke-wsrestart"
	vmCreate(t, name)
	mustBanger(t, "vm", "workspace", "prepare", name, repoDir)

	pre := mustBanger(t, "vm", "ssh", name, "--", "cat", "/root/repo/smoke-file.txt")
	wantContains(t, pre, "smoke-workspace-marker", "workspace stop/start: pre-cycle marker")

	mustBanger(t, "vm", "stop", name)
	mustBanger(t, "vm", "start", name)
	waitForSSH(t, name)

	post := mustBanger(t, "vm", "ssh", name, "--", "cat", "/root/repo/smoke-file.txt")
	wantContains(t, post, "smoke-workspace-marker", "workspace stop/start: post-cycle marker")
}

// testVMExec ports scenario_vm_exec. The longest scenario in the suite
// — covers auto-cd, exit-code propagation, stale-workspace detection,
// --auto-prepare resync, and the not-running refusal. The repodir
// commit added mid-scenario is rolled back via t.Cleanup so subsequent
// repodir-chain scenarios see the original fixture state.
func testVMExec(t *testing.T) {
	const name = "smoke-exec"
	vmCreate(t, name)
	mustBanger(t, "vm", "workspace", "prepare", name, repoDir)

	show := mustBanger(t, "vm", "show", name)
	wantContains(t, show, `"guest_path": "/root/repo"`,
		"vm exec: workspace.guest_path not persisted")

	out := mustBanger(t, "vm", "exec", name, "--", "cat", "smoke-file.txt")
	wantContains(t, out, "smoke-workspace-marker", "vm exec: workspace marker")

	if got := strings.TrimSpace(mustBanger(t, "vm", "exec", name, "--", "pwd")); got != "/root/repo" {
		t.Fatalf("vm exec: pwd got %q, want /root/repo (auto-cd didn't happen)", got)
	}

	res := banger(t, "vm", "exec", name, "--", "sh", "-c", "exit 17")
	wantExit(t, res, 17, "vm exec: exit-code propagation")

	// Advance host HEAD so the workspace goes stale, register the
	// rollback before mutating so a Fatal anywhere below still
	// restores the fixture.
	t.Cleanup(func() {
		cmd := exec.Command("git", "reset", "--hard", "HEAD~1", "-q")
		cmd.Dir = repoDir
		_ = cmd.Run()
	})
	for _, args := range [][]string{
		{"sh", "-c", "echo post-prepare-marker > smoke-exec-new.txt"},
		{"git", "add", "smoke-exec-new.txt"},
		{"git", "commit", "-q", "-m", "add smoke-exec-new.txt after prepare"},
	} {
		cmd := exec.Command(args[0], args[1:]...)
		cmd.Dir = repoDir
		if out, err := cmd.CombinedOutput(); err != nil {
			t.Fatalf("vm exec: stage host commit: %s: %v\n%s", args, err, out)
		}
	}

	stale := banger(t, "vm", "exec", name, "--", "ls", "smoke-exec-new.txt")
	if stale.rc == 0 {
		t.Fatalf("vm exec: stale workspace already had the new file (dirty path didn't take effect)")
	}
	wantContains(t, stale.stderr, "workspace stale", "vm exec: stale-workspace warning on stderr")
	wantContains(t, stale.stderr, "--auto-prepare", "vm exec: stale warning must mention --auto-prepare")

	auto := mustBanger(t, "vm", "exec", name, "--auto-prepare", "--", "cat", "smoke-exec-new.txt")
	wantContains(t, auto, "post-prepare-marker", "vm exec: --auto-prepare didn't re-sync new file")

	clean := banger(t, "vm", "exec", name, "--", "true")
	wantExit(t, clean, 0, "vm exec: post-auto-prepare run")
	wantNotContains(t, clean.stderr, "workspace stale", "vm exec: stale warning persisted after --auto-prepare")

	mustBanger(t, "vm", "stop", name)
	stopped := banger(t, "vm", "exec", name, "--", "true")
	if stopped.rc == 0 {
		t.Fatalf("vm exec: exec on stopped VM unexpectedly succeeded")
	}
	wantContains(t, stopped.stderr, "not running", "vm exec: stopped-VM error message")
}
