package cmd

import (
	"encoding/json"
	"net/http"
	"strings"
	"testing"
	"time"

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

func TestDiagnoseV2BundleTopLevelKeysAndEmptyArrays(t *testing.T) {
	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)
	ctx.SelectedProfileName = "blue"
	ctx.SelectedProfile.ClusterName = "cluster-blue"
	ctx.SelectedProfile.ParentID = "pool-9"
	ctx.SelectedProfile.ClusterDeployTypeV2 = "premium"

	bundle := runDiagnoseBundleJSONForTest(t, ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Since:     "48h",
		Limit:     100,
		Output:    "json",
	}, time.Date(2026, time.April, 22, 9, 30, 0, 0, time.UTC))

	if got, want := stringValue(t, bundle, "schema_version"), "diagnose.v2"; got != want {
		t.Fatalf("schema_version = %q, want %q", got, want)
	}
	if got, want := stringValue(t, bundle, "generated_at_utc"), "2026-04-22T09:30:00Z"; got != want {
		t.Fatalf("generated_at_utc = %q, want %q", got, want)
	}
	if _, ok := bundle["incidents"]; ok {
		t.Fatalf("top-level incidents key exists in V2 JSON: %s", mustMarshalForTest(t, bundle))
	}

	for _, key := range []string{"query_plan", "seed_matches", "related_logs", "events", "groups", "timelines", "warnings"} {
		if _, ok := bundle[key]; !ok {
			t.Fatalf("missing top-level key %q in bundle %s", key, mustMarshalForTest(t, bundle))
		}
	}
	for _, key := range []string{"seed_matches", "related_logs", "events", "groups", "timelines", "warnings"} {
		raw := rawJSONForKey(t, bundle, key)
		if got, want := strings.TrimSpace(string(raw)), "[]"; got != want {
			t.Fatalf("%s json = %s, want %s\nbundle=%s", key, got, want, mustMarshalForTest(t, bundle))
		}
	}
	for _, key := range []string{"scope", "topology", "summaries"} {
		raw := rawJSONForKey(t, bundle, key)
		if len(raw) == 0 || raw[0] != '{' {
			t.Fatalf("%s json = %s, want object", key, raw)
		}
	}
}

func TestDiagnoseBundleQueryScopeClusterSourceValues(t *testing.T) {
	t.Run("explicit cluster flag", func(t *testing.T) {
		clearDiagnoseClusterAndRangeEnv(t)
		client := &recordingClinicClient{
			responses: []clinicResponse{
				{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			},
		}
		ctx := newDiagnoseTestRuntimeContext(client)
		ctx.SelectedProfileName = "blue"

		scope := scopeFromBundleForTest(t, ctx, diagnoseOptions{
			Seed:      "seed",
			ClusterID: "cluster-42",
			Since:     "1h",
			Limit:     100,
			Output:    "json",
		})
		if got, want := stringValue(t, scope, "cluster_source"), "explicit_flag"; got != want {
			t.Fatalf("cluster_source = %q, want %q", got, want)
		}
		if got, want := stringValue(t, scope, "cluster_id"), "cluster-42"; got != want {
			t.Fatalf("cluster_id = %q, want %q", got, want)
		}
	})

	t.Run("environment variable", func(t *testing.T) {
		clearDiagnoseRangeEnv(t)
		t.Setenv(envClinicClusterID, "cluster-env")
		client := &recordingClinicClient{
			responses: []clinicResponse{
				{body: []byte(`{"items":[{"clusterID":"cluster-env","name":"Env Cluster","orgID":"org-env","clusterDeployTypeV2":"dedicated"}]}`)},
				{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			},
		}
		ctx := newDiagnoseTestRuntimeContext(client)

		scope := scopeFromBundleForTest(t, ctx, diagnoseOptions{
			Seed:   "seed",
			Since:  "1h",
			Limit:  100,
			Output: "json",
		})
		if got, want := stringValue(t, scope, "cluster_source"), "environment_variable"; got != want {
			t.Fatalf("cluster_source = %q, want %q", got, want)
		}
		if got, want := stringValue(t, scope, "cluster_name"), "Env Cluster"; got != want {
			t.Fatalf("cluster_name = %q, want %q", got, want)
		}
	})

	t.Run("active profile", func(t *testing.T) {
		clearDiagnoseClusterAndRangeEnv(t)
		client := &recordingClinicClient{
			responses: []clinicResponse{
				{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			},
		}
		ctx := newDiagnoseTestRuntimeContext(client)
		ctx.SelectedProfileName = "blue"
		ctx.SelectedProfile.ClusterName = "cluster-blue"

		scope := scopeFromBundleForTest(t, ctx, diagnoseOptions{
			Seed:   "seed",
			Since:  "1h",
			Limit:  100,
			Output: "json",
		})
		if got, want := stringValue(t, scope, "cluster_source"), "active_profile"; got != want {
			t.Fatalf("cluster_source = %q, want %q", got, want)
		}
		if got, want := boolValue(t, scope, "profile_active"), true; got != want {
			t.Fatalf("profile_active = %t, want %t", got, want)
		}
		if got, want := stringValue(t, scope, "selected_profile"), "blue"; got != want {
			t.Fatalf("selected_profile = %q, want %q", got, want)
		}
	})
}

func TestDiagnoseBundleQueryScopeTimeSourceValues(t *testing.T) {
	testCases := []struct {
		name       string
		opts       diagnoseOptions
		wantSource string
		configure  func(*RuntimeContext)
	}{
		{
			name: "since flag",
			opts: diagnoseOptions{
				Since: "2h",
			},
			wantSource: "since_flag",
		},
		{
			name: "start end flags",
			opts: diagnoseOptions{
				Start: "1712123000",
				End:   "1712123600",
			},
			wantSource: "start_end_flags",
		},
		{
			name: "around window flags",
			opts: diagnoseOptions{
				Around: "2026-04-22T08:00:00Z",
				Window: "30m",
			},
			wantSource: "around_window_flags",
		},
		{
			name: "profile range",
			opts: diagnoseOptions{},
			configure: func(ctx *RuntimeContext) {
				ctx.SelectedProfile.RangeStart = "1712123000"
				ctx.SelectedProfile.RangeEnd = "1712123600"
			},
			wantSource: "profile_range",
		},
		{
			name:       "default range",
			opts:       diagnoseOptions{},
			wantSource: "default",
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			clearDiagnoseClusterAndRangeEnv(t)
			client := &recordingClinicClient{
				responses: []clinicResponse{
					{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
				},
			}
			ctx := newDiagnoseTestRuntimeContext(client)
			ctx.SelectedProfileName = "blue"
			if tc.configure != nil {
				tc.configure(ctx)
			}

			opts := tc.opts
			opts.Seed = "seed"
			opts.ClusterID = "cluster-42"
			opts.Limit = 100
			opts.Output = "json"

			scope := scopeFromBundleForTest(t, ctx, opts)
			if got := stringValue(t, scope, "time_source"); got != tc.wantSource {
				t.Fatalf("time_source = %q, want %q", got, tc.wantSource)
			}
			if got := stringValue(t, scope, "time_interpretation_notes"); !strings.Contains(got, "local timezone is used because profiles do not yet store timezone") {
				t.Fatalf("time_interpretation_notes = %q, want local-timezone profile note", got)
			}
		})
	}
}

func TestDiagnoseQueryPlanSeedEntryIsReproducible(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{"component":"tidb","instance":"10.0.0.1:4000","namespace":"tidbcluster-42"},
					"values":[
						["1712123000000000000","seed one"],
						["1712123000000000001","seed two"]
					]
				}
			]
		}
	}`)
	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: body},
			{body: []byte(`{"components":{"tidb":[{"host":"tidb-0"}]}}`)},
			{body: []byte(`{"items":[]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	bundle := runDiagnoseBundleJSONForTest(t, ctx, diagnoseOptions{
		Seed:      "seed",
		ClusterID: "cluster-42",
		Start:     "1712123000",
		End:       "1712123600",
		Limit:     2,
		NoExpand:  true,
		Output:    "json",
	}, time.Date(2026, time.April, 22, 9, 30, 0, 0, time.UTC))

	plan := sliceValue(t, bundle, "query_plan")
	if got, want := len(plan), 3; got != want {
		t.Fatalf("query_plan length = %d, want %d", got, want)
	}
	record := mapValueFromAny(t, plan[0], "query_plan[0]")
	if got, want := stringValue(t, record, "id"), "q1"; got != want {
		t.Fatalf("query_plan[0].id = %q, want %q", got, want)
	}
	if got, want := stringValue(t, record, "type"), "loki"; got != want {
		t.Fatalf("query_plan[0].type = %q, want %q", got, want)
	}
	if got, want := stringValue(t, record, "purpose"), "seed_exact_match"; got != want {
		t.Fatalf("query_plan[0].purpose = %q, want %q", got, want)
	}
	if got, want := stringValue(t, record, "query"), `{cluster_env="staging", namespace="tidbcluster-42"} |= "seed"`; got != want {
		t.Fatalf("query_plan[0].query = %q, want %q", got, want)
	}
	if got, want := stringValue(t, record, "start_utc"), "2024-04-03T05:43:20Z"; got != want {
		t.Fatalf("query_plan[0].start_utc = %q, want %q", got, want)
	}
	if got, want := stringValue(t, record, "end_utc"), "2024-04-03T05:53:20Z"; got != want {
		t.Fatalf("query_plan[0].end_utc = %q, want %q", got, want)
	}
	if got, want := intValue(t, record, "limit"), 2; got != want {
		t.Fatalf("query_plan[0].limit = %d, want %d", got, want)
	}
	if got, want := stringValue(t, record, "direction"), "backward"; got != want {
		t.Fatalf("query_plan[0].direction = %q, want %q", got, want)
	}
	if got, want := stringValue(t, record, "status"), "success"; got != want {
		t.Fatalf("query_plan[0].status = %q, want %q", got, want)
	}
	if got, want := intValue(t, record, "result_count"), 2; got != want {
		t.Fatalf("query_plan[0].result_count = %d, want %d", got, want)
	}
	if got, want := boolValue(t, record, "hit_limit"), true; got != want {
		t.Fatalf("query_plan[0].hit_limit = %t, want %t", got, want)
	}
	if got := intValue(t, record, "duration_ms"); got < 0 {
		t.Fatalf("query_plan[0].duration_ms = %d, want non-negative", got)
	}
}

func runDiagnoseBundleJSONForTest(t *testing.T, ctx *RuntimeContext, opts diagnoseOptions, now time.Time) map[string]any {
	t.Helper()

	var stdout strings.Builder
	if err := runDiagnoseSearchWithOutput(ctx, opts, now, &stdout); err != nil {
		t.Fatalf("runDiagnoseSearchWithOutput() error = %v", err)
	}

	var bundle map[string]any
	decoder := json.NewDecoder(strings.NewReader(stdout.String()))
	decoder.UseNumber()
	if err := decoder.Decode(&bundle); err != nil {
		t.Fatalf("json.Decode() error = %v\njson=%s", err, stdout.String())
	}
	return bundle
}

func scopeFromBundleForTest(t *testing.T, ctx *RuntimeContext, opts diagnoseOptions) map[string]any {
	t.Helper()

	bundle := runDiagnoseBundleJSONForTest(t, ctx, opts, time.Date(2026, time.April, 22, 9, 30, 0, 0, time.UTC))
	return mapValue(t, bundle, "scope")
}

func rawJSONForKey(t *testing.T, values map[string]any, key string) json.RawMessage {
	t.Helper()

	raw, err := json.Marshal(values[key])
	if err != nil {
		t.Fatalf("json.Marshal(%s) error = %v", key, err)
	}
	return raw
}

func mapValue(t *testing.T, values map[string]any, key string) map[string]any {
	t.Helper()
	return mapValueFromAny(t, values[key], key)
}

func mapValueFromAny(t *testing.T, value any, key string) map[string]any {
	t.Helper()

	mapped, ok := value.(map[string]any)
	if !ok {
		t.Fatalf("%s = %T, want object", key, value)
	}
	return mapped
}

func sliceValue(t *testing.T, values map[string]any, key string) []any {
	t.Helper()

	slice, ok := values[key].([]any)
	if !ok {
		t.Fatalf("%s = %T, want array", key, values[key])
	}
	return slice
}

func stringValue(t *testing.T, values map[string]any, key string) string {
	t.Helper()

	value, ok := values[key].(string)
	if !ok {
		t.Fatalf("%s = %T, want string", key, values[key])
	}
	return value
}

func boolValue(t *testing.T, values map[string]any, key string) bool {
	t.Helper()

	value, ok := values[key].(bool)
	if !ok {
		t.Fatalf("%s = %T, want bool", key, values[key])
	}
	return value
}

func intValue(t *testing.T, values map[string]any, key string) int {
	t.Helper()

	switch value := values[key].(type) {
	case json.Number:
		parsed, err := value.Int64()
		if err != nil {
			t.Fatalf("%s = %q, want integer: %v", key, value, err)
		}
		return int(parsed)
	case float64:
		return int(value)
	default:
		t.Fatalf("%s = %T, want number", key, values[key])
		return 0
	}
}

func mustMarshalForTest(t *testing.T, value any) string {
	t.Helper()

	raw, err := json.Marshal(value)
	if err != nil {
		t.Fatalf("json.Marshal() error = %v", err)
	}
	return string(raw)
}

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

	t.Setenv(envClinicClusterID, "")
	clearDiagnoseRangeEnv(t)
}

func TestDiagnoseBundleScopeCanUseAPIClusterMetadata(t *testing.T) {
	clearDiagnoseClusterAndRangeEnv(t)

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"items":[{"clusterID":"cluster-api","name":"API Cluster","orgID":"org-api","parentID":"pool-api","clusterDeployTypeV2":"premium"}]}`)},
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
		},
	}
	ctx := &RuntimeContext{
		SelectedEnv: "staging",
		SelectedProfile: &config.ProfileRecord{
			ClusterID:           "cluster-42",
			OrgID:               "org-1",
			ClusterDeployTypeV2: "dedicated",
		},
		Client: client,
	}

	scope := scopeFromBundleForTest(t, ctx, diagnoseOptions{
		Seed:      "seed",
		ClusterID: "cluster-api",
		Since:     "1h",
		Limit:     100,
		Output:    "json",
	})

	if got, want := stringValue(t, scope, "cluster_name"), "API Cluster"; got != want {
		t.Fatalf("cluster_name = %q, want %q", got, want)
	}
	if got, want := stringValue(t, scope, "org_id"), "org-api"; got != want {
		t.Fatalf("org_id = %q, want %q", got, want)
	}
	if got, want := stringValue(t, scope, "namespace"), "tidbpool-api"; got != want {
		t.Fatalf("namespace = %q, want %q", got, want)
	}
	if got, want := stringValue(t, scope, "sharedpool_id"), "pool-api"; got != want {
		t.Fatalf("sharedpool_id = %q, want %q", got, want)
	}
	if got, want := client.requests[0].Method, http.MethodGet; got != want {
		t.Fatalf("metadata request method = %q, want %q", got, want)
	}
	if got, want := client.requests[0].Path, dashboardClustersPath; got != want {
		t.Fatalf("metadata request path = %q, want %q", got, want)
	}
}
