// Package logger provides structured logging functionality for AutoSSO.
// This file contains property-based tests for log security.
package logger

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

	"pgregory.net/rapid"
)

// Feature: autosso-refactoring, Property 10: 日志不包含敏感信息
// *对于任意* 日志输出，不应包含密码、密钥、令牌等敏感信息。
//
// **Validates: Requirements 5.7**

// knownSensitiveKeywords contains the sensitive field keywords that should be redacted.
// These match the sensitiveFields map in logger.go.
var knownSensitiveKeywords = []string{
	"password",
	"secret",
	"token",
	"access_key",
	"access_key_id",
	"access_key_secret",
	"client_secret",
	"api_key",
	"credential",
	"credentials",
	"app_secret",
}

// genSensitiveFieldName generates a field name that contains a sensitive keyword.
// This includes exact matches, prefixed, suffixed, and mixed case variations.
func genSensitiveFieldName() *rapid.Generator[string] {
	return rapid.Custom(func(t *rapid.T) string {
		// Pick a random sensitive keyword
		keyword := rapid.SampledFrom(knownSensitiveKeywords).Draw(t, "keyword")

		// Generate a variation of the field name
		variation := rapid.IntRange(0, 5).Draw(t, "variation")
		switch variation {
		case 0:
			// Exact match
			return keyword
		case 1:
			// Prefixed (e.g., "my_password", "user_token")
			prefix := rapid.StringMatching(`[a-z]{1,10}`).Draw(t, "prefix")
			return prefix + "_" + keyword
		case 2:
			// Suffixed (e.g., "password_hash", "token_value")
			suffix := rapid.StringMatching(`[a-z]{1,10}`).Draw(t, "suffix")
			return keyword + "_" + suffix
		case 3:
			// CamelCase (e.g., "userPassword", "apiToken")
			prefix := rapid.StringMatching(`[a-z]{1,10}`).Draw(t, "prefix")
			// Capitalize first letter of keyword
			capitalizedKeyword := strings.ToUpper(keyword[:1]) + keyword[1:]
			return prefix + capitalizedKeyword
		case 4:
			// UPPER_CASE (e.g., "USER_PASSWORD", "API_TOKEN")
			prefix := rapid.StringMatching(`[A-Z]{1,10}`).Draw(t, "prefix")
			return prefix + "_" + strings.ToUpper(keyword)
		default:
			// Mixed case (e.g., "Password", "TOKEN")
			if rapid.Bool().Draw(t, "upper") {
				return strings.ToUpper(keyword)
			}
			return strings.ToUpper(keyword[:1]) + keyword[1:]
		}
	})
}

// genNonSensitiveFieldName generates a field name that does NOT contain any sensitive keywords.
// Note: "message", "time", "level" are reserved zerolog fields and should not be used.
func genNonSensitiveFieldName() *rapid.Generator[string] {
	return rapid.Custom(func(t *rapid.T) string {
		// List of safe field names that don't contain sensitive keywords
		// Excludes reserved zerolog fields: "message", "time", "level"
		safeNames := []string{
			"username",
			"email",
			"account_id",
			"user_id",
			"operation",
			"status",
			"count",
			"duration_ms",
			"request_id",
			"response_code",
			"cloud_type",
			"instance_id",
			"region",
			"project_id",
			"subscription_id",
			"role_name",
			"permission_set",
			"approval_code",
			"chat_id",
			"module_name",
			"host",
			"port",
			"database_name",
			"table_name",
			"file_path",
			"method",
			"url",
			"retry_count",
			"error_code",
		}
		return rapid.SampledFrom(safeNames).Draw(t, "safe_field_name")
	})
}

// genSensitiveValue generates a value that looks like a sensitive credential.
func genSensitiveValue() *rapid.Generator[string] {
	return rapid.Custom(func(t *rapid.T) string {
		// Generate values that look like real credentials
		valueType := rapid.IntRange(0, 4).Draw(t, "value_type")
		switch valueType {
		case 0:
			// Password-like
			return rapid.StringMatching(`[A-Za-z0-9!@#$%^&*]{8,32}`).Draw(t, "password")
		case 1:
			// API key-like
			return rapid.StringMatching(`[A-Za-z0-9]{32,64}`).Draw(t, "api_key")
		case 2:
			// Token-like (JWT-ish)
			return "eyJ" + rapid.StringMatching(`[A-Za-z0-9_-]{20,50}`).Draw(t, "jwt")
		case 3:
			// AWS-style key
			return "AKIA" + rapid.StringMatching(`[A-Z0-9]{16}`).Draw(t, "aws_key")
		default:
			// Generic secret
			return rapid.StringMatching(`[a-zA-Z0-9]{16,48}`).Draw(t, "generic_secret")
		}
	})
}

// genNonSensitiveValue generates a non-sensitive value.
func genNonSensitiveValue() *rapid.Generator[string] {
	return rapid.Custom(func(t *rapid.T) string {
		valueType := rapid.IntRange(0, 4).Draw(t, "value_type")
		switch valueType {
		case 0:
			// Email
			return rapid.StringMatching(`[a-z]{3,10}@example\.com`).Draw(t, "email")
		case 1:
			// ID
			return rapid.StringMatching(`[a-z0-9]{8,16}`).Draw(t, "id")
		case 2:
			// Name
			return rapid.StringMatching(`[A-Za-z]{3,20}`).Draw(t, "name")
		case 3:
			// Number as string
			return rapid.StringMatching(`[0-9]{1,10}`).Draw(t, "number")
		default:
			// Generic string
			return rapid.StringMatching(`[a-zA-Z0-9_-]{5,30}`).Draw(t, "generic")
		}
	})
}

// TestSensitiveFieldsAreRedacted tests that any field with a sensitive key name
// has its value redacted to "[REDACTED]".
//
// Feature: autosso-refactoring, Property 10: 日志不包含敏感信息
// **Validates: Requirements 5.7**
func TestSensitiveFieldsAreRedacted(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		var buf bytes.Buffer
		logger := NewLogger(Config{
			Level:  InfoLevel,
			Output: &buf,
		})

		// Generate a sensitive field name and value
		fieldName := genSensitiveFieldName().Draw(rt, "field_name")
		sensitiveValue := genSensitiveValue().Draw(rt, "sensitive_value")

		// Log with the sensitive field
		logger.Info("test message", String(fieldName, sensitiveValue))

		// Parse the log output
		var logEntry map[string]interface{}
		if err := json.Unmarshal(buf.Bytes(), &logEntry); err != nil {
			rt.Fatalf("Failed to parse log output: %v\nOutput: %s", err, buf.String())
		}

		// Verify the sensitive value is redacted
		actualValue, ok := logEntry[fieldName]
		if !ok {
			rt.Fatalf("Field %s not found in log output", fieldName)
		}

		if actualValue != "[REDACTED]" {
			rt.Fatalf("Sensitive field %s was not redacted.\nExpected: [REDACTED]\nGot: %v\nOriginal value: %s",
				fieldName, actualValue, sensitiveValue)
		}

		// Also verify the original sensitive value does not appear anywhere in the log output
		logOutput := buf.String()
		if strings.Contains(logOutput, sensitiveValue) {
			rt.Fatalf("Sensitive value leaked in log output.\nField: %s\nValue: %s\nLog output: %s",
				fieldName, sensitiveValue, logOutput)
		}
	})
}

// TestSensitiveKeyDetectionIsCaseInsensitive tests that sensitive key detection
// works regardless of case.
//
// Feature: autosso-refactoring, Property 10: 日志不包含敏感信息
// **Validates: Requirements 5.7**
func TestSensitiveKeyDetectionIsCaseInsensitive(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		var buf bytes.Buffer
		logger := NewLogger(Config{
			Level:  InfoLevel,
			Output: &buf,
		})

		// Pick a random sensitive keyword
		keyword := rapid.SampledFrom(knownSensitiveKeywords).Draw(rt, "keyword")

		// Generate different case variations
		caseVariation := rapid.IntRange(0, 3).Draw(rt, "case_variation")
		var fieldName string
		switch caseVariation {
		case 0:
			// lowercase
			fieldName = strings.ToLower(keyword)
		case 1:
			// UPPERCASE
			fieldName = strings.ToUpper(keyword)
		case 2:
			// Title Case
			fieldName = strings.ToUpper(keyword[:1]) + strings.ToLower(keyword[1:])
		default:
			// mIxEd CaSe
			var mixed strings.Builder
			for i, c := range keyword {
				if i%2 == 0 {
					mixed.WriteString(strings.ToUpper(string(c)))
				} else {
					mixed.WriteString(strings.ToLower(string(c)))
				}
			}
			fieldName = mixed.String()
		}

		sensitiveValue := genSensitiveValue().Draw(rt, "sensitive_value")

		// Log with the case-varied field name
		logger.Info("test message", String(fieldName, sensitiveValue))

		// Parse the log output
		var logEntry map[string]interface{}
		if err := json.Unmarshal(buf.Bytes(), &logEntry); err != nil {
			rt.Fatalf("Failed to parse log output: %v", err)
		}

		// Verify the sensitive value is redacted
		actualValue, ok := logEntry[fieldName]
		if !ok {
			rt.Fatalf("Field %s not found in log output", fieldName)
		}

		if actualValue != "[REDACTED]" {
			rt.Fatalf("Sensitive field %s (case variation of %s) was not redacted.\nExpected: [REDACTED]\nGot: %v",
				fieldName, keyword, actualValue)
		}
	})
}

// TestSensitiveKeyDetectionWithPartialMatches tests that sensitive key detection
// works with partial matches (e.g., "my_password" should be redacted).
//
// Feature: autosso-refactoring, Property 10: 日志不包含敏感信息
// **Validates: Requirements 5.7**
func TestSensitiveKeyDetectionWithPartialMatches(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		var buf bytes.Buffer
		logger := NewLogger(Config{
			Level:  InfoLevel,
			Output: &buf,
		})

		// Pick a random sensitive keyword
		keyword := rapid.SampledFrom(knownSensitiveKeywords).Draw(rt, "keyword")

		// Generate a field name with prefix and/or suffix
		matchType := rapid.IntRange(0, 2).Draw(rt, "match_type")
		var fieldName string
		switch matchType {
		case 0:
			// Prefix only (e.g., "my_password")
			prefix := rapid.StringMatching(`[a-z]{2,8}`).Draw(rt, "prefix")
			fieldName = prefix + "_" + keyword
		case 1:
			// Suffix only (e.g., "password_hash")
			suffix := rapid.StringMatching(`[a-z]{2,8}`).Draw(rt, "suffix")
			fieldName = keyword + "_" + suffix
		default:
			// Both prefix and suffix (e.g., "user_password_encrypted")
			prefix := rapid.StringMatching(`[a-z]{2,8}`).Draw(rt, "prefix")
			suffix := rapid.StringMatching(`[a-z]{2,8}`).Draw(rt, "suffix")
			fieldName = prefix + "_" + keyword + "_" + suffix
		}

		sensitiveValue := genSensitiveValue().Draw(rt, "sensitive_value")

		// Log with the partial match field name
		logger.Info("test message", String(fieldName, sensitiveValue))

		// Parse the log output
		var logEntry map[string]interface{}
		if err := json.Unmarshal(buf.Bytes(), &logEntry); err != nil {
			rt.Fatalf("Failed to parse log output: %v", err)
		}

		// Verify the sensitive value is redacted
		actualValue, ok := logEntry[fieldName]
		if !ok {
			rt.Fatalf("Field %s not found in log output", fieldName)
		}

		if actualValue != "[REDACTED]" {
			rt.Fatalf("Sensitive field %s (containing keyword %s) was not redacted.\nExpected: [REDACTED]\nGot: %v",
				fieldName, keyword, actualValue)
		}
	})
}

// TestNonSensitiveFieldsAreNotRedacted tests that non-sensitive fields
// are NOT redacted.
//
// Feature: autosso-refactoring, Property 10: 日志不包含敏感信息
// **Validates: Requirements 5.7**
func TestNonSensitiveFieldsAreNotRedacted(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		var buf bytes.Buffer
		logger := NewLogger(Config{
			Level:  InfoLevel,
			Output: &buf,
		})

		// Generate a non-sensitive field name and value
		fieldName := genNonSensitiveFieldName().Draw(rt, "field_name")
		value := genNonSensitiveValue().Draw(rt, "value")

		// Log with the non-sensitive field
		logger.Info("test message", String(fieldName, value))

		// Parse the log output
		var logEntry map[string]interface{}
		if err := json.Unmarshal(buf.Bytes(), &logEntry); err != nil {
			rt.Fatalf("Failed to parse log output: %v", err)
		}

		// Verify the value is NOT redacted
		actualValue, ok := logEntry[fieldName]
		if !ok {
			rt.Fatalf("Field %s not found in log output", fieldName)
		}

		if actualValue == "[REDACTED]" {
			rt.Fatalf("Non-sensitive field %s was incorrectly redacted.\nExpected: %s\nGot: [REDACTED]",
				fieldName, value)
		}

		if actualValue != value {
			rt.Fatalf("Non-sensitive field %s value mismatch.\nExpected: %s\nGot: %v",
				fieldName, value, actualValue)
		}
	})
}

// TestMixedSensitiveAndNonSensitiveFields tests logging with both sensitive
// and non-sensitive fields together.
//
// Feature: autosso-refactoring, Property 10: 日志不包含敏感信息
// **Validates: Requirements 5.7**
func TestMixedSensitiveAndNonSensitiveFields(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		var buf bytes.Buffer
		logger := NewLogger(Config{
			Level:  InfoLevel,
			Output: &buf,
		})

		// Generate sensitive field
		sensitiveFieldName := genSensitiveFieldName().Draw(rt, "sensitive_field_name")
		sensitiveValue := genSensitiveValue().Draw(rt, "sensitive_value")

		// Generate non-sensitive field
		nonSensitiveFieldName := genNonSensitiveFieldName().Draw(rt, "non_sensitive_field_name")
		nonSensitiveValue := genNonSensitiveValue().Draw(rt, "non_sensitive_value")

		// Log with both fields
		logger.Info("test message",
			String(sensitiveFieldName, sensitiveValue),
			String(nonSensitiveFieldName, nonSensitiveValue),
		)

		// Parse the log output
		var logEntry map[string]interface{}
		if err := json.Unmarshal(buf.Bytes(), &logEntry); err != nil {
			rt.Fatalf("Failed to parse log output: %v", err)
		}

		// Verify sensitive field is redacted
		actualSensitiveValue, ok := logEntry[sensitiveFieldName]
		if !ok {
			rt.Fatalf("Sensitive field %s not found in log output", sensitiveFieldName)
		}
		if actualSensitiveValue != "[REDACTED]" {
			rt.Fatalf("Sensitive field %s was not redacted.\nExpected: [REDACTED]\nGot: %v",
				sensitiveFieldName, actualSensitiveValue)
		}

		// Verify non-sensitive field is NOT redacted
		actualNonSensitiveValue, ok := logEntry[nonSensitiveFieldName]
		if !ok {
			rt.Fatalf("Non-sensitive field %s not found in log output", nonSensitiveFieldName)
		}
		if actualNonSensitiveValue != nonSensitiveValue {
			rt.Fatalf("Non-sensitive field %s value mismatch.\nExpected: %s\nGot: %v",
				nonSensitiveFieldName, nonSensitiveValue, actualNonSensitiveValue)
		}

		// Verify sensitive value doesn't appear anywhere in output
		logOutput := buf.String()
		if strings.Contains(logOutput, sensitiveValue) {
			rt.Fatalf("Sensitive value leaked in log output.\nValue: %s\nLog output: %s",
				sensitiveValue, logOutput)
		}
	})
}

// TestWithFieldsSensitiveFiltering tests that WithFields also filters sensitive data.
//
// Feature: autosso-refactoring, Property 10: 日志不包含敏感信息
// **Validates: Requirements 5.7**
func TestWithFieldsSensitiveFilteringProperty(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		var buf bytes.Buffer
		logger := NewLogger(Config{
			Level:  InfoLevel,
			Output: &buf,
		})

		// Generate sensitive field
		sensitiveFieldName := genSensitiveFieldName().Draw(rt, "sensitive_field_name")
		sensitiveValue := genSensitiveValue().Draw(rt, "sensitive_value")

		// Generate non-sensitive field
		nonSensitiveFieldName := genNonSensitiveFieldName().Draw(rt, "non_sensitive_field_name")
		nonSensitiveValue := genNonSensitiveValue().Draw(rt, "non_sensitive_value")

		// Create logger with context fields
		contextLogger := logger.WithFields(map[string]interface{}{
			sensitiveFieldName:    sensitiveValue,
			nonSensitiveFieldName: nonSensitiveValue,
		})

		contextLogger.Info("test message")

		// Parse the log output
		var logEntry map[string]interface{}
		if err := json.Unmarshal(buf.Bytes(), &logEntry); err != nil {
			rt.Fatalf("Failed to parse log output: %v", err)
		}

		// Verify sensitive field is redacted
		actualSensitiveValue, ok := logEntry[sensitiveFieldName]
		if !ok {
			rt.Fatalf("Sensitive field %s not found in log output", sensitiveFieldName)
		}
		if actualSensitiveValue != "[REDACTED]" {
			rt.Fatalf("Sensitive field %s in WithFields was not redacted.\nExpected: [REDACTED]\nGot: %v",
				sensitiveFieldName, actualSensitiveValue)
		}

		// Verify non-sensitive field is NOT redacted
		actualNonSensitiveValue, ok := logEntry[nonSensitiveFieldName]
		if !ok {
			rt.Fatalf("Non-sensitive field %s not found in log output", nonSensitiveFieldName)
		}
		if actualNonSensitiveValue != nonSensitiveValue {
			rt.Fatalf("Non-sensitive field %s value mismatch.\nExpected: %s\nGot: %v",
				nonSensitiveFieldName, nonSensitiveValue, actualNonSensitiveValue)
		}
	})
}

// TestAllKnownSensitiveKeywordsAreRedacted tests that all known sensitive keywords
// are properly redacted.
//
// Feature: autosso-refactoring, Property 10: 日志不包含敏感信息
// **Validates: Requirements 5.7**
func TestAllKnownSensitiveKeywordsAreRedacted(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		var buf bytes.Buffer
		logger := NewLogger(Config{
			Level:  InfoLevel,
			Output: &buf,
		})

		// Test each known sensitive keyword
		for _, keyword := range knownSensitiveKeywords {
			buf.Reset()

			sensitiveValue := genSensitiveValue().Draw(rt, "sensitive_value_"+keyword)

			// Log with the exact keyword as field name
			logger.Info("test message", String(keyword, sensitiveValue))

			// Parse the log output
			var logEntry map[string]interface{}
			if err := json.Unmarshal(buf.Bytes(), &logEntry); err != nil {
				rt.Fatalf("Failed to parse log output for keyword %s: %v", keyword, err)
			}

			// Verify the sensitive value is redacted
			actualValue, ok := logEntry[keyword]
			if !ok {
				rt.Fatalf("Field %s not found in log output", keyword)
			}

			if actualValue != "[REDACTED]" {
				rt.Fatalf("Known sensitive keyword %s was not redacted.\nExpected: [REDACTED]\nGot: %v",
					keyword, actualValue)
			}
		}
	})
}

// TestSensitiveValueNeverAppearsInOutput tests that sensitive values never appear
// in the raw log output string, regardless of field name.
//
// Feature: autosso-refactoring, Property 10: 日志不包含敏感信息
// **Validates: Requirements 5.7**
func TestSensitiveValueNeverAppearsInOutput(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		var buf bytes.Buffer
		logger := NewLogger(Config{
			Level:  InfoLevel,
			Output: &buf,
		})

		// Generate multiple sensitive fields with values
		numFields := rapid.IntRange(1, 5).Draw(rt, "num_fields")
		sensitiveValues := make([]string, numFields)

		fields := make([]Field, numFields)
		for i := 0; i < numFields; i++ {
			fieldName := genSensitiveFieldName().Draw(rt, "field_name_"+string(rune('a'+i)))
			sensitiveValues[i] = genSensitiveValue().Draw(rt, "sensitive_value_"+string(rune('a'+i)))
			fields[i] = String(fieldName, sensitiveValues[i])
		}

		// Log with all sensitive fields
		logger.Info("test message", fields...)

		// Verify none of the sensitive values appear in the raw output
		logOutput := buf.String()
		for i, sensitiveValue := range sensitiveValues {
			if strings.Contains(logOutput, sensitiveValue) {
				rt.Fatalf("Sensitive value %d leaked in log output.\nValue: %s\nLog output: %s",
					i, sensitiveValue, logOutput)
			}
		}
	})
}
