package mxgateway

import (
	"errors"
	"strings"
	"testing"

	pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
)

// TestScrubReplyStringsRedactsEveryOccurrence covers the multi-occurrence case:
// one secret appearing across ProtocolStatus.Message, DiagnosticMessage, and every
// Statuses[].DiagnosticText must be fully redacted with no residue.
func TestScrubReplyStringsRedactsEveryOccurrence(t *testing.T) {
	const secret = "hunter2"
	reply := &pb.MxCommandReply{
		ProtocolStatus:    &pb.ProtocolStatus{Message: "rejected hunter2 then hunter2 again"},
		DiagnosticMessage: "echoed hunter2 back",
		Statuses: []*pb.MxStatusProxy{
			{DiagnosticText: "first hunter2"},
			{DiagnosticText: "second hunter2 and hunter2"},
		},
	}

	scrubbed := scrubReplyStrings(reply, []string{secret})

	if strings.Contains(scrubbed.GetProtocolStatus().GetMessage(), secret) {
		t.Fatalf("ProtocolStatus.Message still contains the secret: %q", scrubbed.GetProtocolStatus().GetMessage())
	}
	if strings.Contains(scrubbed.GetDiagnosticMessage(), secret) {
		t.Fatalf("DiagnosticMessage still contains the secret: %q", scrubbed.GetDiagnosticMessage())
	}
	for i, status := range scrubbed.GetStatuses() {
		if strings.Contains(status.GetDiagnosticText(), secret) {
			t.Fatalf("Statuses[%d].DiagnosticText still contains the secret: %q", i, status.GetDiagnosticText())
		}
	}
	if !strings.Contains(scrubbed.GetProtocolStatus().GetMessage(), redactedSecretMarker) {
		t.Fatalf("ProtocolStatus.Message missing redaction marker: %q", scrubbed.GetProtocolStatus().GetMessage())
	}

	// The original reply must be untouched (scrubReplyStrings clones).
	if !strings.Contains(reply.GetDiagnosticMessage(), secret) {
		t.Fatal("scrubReplyStrings mutated the original reply instead of cloning it")
	}
}

// TestScrubReplyStringsRedactsOverlappingSecrets covers two secrets where one is a
// substring of the other: both must be fully redacted, with no partial leak of the
// longer secret's non-shared remainder.
func TestScrubReplyStringsRedactsOverlappingSecrets(t *testing.T) {
	const shortSecret = "pass"
	const longSecret = "password123"
	reply := &pb.MxCommandReply{
		DiagnosticMessage: "value was password123 and also pass",
	}

	scrubbed := scrubReplyStrings(reply, []string{longSecret, shortSecret})

	got := scrubbed.GetDiagnosticMessage()
	if strings.Contains(got, shortSecret) {
		t.Fatalf("scrubbed message still contains a secret substring %q: %q", shortSecret, got)
	}
	if strings.Contains(got, longSecret) {
		t.Fatalf("scrubbed message still contains %q: %q", longSecret, got)
	}
	// "123" is the longer secret's remainder past the shared "pass" prefix; it must
	// not survive as a partial leak.
	if strings.Contains(got, "123") {
		t.Fatalf("scrubbed message leaked the longer secret's remainder: %q", got)
	}
}

// TestRedactSecretsEmptyOrNilLeavesErrorUnchanged confirms the no-secret paths keep
// the original typed error verbatim (no wrapping, no scrubbed clone).
func TestRedactSecretsEmptyOrNilLeavesErrorUnchanged(t *testing.T) {
	base := &MxAccessError{Reply: &pb.MxCommandReply{DiagnosticMessage: "boom"}}

	if got := redactSecrets(base); got != error(base) {
		t.Fatalf("redactSecrets with no secrets = %v, want the original error unchanged", got)
	}
	if got := redactSecrets(base, ""); got != error(base) {
		t.Fatalf("redactSecrets with only an empty secret = %v, want the original error unchanged", got)
	}
	if got := redactSecrets(nil, "secret"); got != nil {
		t.Fatalf("redactSecrets(nil, ...) = %v, want nil", got)
	}
}

// TestRedactSecretsRebuildsTypedCommandError confirms a *CommandError (non-MXAccess
// path) is rebuilt with a scrubbed Status and Reply, and errors.As still reaches it.
func TestRedactSecretsRebuildsTypedCommandError(t *testing.T) {
	const secret = "topSecretValue"
	base := &CommandError{
		Op:     "write secured",
		Status: &pb.ProtocolStatus{Message: "rejected topSecretValue"},
		Reply:  &pb.MxCommandReply{DiagnosticMessage: "echoed topSecretValue"},
	}

	redacted := redactSecrets(base, secret)

	var cmdErr *CommandError
	if !errors.As(redacted, &cmdErr) {
		t.Fatalf("redactSecrets result %T does not unwrap to *CommandError", redacted)
	}
	if strings.Contains(cmdErr.Status.GetMessage(), secret) {
		t.Fatalf("CommandError.Status.Message leaked the secret: %q", cmdErr.Status.GetMessage())
	}
	if strings.Contains(cmdErr.Reply.GetDiagnosticMessage(), secret) {
		t.Fatalf("CommandError.Reply.DiagnosticMessage leaked the secret: %q", cmdErr.Reply.GetDiagnosticMessage())
	}
	if strings.Contains(redacted.Error(), secret) {
		t.Fatalf("rendered error leaked the secret: %q", redacted.Error())
	}
}
