package logging

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

	"github.com/rs/zerolog"
)

// sensitivePayload exercises every sensitive field name listed in CLAUDE.md's
// Phase 0 deliverables checklist. Field names are written in a mix of casings
// and naming styles to exercise the normalization layer.
type sensitivePayload struct {
	Password      string            `json:"password"`
	Cookie        string            `json:"cookie"`
	MamID         string            `json:"mam_id"`
	Token         string            `json:"token"`
	Secret        string            `json:"secret"`
	Authorization string            `json:"authorization"`
	APIKey        string            `json:"api_key"`
	SetCookie     string            `json:"set-cookie"`
	RawCookie     string            `json:"raw_cookie"`
	HeaderMap     map[string]string `json:"header_map"`
	Username      string            `json:"username"`
}

func TestRedactCoversAllSensitiveFields(t *testing.T) {
	p := sensitivePayload{
		Password:      "p4ssw0rd!",
		Cookie:        "sid=abc",
		MamID:         "mam_id_value",
		Token:         "tk_live_123",
		Secret:        "shhh",
		Authorization: "Bearer xyz",
		APIKey:        "ak_live_456",
		SetCookie:     "session=def",
		RawCookie:     "raw=ghi",
		HeaderMap: map[string]string{
			"Set-Cookie":    "another=jkl",
			"Authorization": "Bearer abc",
			"X-Request-Id":  "req_001",
		},
		Username: "operator",
	}

	got := Redact(p)
	js, err := json.Marshal(got)
	if err != nil {
		t.Fatalf("marshal redacted payload: %v", err)
	}
	out := string(js)

	for _, plain := range []string{
		"p4ssw0rd!", "sid=abc", "mam_id_value", "tk_live_123",
		"shhh", "Bearer xyz", "ak_live_456", "session=def", "raw=ghi",
		"another=jkl", "Bearer abc",
	} {
		if strings.Contains(out, plain) {
			t.Errorf("redacted payload still contains plaintext %q\noutput=%s", plain, out)
		}
	}

	// Non-sensitive fields must survive.
	if !strings.Contains(out, "operator") {
		t.Errorf("redaction stripped non-sensitive Username\noutput=%s", out)
	}
	if !strings.Contains(out, "req_001") {
		t.Errorf("redaction stripped non-sensitive X-Request-Id\noutput=%s", out)
	}

	// Every sensitive field must explicitly contain the placeholder.
	for _, field := range []string{
		"password", "cookie", "mam_id", "token", "secret",
		"authorization", "api_key", "set-cookie", "raw_cookie",
	} {
		needle := `"` + field + `":"` + RedactedPlaceholder + `"`
		if !strings.Contains(out, needle) {
			t.Errorf("missing redacted marker for %q (looking for %q) in %s", field, needle, out)
		}
	}
}

// TestRedactInsideZerologOutput is the end-to-end test from CLAUDE.md:
// "a test that logs a struct containing [...] fields produces output with
// each value replaced by [REDACTED]".
func TestRedactInsideZerologOutput(t *testing.T) {
	var buf bytes.Buffer
	log := Setup("debug", &buf)
	log.Info().Interface("payload", Redact(sensitivePayload{
		Password: "p", Cookie: "c", MamID: "m", Token: "t", Secret: "s",
		Authorization: "a", APIKey: "k", SetCookie: "sc", RawCookie: "rc",
		Username: "operator",
	})).Msg("test event")

	out := buf.String()
	if !strings.Contains(out, "[REDACTED]") {
		t.Fatalf("expected redaction marker in zerolog output, got: %s", out)
	}
	for _, plain := range []string{
		`"password":"p"`, `"cookie":"c"`, `"mam_id":"m"`,
		`"token":"t"`, `"secret":"s"`, `"authorization":"a"`,
		`"api_key":"k"`, `"set-cookie":"sc"`, `"raw_cookie":"rc"`,
	} {
		if strings.Contains(out, plain) {
			t.Errorf("zerolog leaked sensitive field: %q in %s", plain, out)
		}
	}
}

func TestRedactHandlesNilAndPrimitives(t *testing.T) {
	if got := Redact(nil); got != nil {
		t.Errorf("Redact(nil) = %v, want nil", got)
	}
	if got := Redact("plain"); got != "plain" {
		t.Errorf("Redact(string) = %v, want plain", got)
	}
	if got := Redact(42); got != 42 {
		t.Errorf("Redact(int) = %v, want 42", got)
	}
}

func TestRedactDoesNotLeakUnexportedFields(t *testing.T) {
	type withUnexported struct {
		Public  string
		private string //nolint:unused
	}
	w := withUnexported{Public: "shown", private: "hidden"}
	js, _ := json.Marshal(Redact(w))
	out := string(js)
	if strings.Contains(out, "hidden") {
		t.Errorf("redaction surfaced unexported field: %s", out)
	}
	if !strings.Contains(out, "shown") {
		t.Errorf("redaction dropped public field: %s", out)
	}
}

// Confirm the zerolog logger doesn't emit a default "service" field that
// pollutes downstream tests. Defensive: caught a real regression once.
func TestSetupEmitsServiceField(t *testing.T) {
	var buf bytes.Buffer
	log := Setup("info", &buf)
	log.Info().Msg("hi")
	if !strings.Contains(buf.String(), `"service":"command-center"`) {
		t.Errorf("expected service field, got: %s", buf.String())
	}
	if zerolog.GlobalLevel() != zerolog.InfoLevel {
		t.Errorf("global level not set: %v", zerolog.GlobalLevel())
	}
}
