package connector_inbound

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/pingcap-inc/nexuscrm-connectors/features/connector/capabilities"
)

// ── helpers ──────────────────────────────────────────────────────────────────

const (
	testSecretName = "test-secret"
	testSecretVal  = "super-secret-value"
)

// standardCfg returns a minimal valid [SignatureSchemeConfig] for tests.
func standardCfg() SignatureSchemeConfig {
	return SignatureSchemeConfig{
		Scheme:          SchemeHMACSHA256Template,
		SigningTemplate: "{RawBody}{Timestamp}",
		TimestampHeader: "X-Hub-Timestamp",
		SignatureHeader: "X-Hub-Signature",
		SkewWindow:      5 * time.Minute,
		SecretName:      testSecretName,
	}
}

// makeSignedHeaders computes the HMAC-SHA256 of body+ts and returns headers
// with the signature in hex (default encoding).
func makeSignedHeaders(body []byte, now time.Time, secret []byte) http.Header {
	ts := strconv.FormatInt(now.UnixMilli(), 10)
	mac := hmac.New(sha256.New, secret)
	mac.Write(body)
	mac.Write([]byte(ts))
	sig := hex.EncodeToString(mac.Sum(nil))
	h := http.Header{}
	h.Set("X-Hub-Timestamp", ts)
	h.Set("X-Hub-Signature", sig)
	return h
}

// makeSignedHeadersBase64 is like makeSignedHeaders but encodes the sig as standard base64.
func makeSignedHeadersBase64(body []byte, now time.Time, secret []byte) http.Header {
	ts := strconv.FormatInt(now.UnixMilli(), 10)
	mac := hmac.New(sha256.New, secret)
	mac.Write(body)
	mac.Write([]byte(ts))
	sig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
	h := http.Header{}
	h.Set("X-Hub-Timestamp", ts)
	h.Set("X-Hub-Signature", sig)
	return h
}

func genericReceiptCfg() SignatureSchemeConfig {
	return SignatureSchemeConfig{
		Scheme:          SchemeHMACSHA256Template,
		SigningTemplate: "{RawBody}{Timestamp}",
		TimestampHeader: "X-Test-Timestamp",
		SignatureHeader: "X-Test-Signature",
		SkewWindow:      5 * time.Minute,
		SecretName:      testSecretName,
	}
}

func makeSignedHeadersWithNames(body []byte, now time.Time, secret []byte, timestampHeader, signatureHeader string) http.Header {
	ts := strconv.FormatInt(now.UnixMilli(), 10)
	mac := hmac.New(sha256.New, secret)
	mac.Write(body)
	mac.Write([]byte(ts))
	sig := hex.EncodeToString(mac.Sum(nil))
	h := http.Header{}
	h.Set(timestampHeader, ts)
	h.Set(signatureHeader, sig)
	return h
}

// headersToLower converts an http.Header to the map[string]string shape that
// [connectorcapabilities.VerifierInput.Headers] expects.
func headersToLower(h http.Header) map[string]string {
	m := make(map[string]string, len(h))
	for k, v := range h {
		if len(v) > 0 {
			m[k] = v[0]
		}
	}
	return m
}

// injectedVerifier is a [SignatureVerifier] whose secret is injected by the test —
// it bypasses DB/cipher lookup so the test exercises the signature logic in isolation.
// With the new dispatcher-injected-secret design, this mirrors the production path:
// the dispatcher resolves the secret and passes it in VerifierInput.
type injectedVerifier struct {
	cfg    SignatureSchemeConfig
	secret []byte
	now    func() time.Time
}

func newInjectedVerifier(t *testing.T, cfg SignatureSchemeConfig, secret []byte, now func() time.Time) *injectedVerifier {
	t.Helper()
	canonCfg, err := validateSchemeConfig(cfg)
	if err != nil {
		t.Fatalf("validateSchemeConfig: %v", err)
	}
	return &injectedVerifier{cfg: canonCfg, secret: secret, now: now}
}

func (v *injectedVerifier) Verify(_ context.Context, input *connectorcapabilities.VerifierInput) error {
	tsStr, ok := input.Headers[v.cfg.TimestampHeader]
	if !ok || tsStr == "" {
		return fmt.Errorf("missing timestamp header %q: %w",
			v.cfg.TimestampHeader, connectorcapabilities.ErrInvalidSignature)
	}
	sig, ok := input.Headers[v.cfg.SignatureHeader]
	if !ok || sig == "" {
		return fmt.Errorf("missing signature header %q: %w",
			v.cfg.SignatureHeader, connectorcapabilities.ErrInvalidSignature)
	}

	now := v.now()
	ts, err := parseTimestamp(tsStr, now)
	if err != nil {
		return fmt.Errorf("invalid timestamp: %w", connectorcapabilities.ErrInvalidSignature)
	}
	if err := checkSkew(ts, now, v.cfg.SkewWindow); err != nil {
		return err
	}

	signingStr := expandTemplate(v.cfg.SigningTemplate, input.Headers, input.RawBody,
		input.Method, input.RequestURI, input.RequestURL, v.cfg.TimestampHeader)
	return verifyHMACSHA256Template(signingStr, sig, v.secret, v.cfg.SignatureEncoding)
}

// ── test cases for SignatureVerifier (dispatcher-injected secret path) ───────

// TestSignatureVerifier_HappyHex validates a valid hex-encoded HMAC via the
// new dispatcher-injected-secret path (VerifierInput.Secret populated).
func TestSignatureVerifier_HappyHex(t *testing.T) {
	now := time.Now()
	body := []byte(`{"event":"test"}`)
	headers := makeSignedHeaders(body, now, []byte(testSecretVal))

	sv, err := NewSignatureVerifier(standardCfg())
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}
	sv.now = func() time.Time { return now }

	if err := sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
		Headers:    headersToLower(headers),
		RawBody:    body,
		Secret:     []byte(testSecretVal),
		Algorithm:  "hmac-sha256",
		SecretName: testSecretName,
	}); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

// TestSignatureVerifier_SHA256SecretPrefixTemplate validates providers whose
// legacy signature is SHA-256(secret || raw-body), with no timestamp header.
func TestSignatureVerifier_SHA256SecretPrefixTemplate(t *testing.T) {
	body := []byte(`[{"eventId":1,"objectId":42}]`)
	digest := sha256.New()
	digest.Write([]byte(testSecretVal))
	digest.Write(body)

	sv, err := NewSignatureVerifier(SignatureSchemeConfig{
		Scheme:            SchemeSHA256SecretPrefixTemplate,
		SigningTemplate:   "{RawBody}",
		SignatureHeader:   "X-Legacy-Signature",
		SignatureEncoding: EncodingHex,
		SecretName:        testSecretName,
	})
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}

	input := &connectorcapabilities.VerifierInput{
		Headers: map[string]string{
			"X-Legacy-Signature": hex.EncodeToString(digest.Sum(nil)),
		},
		RawBody:    body,
		Secret:     []byte(testSecretVal),
		Algorithm:  "sha256",
		SecretName: testSecretName,
	}
	if err := sv.Verify(context.Background(), input); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestSignatureVerifier_SHA256SecretPrefixTemplateRejectsTamperedBody(t *testing.T) {
	body := []byte(`[{"eventId":1,"objectId":42}]`)
	digest := sha256.New()
	digest.Write([]byte(testSecretVal))
	digest.Write(body)

	sv, err := NewSignatureVerifier(SignatureSchemeConfig{
		Scheme:            SchemeSHA256SecretPrefixTemplate,
		SigningTemplate:   "{RawBody}",
		SignatureHeader:   "X-Legacy-Signature",
		SignatureEncoding: EncodingHex,
		SecretName:        testSecretName,
	})
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}

	err = sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
		Headers: map[string]string{
			"X-Legacy-Signature": hex.EncodeToString(digest.Sum(nil)),
		},
		RawBody:    append(body, ' '),
		Secret:     []byte(testSecretVal),
		Algorithm:  "sha256",
		SecretName: testSecretName,
	})
	if !errors.Is(err, connectorcapabilities.ErrInvalidSignature) {
		t.Fatalf("want ErrInvalidSignature for tampered body, got %v", err)
	}
}

func TestSignatureVerifier_PopulatesVerifiedReceiptIdentityAndHeaderAllowlist(t *testing.T) {
	now := time.Now()
	body := []byte(`{"delivery":{"id":"delivery-123"}}`)
	headers := makeSignedHeaders(body, now, []byte(testSecretVal))

	cfg := standardCfg()
	cfg.DeliveryIdentityBodyPath = ".delivery.id"
	cfg.ReceiptHeaderAllowlist = []string{"x-receipt-trace", " X-Receipt-Trace ", "X-Request-Id"}
	sv, err := NewSignatureVerifier(cfg)
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}
	sv.now = func() time.Time { return now }

	input := &connectorcapabilities.VerifierInput{
		Headers:                      headersToLower(headers),
		RawBody:                      body,
		Secret:                       []byte(testSecretVal),
		Algorithm:                    "hmac-sha256",
		SecretName:                   testSecretName,
		EnableVerifiedIngressReceipt: true,
	}
	if err := sv.Verify(context.Background(), input); err != nil {
		t.Fatalf("Verify: %v", err)
	}
	if input.DeliveryIdentity != "delivery-123" {
		t.Fatalf("DeliveryIdentity = %q, want delivery-123", input.DeliveryIdentity)
	}
	want := []string{"X-Receipt-Trace", "X-Request-Id"}
	if fmt.Sprint(input.ReceiptHeaderAllowlist) != fmt.Sprint(want) {
		t.Fatalf("ReceiptHeaderAllowlist = %#v, want %#v", input.ReceiptHeaderAllowlist, want)
	}
}

func TestSignatureVerifier_ReceiptIdentityIsInertWhenRuntimeDisabled(t *testing.T) {
	now := time.Now()
	body := []byte(`{"delivery":{}}`)
	headers := makeSignedHeaders(body, now, []byte(testSecretVal))

	cfg := standardCfg()
	cfg.DeliveryIdentityBodyPath = ".delivery.id"
	cfg.ReceiptHeaderAllowlist = []string{"X-Receipt-Trace"}
	sv, err := NewSignatureVerifier(cfg)
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}
	sv.now = func() time.Time { return now }

	input := &connectorcapabilities.VerifierInput{
		Headers:    headersToLower(headers),
		RawBody:    body,
		Secret:     []byte(testSecretVal),
		Algorithm:  "hmac-sha256",
		SecretName: testSecretName,
	}
	if err := sv.Verify(context.Background(), input); err != nil {
		t.Fatalf("Verify with receipt runtime disabled: %v", err)
	}
	if input.DeliveryIdentity != "" {
		t.Fatalf("DeliveryIdentity = %q, want inert while receipt runtime is disabled", input.DeliveryIdentity)
	}
	if len(input.ReceiptHeaderAllowlist) != 0 {
		t.Fatalf("ReceiptHeaderAllowlist = %#v, want inert while receipt runtime is disabled", input.ReceiptHeaderAllowlist)
	}
}

func TestSignatureVerifier_PopulatesVerifiedReceiptCollectionIdentity(t *testing.T) {
	now := time.Now()
	body := []byte(`[
	  {"tenant":"beta","kind":"updated","id":20},
	  {"tenant":"alpha","kind":"created","id":"10"}
	]`)
	headers := makeSignedHeadersWithNames(body, now, []byte(testSecretVal), "X-Test-Timestamp", "X-Test-Signature")

	cfg := genericReceiptCfg()
	cfg.DeliveryIdentityCollectionPath = "."
	cfg.DeliveryIdentityCollectionKeyPaths = []string{".tenant", ".kind", ".id"}
	cfg.ReceiptHeaderAllowlist = []string{"X-Receipt-Trace"}
	sv, err := NewSignatureVerifier(cfg)
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}
	sv.now = func() time.Time { return now }

	input := &connectorcapabilities.VerifierInput{
		Headers:                      headersToLower(headers),
		RawBody:                      body,
		Secret:                       []byte(testSecretVal),
		Algorithm:                    "hmac-sha256",
		SecretName:                   testSecretName,
		EnableVerifiedIngressReceipt: true,
	}
	if err := sv.Verify(context.Background(), input); err != nil {
		t.Fatalf("Verify: %v", err)
	}
	want, err := deliveryIdentityFromCollection(body, ".", cfg.DeliveryIdentityCollectionKeyPaths)
	if err != nil {
		t.Fatalf("deliveryIdentityFromCollection: %v", err)
	}
	if input.DeliveryIdentity != want {
		t.Fatalf("DeliveryIdentity = %q, want %q", input.DeliveryIdentity, want)
	}
	if !strings.HasPrefix(input.DeliveryIdentity, "collection-sha256-v1:") {
		t.Fatalf("DeliveryIdentity = %q, want versioned collection hash", input.DeliveryIdentity)
	}
	if fmt.Sprint(input.ReceiptHeaderAllowlist) != fmt.Sprint([]string{"X-Receipt-Trace"}) {
		t.Fatalf("ReceiptHeaderAllowlist = %#v, want X-Receipt-Trace", input.ReceiptHeaderAllowlist)
	}

	reorderedBody := []byte(`[
	  {"tenant":"alpha","kind":"created","id":"10"},
	  {"tenant":"beta","kind":"updated","id":20}
	]`)
	reorderedHeaders := makeSignedHeadersWithNames(reorderedBody, now, []byte(testSecretVal), "X-Test-Timestamp", "X-Test-Signature")
	reorderedInput := &connectorcapabilities.VerifierInput{
		Headers:                      headersToLower(reorderedHeaders),
		RawBody:                      reorderedBody,
		Secret:                       []byte(testSecretVal),
		Algorithm:                    "hmac-sha256",
		SecretName:                   testSecretName,
		EnableVerifiedIngressReceipt: true,
	}
	if err := sv.Verify(context.Background(), reorderedInput); err != nil {
		t.Fatalf("Verify reordered body: %v", err)
	}
	if reorderedInput.DeliveryIdentity != input.DeliveryIdentity {
		t.Fatalf("reordered DeliveryIdentity = %q, want %q", reorderedInput.DeliveryIdentity, input.DeliveryIdentity)
	}
}

func TestSignatureVerifier_ReceiptCollectionIdentityRejectsMalformedPayload(t *testing.T) {
	now := time.Now()
	tests := []struct {
		name string
		body []byte
		want string
	}{
		{
			name: "empty_collection",
			body: []byte(`[]`),
			want: `missing delivery identity collection path "."`,
		},
		{
			name: "missing_key_part",
			body: []byte(`[{"tenant":"alpha","kind":"created"}]`),
			want: `value at ".id" is null, expected a scalar`,
		},
		{
			name: "composite_key_part",
			body: []byte(`[{"tenant":"alpha","kind":"created","id":{"nested":"10"}}]`),
			want: `expected a scalar`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			headers := makeSignedHeadersWithNames(tt.body, now, []byte(testSecretVal), "X-Test-Timestamp", "X-Test-Signature")
			cfg := genericReceiptCfg()
			cfg.DeliveryIdentityCollectionPath = "."
			cfg.DeliveryIdentityCollectionKeyPaths = []string{".tenant", ".kind", ".id"}
			sv, err := NewSignatureVerifier(cfg)
			if err != nil {
				t.Fatalf("NewSignatureVerifier: %v", err)
			}
			sv.now = func() time.Time { return now }

			err = sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
				Headers:                      headersToLower(headers),
				RawBody:                      tt.body,
				Secret:                       []byte(testSecretVal),
				Algorithm:                    "hmac-sha256",
				SecretName:                   testSecretName,
				EnableVerifiedIngressReceipt: true,
			})
			if err == nil {
				t.Fatal("Verify err = nil, want malformed collection identity rejection")
			}
			if !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("err = %q, want %q", err, tt.want)
			}
		})
	}
}

func TestNewSignatureVerifier_RejectsBodyIdentityWhenBodyIsUnsigned(t *testing.T) {
	cfg := standardCfg()
	cfg.SigningTemplate = "{Timestamp}"
	cfg.DeliveryIdentityBodyPath = ".delivery.id"
	_, err := NewSignatureVerifier(cfg)
	if err == nil {
		t.Fatal("NewSignatureVerifier err = nil, want unsigned body identity rejection")
	}
	if !errors.Is(err, ErrInvalidSchemeConfig) {
		t.Fatalf("err = %v, want ErrInvalidSchemeConfig", err)
	}
}

func TestNewSignatureVerifier_RejectsCollectionIdentityWhenBodyIsUnsigned(t *testing.T) {
	cfg := genericReceiptCfg()
	cfg.SigningTemplate = "{Timestamp}"
	cfg.DeliveryIdentityCollectionPath = "."
	cfg.DeliveryIdentityCollectionKeyPaths = []string{".tenant", ".kind", ".id"}
	_, err := NewSignatureVerifier(cfg)
	if err == nil {
		t.Fatal("NewSignatureVerifier err = nil, want unsigned collection identity rejection")
	}
	if !errors.Is(err, ErrInvalidSchemeConfig) {
		t.Fatalf("err = %v, want ErrInvalidSchemeConfig", err)
	}
}

func TestNewSignatureVerifier_RejectsAmbiguousDeliveryIdentitySources(t *testing.T) {
	cfg := genericReceiptCfg()
	cfg.DeliveryIdentityBodyPath = ".delivery.id"
	cfg.DeliveryIdentityCollectionPath = ".items"
	cfg.DeliveryIdentityCollectionKeyPaths = []string{".id"}
	_, err := NewSignatureVerifier(cfg)
	if err == nil {
		t.Fatal("NewSignatureVerifier err = nil, want ambiguous identity source rejection")
	}
	if !errors.Is(err, ErrInvalidSchemeConfig) {
		t.Fatalf("err = %v, want ErrInvalidSchemeConfig", err)
	}
}

// TestSignatureVerifier_HappyBase64 validates a valid base64-encoded HMAC.
func TestSignatureVerifier_HappyBase64(t *testing.T) {
	now := time.Now()
	body := []byte(`{"event":"test"}`)
	headers := makeSignedHeadersBase64(body, now, []byte(testSecretVal))

	cfg := standardCfg()
	cfg.SignatureEncoding = EncodingBase64
	sv, err := NewSignatureVerifier(cfg)
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}
	sv.now = func() time.Time { return now }

	if err := sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
		Headers:    headersToLower(headers),
		RawBody:    body,
		Secret:     []byte(testSecretVal),
		Algorithm:  "hmac-sha256",
		SecretName: testSecretName,
	}); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

// TestSignatureVerifier_BadSignature verifies that a tampered signature returns ErrInvalidSignature.
func TestSignatureVerifier_BadSignature(t *testing.T) {
	now := time.Now()
	body := []byte(`{"event":"test"}`)
	headers := makeSignedHeaders(body, now, []byte(testSecretVal))
	headers.Set("X-Hub-Signature", "aabbccdd00112233aabbccdd00112233aabbccdd00112233aabbccdd00112233")

	sv, err := NewSignatureVerifier(standardCfg())
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}
	sv.now = func() time.Time { return now }

	verifyErr := sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
		Headers:    headersToLower(headers),
		RawBody:    body,
		Secret:     []byte(testSecretVal),
		Algorithm:  "hmac-sha256",
		SecretName: testSecretName,
	})
	if !errors.Is(verifyErr, connectorcapabilities.ErrInvalidSignature) {
		t.Fatalf("want ErrInvalidSignature, got %v", verifyErr)
	}
}

// TestSignatureVerifier_MissingTimestampHeader verifies rejection when the timestamp header is absent.
func TestSignatureVerifier_MissingTimestampHeader(t *testing.T) {
	now := time.Now()
	body := []byte(`{}`)

	sv, err := NewSignatureVerifier(standardCfg())
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}
	sv.now = func() time.Time { return now }

	verifyErr := sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
		Headers:    map[string]string{"X-Hub-Signature": "aabb"},
		RawBody:    body,
		Secret:     []byte(testSecretVal),
		Algorithm:  "hmac-sha256",
		SecretName: testSecretName,
	})
	if !errors.Is(verifyErr, connectorcapabilities.ErrInvalidSignature) {
		t.Fatalf("want ErrInvalidSignature, got %v", verifyErr)
	}
}

// TestSignatureVerifier_MissingSignatureHeader verifies rejection when the signature header is absent.
func TestSignatureVerifier_MissingSignatureHeader(t *testing.T) {
	now := time.Now()
	body := []byte(`{}`)
	ts := strconv.FormatInt(now.UnixMilli(), 10)

	sv, err := NewSignatureVerifier(standardCfg())
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}
	sv.now = func() time.Time { return now }

	verifyErr := sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
		Headers:    map[string]string{"X-Hub-Timestamp": ts},
		RawBody:    body,
		Secret:     []byte(testSecretVal),
		Algorithm:  "hmac-sha256",
		SecretName: testSecretName,
	})
	if !errors.Is(verifyErr, connectorcapabilities.ErrInvalidSignature) {
		t.Fatalf("want ErrInvalidSignature, got %v", verifyErr)
	}
}

// TestSignatureVerifier_SkewExceeded verifies that a timestamp outside the skew window is rejected.
func TestSignatureVerifier_SkewExceeded(t *testing.T) {
	wallNow := time.Now()
	// Sign with a timestamp 10 minutes in the past (window is 5m).
	oldTime := wallNow.Add(-10 * time.Minute)
	body := []byte(`{}`)
	headers := makeSignedHeaders(body, oldTime, []byte(testSecretVal))

	sv, err := NewSignatureVerifier(standardCfg())
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}
	sv.now = func() time.Time { return wallNow }

	verifyErr := sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
		Headers:    headersToLower(headers),
		RawBody:    body,
		Secret:     []byte(testSecretVal),
		Algorithm:  "hmac-sha256",
		SecretName: testSecretName,
	})
	if !errors.Is(verifyErr, connectorcapabilities.ErrInvalidSignature) {
		t.Fatalf("want ErrInvalidSignature for skew-exceeded, got %v", verifyErr)
	}
}

// TestSignatureVerifier_NilSecret_ServerFault verifies that when input.Secret is
// nil the verifier returns a server fault (NOT ErrInvalidSignature), so the
// dispatcher treats this as a retriable 5xx rather than a terminal 401.
func TestSignatureVerifier_NilSecret_ServerFault(t *testing.T) {
	now := time.Now()
	body := []byte(`{}`)
	headers := makeSignedHeaders(body, now, []byte(testSecretVal))

	sv, err := NewSignatureVerifier(standardCfg())
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}
	sv.now = func() time.Time { return now }

	verifyErr := sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
		Headers:    headersToLower(headers),
		RawBody:    body,
		Secret:     nil, // dispatcher did not inject a secret
		Algorithm:  "hmac-sha256",
		SecretName: testSecretName,
	})
	if verifyErr == nil {
		t.Fatal("expected error for nil secret, got nil")
	}
	// MUST NOT wrap ErrInvalidSignature — a nil secret is a server-config problem,
	// not a bad-credentials verdict. Wrapping it as ErrInvalidSignature would
	// cause the dispatcher to return 401, making the provider stop retrying.
	if errors.Is(verifyErr, connectorcapabilities.ErrInvalidSignature) {
		t.Fatalf("nil secret must NOT return ErrInvalidSignature (would cause terminal 401); got %v", verifyErr)
	}
}

// TestSignatureVerifier_AlgorithmMismatch verifies that an algorithm mismatch
// returns ErrInvalidSignature (deployment misconfiguration — permanent, not retriable).
func TestSignatureVerifier_AlgorithmMismatch(t *testing.T) {
	now := time.Now()
	body := []byte(`{}`)
	headers := makeSignedHeaders(body, now, []byte(testSecretVal))

	sv, err := NewSignatureVerifier(standardCfg())
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}
	sv.now = func() time.Time { return now }

	verifyErr := sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
		Headers:    headersToLower(headers),
		RawBody:    body,
		Secret:     []byte(testSecretVal),
		Algorithm:  "hmac-sha512", // wrong algorithm
		SecretName: testSecretName,
	})
	if !errors.Is(verifyErr, connectorcapabilities.ErrInvalidSignature) {
		t.Fatalf("want ErrInvalidSignature for algorithm mismatch, got %v", verifyErr)
	}
}

// TestSignatureVerifier_AlgorithmMatches verifies the algorithmMatches helper.
func TestSignatureVerifier_AlgorithmMatches(t *testing.T) {
	// algorithmMatches(SchemeHMACSHA256Template, "hmac-sha512") must be false.
	if algorithmMatches(SchemeHMACSHA256Template, "hmac-sha512") {
		t.Fatal("expected algorithmMatches(SchemeHMACSHA256Template, hmac-sha512) == false")
	}
	// A non-empty, wrong algorithm is a definitive mismatch.
	if algorithmMatches(SchemeHMACSHA256Template, "rsa-256") {
		t.Fatal("expected algorithmMatches(SchemeHMACSHA256Template, rsa-256) == false")
	}
	// Empty resolved algorithm is accepted (legacy rows without algorithm column).
	if !algorithmMatches(SchemeHMACSHA256Template, "") {
		t.Fatal("expected algorithmMatches(SchemeHMACSHA256Template, '') == true (legacy)")
	}
	// The correct algorithm is accepted.
	if !algorithmMatches(SchemeHMACSHA256Template, "hmac-sha256") {
		t.Fatal("expected algorithmMatches(SchemeHMACSHA256Template, hmac-sha256) == true")
	}
	if !algorithmMatches(SchemeSHA256SecretPrefixTemplate, "sha256") {
		t.Fatal("expected algorithmMatches(SchemeSHA256SecretPrefixTemplate, sha256) == true")
	}
	if algorithmMatches(SchemeSHA256SecretPrefixTemplate, "hmac-sha256") {
		t.Fatal("expected legacy SHA-256 prefix scheme to reject hmac-sha256 secret metadata")
	}
}

// TestNewSignatureVerifier_RejectsUnknownTemplateToken verifies construction
// fails for a signing template with an unrecognised token.
func TestNewSignatureVerifier_RejectsUnknownTemplateToken(t *testing.T) {
	cfg := standardCfg()
	cfg.SigningTemplate = "{RawBody}{Timestamp}{Foo}" // {Foo} is unknown

	_, err := NewSignatureVerifier(cfg)
	if !errors.Is(err, ErrUnknownTemplateToken) {
		t.Fatalf("want ErrUnknownTemplateToken, got %v", err)
	}
}

// TestNewSignatureVerifier_RejectsUnknownScheme verifies construction fails
// for an unrecognised SchemeName.
func TestNewSignatureVerifier_RejectsUnknownScheme(t *testing.T) {
	cfg := standardCfg()
	cfg.Scheme = SchemeName("unknown-scheme")

	_, err := NewSignatureVerifier(cfg)
	if !errors.Is(err, ErrUnknownScheme) {
		t.Fatalf("want ErrUnknownScheme, got %v", err)
	}
}

// ── B8 fix — Method/RequestURI come from input, not headers ──────────────────

// makeSignedHeadersWithMethodURI computes HMAC-SHA256 over
// "{Method}\n{RequestURI}\n{RawBody}\n{Timestamp}" using server-observed values
// and returns the signed headers.
func makeSignedHeadersWithMethodURI(body []byte, now time.Time, secret []byte, method, requestURI string) http.Header {
	ts := strconv.FormatInt(now.UnixMilli(), 10)
	mac := hmac.New(sha256.New, secret)
	// Template is: {Method}\n{RequestURI}\n{RawBody}\n{Timestamp}
	mac.Write([]byte(method + "\n" + requestURI + "\n" + string(body) + "\n" + ts))
	sig := hex.EncodeToString(mac.Sum(nil))
	h := http.Header{}
	h.Set("X-Hub-Timestamp", ts)
	h.Set("X-Hub-Signature", sig)
	return h
}

func makeSignedHeadersWithMethodURIURL(body []byte, now time.Time, secret []byte, method, requestURI, requestURL string) http.Header {
	ts := strconv.FormatInt(now.UnixMilli(), 10)
	mac := hmac.New(sha256.New, secret)
	// Template is: {Method}\n{RequestURI}\n{RequestURL}\n{RawBody}\n{Timestamp}
	mac.Write([]byte(method + "\n" + requestURI + "\n" + requestURL + "\n" + string(body) + "\n" + ts))
	sig := hex.EncodeToString(mac.Sum(nil))
	h := http.Header{}
	h.Set("X-Hub-Timestamp", ts)
	h.Set("X-Hub-Signature", sig)
	return h
}

// TestVerify_MethodAndRequestURIComeFromInput_NotHeaders proves that the engine
// uses VerifierInput.Method / VerifierInput.RequestURI (server-observed) and
// IGNORES any caller-controlled header values for those tokens.
//
// B8 fix: before this fix, expandTemplate read headers[methodHeader] and
// headers[uriHeader]; an attacker could spoof those headers to manipulate the
// signing string and forge HMAC signatures.
func TestVerify_MethodAndRequestURIComeFromInput_NotHeaders(t *testing.T) {
	const serverMethod = "POST"
	const serverURI = "/webhook/legit"
	const forgedMethod = "GET"
	const forgedURI = "/attacker"

	now := time.Now()
	body := []byte(`{"event":"delivery"}`)
	secret := []byte(testSecretVal)

	// Sign with the SERVER-OBSERVED values — this is what the engine must verify against.
	signedHeaders := makeSignedHeadersWithMethodURI(body, now, secret, serverMethod, serverURI)
	// Inject forged caller-controlled header values that disagree with the server-observed ones.
	signedHeaders.Set("X-Forged-Method", forgedMethod)
	signedHeaders.Set("X-Forged-URI", forgedURI)

	cfg := standardCfg()
	cfg.SigningTemplate = "{Method}\n{RequestURI}\n{RawBody}\n{Timestamp}"

	sv, err := NewSignatureVerifier(cfg)
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}
	sv.now = func() time.Time { return now }

	// 1. With server-observed Method/RequestURI → MUST succeed.
	if err := sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
		Headers:    headersToLower(signedHeaders),
		RawBody:    body,
		Secret:     secret,
		Algorithm:  "hmac-sha256",
		SecretName: testSecretName,
		Method:     serverMethod,
		RequestURI: serverURI,
	}); err != nil {
		t.Fatalf("expected Verify to succeed with server-observed method/URI, got: %v", err)
	}

	// 2. With forged Method/RequestURI in input (pretending an attacker smuggled them
	//    into server-observed fields) → the signature was made with serverMethod/serverURI
	//    so the HMAC must NOT match the forged values → ErrInvalidSignature.
	forgedErr := sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
		Headers:    headersToLower(signedHeaders),
		RawBody:    body,
		Secret:     secret,
		Algorithm:  "hmac-sha256",
		SecretName: testSecretName,
		Method:     forgedMethod,
		RequestURI: forgedURI,
	})
	if !errors.Is(forgedErr, connectorcapabilities.ErrInvalidSignature) {
		t.Fatalf("expected ErrInvalidSignature when method/URI in input disagree with signed values, got: %v", forgedErr)
	}
}

func TestVerify_RequestURIAndRequestURLAreDistinctTemplateTokens(t *testing.T) {
	const serverMethod = "POST"
	const rawRequestURI = "/webhook/legit?delivery=a%2Fb"
	const fullRequestURL = "https://public.example.test/base/webhook/legit?delivery=a%2Fb"

	now := time.Now()
	body := []byte(`{"event":"delivery"}`)
	secret := []byte(testSecretVal)

	signedHeaders := makeSignedHeadersWithMethodURIURL(body, now, secret, serverMethod, rawRequestURI, fullRequestURL)

	cfg := standardCfg()
	cfg.SigningTemplate = "{Method}\n{RequestURI}\n{RequestURL}\n{RawBody}\n{Timestamp}"

	sv, err := NewSignatureVerifier(cfg)
	if err != nil {
		t.Fatalf("NewSignatureVerifier: %v", err)
	}
	sv.now = func() time.Time { return now }

	if err := sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
		Headers:    headersToLower(signedHeaders),
		RawBody:    body,
		Secret:     secret,
		Algorithm:  "hmac-sha256",
		SecretName: testSecretName,
		Method:     serverMethod,
		RequestURI: rawRequestURI,
		RequestURL: fullRequestURL,
	}); err != nil {
		t.Fatalf("expected Verify to succeed with distinct RequestURI/RequestURL values, got: %v", err)
	}

	err = sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
		Headers:    headersToLower(signedHeaders),
		RawBody:    body,
		Secret:     secret,
		Algorithm:  "hmac-sha256",
		SecretName: testSecretName,
		Method:     serverMethod,
		RequestURI: fullRequestURL,
		RequestURL: fullRequestURL,
	})
	if !errors.Is(err, connectorcapabilities.ErrInvalidSignature) {
		t.Fatalf("expected ErrInvalidSignature when RequestURI is not the raw request target, got: %v", err)
	}
}

// ── B9 fix — SignatureEncoding allowlist ──────────────────────────────────────

// TestValidateSchemeConfig_RejectsUnknownEncoding verifies that an unknown
// SignatureEncoding value is rejected at construction with ErrInvalidSchemeConfig,
// preventing a pack from silently shipping a config that fails every delivery.
func TestValidateSchemeConfig_RejectsUnknownEncoding(t *testing.T) {
	cfg := SignatureSchemeConfig{
		Scheme:            SchemeHMACSHA256Template,
		SecretName:        testSecretName,
		SignatureHeader:   "X-Hub-Signature",
		TimestampHeader:   "X-Hub-Timestamp",
		SkewWindow:        5 * time.Minute,
		SigningTemplate:   "{RawBody}",
		SignatureEncoding: "garbage",
	}
	_, err := validateSchemeConfig(cfg)
	if err == nil {
		t.Fatal("expected error for unknown SignatureEncoding, got nil")
	}
	if !errors.Is(err, ErrInvalidSchemeConfig) {
		t.Fatalf("expected ErrInvalidSchemeConfig, got %v", err)
	}
}

// TestValidateSchemeConfig_KnownEncodings verifies that the valid encoding
// values ("", "hex", "base64") do NOT trigger the allowlist rejection.
func TestValidateSchemeConfig_KnownEncodings(t *testing.T) {
	encodings := []SignatureEncoding{"", EncodingHex, EncodingBase64}
	for _, enc := range encodings {
		cfg := SignatureSchemeConfig{
			Scheme:            SchemeHMACSHA256Template,
			SecretName:        testSecretName,
			SignatureHeader:   "X-Hub-Signature",
			TimestampHeader:   "X-Hub-Timestamp",
			SkewWindow:        5 * time.Minute,
			SigningTemplate:   "{RawBody}",
			SignatureEncoding: enc,
		}
		if _, err := validateSchemeConfig(cfg); err != nil {
			t.Fatalf("encoding %q should be valid, got: %v", enc, err)
		}
	}
}

// ── B10 fix — header-name canonicalization at construction ────────────────────

// TestNewSignatureVerifier_StoresCanonicalizedCfg verifies that validateSchemeConfig
// normalizes header-name fields via http.CanonicalHeaderKey so that the stored cfg
// carries canonical form regardless of what casing the pack manifest used.
func TestNewSignatureVerifier_StoresCanonicalizedCfg(t *testing.T) {
	tests := []struct {
		name           string
		inputTimestamp string
		inputSignature string
		wantTimestamp  string
		wantSignature  string
	}{
		{
			name:           "lowercase → canonical",
			inputTimestamp: "x-hub-timestamp",
			inputSignature: "x-hub-signature",
			wantTimestamp:  "X-Hub-Timestamp",
			wantSignature:  "X-Hub-Signature",
		},
		{
			name:           "already canonical → unchanged",
			inputTimestamp: "X-Hub-Timestamp",
			inputSignature: "X-Hub-Signature",
			wantTimestamp:  "X-Hub-Timestamp",
			wantSignature:  "X-Hub-Signature",
		},
		{
			name:           "whitespace + lowercase → trimmed and canonical",
			inputTimestamp: "  x-hub-timestamp  ",
			inputSignature: "  x-hub-signature  ",
			wantTimestamp:  "X-Hub-Timestamp",
			wantSignature:  "X-Hub-Signature",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := SignatureSchemeConfig{
				Scheme:          SchemeHMACSHA256Template,
				SigningTemplate: "{RawBody}{Timestamp}",
				TimestampHeader: tt.inputTimestamp,
				SignatureHeader: tt.inputSignature,
				SkewWindow:      5 * time.Minute,
				SecretName:      testSecretName,
			}
			sv, err := NewSignatureVerifier(cfg)
			if err != nil {
				t.Fatalf("NewSignatureVerifier: %v", err)
			}
			if sv.cfg.TimestampHeader != tt.wantTimestamp {
				t.Errorf("stored TimestampHeader = %q, want %q", sv.cfg.TimestampHeader, tt.wantTimestamp)
			}
			if sv.cfg.SignatureHeader != tt.wantSignature {
				t.Errorf("stored SignatureHeader = %q, want %q", sv.cfg.SignatureHeader, tt.wantSignature)
			}
		})
	}
}

// TestVerify_CanonicalizesHeaderNamesFromConfig verifies the end-to-end fix for B10:
// a pack manifest writing lowercase header names in the config must still match the
// canonical-cased keys produced by the dispatcher's flattenHeaders (which copies
// from http.Header — Go's http.Header stores keys in canonical form).
func TestVerify_CanonicalizesHeaderNamesFromConfig(t *testing.T) {
	now := time.Now()
	body := []byte(`{"event":"test"}`)
	secret := []byte(testSecretVal)

	// Dispatcher produces canonical-cased keys (mirrors flattenHeaders behavior).
	signedHeaders := makeSignedHeaders(body, now, secret)
	canonicalInput := headersToLower(signedHeaders) // X-Hub-Timestamp, X-Hub-Signature

	t.Run("lowercase config + canonical input headers → Verify succeeds", func(t *testing.T) {
		cfg := standardCfg()
		cfg.TimestampHeader = "x-hub-timestamp" // lowercase — legal HTTP, must be canonicalized
		cfg.SignatureHeader = "x-hub-signature"

		sv, err := NewSignatureVerifier(cfg)
		if err != nil {
			t.Fatalf("NewSignatureVerifier: %v", err)
		}
		sv.now = func() time.Time { return now }

		if err := sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
			Headers:    canonicalInput,
			RawBody:    body,
			Secret:     secret,
			Algorithm:  "hmac-sha256",
			SecretName: testSecretName,
		}); err != nil {
			t.Fatalf("expected Verify to succeed, got: %v", err)
		}
	})

	t.Run("canonical config + canonical input headers → Verify succeeds (unchanged behavior)", func(t *testing.T) {
		sv, err := NewSignatureVerifier(standardCfg())
		if err != nil {
			t.Fatalf("NewSignatureVerifier: %v", err)
		}
		sv.now = func() time.Time { return now }

		if err := sv.Verify(context.Background(), &connectorcapabilities.VerifierInput{
			Headers:    canonicalInput,
			RawBody:    body,
			Secret:     secret,
			Algorithm:  "hmac-sha256",
			SecretName: testSecretName,
		}); err != nil {
			t.Fatalf("expected Verify to succeed, got: %v", err)
		}
	})
}

// ── Fix 1 — TimestampHeader == SignatureHeader collision ──────────────────────

// TestValidateSchemeConfig_RejectsTimestampHeaderEqualSignatureHeader verifies
// that validateSchemeConfig rejects a config where TimestampHeader and
// SignatureHeader are identical after canonicalization. Reading both from the
// same header slot causes Verify to parse the HMAC digest as a timestamp →
// timestamp parse error → ErrInvalidSignature for every delivery (terminal 401,
// silent-drop class).
func TestValidateSchemeConfig_RejectsTimestampHeaderEqualSignatureHeader(t *testing.T) {
	cases := []struct {
		name      string
		tsHeader  string
		sigHeader string
	}{
		{name: "exact_match", tsHeader: "X-Hub-Sig", sigHeader: "X-Hub-Sig"},
		{name: "case_variants_canonicalize_equal", tsHeader: "X-Hub-Sig", sigHeader: "x-hub-sig"},
		{name: "whitespace_trimmed_then_equal", tsHeader: "  X-Hub-Sig  ", sigHeader: "x-hub-sig"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			cfg := SignatureSchemeConfig{
				Scheme:          SchemeHMACSHA256Template,
				SecretName:      testSecretName,
				TimestampHeader: tc.tsHeader,
				SignatureHeader: tc.sigHeader,
				SkewWindow:      5 * time.Minute,
				SigningTemplate: "{RawBody}{Timestamp}",
			}
			_, err := validateSchemeConfig(cfg)
			if err == nil {
				t.Fatal("expected error for TimestampHeader == SignatureHeader, got nil")
			}
			if !errors.Is(err, ErrInvalidSchemeConfig) {
				t.Fatalf("expected ErrInvalidSchemeConfig, got %v", err)
			}
		})
	}
}

// ── Fix 2 — SecretName whitespace-only ───────────────────────────────────────

// TestValidateSchemeConfig_RejectsWhitespaceOnlySecretName verifies that a
// SecretName consisting entirely of whitespace is rejected at construction with
// ErrInvalidSchemeConfig. A blank NamedCredential developer name causes secret
// resolution to fail from the first delivery.
func TestValidateSchemeConfig_RejectsWhitespaceOnlySecretName(t *testing.T) {
	cases := []struct {
		name       string
		secretName string
	}{
		{name: "single_space", secretName: " "},
		{name: "tab_newline", secretName: "\t\n"},
		{name: "many_spaces", secretName: "   "},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			cfg := SignatureSchemeConfig{
				Scheme:          SchemeHMACSHA256Template,
				SecretName:      tc.secretName,
				TimestampHeader: "X-Timestamp",
				SignatureHeader: "X-Signature",
				SkewWindow:      5 * time.Minute,
				SigningTemplate: "{RawBody}",
			}
			_, err := validateSchemeConfig(cfg)
			if err == nil {
				t.Fatal("expected error for whitespace-only SecretName, got nil")
			}
			if !errors.Is(err, ErrInvalidSchemeConfig) {
				t.Fatalf("expected ErrInvalidSchemeConfig, got %v", err)
			}
		})
	}
}

// TestNewSignatureVerifier_RejectsWhitespaceOnlyRequiredHeaders verifies the fix for B11:
// a whitespace-only TimestampHeader or SignatureHeader (e.g. "   ", "\t \n") must be
// rejected at construction time with ErrInvalidSchemeConfig. Before the fix,
// validateSchemeConfig checked for literal "" BEFORE TrimSpace, so "   " passed the
// empty-check, was trimmed to "" by CanonicalHeaderKey, and was silently stored.
// Runtime Verify then looked up input.Headers[""] → miss → ErrInvalidSignature →
// every legitimate delivery for that pack rejected as a terminal 401 (silent-drop class).
func TestNewSignatureVerifier_RejectsWhitespaceOnlyRequiredHeaders(t *testing.T) {
	cases := []struct {
		name string
		cfg  SignatureSchemeConfig
	}{
		{
			name: "timestamp_whitespace_only",
			cfg: SignatureSchemeConfig{
				Scheme:          SchemeHMACSHA256Template,
				SecretName:      "sec",
				TimestampHeader: "   ", // spaces only
				SignatureHeader: "X-Signature",
				SkewWindow:      5 * time.Minute,
				SigningTemplate: "{RawBody}",
			},
		},
		{
			name: "signature_whitespace_only",
			cfg: SignatureSchemeConfig{
				Scheme:          SchemeHMACSHA256Template,
				SecretName:      "sec",
				TimestampHeader: "X-Timestamp",
				SignatureHeader: "\t \n", // tabs + newline + space
				SkewWindow:      5 * time.Minute,
				SigningTemplate: "{RawBody}",
			},
		},
		{
			name: "both_whitespace_only",
			cfg: SignatureSchemeConfig{
				Scheme:          SchemeHMACSHA256Template,
				SecretName:      "sec",
				TimestampHeader: " ",
				SignatureHeader: " ",
				SkewWindow:      5 * time.Minute,
				SigningTemplate: "{RawBody}",
			},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			_, err := NewSignatureVerifier(tc.cfg)
			if err == nil {
				t.Fatal("expected error for whitespace-only required header, got nil")
			}
			if !errors.Is(err, ErrInvalidSchemeConfig) {
				t.Fatalf("expected ErrInvalidSchemeConfig, got %v", err)
			}
		})
	}
}
