package audit

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

	"github.com/gin-gonic/gin"
)

func TestScrubJSONPayload_RedactsSecretValueCaseInsensitive(t *testing.T) {
	t.Parallel()

	payload := []byte(`{
		"secretValue":"top-secret",
		"SecretValue":"mixed-case-secret",
		"safe":"keep-me",
		"nested":{"secretValue":"nested-secret"},
		"items":[{"SecretValue":"array-secret"}]
	}`)

	got, err := ScrubJSONPayload(payload)
	if err != nil {
		t.Fatalf("ScrubJSONPayload: %v", err)
	}
	if strings.Contains(string(got), "top-secret") ||
		strings.Contains(string(got), "mixed-case-secret") ||
		strings.Contains(string(got), "nested-secret") ||
		strings.Contains(string(got), "array-secret") {
		t.Fatalf("scrubbed payload leaked secretValue plaintext: %s", got)
	}

	var decoded map[string]any
	if err := json.Unmarshal(got, &decoded); err != nil {
		t.Fatalf("json.Unmarshal scrubbed payload: %v", err)
	}
	if got := decoded["secretValue"]; got != PIIScrubSentinel {
		t.Fatalf("secretValue = %v, want %q", got, PIIScrubSentinel)
	}
	if got := decoded["SecretValue"]; got != PIIScrubSentinel {
		t.Fatalf("SecretValue = %v, want %q", got, PIIScrubSentinel)
	}
	if got := decoded["safe"]; got != "keep-me" {
		t.Fatalf("safe = %v, want %q", got, "keep-me")
	}

	nested, ok := decoded["nested"].(map[string]any)
	if !ok {
		t.Fatalf("nested type = %T, want map[string]any", decoded["nested"])
	}
	if got := nested["secretValue"]; got != PIIScrubSentinel {
		t.Fatalf("nested.secretValue = %v, want %q", got, PIIScrubSentinel)
	}

	items, ok := decoded["items"].([]any)
	if !ok || len(items) != 1 {
		t.Fatalf("items = %T %#v, want len 1 slice", decoded["items"], decoded["items"])
	}
	item, ok := items[0].(map[string]any)
	if !ok {
		t.Fatalf("items[0] type = %T, want map[string]any", items[0])
	}
	if got := item["SecretValue"]; got != PIIScrubSentinel {
		t.Fatalf("items[0].SecretValue = %v, want %q", got, PIIScrubSentinel)
	}
}

func TestIsSensitiveHeaderName(t *testing.T) {
	t.Parallel()

	for _, name := range []string{
		"Authorization",
		"Cookie",
		"Proxy-Authorization",
		"X-Test-Signature",
		"X-Upstream-Token",
		"X-Client-Secret",
		"X-Foo-Hmac-Sha256",
		"X-Api-Credential",
		"X-Session-Id",
	} {
		if !IsSensitiveHeaderName(name) {
			t.Fatalf("IsSensitiveHeaderName(%q) = false, want true", name)
		}
	}

	for _, name := range []string{"Content-Type", "X-Request-Id", "X-Correlation-Id"} {
		if IsSensitiveHeaderName(name) {
			t.Fatalf("IsSensitiveHeaderName(%q) = true, want false", name)
		}
	}
}

func TestBuildRow_ScrubsCapturedJSONSecretValue(t *testing.T) {
	t.Parallel()

	gin.SetMode(gin.TestMode)
	recorder := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(recorder)

	rawBody := []byte(`{"secretValue":"super-secret-value","label":"Example NC"}`)
	req := httptest.NewRequest("POST", "/api/example", bytes.NewReader(rawBody))
	req.Header.Set("Content-Type", "application/json")
	c.Request = req
	c.Writer.WriteHeader(200)

	row := buildRow(c, "req-1", time.Unix(1700000000, 0).UTC(), rawBody, len(rawBody), false, nil, 25*time.Millisecond)

	if strings.Contains(row.Body, "super-secret-value") {
		t.Fatalf("captured body leaked plaintext secretValue: %s", row.Body)
	}
	if !strings.Contains(row.Body, `"secretValue":"`+PIIScrubSentinel+`"`) {
		t.Fatalf("captured body missing scrubbed secretValue sentinel: %s", row.Body)
	}
	if !strings.Contains(row.Body, `"label":"Example NC"`) {
		t.Fatalf("captured body lost safe fields: %s", row.Body)
	}
}

func TestBuildRow_ScrubsNamedCredentialSecretWriteWithoutJSONContentType(t *testing.T) {
	t.Parallel()

	for _, path := range secretValueWriteTestPaths() {
		t.Run(path.name, func(t *testing.T) {
			for _, contentType := range []string{"", "text/plain"} {
				t.Run("ct="+contentType, func(t *testing.T) {
					gin.SetMode(gin.TestMode)
					recorder := httptest.NewRecorder()
					c, _ := gin.CreateTestContext(recorder)

					rawBody := []byte(`{"secretValue":"super-secret-value"}`)
					req := httptest.NewRequest("POST", path.path, bytes.NewReader(rawBody))
					if contentType != "" {
						req.Header.Set("Content-Type", contentType)
					}
					c.Request = req
					c.Writer.WriteHeader(400)

					row := buildRow(c, "req-2", time.Unix(1700000001, 0).UTC(), rawBody, len(rawBody), false, nil, 25*time.Millisecond)

					if strings.Contains(row.Body, "super-secret-value") {
						t.Fatalf("captured body leaked plaintext secretValue: %s", row.Body)
					}
					if row.Body != `{"secretValue":"`+PIIScrubSentinel+`"}` {
						t.Fatalf("captured body = %s, want scrubbed secretValue only", row.Body)
					}
				})
			}
		})
	}
}

func TestBuildRow_ScrubsSecretValueWriteMalformedJSONFailClosed(t *testing.T) {
	t.Parallel()

	for _, path := range secretValueWriteTestPaths() {
		t.Run(path.name, func(t *testing.T) {
			gin.SetMode(gin.TestMode)
			recorder := httptest.NewRecorder()
			c, _ := gin.CreateTestContext(recorder)

			rawBody := []byte(`{"secretValue":"super-secret-value"`)
			req := httptest.NewRequest("POST", path.path, bytes.NewReader(rawBody))
			req.Header.Set("Content-Type", "application/json")
			c.Request = req
			c.Writer.WriteHeader(400)

			row := buildRow(c, "req-3", time.Unix(1700000002, 0).UTC(), rawBody, len(rawBody), false, nil, 25*time.Millisecond)

			if strings.Contains(row.Body, "super-secret-value") {
				t.Fatalf("captured body leaked plaintext secretValue: %s", row.Body)
			}
			if row.Body != PIIScrubSentinel {
				t.Fatalf("captured body = %q, want %q on malformed sensitive JSON", row.Body, PIIScrubSentinel)
			}
		})
	}
}

func TestBuildRow_ScrubsSecretValueWriteSchemaInvalidJSONFailClosed(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name string
		body string
	}{
		{name: "bare string", body: `"super-secret-value"`},
		{name: "wrong key", body: `{"value":"super-secret-value"}`},
		{name: "snake case key", body: `{"secret_value":"super-secret-value"}`},
		{name: "extra field", body: `{"secretValue":"super-secret-value","label":"Example NC"}`},
		{name: "empty body", body: ``},
		{name: "empty secret value", body: `{"secretValue":""}`},
		{name: "blank secret value", body: `{"secretValue":"   "}`},
		{name: "null secret value", body: `{"secretValue":null}`},
		{name: "non-string secret value", body: `{"secretValue":{"token":"super-secret-value"}}`},
	}

	for _, path := range secretValueWriteTestPaths() {
		t.Run(path.name, func(t *testing.T) {
			for _, tt := range tests {
				t.Run(tt.name, func(t *testing.T) {
					gin.SetMode(gin.TestMode)
					recorder := httptest.NewRecorder()
					c, _ := gin.CreateTestContext(recorder)

					rawBody := []byte(tt.body)
					req := httptest.NewRequest("POST", path.path, bytes.NewReader(rawBody))
					req.Header.Set("Content-Type", "application/json")
					c.Request = req
					c.Writer.WriteHeader(400)

					row := buildRow(c, "req-4", time.Unix(1700000003, 0).UTC(), rawBody, len(rawBody), false, nil, 25*time.Millisecond)

					if strings.Contains(row.Body, "super-secret-value") {
						t.Fatalf("captured body leaked plaintext secretValue: %s", row.Body)
					}
					if row.Body != PIIScrubSentinel {
						t.Fatalf("captured body = %q, want %q on schema-invalid sensitive JSON", row.Body, PIIScrubSentinel)
					}
				})
			}
		})
	}
}

func secretValueWriteTestPaths() []struct {
	name string
	path string
} {
	return []struct {
		name string
		path string
	}{
		{name: "named credential secret", path: "/api/named-credentials/ExampleNC/secrets/Token"},
		{name: "webhook secret rotation", path: "/services/data/v63.0/connector-webhooks/secrets/whsec-1/rotate"},
	}
}
