package restic

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

	"git.secluded.site/keld/internal/config"
)

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

	tests := []struct {
		name string
		exe  string
		cfg  *config.ResolvedConfig
		want []string
	}{
		{
			name: "command flags and args",
			exe:  "restic",
			cfg: &config.ResolvedConfig{
				Command: "backup",
				Flags: []config.Flag{
					{Name: "--repo", Value: "/repo"},
					{Name: "--json"},
					{Name: "-o", Value: "s3.connections=5"},
				},
				Arguments: []string{"/home/alice", "/var/lib"},
			},
			want: []string{"restic", "backup", "--repo", "/repo", "--json", "-o", "s3.connections=5", "/home/alice", "/var/lib"},
		},
		{
			name: "no command still includes executable",
			exe:  "restic",
			cfg: &config.ResolvedConfig{
				Flags: []config.Flag{{Name: "--json"}},
			},
			want: []string{"restic", "--json"},
		},
	}

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

			got := buildArgv(tt.exe, tt.cfg)
			if !reflect.DeepEqual(got, tt.want) {
				t.Fatalf("buildArgv() mismatch: got %#v, want %#v", got, tt.want)
			}
		})
	}
}

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

	const key = "KELD_TEST_BUILD_ENV"
	const val = "set-by-test"

	env := buildEnv(map[string]string{key: val})

	if !containsEntry(env, key+"="+val) {
		t.Fatalf("buildEnv() missing %s=%s in %#v", key, val, env)
	}
}

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

	cfg := &config.ResolvedConfig{
		Command:      "backup",
		SectionsRead: []string{"global", "global.backup", "home", "home.backup"},
		Workdir:      "/tmp/work",
		Environ: map[string]string{
			"RESTIC_REPOSITORY": "/repos/home",
			"RESTIC_PASSWORD":   "secret",
		},
		Flags: []config.Flag{
			{Name: "--repo", Value: "/repos/home"},
			{Name: "--json"},
		},
		Arguments: []string{"/home/alice"},
	}

	output := DryRun(cfg)

	for _, fragment := range []string{
		"config sections: global → global.backup → home → home.backup",
		"workdir: /tmp/work",
		"environ:",
		"RESTIC_REPOSITORY=/repos/home",
		"RESTIC_PASSWORD=secret",
		"command: \"restic\" \"backup\" \"--repo\" \"/repos/home\" \"--json\" \"/home/alice\"",
	} {
		if !strings.Contains(output, fragment) {
			t.Fatalf("DryRun() missing fragment %q in output:\n%s", fragment, output)
		}
	}
}

func TestDryRunExecutableOverride(t *testing.T) {
	tmpDir := t.TempDir()
	homeDir := filepath.Join(tmpDir, "home")
	if err := os.MkdirAll(homeDir, 0o755); err != nil {
		t.Fatalf("creating temp HOME: %v", err)
	}

	t.Setenv("HOME", homeDir)
	t.Setenv("KELD_EXECUTABLE", "~/bin/restic-alt")

	cfg := &config.ResolvedConfig{Command: "snapshots"}
	output := DryRun(cfg)

	expectedPrefix := `command: "` + filepath.Join(homeDir, "bin", "restic-alt") + `" "snapshots"`
	if !strings.Contains(output, expectedPrefix) {
		t.Fatalf("DryRun() command mismatch: want fragment %q in output %q", expectedPrefix, output)
	}
}

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

	tests := []struct {
		name      string
		environ   map[string]string
		want      map[string]string
		wantError string
	}{
		{
			name: "command stdout becomes env var value",
			environ: map[string]string{
				"AWS_ACCESS_KEY_ID_COMMAND": "echo my-key-id",
			},
			want: map[string]string{
				"AWS_ACCESS_KEY_ID": "my-key-id",
			},
		},
		{
			name: "trailing newline stripped",
			environ: map[string]string{
				"MY_SECRET_COMMAND": "printf 'hello\\n\\n'",
			},
			want: map[string]string{
				"MY_SECRET": "hello",
			},
		},
		{
			name: "multi-line output preserves internal newlines",
			environ: map[string]string{
				"MY_CERT_COMMAND": "printf 'line1\\nline2\\nline3\\n'",
			},
			want: map[string]string{
				"MY_CERT": "line1\nline2\nline3",
			},
		},
		{
			name: "command takes precedence over literal value",
			environ: map[string]string{
				"MY_KEY":         "old-literal",
				"MY_KEY_COMMAND": "echo from-command",
			},
			want: map[string]string{
				"MY_KEY": "from-command",
			},
		},
		{
			name: "restic native RESTIC_PASSWORD_COMMAND passes through",
			environ: map[string]string{
				"RESTIC_PASSWORD_COMMAND": "op read 'op://Vault/Backup/password'",
			},
			want: map[string]string{
				"RESTIC_PASSWORD_COMMAND": "op read 'op://Vault/Backup/password'",
			},
		},
		{
			name: "restic native RESTIC_FROM_PASSWORD_COMMAND passes through",
			environ: map[string]string{
				"RESTIC_FROM_PASSWORD_COMMAND": "pass show backup/source",
			},
			want: map[string]string{
				"RESTIC_FROM_PASSWORD_COMMAND": "pass show backup/source",
			},
		},
		{
			name: "non-command keys left untouched",
			environ: map[string]string{
				"AWS_ACCESS_KEY_ID": "literal-key",
				"RCLONE_BWLIMIT":    "1M",
			},
			want: map[string]string{
				"AWS_ACCESS_KEY_ID": "literal-key",
				"RCLONE_BWLIMIT":    "1M",
			},
		},
		{
			name: "mixed commands and literals",
			environ: map[string]string{
				"AWS_ACCESS_KEY_ID_COMMAND":     "echo resolved-id",
				"AWS_SECRET_ACCESS_KEY_COMMAND": "echo resolved-secret",
				"RCLONE_BWLIMIT":                "1M",
				"RESTIC_PASSWORD_COMMAND":       "pass show backup",
			},
			want: map[string]string{
				"AWS_ACCESS_KEY_ID":       "resolved-id",
				"AWS_SECRET_ACCESS_KEY":   "resolved-secret",
				"RCLONE_BWLIMIT":          "1M",
				"RESTIC_PASSWORD_COMMAND": "pass show backup",
			},
		},
		{
			name: "command failure returns error",
			environ: map[string]string{
				"BAD_KEY_COMMAND": "false",
			},
			wantError: "environ BAD_KEY_COMMAND",
		},
		{
			name: "command stderr included in error",
			environ: map[string]string{
				"BAD_KEY_COMMAND": "echo 'oh no' >&2; exit 1",
			},
			wantError: "oh no",
		},
		{
			name:    "bare _COMMAND key with empty base is ignored",
			environ: map[string]string{"_COMMAND": "echo nope"},
			want:    map[string]string{"_COMMAND": "echo nope"},
		},
	}

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

			err := resolveEnvironCommands(tt.environ, "")

			if tt.wantError != "" {
				if err == nil {
					t.Fatalf("expected error containing %q, got nil", tt.wantError)
				}
				if !strings.Contains(err.Error(), tt.wantError) {
					t.Fatalf("error %q does not contain %q", err.Error(), tt.wantError)
				}
				return
			}

			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}

			if !reflect.DeepEqual(tt.environ, tt.want) {
				t.Fatalf("environ mismatch:\n  got:  %v\n  want: %v", tt.environ, tt.want)
			}
		})
	}
}

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

	cfg := &config.ResolvedConfig{
		Command: "backup",
		Environ: map[string]string{
			"AWS_ACCESS_KEY_ID_COMMAND": "echo test-key",
			"RESTIC_PASSWORD_COMMAND":   "pass show backup",
		},
		Arguments: []string{"/home/alice"},
	}

	output := DryRun(cfg)

	// DryRun must NOT resolve _COMMAND keys — that would leak secrets.
	if !strings.Contains(output, "AWS_ACCESS_KEY_ID_COMMAND=echo test-key") {
		t.Fatalf("DryRun() should preserve AWS_ACCESS_KEY_ID_COMMAND as-is:\n%s", output)
	}

	// Restic-native commands should also pass through unchanged.
	if !strings.Contains(output, "RESTIC_PASSWORD_COMMAND=pass show backup") {
		t.Fatalf("DryRun() should preserve RESTIC_PASSWORD_COMMAND:\n%s", output)
	}
}

func containsEntry(values []string, needle string) bool {
	for _, value := range values {
		if value == needle {
			return true
		}
	}
	return false
}
