package cli

import (
	"bytes"
	"context"
	"io"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"banger/internal/api"
	"banger/internal/model"
	"banger/internal/toolingplan"
)

func TestVMRunRejectsDetachWithRm(t *testing.T) {
	cmd := NewBangerCommand()
	cmd.SetArgs([]string{"vm", "run", "-d", "--rm"})

	err := cmd.Execute()
	if err == nil || !strings.Contains(err.Error(), "cannot combine --detach with --rm") {
		t.Fatalf("Execute() error = %v, want --detach + --rm rejection", err)
	}
}

func TestVMRunRejectsDetachWithCommand(t *testing.T) {
	cmd := NewBangerCommand()
	cmd.SetArgs([]string{"vm", "run", "-d", "--", "whoami"})

	err := cmd.Execute()
	if err == nil || !strings.Contains(err.Error(), "cannot combine --detach with a guest command") {
		t.Fatalf("Execute() error = %v, want --detach + command rejection", err)
	}
}

func TestRepoHasMiseFiles(t *testing.T) {
	dir := t.TempDir()
	got, err := repoHasMiseFiles(dir)
	if err != nil {
		t.Fatalf("repoHasMiseFiles(empty): %v", err)
	}
	if got {
		t.Fatalf("repoHasMiseFiles(empty) = true, want false")
	}

	if err := os.WriteFile(filepath.Join(dir, ".mise.toml"), []byte(""), 0o600); err != nil {
		t.Fatalf("write .mise.toml: %v", err)
	}
	got, err = repoHasMiseFiles(dir)
	if err != nil {
		t.Fatalf("repoHasMiseFiles(.mise.toml): %v", err)
	}
	if !got {
		t.Fatalf("repoHasMiseFiles(.mise.toml) = false, want true")
	}

	dir2 := t.TempDir()
	if err := os.WriteFile(filepath.Join(dir2, ".tool-versions"), []byte(""), 0o600); err != nil {
		t.Fatalf("write .tool-versions: %v", err)
	}
	got, err = repoHasMiseFiles(dir2)
	if err != nil {
		t.Fatalf("repoHasMiseFiles(.tool-versions): %v", err)
	}
	if !got {
		t.Fatalf("repoHasMiseFiles(.tool-versions) = false, want true")
	}
}

// runVMRunDepsRunningVM returns a deps wired so runVMRun reaches a
// point where it would create a VM and proceed — used by precondition
// tests that should refuse before any of these fakes get called.
func runVMRunDepsRunningVM(t *testing.T) (*deps, *model.VMRecord) {
	t.Helper()
	d := defaultDeps()
	vm := &model.VMRecord{
		ID:   "vm-id",
		Name: "devbox",
		Runtime: model.VMRuntime{
			State:   model.VMStateRunning,
			GuestIP: "172.16.0.2",
			DNSName: "devbox.vm",
		},
	}
	d.vmCreateBegin = func(context.Context, string, api.VMCreateParams) (api.VMCreateBeginResult, error) {
		return api.VMCreateBeginResult{Operation: api.VMCreateOperation{ID: "op-1", Stage: "ready", Done: true, Success: true, VM: vm}}, nil
	}
	d.guestWaitForSSH = func(context.Context, string, string, time.Duration) error { return nil }
	d.vmWorkspacePrepare = func(context.Context, string, api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) {
		return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo", RepoName: "repo", RepoRoot: "/tmp/repo"}}, nil
	}
	d.buildVMRunToolingPlan = func(context.Context, string) toolingplan.Plan {
		return toolingplan.Plan{}
	}
	d.vmHealth = func(context.Context, string, string) (api.VMHealthResult, error) {
		return api.VMHealthResult{Healthy: true}, nil
	}
	d.sshExec = func(context.Context, io.Reader, io.Writer, io.Writer, []string) error { return nil }
	return d, vm
}

func TestRunVMRunRefusesBootstrapWithoutNAT(t *testing.T) {
	repoRoot := t.TempDir()
	if err := os.WriteFile(filepath.Join(repoRoot, ".mise.toml"), []byte(""), 0o600); err != nil {
		t.Fatalf("write .mise.toml: %v", err)
	}

	d := defaultDeps()
	d.vmCreateBegin = func(context.Context, string, api.VMCreateParams) (api.VMCreateBeginResult, error) {
		t.Fatal("vmCreateBegin should not be called when NAT precondition refuses")
		return api.VMCreateBeginResult{}, nil
	}

	repo := vmRunRepo{sourcePath: repoRoot}
	var stdout, stderr bytes.Buffer
	err := d.runVMRun(
		context.Background(),
		"/tmp/bangerd.sock",
		model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
		strings.NewReader(""),
		&stdout, &stderr,
		api.VMCreateParams{Name: "devbox", NATEnabled: false},
		&repo,
		nil,
		false, false, false, false,
	)
	if err == nil || !strings.Contains(err.Error(), "tooling bootstrap requires --nat") {
		t.Fatalf("runVMRun = %v, want NAT precondition refusal", err)
	}
}

func TestRunVMRunBootstrapPreconditionRespectsNoBootstrap(t *testing.T) {
	repoRoot := t.TempDir()
	if err := os.WriteFile(filepath.Join(repoRoot, ".mise.toml"), []byte(""), 0o600); err != nil {
		t.Fatalf("write .mise.toml: %v", err)
	}

	d, _ := runVMRunDepsRunningVM(t)
	dialed := false
	d.guestDial = func(context.Context, string, string) (vmRunGuestClient, error) {
		dialed = true
		return &testVMRunGuestClient{}, nil
	}

	repo := vmRunRepo{sourcePath: repoRoot}
	var stdout, stderr bytes.Buffer
	err := d.runVMRun(
		context.Background(),
		"/tmp/bangerd.sock",
		model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
		strings.NewReader(""),
		&stdout, &stderr,
		api.VMCreateParams{Name: "devbox", NATEnabled: false},
		&repo,
		nil,
		false, false, true, false, // skipBootstrap = true
	)
	if err != nil {
		t.Fatalf("runVMRun: %v", err)
	}
	if dialed {
		t.Fatal("guestDial should not be called when --no-bootstrap is set")
	}
}

func TestRunVMRunBootstrapPreconditionPassesWithoutMiseFiles(t *testing.T) {
	repoRoot := t.TempDir() // empty repo, no mise files

	d, _ := runVMRunDepsRunningVM(t)
	dialed := false
	d.guestDial = func(context.Context, string, string) (vmRunGuestClient, error) {
		dialed = true
		return &testVMRunGuestClient{}, nil
	}

	repo := vmRunRepo{sourcePath: repoRoot}
	var stdout, stderr bytes.Buffer
	err := d.runVMRun(
		context.Background(),
		"/tmp/bangerd.sock",
		model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
		strings.NewReader(""),
		&stdout, &stderr,
		api.VMCreateParams{Name: "devbox", NATEnabled: false},
		&repo,
		nil,
		false, false, false, false,
	)
	if err != nil {
		t.Fatalf("runVMRun: %v", err)
	}
	// Bootstrap dispatch happens (no mise file gating) but dial still
	// gets called because the harness pipeline runs.
	if !dialed {
		t.Fatal("guestDial should be called for bootstrap dispatch")
	}
}

func TestRunVMRunDetachSkipsSshAttach(t *testing.T) {
	d, _ := runVMRunDepsRunningVM(t)
	d.guestDial = func(context.Context, string, string) (vmRunGuestClient, error) {
		return &testVMRunGuestClient{}, nil
	}
	sshExecCalls := 0
	d.sshExec = func(context.Context, io.Reader, io.Writer, io.Writer, []string) error {
		sshExecCalls++
		return nil
	}

	var stdout, stderr bytes.Buffer
	err := d.runVMRun(
		context.Background(),
		"/tmp/bangerd.sock",
		model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
		strings.NewReader(""),
		&stdout, &stderr,
		api.VMCreateParams{Name: "devbox"},
		nil,                // bare mode
		nil,                // no command
		false, true, false, false, // detach = true
	)
	if err != nil {
		t.Fatalf("runVMRun: %v", err)
	}
	if sshExecCalls != 0 {
		t.Fatalf("sshExec called %d times, want 0 in detach mode", sshExecCalls)
	}
	if !strings.Contains(stderr.String(), "reconnect with: banger vm ssh devbox") {
		t.Fatalf("stderr = %q, want reconnect hint", stderr.String())
	}
}

func TestRunVMRunDetachUsesSyncBootstrapPath(t *testing.T) {
	repoRoot := t.TempDir()

	d, _ := runVMRunDepsRunningVM(t)
	fakeClient := &testVMRunGuestClient{}
	d.guestDial = func(context.Context, string, string) (vmRunGuestClient, error) {
		return fakeClient, nil
	}
	sshExecCalls := 0
	d.sshExec = func(context.Context, io.Reader, io.Writer, io.Writer, []string) error {
		sshExecCalls++
		return nil
	}

	repo := vmRunRepo{sourcePath: repoRoot}
	var stdout, stderr bytes.Buffer
	err := d.runVMRun(
		context.Background(),
		"/tmp/bangerd.sock",
		model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
		strings.NewReader(""),
		&stdout, &stderr,
		api.VMCreateParams{Name: "devbox", NATEnabled: true},
		&repo,
		nil,
		false, true, false, false, // detach = true
	)
	if err != nil {
		t.Fatalf("runVMRun: %v", err)
	}
	if sshExecCalls != 0 {
		t.Fatalf("sshExec called %d times, want 0 in detach mode", sshExecCalls)
	}
	if len(fakeClient.uploads) != 1 {
		t.Fatalf("uploads = %d, want 1 (harness upload)", len(fakeClient.uploads))
	}
	// Sync mode should invoke the tee'd wrapper, not the nohup launcher.
	if strings.Contains(fakeClient.launchScript, "nohup") {
		t.Fatalf("detach mode should not use nohup launcher; got: %q", fakeClient.launchScript)
	}
	if !strings.Contains(fakeClient.launchScript, "tee") {
		t.Fatalf("detach mode should tee output to log; got: %q", fakeClient.launchScript)
	}
}
