package cli

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"strings"
	"testing"

	"banger/internal/api"
	"banger/internal/model"

	"github.com/spf13/cobra"
)

// stubPruneSeams installs list + delete fakes onto the caller's *deps
// and returns a pointer to a slice that records every ID passed to the
// delete fake.
func stubPruneSeams(t *testing.T, d *deps, vms []model.VMRecord, listErr error, deleteErr map[string]error) *[]string {
	t.Helper()

	var deleted []string
	d.vmList = func(ctx context.Context, socketPath string) (api.VMListResult, error) {
		return api.VMListResult{VMs: vms}, listErr
	}
	d.vmDelete = func(ctx context.Context, socketPath, idOrName string) error {
		if err, ok := deleteErr[idOrName]; ok {
			return err
		}
		deleted = append(deleted, idOrName)
		return nil
	}
	return &deleted
}

func newPruneTestCmd(stdin string) (*cobra.Command, *bytes.Buffer, *bytes.Buffer) {
	cmd := &cobra.Command{Use: "prune"}
	cmd.SetContext(context.Background())
	stdout := &bytes.Buffer{}
	stderr := &bytes.Buffer{}
	cmd.SetIn(strings.NewReader(stdin))
	cmd.SetOut(stdout)
	cmd.SetErr(stderr)
	return cmd, stdout, stderr
}

func TestPromptYesNo(t *testing.T) {
	cases := map[string]bool{
		"y\n":        true,
		"Y\n":        true,
		"yes\n":      true,
		"YES\n":      true,
		"  y  \n":    true,
		"n\n":        false,
		"no\n":       false,
		"\n":         false,
		"anything\n": false,
	}
	for input, want := range cases {
		out := &bytes.Buffer{}
		got, err := promptYesNo(strings.NewReader(input), out, "go? ")
		if err != nil {
			t.Errorf("input %q: error %v", input, err)
			continue
		}
		if got != want {
			t.Errorf("input %q: got %v, want %v", input, got, want)
		}
		if !strings.Contains(out.String(), "go?") {
			t.Errorf("input %q: prompt not written; got %q", input, out.String())
		}
	}
}

func TestPromptYesNoEOF(t *testing.T) {
	got, err := promptYesNo(strings.NewReader(""), &bytes.Buffer{}, "? ")
	if err != nil {
		t.Fatalf("EOF should not error: %v", err)
	}
	if got {
		t.Fatal("EOF should be treated as no")
	}
}

func TestRunVMPruneNoVictims(t *testing.T) {
	d := defaultDeps()
	stubPruneSeams(t, d, []model.VMRecord{
		{ID: "id-1", Name: "running-vm", State: model.VMStateRunning},
	}, nil, nil)

	cmd, stdout, _ := newPruneTestCmd("")
	if err := d.runVMPrune(cmd, "sock", false); err != nil {
		t.Fatalf("d.runVMPrune: %v", err)
	}
	if !strings.Contains(stdout.String(), "no non-running VMs") {
		t.Errorf("expected no-op message, got %q", stdout.String())
	}
}

func TestRunVMPruneAbortedByUser(t *testing.T) {
	d := defaultDeps()
	deleted := stubPruneSeams(t, d, []model.VMRecord{
		{ID: "id-1", Name: "stale", State: model.VMStateStopped},
	}, nil, nil)

	cmd, stdout, _ := newPruneTestCmd("n\n")
	if err := d.runVMPrune(cmd, "sock", false); err != nil {
		t.Fatalf("d.runVMPrune: %v", err)
	}
	if !strings.Contains(stdout.String(), "aborted") {
		t.Errorf("expected 'aborted' output, got %q", stdout.String())
	}
	if len(*deleted) != 0 {
		t.Errorf("should not have deleted anything, got %v", *deleted)
	}
}

func TestRunVMPruneConfirmedDeletesNonRunning(t *testing.T) {
	d := defaultDeps()
	deleted := stubPruneSeams(t, d, []model.VMRecord{
		{ID: "id-run", Name: "keeper", State: model.VMStateRunning},
		{ID: "id-stop", Name: "stale", State: model.VMStateStopped},
		{ID: "id-err", Name: "broken", State: model.VMStateError},
		{ID: "id-created", Name: "fresh", State: model.VMStateCreated},
	}, nil, nil)

	cmd, stdout, _ := newPruneTestCmd("y\n")
	if err := d.runVMPrune(cmd, "sock", false); err != nil {
		t.Fatalf("d.runVMPrune: %v", err)
	}
	// Deleted must be exactly the three non-running IDs, in list order.
	want := []string{"id-stop", "id-err", "id-created"}
	if len(*deleted) != len(want) {
		t.Fatalf("deleted = %v, want %v", *deleted, want)
	}
	for i, id := range want {
		if (*deleted)[i] != id {
			t.Errorf("deleted[%d] = %q, want %q", i, (*deleted)[i], id)
		}
	}
	for _, want := range []string{"stale", "broken", "fresh"} {
		if !strings.Contains(stdout.String(), "deleted "+want) {
			t.Errorf("output missing 'deleted %s':\n%s", want, stdout.String())
		}
	}
	if strings.Contains(stdout.String(), "deleted keeper") {
		t.Errorf("running VM should not be deleted:\n%s", stdout.String())
	}
}

func TestRunVMPruneForceSkipsPrompt(t *testing.T) {
	d := defaultDeps()
	deleted := stubPruneSeams(t, d, []model.VMRecord{
		{ID: "id-1", Name: "stale", State: model.VMStateStopped},
	}, nil, nil)

	// Empty stdin + force=true: must not block on prompt.
	cmd, stdout, _ := newPruneTestCmd("")
	if err := d.runVMPrune(cmd, "sock", true); err != nil {
		t.Fatalf("d.runVMPrune: %v", err)
	}
	if len(*deleted) != 1 || (*deleted)[0] != "id-1" {
		t.Errorf("deleted = %v, want [id-1]", *deleted)
	}
	// Prompt should not appear in output.
	if strings.Contains(stdout.String(), "Delete these VMs?") {
		t.Errorf("force=true should skip prompt:\n%s", stdout.String())
	}
}

func TestRunVMPruneReportsPartialFailure(t *testing.T) {
	d := defaultDeps()
	stubPruneSeams(t, d,
		[]model.VMRecord{
			{ID: "id-a", Name: "a", State: model.VMStateStopped},
			{ID: "id-b", Name: "b", State: model.VMStateStopped},
		},
		nil,
		map[string]error{"id-a": errors.New("simulated")},
	)

	cmd, _, stderr := newPruneTestCmd("")
	err := d.runVMPrune(cmd, "sock", true)
	if err == nil {
		t.Fatal("expected non-zero exit when any delete fails")
	}
	if !strings.Contains(err.Error(), "1 VM(s) failed") {
		t.Errorf("unexpected error: %v", err)
	}
	if !strings.Contains(stderr.String(), "delete a:") {
		t.Errorf("stderr missing failure log: %q", stderr.String())
	}
}

func TestRunVMPruneListErrorPropagates(t *testing.T) {
	d := defaultDeps()
	stubPruneSeams(t, d, nil, fmt.Errorf("rpc failed"), nil)

	cmd, _, _ := newPruneTestCmd("")
	err := d.runVMPrune(cmd, "sock", true)
	if err == nil || !strings.Contains(err.Error(), "rpc failed") {
		t.Fatalf("expected rpc error to propagate, got %v", err)
	}
}
