package utils

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

	. "github.com/smartystreets/goconvey/convey"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

func TestRedactFields(t *testing.T) {
	Convey("TestRedactFields", t, func() {
		Convey("should redact sensitive primitive fields", func() {
			fields := []zap.Field{
				zap.String("password", "secret123"),
				zap.String("sastoken", "sas123"),
				zap.String("ServiceAccountKey", "key123"),
				zap.String("db_password", "dbpass"),
				zap.Int("password", 12345),
				zap.Int("sastoken", 99999),
				zap.Int("ServiceAccountKey", 88888),
				zap.Float64("password", 3.14),
				zap.String("username", "user1"),
				zap.String("token", "abc123"),
				zap.Int("age", 30),
			}
			redacted := redactFields(fields)

			// Sensitive fields should be redacted
			So(redacted[0].String, ShouldEqual, redactedValue)
			So(redacted[1].String, ShouldEqual, redactedValue)
			So(redacted[2].String, ShouldEqual, redactedValue)
			So(redacted[3].String, ShouldEqual, redactedValue)
			So(redacted[4].String, ShouldEqual, redactedValue)
			So(redacted[5].String, ShouldEqual, redactedValue)
			So(redacted[6].String, ShouldEqual, redactedValue)
			So(redacted[7].String, ShouldEqual, redactedValue)

			// Non-sensitive fields should be preserved
			So(redacted[8].String, ShouldEqual, "user1")
			So(redacted[9].String, ShouldEqual, "abc123")
			So(redacted[10].Integer, ShouldEqual, 30)
		})

		Convey("should handle composite types", func() {
			type User struct {
				Username string
				Password string
				Email    string
			}
			user := User{
				Username: "user1",
				Password: "secret123",
				Email:    "user@example.com",
			}
			fields := []zap.Field{
				zap.Any("user", user),
				zap.Any("password", user), // sensitive key with composite type
				zap.Any("data", nil),
			}
			redacted := redactFields(fields)

			So(redacted[0].Type, ShouldEqual, zapcore.ReflectType)
			So(redacted[1].Type, ShouldEqual, zapcore.ReflectType)
			So(redacted[2].Key, ShouldEqual, "data")

			// Verify user struct was processed
			userMap, isMap := redacted[0].Interface.(map[string]any)
			if isMap {
				So(userMap["Password"], ShouldEqual, redactedValue)
			}
		})

		Convey("should preserve non-sensitive fields", func() {
			fields := []zap.Field{
				zap.String("name", "John"),
				zap.Int("age", 30),
				zap.Bool("active", true),
			}
			redacted := redactFields(fields)

			So(redacted[0].String, ShouldEqual, "John")
			So(redacted[1].Integer, ShouldEqual, 30)
			So(redacted[2].Integer, ShouldEqual, 1) // true is 1
		})

		Convey("should handle zero and empty values", func() {
			fields := []zap.Field{
				// Sensitive fields with zero/empty values
				zap.String("password", ""),
				zap.Int("db_password", 0),
				zap.Bool("sastoken", false),
				// Non-sensitive fields with zero/empty values
				zap.String("username", ""),
				zap.Int("age", 0),
				zap.Bool("active", false),
			}
			redacted := redactFields(fields)

			// Empty values are intentionally not redacted even for sensitive fields
			// This preserves the original value (empty string, 0, false) instead of redacting
			So(redacted[0].String, ShouldEqual, "")
			So(redacted[1].Integer, ShouldEqual, 0)
			So(redacted[2].Integer, ShouldEqual, 0) // false is 0
			So(redacted[3].String, ShouldEqual, "")
			So(redacted[4].Integer, ShouldEqual, 0)
			So(redacted[5].Integer, ShouldEqual, 0) // false is 0
		})
	})
}

func TestRedactAnyValue(t *testing.T) {
	Convey("TestRedactAnyValue", t, func() {
		Convey("should redact sensitive fields in structs", func() {
			type Credentials struct {
				Username          string
				Password          string
				SasToken          string
				ServiceAccountKey string
				Token             string
			}
			creds := Credentials{
				Username:          "user1",
				Password:          "secret123",
				SasToken:          "sas123",
				ServiceAccountKey: "key123",
				Token:             "abc123",
			}
			redacted, _ := redactAnyValue(reflect.ValueOf(creds), false, 0)
			redactedMap := redacted.(map[string]any)

			So(redactedMap["Username"], ShouldEqual, "user1")
			So(redactedMap["Password"], ShouldEqual, redactedValue)
			So(redactedMap["SasToken"], ShouldEqual, redactedValue)
			So(redactedMap["ServiceAccountKey"], ShouldEqual, redactedValue)
			So(redactedMap["Token"], ShouldEqual, "abc123")
		})

		Convey("should redact fields based on struct tags", func() {
			type Config struct {
				Token    string `json:"sastoken"`
				Password string `json:"password"`
				APIKey   string `json:"api_key"`
			}
			config := Config{
				Token:    "token123",
				Password: "pass123",
				APIKey:   "key123",
			}
			redacted, _ := redactAnyValue(reflect.ValueOf(config), false, 0)
			redactedMap := redacted.(map[string]any)

			So(redactedMap["Token"], ShouldEqual, redactedValue)
			So(redactedMap["Password"], ShouldEqual, redactedValue)
			So(redactedMap["APIKey"], ShouldEqual, "key123")
		})

		Convey("should handle nested structures", func() {
			type Inner struct {
				Secret string
			}
			type Outer struct {
				Name  string
				Inner Inner
			}
			outer := Outer{
				Name:  "test",
				Inner: Inner{Secret: "hidden"},
			}
			redacted, _ := redactAnyValue(reflect.ValueOf(outer), false, 0)
			redactedMap := redacted.(map[string]any)

			So(redactedMap["Name"], ShouldEqual, "test")
			innerMap := redactedMap["Inner"].(map[string]any)
			So(innerMap["Secret"], ShouldEqual, "hidden")
		})

		Convey("should handle maps with sensitive keys", func() {
			data := map[string]any{
				"username":          "user1",
				"password":          "secret123",
				"sastoken":          "sas123",
				"ServiceAccountKey": "key123",
				"db_password":       "dbpass",
				"email":             "user@example.com",
			}
			redacted, _ := redactAnyValue(reflect.ValueOf(data), false, 0)
			redactedMap := redacted.(map[string]any)

			So(redactedMap["username"], ShouldEqual, "user1")
			So(redactedMap["password"], ShouldEqual, redactedValue)
			So(redactedMap["sastoken"], ShouldEqual, redactedValue)
			So(redactedMap["ServiceAccountKey"], ShouldEqual, redactedValue)
			So(redactedMap["db_password"], ShouldEqual, redactedValue)
			So(redactedMap["email"], ShouldEqual, "user@example.com")
		})

		Convey("should handle slices and arrays", func() {
			type Item struct {
				Name     string
				Password string
			}
			items := []Item{
				{Name: "item1", Password: "pass1"},
				{Name: "item2", Password: "pass2"},
			}
			redacted, _ := redactAnyValue(reflect.ValueOf(items), false, 0)
			redactedSlice := redacted.([]any)

			So(len(redactedSlice), ShouldEqual, 2)
			item1 := redactedSlice[0].(map[string]any)
			So(item1["Name"], ShouldEqual, "item1")
			So(item1["Password"], ShouldEqual, redactedValue)
		})

		Convey("should handle pointers", func() {
			type User struct {
				Username string
				Password string
			}
			user := &User{
				Username: "user1",
				Password: "secret123",
			}
			redacted, _ := redactAnyValue(reflect.ValueOf(user), false, 0)
			redactedMap := redacted.(map[string]any)

			So(redactedMap["Username"], ShouldEqual, "user1")
			So(redactedMap["Password"], ShouldEqual, redactedValue)
		})

		Convey("should handle nil values", func() {
			var user *struct{ Username string }
			redacted, _ := redactAnyValue(reflect.ValueOf(user), false, 0)
			So(redacted, ShouldBeNil)

			redacted, _ = redactAnyValue(reflect.ValueOf(nil), false, 0)
			So(redacted, ShouldBeNil)
		})

		Convey("should skip unexported fields", func() {
			type TestStruct struct {
				PublicField  string
				privateField string
			}
			ts := TestStruct{
				PublicField:  "public",
				privateField: "private",
			}
			redacted, _ := redactAnyValue(reflect.ValueOf(ts), false, 0)
			redactedMap := redacted.(map[string]any)

			So(redactedMap["PublicField"], ShouldEqual, "public")
			_, exists := redactedMap["privateField"]
			So(exists, ShouldBeFalse)
		})

		Convey("should handle primitives", func() {
			testCases := []struct {
				name      string
				value     any
				sensitive bool
				expected  any
				changed   bool
			}{
				{"string non-sensitive", "hello", false, "hello", false},
				{"string sensitive", "hello", true, redactedValue, true},
				{"string empty sensitive", "", true, "", false}, // empty values not redacted
				{"int non-sensitive", 42, false, 42, false},
				{"int sensitive", 42, true, redactedValue, true},
				{"int zero sensitive", 0, true, 0, false}, // empty values not redacted
				{"bool non-sensitive", true, false, true, false},
				{"bool sensitive", true, true, redactedValue, true},
				{"bool false sensitive", false, true, false, false}, // empty values not redacted
			}

			for _, tc := range testCases {
				redacted, changed := redactAnyValue(reflect.ValueOf(tc.value), tc.sensitive, 0)
				So(redacted, ShouldEqual, tc.expected)
				So(changed, ShouldEqual, tc.changed)
			}
		})

		Convey("should preserve composite structure when isSensitive=true", func() {
			type Config struct {
				Host     string
				Password string
			}
			config := Config{Host: "localhost", Password: "secret"}
			redacted, changed := redactAnyValue(reflect.ValueOf(config), true, 0)

			So(changed, ShouldBeTrue)
			redactedMap := redacted.(map[string]any)
			So(redactedMap["Host"], ShouldEqual, "localhost")
			So(redactedMap["Password"], ShouldEqual, redactedValue)
		})

		Convey("should handle non-string-key maps", func() {
			data := map[int]string{1: "one", 2: "two"}
			redacted, changed := redactAnyValue(reflect.ValueOf(data), false, 0)
			So(redacted, ShouldResemble, data)
			So(changed, ShouldBeFalse)

			redacted, changed = redactAnyValue(reflect.ValueOf(data), true, 0)
			So(redacted, ShouldEqual, redactedValue)
			So(changed, ShouldBeTrue)
		})

		Convey("should respect maxRedactDepth", func() {
			type DeepStruct struct {
				Value any
			}
			var deep any = DeepStruct{Value: "test"}
			for i := 0; i < maxRedactDepth+1; i++ {
				deep = DeepStruct{Value: deep}
			}
			redacted, _ := redactAnyValue(reflect.ValueOf(deep), false, 0)
			So(redacted, ShouldNotBeNil)
		})
	})
}

func TestIsSensitiveKey(t *testing.T) {
	Convey("TestIsSensitiveKey", t, func() {
		Convey("should detect sensitive keys", func() {
			sensitiveKeys := []string{
				"password", "Password", "PASSWORD",
				"user_password", "db_password", "user-password", "session.password",
				"sastoken", "SasToken", "SASTOKEN",
				"ServiceAccountKey", "serviceaccountkey", "SERVICEACCOUNTKEY",
			}
			for _, key := range sensitiveKeys {
				So(isSensitiveKey(key), ShouldBeTrue)
			}
		})

		Convey("should not detect non-sensitive keys", func() {
			nonSensitiveKeys := []string{
				"username", "email", "name", "id",
				"password_policy", "customer_token_count",
				"secret", "token", "api_key", "api_token",
			}
			for _, key := range nonSensitiveKeys {
				So(isSensitiveKey(key), ShouldBeFalse)
			}
		})
	})
}

func TestWrapWithRedactor(t *testing.T) {
	Convey("TestWrapWithRedactor", t, func() {
		Convey("should return a zap.Option", func() {
			opt := WrapWithRedactor()
			So(opt, ShouldNotBeNil)
		})

		Convey("should wrap core with redactingCore", func() {
			core := zapcore.NewNopCore()
			opt := WrapWithRedactor()
			logger := zap.New(core, opt)
			So(logger, ShouldNotBeNil)
		})
	})
}

func TestLoggerE2E(t *testing.T) {
	Convey("TestLoggerE2E", t, func() {
		// Setup logger
		var buf bytes.Buffer
		encoderConfig := zap.NewProductionEncoderConfig()
		encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
		core := zapcore.NewCore(
			zapcore.NewJSONEncoder(encoderConfig),
			zapcore.AddSync(&buf),
			zapcore.InfoLevel,
		)
		redactOpt := WrapWithRedactor()
		logger := zap.New(core, redactOpt)

		Convey("should redact top-level sensitive fields", func() {
			logger.Info("user login attempt",
				zap.String("username", "john_doe"),
				zap.String("password", "mySecretPassword123"),
				zap.String("sastoken", "sas123"),
				zap.String("ServiceAccountKey", "key123"),
				zap.String("token", "abc123xyz"),
			)

			output := buf.String()
			So(output, ShouldContainSubstring, "john_doe")
			So(output, ShouldContainSubstring, redactedValue)
			So(output, ShouldNotContainSubstring, "mySecretPassword123")
			So(output, ShouldNotContainSubstring, "sas123") // sastoken should be redacted
			So(output, ShouldNotContainSubstring, "key123") // ServiceAccountKey should be redacted
			So(output, ShouldContainSubstring, "abc123xyz") // token not redacted
		})

		Convey("should redact nested struct fields", func() {
			buf.Reset()
			type UserCredentials struct {
				Username          string
				Password          string
				SasToken          string
				ServiceAccountKey string
				Token             string
			}
			creds := UserCredentials{
				Username:          "jane_doe",
				Password:          "secretPass456",
				SasToken:          "sas789",
				ServiceAccountKey: "key789",
				Token:             "token789",
			}
			logger.Info("user authentication", zap.Any("credentials", creds))

			var logEntry map[string]any
			err := json.Unmarshal(buf.Bytes(), &logEntry)
			So(err, ShouldBeNil)

			credsMap := logEntry["credentials"].(map[string]any)
			So(credsMap["Username"], ShouldEqual, "jane_doe")
			So(credsMap["Password"], ShouldEqual, redactedValue)
			So(credsMap["SasToken"], ShouldEqual, redactedValue)
			So(credsMap["ServiceAccountKey"], ShouldEqual, redactedValue)
			So(credsMap["Token"], ShouldEqual, "token789")
		})

		Convey("should redact map keys", func() {
			buf.Reset()
			config := map[string]any{
				"username_password": "userpass",
				"db_password":       "dbPass789",
				"sastoken":          "sas123",
				"ServiceAccountKey": "key123",
				"api_key":           "key123456",
			}
			logger.Info("configuration loaded", zap.Any("config", config))

			var logEntry map[string]any
			err := json.Unmarshal(buf.Bytes(), &logEntry)
			So(err, ShouldBeNil)

			configMap := logEntry["config"].(map[string]any)
			So(configMap["username_password"], ShouldEqual, redactedValue)
			So(configMap["db_password"], ShouldEqual, redactedValue)
			So(configMap["sastoken"], ShouldEqual, redactedValue)
			So(configMap["ServiceAccountKey"], ShouldEqual, redactedValue)
			So(configMap["api_key"], ShouldEqual, "key123456")
		})

		Convey("should redact at all log levels", func() {
			buf.Reset()
			logger.Debug("debug", zap.String("password", "debugPass"))
			logger.Info("info", zap.String("password", "infoPass"))
			logger.Warn("warn", zap.String("password", "warnPass"))
			logger.Error("error", zap.String("password", "errorPass"))

			output := buf.String()
			lines := strings.Split(strings.TrimSpace(output), "\n")
			So(len(lines), ShouldBeGreaterThanOrEqualTo, 3)

			for _, line := range lines {
				if !strings.Contains(line, "password") {
					continue
				}
				So(line, ShouldContainSubstring, redactedValue)
				So(line, ShouldNotContainSubstring, "debugPass")
				So(line, ShouldNotContainSubstring, "infoPass")
				So(line, ShouldNotContainSubstring, "warnPass")
				So(line, ShouldNotContainSubstring, "errorPass")
			}
		})

		Convey("should redact based on struct tags", func() {
			buf.Reset()
			type Config struct {
				Token    string `json:"sastoken"`
				Password string `json:"password"`
				APIKey   string `json:"api_key"`
			}
			cfg := Config{
				Token:    "token123",
				Password: "adminPass",
				APIKey:   "apiKey123",
			}
			logger.Info("config loaded", zap.Any("settings", cfg))

			var logEntry map[string]any
			err := json.Unmarshal(buf.Bytes(), &logEntry)
			So(err, ShouldBeNil)

			settingsMap := logEntry["settings"].(map[string]any)
			So(settingsMap["Token"], ShouldEqual, redactedValue)
			So(settingsMap["Password"], ShouldEqual, redactedValue)
			So(settingsMap["APIKey"], ShouldEqual, "apiKey123")
		})
	})
}
