package cmd

import (
	"bytes"
	"encoding/json"
	"io"
	"path/filepath"
	"strings"
	"testing"

	"github.com/tidbcloud/clinic-cli/internal/config"
)

func TestAuthSetKeyPersistsAPIKeyForTargetEnv(t *testing.T) {
	setTestConfigHome(t)

	cfg := mustLoadConfig(t)
	promptOut := &bytes.Buffer{}
	ctx := &RuntimeContext{
		SelectedEnv:    "prod",
		Config:         cfg,
		DiagnosticsOut: promptOut,
	}

	var stdout bytes.Buffer
	hookCalled := false
	if err := runAuthSetKeyWithHooks(ctx, []string{"--env", "staging"}, &stdout, func(promptWriter io.Writer) (string, error) {
		hookCalled = true
		if promptWriter != promptOut {
			t.Fatal("prompt writer did not use runtime diagnostics output")
		}
		return "staging-secret-key", nil
	}); err != nil {
		t.Fatalf("runAuthSetKeyWithHooks(set-key) error = %v", err)
	}
	if !hookCalled {
		t.Fatal("interactive api-key reader was not called")
	}

	cfg = mustLoadConfig(t)
	if got := cfg.Environments["staging"].APIKey; got != "staging-secret-key" {
		t.Fatalf("staging API key = %q, want %q", got, "staging-secret-key")
	}
}

func TestAuthSetKeyRejectsLegacyAPIKeyFlag(t *testing.T) {
	setTestConfigHome(t)

	err := RunWithIO([]string{"auth", "set-key", "--env", "staging", "--api-key", "staging-secret-key"}, &bytes.Buffer{})
	if err == nil {
		t.Fatal("RunWithIO(set-key) error = nil, want unknown-flag error")
	}
	if got := err.Error(); !strings.Contains(got, "api-key") || !(strings.Contains(got, "not defined") || strings.Contains(got, "unknown flag")) {
		t.Fatalf("RunWithIO(set-key) error = %q, want unknown --api-key flag message", got)
	}
}

func TestAuthLocalEnvFlagStillTargetsEnvironment(t *testing.T) {
	setTestConfigHome(t)

	cfg := mustLoadConfig(t)
	prod := cfg.Environments["prod"]
	prod.APIKey = "prod-secret-key"
	cfg.Environments["prod"] = prod
	staging := cfg.Environments["staging"]
	staging.APIKey = "staging-secret-key"
	cfg.Environments["staging"] = staging
	if err := cfg.Save(); err != nil {
		t.Fatalf("cfg.Save() error = %v", err)
	}

	var runtimeURL string
	runner := rootRunner{
		loadConfig: config.LoadOrInit,
		newClient: func(env config.Environment) (Client, error) {
			runtimeURL = env.URL
			return &recordingClinicClient{}, nil
		},
		handlers: map[string]commandHandler{},
	}

	var stdout bytes.Buffer
	if err := runner.runWithStderr([]string{"auth", "--env", "prod", "unset-key", "--env", "staging"}, &stdout, io.Discard); err != nil {
		t.Fatalf("RunWithIO(auth unset-key local env) error = %v", err)
	}

	if got, want := runtimeURL, cfg.Environments["prod"].URL; got != want {
		t.Fatalf("runtime env URL = %q, want root --env prod URL %q", got, want)
	}

	loaded := mustLoadConfig(t)
	if got := loaded.Environments["staging"].APIKey; got != "" {
		t.Fatalf("staging API key = %q, want cleared by auth local --env", got)
	}
	if got := loaded.Environments["prod"].APIKey; got != "prod-secret-key" {
		t.Fatalf("prod API key = %q, want unchanged", got)
	}
}

func TestAuthUnsetKeyClearsPersistedAPIKey(t *testing.T) {
	setTestConfigHome(t)

	cfg := mustLoadConfig(t)
	env := cfg.Environments["prod"]
	env.APIKey = "prod-secret-key"
	cfg.Environments["prod"] = env
	if err := cfg.Save(); err != nil {
		t.Fatalf("cfg.Save() error = %v", err)
	}

	if err := RunWithIO([]string{"auth", "unset-key", "--env", "prod"}, &bytes.Buffer{}); err != nil {
		t.Fatalf("RunWithIO(unset-key) error = %v", err)
	}

	cfg = mustLoadConfig(t)
	if got := cfg.Environments["prod"].APIKey; got != "" {
		t.Fatalf("prod API key = %q, want empty string", got)
	}
}

func TestAuthSetURLRejectsInvalidSchemeAndLeavesConfigUnchanged(t *testing.T) {
	setTestConfigHome(t)

	cfg := mustLoadConfig(t)
	wantURL := cfg.Environments["dev"].URL

	err := RunWithIO([]string{"auth", "set-url", "--env", "dev", "--url", "ftp://dev-clinic.pingcap.com"}, &bytes.Buffer{})
	if err == nil {
		t.Fatal("RunWithIO(set-url) error = nil, want validation error")
	}
	if got := err.Error(); !strings.Contains(got, "http") || !strings.Contains(got, "https") {
		t.Fatalf("RunWithIO(set-url) error = %q, want http/https validation message", got)
	}

	cfg = mustLoadConfig(t)
	if got := cfg.Environments["dev"].URL; got != wantURL {
		t.Fatalf("dev URL = %q, want %q", got, wantURL)
	}
}

func TestAuthShowMasksAPIKeyInOutput(t *testing.T) {
	setTestConfigHome(t)

	cfg := mustLoadConfig(t)
	env := cfg.Environments["prod"]
	env.APIKey = "prod-secret-key"
	cfg.Environments["prod"] = env
	if err := cfg.Save(); err != nil {
		t.Fatalf("cfg.Save() error = %v", err)
	}

	var stdout bytes.Buffer
	if err := RunWithIO([]string{"auth", "show", "--env", "prod"}, &stdout); err != nil {
		t.Fatalf("RunWithIO(show) error = %v", err)
	}

	output := stdout.String()
	if strings.Contains(output, "prod-secret-key") {
		t.Fatalf("auth show output leaked full API key: %s", output)
	}

	var got struct {
		Env          string `json:"env"`
		URL          string `json:"url"`
		HasAPIKey    bool   `json:"has_api_key"`
		APIKeyMasked string `json:"api_key_masked"`
	}
	if err := json.Unmarshal(stdout.Bytes(), &got); err != nil {
		t.Fatalf("json.Unmarshal(output) error = %v\noutput=%s", err, output)
	}
	if got.Env != "prod" {
		t.Fatalf("output env = %q, want %q", got.Env, "prod")
	}
	if !got.HasAPIKey {
		t.Fatal("output has_api_key = false, want true")
	}
	if got.APIKeyMasked != "********" {
		t.Fatalf("output api_key_masked = %q, want %q", got.APIKeyMasked, "********")
	}
}

func TestEnvUsePersistsActiveEnvAndEnvCommandsReflectIt(t *testing.T) {
	setTestConfigHome(t)

	var stdout bytes.Buffer
	if err := RunWithIO([]string{"env", "use", "staging"}, &stdout); err != nil {
		t.Fatalf("RunWithIO(env use) error = %v", err)
	}

	cfg := mustLoadConfig(t)
	if got := cfg.ActiveEnv; got != "staging" {
		t.Fatalf("ActiveEnv = %q, want %q", got, "staging")
	}

	stdout.Reset()
	if err := RunWithIO([]string{"env", "current"}, &stdout); err != nil {
		t.Fatalf("RunWithIO(env current) error = %v", err)
	}

	var current struct {
		ActiveEnv string `json:"active_env"`
	}
	if err := json.Unmarshal(stdout.Bytes(), &current); err != nil {
		t.Fatalf("json.Unmarshal(env current) error = %v\noutput=%s", err, stdout.String())
	}
	if current.ActiveEnv != "staging" {
		t.Fatalf("env current active_env = %q, want %q", current.ActiveEnv, "staging")
	}

	stdout.Reset()
	if err := RunWithIO([]string{"env", "list"}, &stdout); err != nil {
		t.Fatalf("RunWithIO(env list) error = %v", err)
	}

	var listed struct {
		ActiveEnv    string `json:"active_env"`
		Environments []struct {
			Name   string `json:"name"`
			Active bool   `json:"active"`
		} `json:"environments"`
	}
	if err := json.Unmarshal(stdout.Bytes(), &listed); err != nil {
		t.Fatalf("json.Unmarshal(env list) error = %v\noutput=%s", err, stdout.String())
	}
	if listed.ActiveEnv != "staging" {
		t.Fatalf("env list active_env = %q, want %q", listed.ActiveEnv, "staging")
	}

	want := []string{"prod", "staging", "dev"}
	if len(listed.Environments) != len(want) {
		t.Fatalf("len(env list) = %d, want %d", len(listed.Environments), len(want))
	}
	for i, name := range want {
		if listed.Environments[i].Name != name {
			t.Fatalf("env list[%d].name = %q, want %q", i, listed.Environments[i].Name, name)
		}
		if listed.Environments[i].Active != (name == "staging") {
			t.Fatalf("env list[%d].active = %v, want %v", i, listed.Environments[i].Active, name == "staging")
		}
	}
}

func TestSwitchWithoutProfileMatchesEnvUseBehavior(t *testing.T) {
	setTestConfigHome(t)

	cfg := mustLoadConfig(t)
	cfg.ActiveEnv = "prod"
	cfg.EnvProfiles["staging"] = config.EnvProfileContainer{
		ActiveProfile: "blue",
		Profiles: map[string]config.ProfileRecord{
			"blue": {
				ClusterID: "staging-blue",
			},
		},
	}
	if err := cfg.Save(); err != nil {
		t.Fatalf("cfg.Save() error = %v", err)
	}

	var stdout bytes.Buffer
	if err := RunWithIO([]string{"switch", "staging"}, &stdout); err != nil {
		t.Fatalf("RunWithIO(switch env only) error = %v", err)
	}

	loaded := mustLoadConfig(t)
	if got := loaded.ActiveEnv; got != "staging" {
		t.Fatalf("ActiveEnv = %q, want %q", got, "staging")
	}
	if got := loaded.EnvProfiles["staging"].ActiveProfile; got != "blue" {
		t.Fatalf("EnvProfiles[staging].ActiveProfile = %q, want %q", got, "blue")
	}

	var got map[string]any
	if err := json.Unmarshal(stdout.Bytes(), &got); err != nil {
		t.Fatalf("json.Unmarshal(switch env only) error = %v\noutput=%s", err, stdout.String())
	}
	if got["active_env"] != "staging" {
		t.Fatalf("output active_env = %#v, want %q", got["active_env"], "staging")
	}
	if _, exists := got["active_profile"]; exists {
		t.Fatalf("output included active_profile = %#v, want omitted key when profile is not provided", got["active_profile"])
	}
}

func TestSwitchWithProfileSetsActiveEnvAndTargetEnvProfile(t *testing.T) {
	setTestConfigHome(t)

	cfg := mustLoadConfig(t)
	cfg.ActiveEnv = "prod"
	cfg.EnvProfiles["prod"] = config.EnvProfileContainer{
		ActiveProfile: "red",
		Profiles: map[string]config.ProfileRecord{
			"red": {
				ClusterID: "prod-red",
			},
		},
	}
	cfg.EnvProfiles["staging"] = config.EnvProfileContainer{
		ActiveProfile: "green",
		Profiles: map[string]config.ProfileRecord{
			"green": {
				ClusterID: "staging-green",
			},
			"blue": {
				ClusterID: "staging-blue",
			},
		},
	}
	if err := cfg.Save(); err != nil {
		t.Fatalf("cfg.Save() error = %v", err)
	}

	var stdout bytes.Buffer
	if err := RunWithIO([]string{"switch", "staging", "blue"}, &stdout); err != nil {
		t.Fatalf("RunWithIO(switch env+profile) error = %v", err)
	}

	loaded := mustLoadConfig(t)
	if got := loaded.ActiveEnv; got != "staging" {
		t.Fatalf("ActiveEnv = %q, want %q", got, "staging")
	}
	if got := loaded.EnvProfiles["staging"].ActiveProfile; got != "blue" {
		t.Fatalf("EnvProfiles[staging].ActiveProfile = %q, want %q", got, "blue")
	}
	if got := loaded.EnvProfiles["prod"].ActiveProfile; got != "red" {
		t.Fatalf("EnvProfiles[prod].ActiveProfile = %q, want %q", got, "red")
	}

	var got struct {
		ActiveEnv     string `json:"active_env"`
		ActiveProfile string `json:"active_profile"`
	}
	if err := json.Unmarshal(stdout.Bytes(), &got); err != nil {
		t.Fatalf("json.Unmarshal(switch env+profile) error = %v\noutput=%s", err, stdout.String())
	}
	if got.ActiveEnv != "staging" {
		t.Fatalf("output active_env = %q, want %q", got.ActiveEnv, "staging")
	}
	if got.ActiveProfile != "blue" {
		t.Fatalf("output active_profile = %q, want %q", got.ActiveProfile, "blue")
	}
}

func TestSwitchWithUnknownProfileDoesNotPersistEnvChange(t *testing.T) {
	setTestConfigHome(t)

	cfg := mustLoadConfig(t)
	cfg.ActiveEnv = "prod"
	cfg.EnvProfiles["staging"] = config.EnvProfileContainer{
		ActiveProfile: "blue",
		Profiles: map[string]config.ProfileRecord{
			"blue": {
				ClusterID: "staging-blue",
			},
		},
	}
	if err := cfg.Save(); err != nil {
		t.Fatalf("cfg.Save() error = %v", err)
	}

	err := RunWithIO([]string{"switch", "staging", "missing"}, &bytes.Buffer{})
	if err == nil {
		t.Fatal("RunWithIO(switch unknown profile) error = nil, want validation error")
	}
	if got, want := err.Error(), "unknown profile: missing in env staging"; got != want {
		t.Fatalf("RunWithIO(switch unknown profile) error = %q, want %q", got, want)
	}

	loaded := mustLoadConfig(t)
	if got := loaded.ActiveEnv; got != "prod" {
		t.Fatalf("ActiveEnv = %q, want %q when switch fails", got, "prod")
	}
	if got := loaded.EnvProfiles["staging"].ActiveProfile; got != "blue" {
		t.Fatalf("EnvProfiles[staging].ActiveProfile = %q, want %q when switch fails", got, "blue")
	}
}

func setTestConfigHome(t *testing.T) {
	t.Helper()

	xdgHome := t.TempDir()
	t.Setenv("XDG_CONFIG_HOME", xdgHome)
	t.Setenv("HOME", filepath.Join(t.TempDir(), "home"))
}

func mustLoadConfig(t *testing.T) *config.Config {
	t.Helper()

	cfg, err := config.LoadOrInit()
	if err != nil {
		t.Fatalf("config.LoadOrInit() error = %v", err)
	}
	return cfg
}
