package cli

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

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

	"github.com/spf13/cobra"
)

func TestHumanSize(t *testing.T) {
	cases := []struct {
		bytes int64
		want  string
	}{
		{-1, "-"},
		{0, "-"},
		{1, "1 B"},
		{1023, "1023 B"},
		{1024, "1.0 KiB"},
		{2048, "2.0 KiB"},
		{1024 * 1024, "1.0 MiB"},
		{5 * 1024 * 1024, "5.0 MiB"},
		{1024 * 1024 * 1024, "1.0 GiB"},
		{3 * 1024 * 1024 * 1024, "3.0 GiB"},
	}
	for _, tc := range cases {
		if got := humanSize(tc.bytes); got != tc.want {
			t.Errorf("humanSize(%d) = %q, want %q", tc.bytes, got, tc.want)
		}
	}
}

func TestDashIfEmpty(t *testing.T) {
	cases := map[string]string{
		"":         "-",
		"   ":      "-",
		"\t\n":     "-",
		"value":    "value",
		"  hello ": "  hello ",
	}
	for in, want := range cases {
		if got := dashIfEmpty(in); got != want {
			t.Errorf("dashIfEmpty(%q) = %q, want %q", in, got, want)
		}
	}
}

func TestExitCodeErrorError(t *testing.T) {
	e := ExitCodeError{Code: 42}
	got := e.Error()
	if !strings.Contains(got, "42") {
		t.Fatalf("error %q missing code", got)
	}

	var target ExitCodeError
	if !errors.As(error(e), &target) {
		t.Fatal("errors.As failed to match ExitCodeError")
	}
	if target.Code != 42 {
		t.Fatalf("target.Code = %d, want 42", target.Code)
	}
}

func TestShortID(t *testing.T) {
	cases := map[string]string{
		"":                     "",
		"abc":                  "abc",
		"0123456789ab":         "0123456789ab",
		"0123456789abcd":       "0123456789ab",
		"0123456789abcdefghij": "0123456789ab",
	}
	for in, want := range cases {
		if got := shortID(in); got != want {
			t.Errorf("shortID(%q) = %q, want %q", in, got, want)
		}
	}
}

func TestImageNameIndex(t *testing.T) {
	images := []model.Image{
		{ID: "id-a", Name: "alpha"},
		{ID: "id-b", Name: "beta"},
	}
	idx := imageNameIndex(images)
	if len(idx) != 2 {
		t.Fatalf("len = %d, want 2", len(idx))
	}
	if idx["id-a"] != "alpha" || idx["id-b"] != "beta" {
		t.Fatalf("unexpected index %v", idx)
	}

	empty := imageNameIndex(nil)
	if empty == nil || len(empty) != 0 {
		t.Fatalf("expected empty non-nil map, got %v", empty)
	}
}

func TestHelpNoArgs(t *testing.T) {
	called := false
	cmd := &cobra.Command{
		Use: "x",
		RunE: func(cmd *cobra.Command, args []string) error {
			called = true
			return nil
		},
	}
	cmd.SetOut(&bytes.Buffer{})
	cmd.SetErr(&bytes.Buffer{})

	if err := helpNoArgs(cmd, nil); err != nil {
		t.Fatalf("helpNoArgs(nil): %v", err)
	}
	if called {
		t.Fatal("helpNoArgs should not invoke Run")
	}

	if err := helpNoArgs(cmd, []string{"bogus"}); err == nil {
		t.Fatal("expected error for unexpected args")
	}
}

func TestArgsValidators(t *testing.T) {
	cmd := &cobra.Command{Use: "x"}

	exact := exactArgsUsage(2, "need exactly two")
	if err := exact(cmd, []string{"a", "b"}); err != nil {
		t.Fatalf("exact(2 args): %v", err)
	}
	if err := exact(cmd, []string{"a"}); err == nil {
		t.Fatal("expected error for 1 arg with exactArgsUsage(2)")
	}

	minArgs := minArgsUsage(1, "need at least one")
	if err := minArgs(cmd, []string{"a"}); err != nil {
		t.Fatalf("min(1 arg): %v", err)
	}
	if err := minArgs(cmd, nil); err == nil {
		t.Fatal("expected error for 0 args with minArgsUsage(1)")
	}

	maxArgs := maxArgsUsage(1, "at most one")
	if err := maxArgs(cmd, []string{"a"}); err != nil {
		t.Fatalf("max(1 arg): %v", err)
	}
	if err := maxArgs(cmd, []string{"a", "b"}); err == nil {
		t.Fatal("expected error for 2 args with maxArgsUsage(1)")
	}

	noArgs := noArgsUsage("none allowed")
	if err := noArgs(cmd, nil); err != nil {
		t.Fatalf("no args: %v", err)
	}
	if err := noArgs(cmd, []string{"a"}); err == nil {
		t.Fatal("expected error for args with noArgsUsage")
	}
}

func TestPrintKernelListTable(t *testing.T) {
	var buf bytes.Buffer
	entries := []api.KernelEntry{
		{Name: "generic-6.12", Distro: "debian", Arch: "x86_64", KernelVersion: "6.12", ImportedAt: "2026-01-01"},
		{Name: "bare"},
	}
	if err := printKernelListTable(&buf, entries); err != nil {
		t.Fatalf("printKernelListTable: %v", err)
	}
	got := buf.String()
	for _, want := range []string{"NAME", "DISTRO", "generic-6.12", "bare"} {
		if !strings.Contains(got, want) {
			t.Errorf("output missing %q:\n%s", want, got)
		}
	}
	// Empty fields render as "-".
	if !strings.Contains(got, "-") {
		t.Errorf("expected dash for empty fields, got:\n%s", got)
	}
}

func TestPrintKernelCatalogTable(t *testing.T) {
	var buf bytes.Buffer
	entries := []api.KernelCatalogEntry{
		{Name: "generic-6.12", Arch: "x86_64", KernelVersion: "6.12", SizeBytes: 2 * 1024 * 1024, Pulled: true},
		{Name: "new-kernel", SizeBytes: 0, Pulled: false},
	}
	if err := printKernelCatalogTable(&buf, entries); err != nil {
		t.Fatalf("printKernelCatalogTable: %v", err)
	}
	got := buf.String()
	for _, want := range []string{"generic-6.12", "pulled", "available", "new-kernel"} {
		if !strings.Contains(got, want) {
			t.Errorf("output missing %q:\n%s", want, got)
		}
	}
	if !strings.Contains(got, "2.0 MiB") {
		t.Errorf("expected humanSize(2 MiB), got:\n%s", got)
	}
}

func TestPrintJSON(t *testing.T) {
	var buf bytes.Buffer
	if err := printJSON(&buf, map[string]int{"a": 1, "b": 2}); err != nil {
		t.Fatalf("printJSON: %v", err)
	}
	got := buf.String()
	if !strings.Contains(got, `"a": 1`) || !strings.Contains(got, `"b": 2`) {
		t.Errorf("unexpected JSON output:\n%s", got)
	}
	if !strings.HasSuffix(got, "\n") {
		t.Error("printJSON should terminate with newline")
	}
}

func TestPrintJSONUnmarshalableValue(t *testing.T) {
	var buf bytes.Buffer
	// Channels are not JSON-marshalable.
	err := printJSON(&buf, make(chan int))
	if err == nil {
		t.Fatal("expected error for unmarshalable value")
	}
}

func TestPrintVMSummary(t *testing.T) {
	var buf bytes.Buffer
	vm := model.VMRecord{
		ID:    "0123456789abcdef",
		Name:  "demo",
		State: model.VMStateRunning,
	}
	vm.Runtime.GuestIP = "172.16.0.5"
	vm.Runtime.DNSName = "demo.vm"
	vm.Spec.WorkDiskSizeBytes = 0
	if err := printVMSummary(&buf, vm); err != nil {
		t.Fatalf("printVMSummary: %v", err)
	}
	got := buf.String()
	for _, want := range []string{"0123456789ab", "demo", "172.16.0.5", "demo.vm"} {
		if !strings.Contains(got, want) {
			t.Errorf("summary missing %q:\n%s", want, got)
		}
	}
}

func TestPrintImageSummary(t *testing.T) {
	var buf bytes.Buffer
	img := model.Image{ID: "img-id", Name: "debian-bookworm", Managed: true, RootfsPath: "/var/rootfs.ext4"}
	if err := printImageSummary(&buf, img); err != nil {
		t.Fatalf("printImageSummary: %v", err)
	}
	got := buf.String()
	for _, want := range []string{"debian-bookworm", "true", "/var/rootfs.ext4"} {
		if !strings.Contains(got, want) {
			t.Errorf("summary missing %q:\n%s", want, got)
		}
	}
}

func TestVMImageLabel(t *testing.T) {
	names := map[string]string{"img-1": "debian"}
	if got := vmImageLabel("img-1", names); got != "debian" {
		t.Errorf("got %q, want debian", got)
	}
	if got := vmImageLabel("img-2", names); got != "img-2" {
		t.Errorf("fallback: got %q, want img-2", got)
	}
}

// failWriter lets us exercise io-error branches of the printers.
type failWriter struct{}

func (failWriter) Write([]byte) (int, error) { return 0, fmt.Errorf("boom") }

func TestPrintersPropagateWriteErrors(t *testing.T) {
	kernels := []api.KernelEntry{{Name: "k"}}
	if err := printKernelListTable(failWriter{}, kernels); err == nil {
		t.Error("expected write error from printKernelListTable")
	}
	catalog := []api.KernelCatalogEntry{{Name: "k"}}
	if err := printKernelCatalogTable(failWriter{}, catalog); err == nil {
		t.Error("expected write error from printKernelCatalogTable")
	}
}
