package cli

import (
	"context"
	"errors"
	"reflect"
	"testing"

	"banger/internal/api"

	"github.com/spf13/cobra"
)

// stubCompletionSeams installs test doubles for the daemon ping + lister
// seams on the caller's *deps. Tests opt into the sub-functions they
// actually need.
func stubCompletionSeams(
	t *testing.T,
	d *deps,
	pingErr error,
	names map[string][]string,
	listErr error) {
	t.Helper()

	d.daemonPing = func(ctx context.Context, socketPath string) (api.PingResult, error) {
		if pingErr != nil {
			return api.PingResult{}, pingErr
		}
		return api.PingResult{}, nil
	}
	d.completionLister = func(ctx context.Context, socketPath, method string) ([]string, error) {
		if listErr != nil {
			return nil, listErr
		}
		return names[method], nil
	}
}

func TestFilterPrefix(t *testing.T) {
	cases := []struct {
		name       string
		candidates []string
		exclude    []string
		prefix     string
		want       []string
	}{
		{"no filter", []string{"a", "b"}, nil, "", []string{"a", "b"}},
		{"prefix match", []string{"apple", "banana", "apricot"}, nil, "ap", []string{"apple", "apricot"}},
		{"exclude already entered", []string{"a", "b", "c"}, []string{"b"}, "", []string{"a", "c"}},
		{"prefix + exclude", []string{"alpha", "avocado", "banana"}, []string{"alpha"}, "a", []string{"avocado"}},
		{"exact case sensitive", []string{"VM", "vm"}, nil, "v", []string{"vm"}},
		{"empty candidates", nil, nil, "any", nil},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := filterPrefix(tc.candidates, tc.exclude, tc.prefix)
			if !reflect.DeepEqual(got, tc.want) {
				// Allow nil == empty
				if len(got) == 0 && len(tc.want) == 0 {
					return
				}
				t.Errorf("got %v, want %v", got, tc.want)
			}
		})
	}
}

func testCmdWithCtx() *cobra.Command {
	cmd := &cobra.Command{Use: "test"}
	cmd.SetContext(context.Background())
	return cmd
}

func TestCompleteVMNamesHappyPath(t *testing.T) {
	d := defaultDeps()
	stubCompletionSeams(t, d, nil, map[string][]string{"vm.list": {"alpha", "beta", "gamma"}}, nil)

	got, directive := d.completeVMNames(testCmdWithCtx(), nil, "")
	if directive != cobra.ShellCompDirectiveNoFileComp {
		t.Errorf("directive = %d, want NoFileComp", directive)
	}
	if !reflect.DeepEqual(got, []string{"alpha", "beta", "gamma"}) {
		t.Errorf("got %v", got)
	}
}

func TestCompleteVMNamesDaemonDown(t *testing.T) {
	d := defaultDeps()
	stubCompletionSeams(t, d, errors.New("connection refused"), nil, nil)

	got, directive := d.completeVMNames(testCmdWithCtx(), nil, "")
	if len(got) != 0 {
		t.Errorf("daemon-down should return no suggestions, got %v", got)
	}
	if directive != cobra.ShellCompDirectiveNoFileComp {
		t.Errorf("directive = %d, want NoFileComp", directive)
	}
}

func TestCompleteVMNamesRPCError(t *testing.T) {
	d := defaultDeps()
	stubCompletionSeams(t, d, nil, nil, errors.New("rpc failed"))

	got, _ := d.completeVMNames(testCmdWithCtx(), nil, "")
	if len(got) != 0 {
		t.Errorf("rpc error should return no suggestions, got %v", got)
	}
}

func TestCompleteVMNamesExcludesAlreadyEntered(t *testing.T) {
	d := defaultDeps()
	stubCompletionSeams(t, d, nil, map[string][]string{"vm.list": {"alpha", "beta", "gamma"}}, nil)

	got, _ := d.completeVMNames(testCmdWithCtx(), []string{"alpha"}, "")
	want := []string{"beta", "gamma"}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("got %v, want %v", got, want)
	}
}

func TestCompleteVMNamesPrefixFilter(t *testing.T) {
	d := defaultDeps()
	stubCompletionSeams(t, d, nil, map[string][]string{"vm.list": {"alpha", "beta", "alphabet"}}, nil)

	got, _ := d.completeVMNames(testCmdWithCtx(), nil, "alp")
	want := []string{"alpha", "alphabet"}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("got %v, want %v", got, want)
	}
}

func TestCompleteVMNameOnlyAtPos0(t *testing.T) {
	d := defaultDeps()
	stubCompletionSeams(t, d, nil, map[string][]string{"vm.list": {"alpha"}}, nil)

	atPos0, _ := d.completeVMNameOnlyAtPos0(testCmdWithCtx(), nil, "")
	if len(atPos0) != 1 || atPos0[0] != "alpha" {
		t.Errorf("pos 0: got %v", atPos0)
	}

	atPos1, _ := d.completeVMNameOnlyAtPos0(testCmdWithCtx(), []string{"alpha"}, "")
	if len(atPos1) != 0 {
		t.Errorf("pos 1+ should be silent, got %v", atPos1)
	}
}

func TestCompleteImageNames(t *testing.T) {
	d := defaultDeps()
	stubCompletionSeams(t, d, nil, map[string][]string{"image.list": {"debian-bookworm", "alpine"}}, nil)

	got, _ := d.completeImageNames(testCmdWithCtx(), nil, "")
	if !reflect.DeepEqual(got, []string{"debian-bookworm", "alpine"}) {
		t.Errorf("got %v", got)
	}
}

func TestCompleteKernelNames(t *testing.T) {
	d := defaultDeps()
	stubCompletionSeams(t, d, nil, map[string][]string{"kernel.list": {"generic-6.12"}}, nil)

	got, _ := d.completeKernelNames(testCmdWithCtx(), nil, "")
	if len(got) != 1 || got[0] != "generic-6.12" {
		t.Errorf("got %v", got)
	}
}

func TestCompleteImageNameOnlyAtPos0SilentAfterFirst(t *testing.T) {
	d := defaultDeps()
	stubCompletionSeams(t, d, nil, map[string][]string{"image.list": {"alpine"}}, nil)

	after, _ := d.completeImageNameOnlyAtPos0(testCmdWithCtx(), []string{"alpine"}, "")
	if len(after) != 0 {
		t.Errorf("expected silence at pos 1+, got %v", after)
	}
}
