package audit

import (
	"strings"
	"testing"
	"unicode/utf8"

	"github.com/stretchr/testify/assert"
)

func TestSanitizePath(t *testing.T) {
	sanitizer := NewSanitizer(nil, nil)

	tests := []struct {
		name    string
		rawPath string
		want    string
	}{
		{
			name:    "no query params",
			rawPath: "/api/v1/users",
			want:    "/api/v1/users",
		},
		{
			name:    "redact token param",
			rawPath: "/api/v1/auth?token=secret123&user=admin",
			want:    "/api/v1/auth?token=%5BREDACTED%5D&user=admin",
		},
		{
			name:    "redact password param",
			rawPath: "/login?username=admin&password=supersecret",
			want:    "/login?password=%5BREDACTED%5D&username=admin",
		},
		{
			name:    "redact multiple sensitive params",
			rawPath: "/api?api_key=key123&secret=shh&name=test",
			want:    "/api?api_key=%5BREDACTED%5D&name=test&secret=%5BREDACTED%5D",
		},
		{
			name:    "case insensitive param matching",
			rawPath: "/api?TOKEN=abc&PASSWORD=def",
			want:    "/api?PASSWORD=%5BREDACTED%5D&TOKEN=%5BREDACTED%5D",
		},
		{
			name:    "empty path",
			rawPath: "",
			want:    "",
		},
		{
			name:    "path with fragment",
			rawPath: "/page?token=secret#section",
			want:    "/page?token=%5BREDACTED%5D#section",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := sanitizer.SanitizePath(tt.rawPath)
			assert.Equal(t, tt.want, result)
		})
	}
}

func TestSanitizeBody(t *testing.T) {
	deepJSON := `{"l1":{"l2":{"l3":{"l4":{"l5":{"l6":{"l7":{"l8":{"l9":{"l10":` +
		`{"l11":{"l12":{"l13":{"l14":{"l15":{"l16":{"l17":{"l18":{"l19":{"l20":` +
		`{"l21":{"l22":{"password":"secret"}}}` +
		`}}}}}}}}}}}}}}}}}}}}}`
	shallowJSON := `{"l1":{"l2":{"l3":{"password":"secret"}}}}`

	tests := []struct {
		name                 string
		additionalBodyFields []string
		body                 []byte
		maxSize              int
		alreadyTruncated     bool
		want                 string
		wantSet              bool
		contains             []string
		validate             func(t *testing.T, result string)
	}{
		{
			name:    "empty body",
			body:    []byte{},
			maxSize: 256,
			want:    "",
			wantSet: true,
		},
		{
			name:    "simple JSON without sensitive fields",
			body:    []byte(`{"name":"test","value":123}`),
			maxSize: 256,
			want:    `{"name":"test","value":123}`,
			wantSet: true,
		},
		{
			name:     "JSON with password field",
			body:     []byte(`{"username":"admin","password":"secret123"}`),
			maxSize:  256,
			contains: []string{`"username":"admin"`, `"password":"[REDACTED]"`},
		},
		{
			name:     "JSON with nested sensitive field",
			body:     []byte(`{"user":{"name":"john","token":"abc123"}}`),
			maxSize:  256,
			contains: []string{`"name":"john"`, `"token":"[REDACTED]"`},
		},
		{
			name:     "JSON with multiple sensitive fields",
			body:     []byte(`{"api_key":"key1","secret":"shh","data":"public"}`),
			maxSize:  256,
			contains: []string{`"api_key":"[REDACTED]"`, `"secret":"[REDACTED]"`, `"data":"public"`},
		},
		{
			name:    "non-JSON body",
			body:    []byte(`plain text content`),
			maxSize: 256,
			want:    "plain text content",
			wantSet: true,
		},
		{
			name:    "body exceeds maxSize",
			body:    []byte(`{"data":"this is a very long string that will be truncated"}`),
			maxSize: 20,
			want:    `{"data":"this is a v...[truncated]`,
			wantSet: true,
		},
		{
			name:    "non-JSON body exceeds maxSize",
			body:    []byte(`this is a plain text that is too long`),
			maxSize: 15,
			want:    "this is a plain...[truncated]",
			wantSet: true,
		},
		{
			name:     "array with sensitive fields",
			body:     []byte(`[{"password":"secret1"},{"password":"secret2"}]`),
			maxSize:  256,
			contains: []string{`"password":"[REDACTED]"`},
		},
		{
			name:                 "custom sensitive fields",
			additionalBodyFields: []string{"custom_secret", "internal_key"},
			body:                 []byte(`{"custom_secret":"mysecret","internal_key":"key123","public":"data"}`),
			maxSize:              256,
			contains: []string{
				`"custom_secret":"[REDACTED]"`,
				`"internal_key":"[REDACTED]"`,
				`"public":"data"`,
			},
		},
		{
			name:    "truncate in middle of Chinese character",
			body:    []byte(`{"name":"中文测试数据"}`),
			maxSize: 15,
			validate: func(t *testing.T, result string) {
				assert.True(t, strings.HasSuffix(result, truncatedSuffix), "should end with truncated suffix")
				truncatedPart := strings.TrimSuffix(result, truncatedSuffix)
				assert.True(t, utf8.ValidString(truncatedPart), "truncated part should be valid UTF-8")
				assert.LessOrEqual(t, len(result), 15+len(truncatedSuffix), "result length should not exceed limit")
			},
		},
		{
			name:    "truncate with emoji",
			body:    []byte(`{"emoji":"😀😀😀"}`),
			maxSize: 12,
			validate: func(t *testing.T, result string) {
				assert.True(t, strings.HasSuffix(result, truncatedSuffix), "should end with truncated suffix")
				truncatedPart := strings.TrimSuffix(result, truncatedSuffix)
				assert.True(t, utf8.ValidString(truncatedPart), "truncated part should be valid UTF-8")
				assert.LessOrEqual(t, len(result), 12+len(truncatedSuffix), "result length should not exceed limit")
			},
		},
		{
			name:    "truncate Japanese text",
			body:    []byte(`{"text":"日本語テスト"}`),
			maxSize: 10,
			validate: func(t *testing.T, result string) {
				assert.True(t, strings.HasSuffix(result, truncatedSuffix), "should end with truncated suffix")
				truncatedPart := strings.TrimSuffix(result, truncatedSuffix)
				assert.True(t, utf8.ValidString(truncatedPart), "truncated part should be valid UTF-8")
				assert.LessOrEqual(t, len(result), 10+len(truncatedSuffix), "result length should not exceed limit")
			},
		},
		{
			name:    "deep nesting beyond recursion depth",
			body:    []byte(deepJSON),
			maxSize: 1024,
			contains: []string{
				`"password":"secret"`,
			},
		},
		{
			name:    "shallow nesting within recursion depth",
			body:    []byte(shallowJSON),
			maxSize: 256,
			contains: []string{
				`"password":"[REDACTED]"`,
			},
		},
		// Test alreadyTruncated flag
		{
			name:             "already truncated JSON - adds suffix even if under maxSize",
			body:             []byte(`{"name":"test"}`),
			maxSize:          256,
			alreadyTruncated: true,
			want:             `{"name":"test"}...[truncated]`,
			wantSet:          true,
		},
		{
			name:             "already truncated non-JSON - adds suffix even if under maxSize",
			body:             []byte(`plain text`),
			maxSize:          256,
			alreadyTruncated: true,
			want:             "plain text...[truncated]",
			wantSet:          true,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sanitizer := NewSanitizer(nil, tt.additionalBodyFields)
			result := sanitizer.SanitizeBody(tt.body, tt.maxSize, tt.alreadyTruncated)
			if tt.wantSet {
				assert.Equal(t, tt.want, result)
			}
			for _, s := range tt.contains {
				assert.Contains(t, result, s)
			}
			if tt.validate != nil {
				tt.validate(t, result)
			}
		})
	}
}

func TestSanitizeQueryParams(t *testing.T) {
	sanitizer := NewSanitizer(nil, nil)

	tests := []struct {
		name     string
		rawQuery string
		want     string
		contains string
	}{
		{
			name:     "empty query",
			rawQuery: "",
			want:     "",
		},
		{
			name:     "no sensitive params",
			rawQuery: "name=test&page=1",
			want:     "name=test&page=1",
		},
		{
			name:     "redact token param",
			rawQuery: "token=secret&name=test",
			contains: "token=%5BREDACTED%5D",
		},
		{
			name:     "redact password param",
			rawQuery: "username=admin&password=secret",
			contains: "password=%5BREDACTED%5D",
		},
		{
			name:     "case insensitive",
			rawQuery: "TOKEN=abc&PASSWORD=def",
			contains: "PASSWORD=%5BREDACTED%5D",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := sanitizer.SanitizeQueryParams(tt.rawQuery)
			if tt.want != "" {
				assert.Equal(t, tt.want, result)
			}
			if tt.contains != "" {
				assert.Contains(t, result, tt.contains)
			}
		})
	}
}

func TestTruncateUTF8(t *testing.T) {
	tests := []struct {
		name          string
		input         string
		maxLen        int
		want          string
		skipUTF8Check bool // skip UTF-8 validation for invalid input tests
	}{
		// Basic cases
		{
			name:   "empty string",
			input:  "",
			maxLen: 10,
			want:   "",
		},
		{
			name:   "string shorter than maxLen",
			input:  "hello",
			maxLen: 10,
			want:   "hello",
		},
		{
			name:   "string equal to maxLen",
			input:  "hello",
			maxLen: 5,
			want:   "hello",
		},
		{
			name:   "string longer than maxLen (ASCII)",
			input:  "hello world",
			maxLen: 5,
			want:   "hello",
		},

		// Edge cases for maxLen
		{
			name:   "maxLen is zero",
			input:  "hello",
			maxLen: 0,
			want:   "",
		},
		{
			name:   "maxLen is negative",
			input:  "hello",
			maxLen: -1,
			want:   "",
		},
		{
			name:   "maxLen is one with ASCII",
			input:  "hello",
			maxLen: 1,
			want:   "h",
		},

		// Chinese characters (3 bytes each)
		{
			name:   "Chinese string not truncated",
			input:  "中文",
			maxLen: 10,
			want:   "中文",
		},
		{
			name:   "Chinese string exact boundary",
			input:  "中文",
			maxLen: 6, // 2 Chinese chars = 6 bytes
			want:   "中文",
		},
		{
			name:   "truncate after first Chinese char",
			input:  "中文测试",
			maxLen: 3, // exactly 1 Chinese char
			want:   "中",
		},
		{
			name:   "truncate in middle of Chinese char (maxLen=4)",
			input:  "中文",
			maxLen: 4, // 3 + partial
			want:   "中",
		},
		{
			name:   "truncate in middle of Chinese char (maxLen=5)",
			input:  "中文",
			maxLen: 5, // 3 + partial
			want:   "中",
		},
		{
			name:   "maxLen smaller than single Chinese char",
			input:  "中",
			maxLen: 2,
			want:   "",
		},

		// Mixed ASCII and multibyte
		{
			name:   "mixed ASCII and Chinese partial",
			input:  "hi中文",
			maxLen: 4, // "hi" (2) + partial Chinese (need 3)
			want:   "hi",
		},
		{
			name:   "mixed ASCII and Chinese exact fit",
			input:  "hi中",
			maxLen: 5, // "hi" (2) + "中" (3) = 5
			want:   "hi中",
		},
		{
			name:   "mixed Chinese and ASCII",
			input:  "中文hi",
			maxLen: 7, // "中文" (6) + "h" (1)
			want:   "中文h",
		},
		{
			name:   "ASCII emoji ASCII",
			input:  "a😀b",
			maxLen: 5, // "a" (1) + emoji (4) = 5
			want:   "a😀",
		},
		{
			name:   "ASCII emoji truncated",
			input:  "a😀b",
			maxLen: 4, // not enough for emoji
			want:   "a",
		},

		// 2-byte UTF-8 characters (e.g., Latin extended, Greek)
		{
			name:   "2-byte chars (Greek)",
			input:  "αβγ", // each 2 bytes
			maxLen: 4,
			want:   "αβ",
		},
		{
			name:   "2-byte char truncated in middle",
			input:  "αβ",
			maxLen: 3, // 2 + partial
			want:   "α",
		},

		// Complex mixed
		{
			name:   "complex mixed characters",
			input:  "a中😀β", // "a"(1) + "中"(3) + "😀"(4) + "β"(2) = 10 bytes total
			maxLen: 8,      // fits "a"(1) + "中"(3) + "😀"(4) = 8 bytes
			want:   "a中😀",
		},
		{
			name:   "complex mixed partial",
			input:  "a中😀β", // "a"(1) + "中"(3) + "😀"(4) + "β"(2) = 10 bytes total
			maxLen: 7,      // fits "a"(1) + "中"(3) = 4 bytes, not enough for "😀"(4)
			want:   "a中",
		},

		// Edge: all same multibyte
		{
			name:   "multiple 4-byte chars boundary test",
			input:  "😀😃😄😁",
			maxLen: 12, // exactly 3 emojis (3 * 4 = 12 bytes)
			want:   "😀😃😄",
		},

		// Invalid UTF-8 input (document behavior)
		// Note: range iterates invalid bytes as RuneError (U+FFFD), and RuneLen(RuneError)=3
		// So invalid bytes are effectively skipped unless there's enough room for 3 bytes
		{
			name:          "invalid UTF-8 sequence",
			input:         string([]byte{0x80, 0x81, 0x82}), // invalid UTF-8 bytes
			maxLen:        2,
			want:          "", // each invalid byte treated as RuneError needing 3 bytes
			skipUTF8Check: true,
		},
		{
			name:          "mixed valid and invalid UTF-8",
			input:         "a" + string([]byte{0x80, 0x81}) + "b", // "a" + invalid + "b"
			maxLen:        3,
			want:          "a", // "a"(1) + RuneError needs 3 more bytes, not enough
			skipUTF8Check: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := truncateUTF8(tt.input, tt.maxLen)
			assert.Equal(t, tt.want, result)
			// Verify result is always valid UTF-8 (skip for invalid input tests)
			if !tt.skipUTF8Check {
				assert.True(t, utf8.ValidString(result), "result should be valid UTF-8")
			}
			// Verify result length doesn't exceed maxLen
			if tt.maxLen > 0 {
				assert.LessOrEqual(t, len(result), tt.maxLen, "result byte length should not exceed maxLen")
			}
		})
	}
}
