package utils

import (
	"bytes"
	"encoding/json"
	"strings"
	"testing"

	cspb "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	execv1 "github.com/tidbcloud/cluster-service-ng/api/executor/v1"
	"google.golang.org/protobuf/encoding/protojson"
)

func TestRedactSecrets(t *testing.T) {
	tests := []struct {
		name     string
		input    string
		expected string
	}{
		{
			name:     "empty string",
			input:    "",
			expected: "",
		},
		{
			name:     "no secrets",
			input:    `{"name": "test", "value": "data"}`,
			expected: `{"name":"test","value":"data"}`,
		},
		{
			name:     "password field",
			input:    `{"username": "user", "password": "secret123"}`,
			expected: `{"password":"***","username":"user"}`,
		},
		{
			name:     "passwd field",
			input:    `{"user": "admin", "passwd": "admin123"}`,
			expected: `{"passwd":"***","user":"admin"}`,
		},
		{
			name:     "credential field",
			input:    `{"api_key": "key123", "credential": "secret"}`,
			expected: `{"api_key":"***","credential":"***"}`,
		},
		{
			name:     "token field",
			input:    `{"auth": "basic", "token": "jwt123"}`,
			expected: `{"auth":"basic","token":"***"}`,
		},
		{
			name:     "ca field",
			input:    `{"cert": "data", "ca": "certificate"}`,
			expected: `{"ca":"***","cert":"data"}`,
		},
		{
			name:     "secrt field",
			input:    `{"public": "pub", "secrt": "private"}`,
			expected: `{"public":"pub","secrt":"***"}`,
		},
		{
			name:     "case insensitive password",
			input:    `{"PASSWORD": "secret", "PASSWD": "pass"}`,
			expected: `{"PASSWD":"***","PASSWORD":"***"}`,
		},
		{
			name:     "nested password",
			input:    `{"config": {"auth": {"password": "secret"}}}`,
			expected: `{"config":{"auth":{"password":"***"}}}`,
		},
		{
			name:     "array with password",
			input:    `{"users": [{"name": "user1", "password": "pass1"}, {"name": "user2", "password": "pass2"}]}`,
			expected: `{"users":[{"name":"user1","password":"***"},{"name":"user2","password":"***"}]}`,
		},
		{
			name:     "invalid JSON",
			input:    `{"invalid": json}`,
			expected: `<non-JSON payload omitted; size=17>`,
		},
		{
			name:     "root array",
			input:    `[{"password":"secret"},{"name":"safe"}]`,
			expected: `[{"password":"***"},{"name":"safe"}]`,
		},
		{
			name:     "field naming variants",
			input:    `{"clientSecret":"a","access-key-id":"b","private_key":"c","Authorization":"d"}`,
			expected: `{"Authorization":"***","access-key-id":"***","clientSecret":"***","private_key":"***"}`,
		},
		{
			name:     "non-string sensitive value",
			input:    `{"token":{"value":"nested-secret"},"enabled":true}`,
			expected: `{"enabled":true,"token":"***"}`,
		},
		{
			name:     "complex nested structure",
			input:    `{"level1": {"level2": {"level3": {"password": "deep", "token": "nested"}}}}`,
			expected: `{"level1":{"level2":{"level3":{"password":"***","token":"***"}}}}`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := RedactSecrets(tt.input)
			if result != tt.expected {
				t.Errorf("RedactSecrets() = %v, want %v", result, tt.expected)
			}
		})
	}
}

func TestRedactSecretsWithRealJSON(t *testing.T) {
	// Test with a more realistic JSON structure
	input := `{
		"apiVersion": "v1",
		"kind": "Secret",
		"metadata": {
			"name": "my-secret"
		},
		"data": {
			"username": "dXNlcg==",
			"password": "cGFzc3dvcmQ=",
			"token": "dG9rZW4=",
			"config": {
				"api_key": "key123",
				"credential": "secret123"
			}
		}
	}`

	result := RedactSecrets(input)

	// Verify that sensitive fields are redacted
	if !contains(result, `"password":"***"`) {
		t.Error("password field should be redacted")
	}
	if !contains(result, `"token":"***"`) {
		t.Error("token field should be redacted")
	}
	if !contains(result, `"credential":"***"`) {
		t.Error("credential field should be redacted")
	}

	// Verify that non-sensitive fields are preserved
	if !contains(result, `"username":"dXNlcg=="`) {
		t.Error("username field should be preserved")
	}
	if !contains(result, `"api_key":"***"`) {
		t.Error("api_key field should be redacted")
	}
}

func TestRedactSecretsEmbeddedExecutorActionArg(t *testing.T) {
	const (
		password  = "db-password-do-not-log"
		accountID = "605821132139"
		roleARN   = "arn:aws:iam::605821132139:role/TiDBCloud-S3-Role"
	)
	exportPayload, err := json.Marshal(map[string]any{
		"exportId": "632384178431830391",
		"password": password,
		"cloudStorage": map[string]any{
			"type": "S3",
			"s3": map[string]any{
				"authType": "ROLE_ARN",
				"roleArn":  roleARN,
			},
		},
	})
	if err != nil {
		t.Fatal(err)
	}
	submitPayload, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(&execv1.SubmitRequest{
		Scope:    "cluster-ng-server",
		PlanName: "create-export",
		ExecId:   "create-export-632384178431830391",
		Args: map[string]*execv1.ActionArg{
			"export": {
				Arg: &execv1.ActionArg_ActionArgString{ActionArgString: string(exportPayload)},
			},
		},
	})
	if err != nil {
		t.Fatal(err)
	}

	result := RedactSecrets(string(submitPayload))
	if strings.Contains(result, password) {
		t.Fatalf("embedded export password leaked: %s", result)
	}
	for _, safeValue := range []string{accountID, roleARN, "632384178431830391"} {
		if !strings.Contains(result, safeValue) {
			t.Errorf("expected safe troubleshooting value %q to be preserved: %s", safeValue, result)
		}
	}
	if !strings.Contains(result, `password\":\"***`) {
		t.Errorf("embedded password was not visibly redacted: %s", result)
	}
}

func TestRedactSecretsCloudCredentialVariants(t *testing.T) {
	input := `{"service_account_key":"gcp-key","x-amz-signature":"aws-signature","roleArn":"safe-role"}`
	result := RedactSecrets(input)

	for _, secret := range []string{"gcp-key", "aws-signature"} {
		if strings.Contains(result, secret) {
			t.Fatalf("credential %q leaked: %s", secret, result)
		}
	}
	if !strings.Contains(result, "safe-role") {
		t.Fatalf("role ARN should be preserved: %s", result)
	}
}

func TestRedactSecretsAzureSetAuditLogSetting(t *testing.T) {
	const sasToken = "sv=2024-01-01&ss=bf&srt=sco&sig=azure-sas-token"
	request := &cspb.SetAuditLogSettingRequest{
		ClusterId: "123",
		AuditLogSetting: &cspb.Cluster_AuditLogSetting{
			BucketUrl: "https://account.blob.core.windows.net/audit",
			Region:    "eastus",
			ProviderIdentity: &cspb.Cluster_AuditLogSetting_ProviderIdentity{
				CloudRoleIdentity: sasToken,
				ExternalId:        "safe-external-id",
			},
		},
	}
	body, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(request)
	if err != nil {
		t.Fatal(err)
	}
	redacted := RedactSecrets(string(body))
	if strings.Contains(redacted, sasToken) {
		t.Fatalf("Azure SAS token leaked: %s", redacted)
	}
	for _, safeValue := range []string{"123", "account.blob.core.windows.net/audit", "eastus", "safe-external-id"} {
		if !strings.Contains(redacted, safeValue) {
			t.Errorf("expected safe value %q to be preserved: %s", safeValue, redacted)
		}
	}
	if !strings.Contains(redacted, `"cloud_role_identity":"***"`) {
		t.Fatalf("cloud_role_identity was not visibly redacted: %s", redacted)
	}
}

func TestRedactURL(t *testing.T) {
	const raw = "https://user:userinfo-value@example.com/bucket/path?cluster_id=123&X-Amz-Credential=credential-value&X-Amz-Signature=signature-value#fragment-value"
	result := RedactURL(raw)

	for _, secret := range []string{"userinfo-value", "credential-value", "signature-value", "fragment-value"} {
		if strings.Contains(result, secret) {
			t.Fatalf("URL credential %q leaked: %s", secret, result)
		}
	}
	for _, safeValue := range []string{"example.com", "/bucket/path", "cluster_id=123"} {
		if !strings.Contains(result, safeValue) {
			t.Fatalf("expected URL value %q to be preserved: %s", safeValue, result)
		}
	}
}

func TestRedactedJSONMarshalFailure(t *testing.T) {
	result := RedactedJSON(make(chan int))
	if strings.Contains(result, "0x") || !strings.Contains(result, "type=chan int") {
		t.Fatalf("unexpected marshal failure log value: %s", result)
	}
}

func TestRedactSecretsEmbeddedJSONLimits(t *testing.T) {
	t.Run("oversized carrier fails closed", func(t *testing.T) {
		embedded := `{"value":"` + strings.Repeat("x", maxEmbeddedJSONBytes) + `"}`
		payload, err := json.Marshal(map[string]string{"action_arg_string": embedded})
		if err != nil {
			t.Fatal(err)
		}
		if got := RedactSecrets(string(payload)); got != `{"action_arg_string":"***"}` {
			t.Fatalf("unexpected oversized carrier result: %s", got)
		}
	})

	t.Run("malformed carrier fails closed", func(t *testing.T) {
		payload, err := json.Marshal(map[string]string{"action_arg_string": `{"password":`})
		if err != nil {
			t.Fatal(err)
		}
		if got := RedactSecrets(string(payload)); got != `{"action_arg_string":"***"}` {
			t.Fatalf("unexpected malformed carrier result: %s", got)
		}
	})

	t.Run("recursive carrier depth is bounded", func(t *testing.T) {
		embedded := `{"password":"deep-secret"}`
		for range maxEmbeddedJSONDepth + 1 {
			data, err := json.Marshal(map[string]string{"action_arg_string": embedded})
			if err != nil {
				t.Fatal(err)
			}
			embedded = string(data)
		}
		payload, err := json.Marshal(map[string]string{"action_arg_string": embedded})
		if err != nil {
			t.Fatal(err)
		}
		result := RedactSecrets(string(payload))
		if strings.Contains(result, "deep-secret") {
			t.Fatalf("secret leaked past recursion bound: %s", result)
		}
	})
}

func TestDfsRedactMap(t *testing.T) {
	tests := []struct {
		name     string
		input    map[string]any
		expected map[string]any
	}{
		{
			name:     "empty map",
			input:    map[string]any{},
			expected: map[string]any{},
		},
		{
			name: "simple password redaction",
			input: map[string]any{
				"name":     "test",
				"password": "secret",
			},
			expected: map[string]any{
				"name":     "test",
				"password": "***",
			},
		},
		{
			name: "nested password redaction",
			input: map[string]any{
				"config": map[string]any{
					"auth": map[string]any{
						"password": "secret",
						"username": "user",
					},
				},
			},
			expected: map[string]any{
				"config": map[string]any{
					"auth": map[string]any{
						"password": "***",
						"username": "user",
					},
				},
			},
		},
	}

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

			// Convert to JSON for comparison
			inputJSON, _ := json.Marshal(tt.input)
			expectedJSON, _ := json.Marshal(tt.expected)

			if !bytes.Equal(inputJSON, expectedJSON) {
				t.Errorf("dfsRedactMap() = %v, want %v", string(inputJSON), string(expectedJSON))
			}
		})
	}
}

func TestDfsRedactSlice(t *testing.T) {
	tests := []struct {
		name     string
		input    []any
		expected []any
	}{
		{
			name:     "empty slice",
			input:    []any{},
			expected: []any{},
		},
		{
			name: "slice with password maps",
			input: []any{
				map[string]any{"name": "user1", "password": "pass1"},
				map[string]any{"name": "user2", "password": "pass2"},
			},
			expected: []any{
				map[string]any{"name": "user1", "password": "***"},
				map[string]any{"name": "user2", "password": "***"},
			},
		},
		{
			name: "nested slice with passwords",
			input: []any{
				map[string]any{
					"users": []any{
						map[string]any{"name": "user1", "password": "pass1"},
						map[string]any{"name": "user2", "password": "pass2"},
					},
				},
			},
			expected: []any{
				map[string]any{
					"users": []any{
						map[string]any{"name": "user1", "password": "***"},
						map[string]any{"name": "user2", "password": "***"},
					},
				},
			},
		},
	}

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

			// Convert to JSON for comparison
			inputJSON, _ := json.Marshal(tt.input)
			expectedJSON, _ := json.Marshal(tt.expected)

			if !bytes.Equal(inputJSON, expectedJSON) {
				t.Errorf("dfsRedactSlice() = %v, want %v", string(inputJSON), string(expectedJSON))
			}
		})
	}
}

func contains(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr || len(s) > len(substr) &&
		(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
			func() bool {
				for i := 1; i <= len(s)-len(substr); i++ {
					if s[i:i+len(substr)] == substr {
						return true
					}
				}
				return false
			}()))
}
