package cmd

import (
	"bytes"
	"io"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"

	"github.com/spf13/cobra"
)

func TestParsePassthrough(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name string
		args []string
		want map[string][]string
	}{
		{
			name: "no passthrough",
			args: nil,
			want: nil,
		},
		{
			name: "flag with value",
			args: []string{"--repo", "/repo"},
			want: map[string][]string{
				"repo": {"/repo"},
			},
		},
		{
			name: "repeated and equals flags",
			args: []string{"--tag=daily", "--tag", "weekly"},
			want: map[string][]string{
				"tag": {"daily", "weekly"},
			},
		},
		{
			name: "boolean flags",
			args: []string{"-v", "--json"},
			want: map[string][]string{
				"v":    nil,
				"json": nil,
			},
		},
		{
			name: "positional arguments become _arguments",
			args: []string{"/src", "/dst"},
			want: map[string][]string{
				overrideArgumentsKey: {"/src", "/dst"},
			},
		},
		{
			name: "mixed flags and positional arguments",
			args: []string{"--repo", "/repo", "/src", "/dst"},
			want: map[string][]string{
				"repo":               {"/repo"},
				overrideArgumentsKey: {"/src", "/dst"},
			},
		},
		{
			name: "double-dash preserves literal arguments",
			args: []string{"--repo", "/repo", "--", "--literal", "path"},
			want: map[string][]string{
				"repo":               {"/repo"},
				overrideArgumentsKey: {"--literal", "path"},
			},
		},
		{
			name: "single dash is positional argument",
			args: []string{"-"},
			want: map[string][]string{
				overrideArgumentsKey: {"-"},
			},
		},
		{
			name: "flag without value before another flag is boolean",
			args: []string{"--host", "--json"},
			want: map[string][]string{
				"host": nil,
				"json": nil,
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			got := parsePassthrough(tt.args)
			if !reflect.DeepEqual(got, tt.want) {
				t.Fatalf("overrides mismatch: got %#v, want %#v", got, tt.want)
			}
		})
	}
}

func TestRunCommandAppliesRootFlagsAndPassthrough(t *testing.T) {
	configFile := setupCommandConfig(t)
	setRootFlagValuesForTest(t, "home@cloud", true, configFile)
	t.Setenv("KELD_CONFIG_FILE", "")
	t.Setenv("KELD_DRYRUN", "")

	backup := lookupSubcommand(t, "backup")
	out, err := captureStdout(t, func() error {
		return runCommand("backup", backup, []string{"--tag", "daily", "/src"}, nil, nil)
	})
	if err != nil {
		t.Fatalf("runCommand returned error: %v", err)
	}

	if got := os.Getenv("KELD_CONFIG_FILE"); got != configFile {
		t.Fatalf("KELD_CONFIG_FILE mismatch: got %q, want %q", got, configFile)
	}
	if got := os.Getenv("KELD_DRYRUN"); got != "1" {
		t.Fatalf("KELD_DRYRUN mismatch: got %q, want %q", got, "1")
	}

	for _, want := range []string{"\"backup\"", "\"--tag\" \"daily\"", "\"/src\""} {
		if !strings.Contains(out, want) {
			t.Fatalf("dry-run output missing %q:\n%s", want, out)
		}
	}
}

func TestRunCommandRejectsUnknownPreset(t *testing.T) {
	configFile := setupCommandConfig(t)
	setRootFlagValuesForTest(t, "missing", true, configFile)

	backup := lookupSubcommand(t, "backup")
	err := runCommand("backup", backup, nil, nil, nil)
	if err == nil {
		t.Fatal("expected unknown preset error")
	}
	if !strings.Contains(err.Error(), "unknown preset") {
		t.Fatalf("expected unknown preset error, got: %v", err)
	}
}

func TestSubcommandHelpShowsCobraHelp(t *testing.T) {
	backup := lookupSubcommand(t, "backup")
	buf := &bytes.Buffer{}
	backup.SetOut(buf)
	backup.SetErr(buf)
	t.Cleanup(func() {
		backup.SetOut(os.Stdout)
		backup.SetErr(os.Stderr)
	})

	err := backup.RunE(backup, []string{"--help"})
	if err != nil {
		t.Fatalf("backup --help returned error: %v", err)
	}

	helpText := buf.String()
	if !strings.Contains(helpText, "Usage:") {
		t.Fatalf("help output missing Usage section:\n%s", helpText)
	}
	if !strings.Contains(helpText, "--exclude") {
		t.Fatalf("help output missing backup flag listing:\n%s", helpText)
	}
	if !strings.Contains(helpText, "--repo") {
		t.Fatalf("help output missing inherited global flags:\n%s", helpText)
	}
}

func setupCommandConfig(t *testing.T) string {
	t.Helper()

	dir := t.TempDir()
	cfg := filepath.Join(dir, "config.toml")
	err := os.WriteFile(cfg, []byte(`
[global]
repo = "/repos/default"

["home@"]
tag = "home"

["@cloud"]
repo = "/repos/cloud"

[archive]
json = true
`), 0o600)
	if err != nil {
		t.Fatalf("writing fixture: %v", err)
	}
	t.Setenv("HOME", dir)

	return cfg
}

func setRootFlagValuesForTest(t *testing.T, preset string, showCommand bool, configFile string) {
	t.Helper()

	prevPreset, prevShowCommand, prevConfigFile := flagPreset, flagShowCmd, flagConfigFile
	flagPreset = preset
	flagShowCmd = showCommand
	flagConfigFile = configFile
	t.Cleanup(func() {
		flagPreset = prevPreset
		flagShowCmd = prevShowCommand
		flagConfigFile = prevConfigFile
	})
}

func lookupSubcommand(t *testing.T, name string) *cobra.Command {
	t.Helper()

	for _, cmd := range rootCmd.Commands() {
		if cmd.Name() == name {
			return cmd
		}
	}
	t.Fatalf("subcommand %q not found", name)
	return nil
}

func captureStdout(t *testing.T, run func() error) (string, error) {
	t.Helper()

	oldStdout := os.Stdout
	reader, writer, err := os.Pipe()
	if err != nil {
		t.Fatalf("creating stdout pipe: %v", err)
	}
	os.Stdout = writer
	t.Cleanup(func() {
		os.Stdout = oldStdout
	})

	runErr := run()
	_ = writer.Close()

	out, readErr := io.ReadAll(reader)
	if readErr != nil {
		t.Fatalf("reading stdout: %v", readErr)
	}
	_ = reader.Close()

	return string(out), runErr
}

func TestValidatePreset(t *testing.T) {
	// Set up a config fixture with known presets.
	dir := t.TempDir()
	cfg := filepath.Join(dir, "config.toml")
	err := os.WriteFile(cfg, []byte(`
[global]
verbose = true

["music@"]
tag = "music"

["@hetzner"]
repo = "sftp:hetzner"

["@b2"]
repo = "s3:b2"

[archive]
json = true
`), 0o600)
	if err != nil {
		t.Fatalf("writing fixture: %v", err)
	}
	t.Setenv("KELD_CONFIG_FILE", cfg)
	t.Setenv("HOME", dir)

	tests := []struct {
		name    string
		preset  string
		wantErr bool
	}{
		{name: "valid plain preset", preset: "archive", wantErr: false},
		{name: "valid composite preset", preset: "music@hetzner", wantErr: false},
		{name: "valid bare suffix", preset: "@b2", wantErr: false},
		{name: "unknown preset", preset: "nope", wantErr: true},
		{name: "unknown composite", preset: "photos@hetzner", wantErr: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validatePreset(tt.preset)
			if (err != nil) != tt.wantErr {
				t.Fatalf("validatePreset(%q): got err=%v, wantErr=%v", tt.preset, err, tt.wantErr)
			}
		})
	}
}

func TestValidatePresetNoConfig(t *testing.T) {
	// Point at an empty config so there are no presets at all.
	dir := t.TempDir()
	cfg := filepath.Join(dir, "config.toml")
	if err := os.WriteFile(cfg, []byte("[global]\n"), 0o600); err != nil {
		t.Fatalf("writing fixture: %v", err)
	}
	t.Setenv("KELD_CONFIG_FILE", cfg)
	t.Setenv("HOME", dir)

	err := validatePreset("anything")
	if err == nil {
		t.Fatal("expected error for unknown preset with empty config")
	}
	if !strings.Contains(err.Error(), "no presets defined") {
		t.Fatalf("expected 'no presets defined' message, got: %v", err)
	}
}
