package cmd

import (
	"errors"
	"io"
	"strings"
	"testing"

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

func TestRootParsesLeadingEnvAndProfileBeforeSubcommand(t *testing.T) {
	cfg := testRootConfig("prod")
	cfg.EnvProfiles["staging"] = config.EnvProfileContainer{
		ActiveProfile: "blue",
		Profiles: map[string]config.ProfileRecord{
			"blue": {
				ClusterID: "cluster-staging-blue",
			},
		},
	}

	var gotCtx *RuntimeContext
	var gotArgs []string

	runner := rootRunner{
		loadConfig: func() (*config.Config, error) {
			return cfg, nil
		},
		handlers: map[string]commandHandler{
			"legacy": func(ctx *RuntimeContext, args []string, _ io.Writer) error {
				gotCtx = ctx
				gotArgs = append([]string(nil), args...)
				return nil
			},
		},
	}

	if err := runner.run([]string{"--profile", "blue", "--env", "staging", "legacy", "query"}, io.Discard); err != nil {
		t.Fatalf("run() error = %v", err)
	}

	if gotCtx == nil {
		t.Fatal("handler did not receive runtime context")
	}
	if gotCtx.SelectedEnv != "staging" {
		t.Fatalf("SelectedEnv = %q, want %q", gotCtx.SelectedEnv, "staging")
	}
	if gotCtx.SelectedProfileName != "blue" {
		t.Fatalf("SelectedProfileName = %q, want %q", gotCtx.SelectedProfileName, "blue")
	}
	if gotCtx.SelectedProfile == nil {
		t.Fatal("SelectedProfile = nil, want profile record")
	}
	if gotCtx.SelectedProfile.ClusterID != "cluster-staging-blue" {
		t.Fatalf("SelectedProfile.ClusterID = %q, want %q", gotCtx.SelectedProfile.ClusterID, "cluster-staging-blue")
	}
	if gotCtx.Config != cfg {
		t.Fatal("runtime context did not include loaded config")
	}
	if len(gotArgs) != 1 || gotArgs[0] != "query" {
		t.Fatalf("handler args = %#v, want []string{\"query\"}", gotArgs)
	}
}

func TestRootPersistentFlagsWorkBeforeCommand(t *testing.T) {
	cfg := testRootConfig("prod")
	cfg.EnvProfiles["staging"] = config.EnvProfileContainer{
		ActiveProfile: "blue",
		Profiles: map[string]config.ProfileRecord{
			"blue": {
				ClusterID: "cluster-staging-blue",
			},
		},
	}

	var gotCtx *RuntimeContext
	var gotArgs []string

	runner := rootRunner{
		loadConfig: func() (*config.Config, error) {
			return cfg, nil
		},
		handlers: map[string]commandHandler{
			"legacy": func(ctx *RuntimeContext, args []string, _ io.Writer) error {
				gotCtx = ctx
				gotArgs = append([]string(nil), args...)
				return nil
			},
		},
	}

	if err := runner.run([]string{"--profile", "blue", "--env", "staging", "legacy", "query"}, io.Discard); err != nil {
		t.Fatalf("run() error = %v", err)
	}

	assertRuntimeContextSelection(t, gotCtx, cfg, "staging", "blue", "cluster-staging-blue")
	if len(gotArgs) != 1 || gotArgs[0] != "query" {
		t.Fatalf("handler args = %#v, want []string{\"query\"}", gotArgs)
	}
}

func TestRootPersistentFlagsWorkAfterCommandFamily(t *testing.T) {
	cfg := testRootConfig("prod")
	cfg.EnvProfiles["staging"] = config.EnvProfileContainer{
		ActiveProfile: "blue",
		Profiles: map[string]config.ProfileRecord{
			"blue": {
				ClusterID: "cluster-staging-blue",
			},
		},
	}

	var gotCtx *RuntimeContext
	var gotArgs []string

	runner := rootRunner{
		loadConfig: func() (*config.Config, error) {
			return cfg, nil
		},
		handlers: map[string]commandHandler{
			"legacy": func(ctx *RuntimeContext, args []string, _ io.Writer) error {
				gotCtx = ctx
				gotArgs = append([]string(nil), args...)
				return nil
			},
		},
	}

	if err := runner.run([]string{"legacy", "--profile", "blue", "--env", "staging", "query"}, io.Discard); err != nil {
		t.Fatalf("run() error = %v", err)
	}

	assertRuntimeContextSelection(t, gotCtx, cfg, "staging", "blue", "cluster-staging-blue")
	if len(gotArgs) != 1 || gotArgs[0] != "query" {
		t.Fatalf("handler args = %#v, want []string{\"query\"}", gotArgs)
	}
}

func TestRootFallsBackToActiveEnvWithoutActiveProfile(t *testing.T) {
	cfg := testRootConfig("staging")

	var gotCtx *RuntimeContext

	runner := rootRunner{
		loadConfig: func() (*config.Config, error) {
			return cfg, nil
		},
		handlers: map[string]commandHandler{
			"legacy": func(ctx *RuntimeContext, _ []string, _ io.Writer) error {
				gotCtx = ctx
				return nil
			},
		},
	}

	if err := runner.run([]string{"legacy"}, io.Discard); err != nil {
		t.Fatalf("run() error = %v", err)
	}

	if gotCtx == nil {
		t.Fatal("handler did not receive runtime context")
	}
	if gotCtx.SelectedEnv != "staging" {
		t.Fatalf("SelectedEnv = %q, want %q", gotCtx.SelectedEnv, "staging")
	}
	if gotCtx.SelectedProfileName != "" {
		t.Fatalf("SelectedProfileName = %q, want empty", gotCtx.SelectedProfileName)
	}
	if gotCtx.SelectedProfile != nil {
		t.Fatalf("SelectedProfile = %+v, want nil", gotCtx.SelectedProfile)
	}
}

func TestRootFallsBackToActiveProfile(t *testing.T) {
	cfg := testRootConfig("staging")
	cfg.EnvProfiles["staging"] = config.EnvProfileContainer{
		ActiveProfile: "blue",
		Profiles: map[string]config.ProfileRecord{
			"blue": {
				ClusterID: "cluster-staging-blue",
			},
		},
	}

	var gotCtx *RuntimeContext

	runner := rootRunner{
		loadConfig: func() (*config.Config, error) {
			return cfg, nil
		},
		handlers: map[string]commandHandler{
			"legacy": func(ctx *RuntimeContext, _ []string, _ io.Writer) error {
				gotCtx = ctx
				return nil
			},
		},
	}

	if err := runner.run([]string{"legacy"}, io.Discard); err != nil {
		t.Fatalf("run() error = %v", err)
	}

	if gotCtx == nil {
		t.Fatal("handler did not receive runtime context")
	}
	if gotCtx.SelectedEnv != "staging" {
		t.Fatalf("SelectedEnv = %q, want %q", gotCtx.SelectedEnv, "staging")
	}
	if gotCtx.SelectedProfileName != "blue" {
		t.Fatalf("SelectedProfileName = %q, want %q", gotCtx.SelectedProfileName, "blue")
	}
	if gotCtx.SelectedProfile == nil {
		t.Fatal("SelectedProfile = nil, want profile record")
	}
	if gotCtx.SelectedProfile.ClusterID != "cluster-staging-blue" {
		t.Fatalf("SelectedProfile.ClusterID = %q, want %q", gotCtx.SelectedProfile.ClusterID, "cluster-staging-blue")
	}
}

func TestRootRejectsUnknownEnv(t *testing.T) {
	cfg := testRootConfig("prod")

	runner := rootRunner{
		loadConfig: func() (*config.Config, error) {
			return cfg, nil
		},
		handlers: map[string]commandHandler{
			"legacy": func(*RuntimeContext, []string, io.Writer) error {
				return errors.New("handler should not be called")
			},
		},
	}

	err := runner.run([]string{"--env", "qa", "legacy"}, io.Discard)
	if err == nil {
		t.Fatal("run() error = nil, want invalid environment error")
	}
	if got := err.Error(); got != "unknown environment: qa" {
		t.Fatalf("run() error = %q, want %q", got, "unknown environment: qa")
	}
}

func TestRootRejectsUnknownProfileInSelectedEnv(t *testing.T) {
	cfg := testRootConfig("staging")

	runner := rootRunner{
		loadConfig: func() (*config.Config, error) {
			return cfg, nil
		},
		handlers: map[string]commandHandler{
			"legacy": func(*RuntimeContext, []string, io.Writer) error {
				return errors.New("handler should not be called")
			},
		},
	}

	err := runner.run([]string{"--env", "staging", "--profile", "missing", "legacy"}, io.Discard)
	if err == nil {
		t.Fatal("run() error = nil, want invalid profile error")
	}
	if got := err.Error(); got != "unknown profile: missing in env staging" {
		t.Fatalf("run() error = %q, want %q", got, "unknown profile: missing in env staging")
	}
}

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

	runner := rootRunner{
		loadConfig: func() (*config.Config, error) {
			return nil, errors.New("config should not load for version command")
		},
		handlers: map[string]commandHandler{
			"auth": func(*RuntimeContext, []string, io.Writer) error {
				return errors.New("handler should not be called")
			},
		},
	}

	var stdout strings.Builder
	if err := runner.run([]string{"version", "--env", "staging"}, &stdout); err != nil {
		t.Fatalf("run(version) error = %v", err)
	}

	if got, want := stdout.String(), "dev\n"; got != want {
		t.Fatalf("version output = %q, want %q", got, want)
	}
}

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

	var stdout strings.Builder
	err := RunWithIO([]string{"env", "use", "staging", "extra"}, &stdout)
	if err == nil {
		t.Fatal("RunWithIO(env use extra) error = nil, want Cobra arity error")
	}
	if got := err.Error(); !strings.Contains(got, "accepts 1 arg(s), received 2") {
		t.Fatalf("RunWithIO(env use extra) error = %q, want Cobra exact-args error", got)
	}
}

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

	testCases := []struct {
		name string
		args []string
		want string
	}{
		{
			name: "missing env",
			args: []string{"switch"},
			want: "accepts between 1 and 2 arg(s), received 0",
		},
		{
			name: "too many args",
			args: []string{"switch", "staging", "blue", "extra"},
			want: "accepts between 1 and 2 arg(s), received 3",
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			var stdout strings.Builder
			err := RunWithIO(tc.args, &stdout)
			if err == nil {
				t.Fatalf("RunWithIO(%v) error = nil, want Cobra arity error", tc.args)
			}
			if got := err.Error(); !strings.Contains(got, tc.want) {
				t.Fatalf("RunWithIO(%v) error = %q, want to contain %q", tc.args, got, tc.want)
			}
		})
	}
}

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

	var stdout strings.Builder
	if err := RunWithIO([]string{"env", "use", "staging", "--env", "prod"}, &stdout); err != nil {
		t.Fatalf("RunWithIO(env use with trailing global flag) error = %v", err)
	}
	cfg := mustLoadConfig(t)
	if got := cfg.ActiveEnv; got != "staging" {
		t.Fatalf("ActiveEnv after env use = %q, want %q", got, "staging")
	}

	stdout.Reset()
	if err := RunWithIO([]string{"switch", "dev", "--env", "prod"}, &stdout); err != nil {
		t.Fatalf("RunWithIO(switch with trailing global flag) error = %v", err)
	}
	cfg = mustLoadConfig(t)
	if got := cfg.ActiveEnv; got != "dev" {
		t.Fatalf("ActiveEnv after switch = %q, want %q", got, "dev")
	}

	stdout.Reset()
	if err := RunWithIO([]string{"completion", "bash", "--env", "staging"}, &stdout); err != nil {
		t.Fatalf("RunWithIO(completion with trailing global flag) error = %v", err)
	}
	if !strings.Contains(stdout.String(), "__start_clinic") {
		t.Fatalf("completion output missing __start_clinic helper")
	}
}

func TestRootPersistentFlagsWorkAfterDataQueryArgs(t *testing.T) {
	cfg := testRootConfig("prod")
	cfg.EnvProfiles["staging"] = config.EnvProfileContainer{
		ActiveProfile: "blue",
		Profiles: map[string]config.ProfileRecord{
			"blue": {
				ClusterID: "cluster-staging-blue",
			},
		},
	}

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"rows":[{"v":1}]}`)}}}
	var gotClientURL string

	runner := rootRunner{
		loadConfig: func() (*config.Config, error) {
			return cfg, nil
		},
		newClient: func(env config.Environment) (Client, error) {
			gotClientURL = env.URL
			return client, nil
		},
		handlers: map[string]commandHandler{
			"metrics": func(*RuntimeContext, []string, io.Writer) error {
				return nil
			},
		},
	}

	var stdout strings.Builder
	if err := runner.run([]string{"data", "query", "--sql", "select 1", "--env", "staging"}, &stdout); err != nil {
		t.Fatalf("run(data query with trailing --env) error = %v", err)
	}

	if got, want := gotClientURL, cfg.Environments["staging"].URL; got != want {
		t.Fatalf("newClient env URL = %q, want %q", got, want)
	}
	if got, want := client.request.Path, "/data-proxy/query"; got != want {
		t.Fatalf("request path = %q, want %q", got, want)
	}
	payload, ok := client.request.Body.(map[string]any)
	if !ok {
		t.Fatalf("request body type = %T, want map[string]any", client.request.Body)
	}
	if got, want := payload["clusterId"], "cluster-staging-blue"; got != want {
		t.Fatalf("request body clusterId = %#v, want %#v", got, want)
	}
}

func TestRootPersistentFlagsWorkAfterRawRequestArgs(t *testing.T) {
	cfg := testRootConfig("prod")
	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"ok":true}`)}}}
	var gotClientURL string

	runner := rootRunner{
		loadConfig: func() (*config.Config, error) {
			return cfg, nil
		},
		newClient: func(env config.Environment) (Client, error) {
			gotClientURL = env.URL
			return client, nil
		},
		handlers: map[string]commandHandler{
			"metrics": func(*RuntimeContext, []string, io.Writer) error {
				return nil
			},
		},
	}

	var stdout strings.Builder
	if err := runner.run([]string{"raw", "request", "--method", "GET", "--path", "/clinic/api/v1/dashboard/clusters", "--env", "staging"}, &stdout); err != nil {
		t.Fatalf("run(raw request with trailing --env) error = %v", err)
	}

	if got, want := gotClientURL, cfg.Environments["staging"].URL; got != want {
		t.Fatalf("newClient env URL = %q, want %q", got, want)
	}
	if got, want := client.request.Path, "/clinic/api/v1/dashboard/clusters"; got != want {
		t.Fatalf("request path = %q, want %q", got, want)
	}
}

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

	handlers := defaultRootRunner().handlers
	if _, ok := handlers["diagnose"]; !ok {
		t.Fatal("defaultRootRunner().handlers missing diagnose")
	}
}

func assertRuntimeContextSelection(t *testing.T, gotCtx *RuntimeContext, wantConfig *config.Config, wantEnv, wantProfile, wantClusterID string) {
	t.Helper()

	if gotCtx == nil {
		t.Fatal("handler did not receive runtime context")
	}
	if gotCtx.SelectedEnv != wantEnv {
		t.Fatalf("SelectedEnv = %q, want %q", gotCtx.SelectedEnv, wantEnv)
	}
	if gotCtx.SelectedProfileName != wantProfile {
		t.Fatalf("SelectedProfileName = %q, want %q", gotCtx.SelectedProfileName, wantProfile)
	}
	if gotCtx.SelectedProfile == nil {
		t.Fatal("SelectedProfile = nil, want profile record")
	}
	if gotCtx.SelectedProfile.ClusterID != wantClusterID {
		t.Fatalf("SelectedProfile.ClusterID = %q, want %q", gotCtx.SelectedProfile.ClusterID, wantClusterID)
	}
	if gotCtx.Config != wantConfig {
		t.Fatal("runtime context did not include loaded config")
	}
}

func testRootConfig(activeEnv string) *config.Config {
	return &config.Config{
		ActiveEnv: activeEnv,
		Environments: map[string]config.Environment{
			"prod": {
				URL: "https://clinic.pingcap.com",
			},
			"staging": {
				URL: "https://staging-clinic.pingcap.com",
			},
			"dev": {
				URL: "https://dev-clinic.pingcap.com",
			},
		},
		EnvProfiles: map[string]config.EnvProfileContainer{
			"prod": {
				Profiles: map[string]config.ProfileRecord{},
			},
			"staging": {
				Profiles: map[string]config.ProfileRecord{},
			},
			"dev": {
				Profiles: map[string]config.ProfileRecord{},
			},
		},
	}
}
