package externaldelivery

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

	"github.com/nexuscrm/shared/pkg/constants"
)

// listColumns matches the 13-column SELECT in ListAttempts (no RequestPayload).
var listColumns = []string{
	"Id", "Channel", "Status",
	"SourceObjectApiName", "SourceRecordId", "ActionId",
	"TargetSummary",
	"ResponseStatus",
	"ErrorMessage", "ReplayOfId", "CreatedById",
	"CreatedDate", "LastModifiedDate",
}

// fullColumns matches the 14-column SELECT in GetAttempt (includes RequestPayload).
var fullColumns = []string{
	"Id", "Channel", "Status",
	"SourceObjectApiName", "SourceRecordId", "ActionId",
	"TargetSummary",
	"RequestPayload",
	"ResponseStatus",
	"ErrorMessage", "ReplayOfId", "CreatedById",
	"CreatedDate", "LastModifiedDate",
}

func TestExternalDeliveryWriteContextDropsUnsafeValuesAfterCancel(t *testing.T) {
	type unsafeKey struct{}

	sourceCtx := context.WithValue(context.Background(), constants.ContextKeyRequestID, "req-123")
	sourceCtx = context.WithValue(sourceCtx, constants.ContextKeyTraceparent, "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
	sourceCtx = context.WithValue(sourceCtx, unsafeKey{}, "request-scoped")
	sourceCtx, cancelSource := context.WithCancel(sourceCtx)
	cancelSource()

	writeCtx, cancel := externalDeliveryWriteContext(sourceCtx)
	defer cancel()

	if err := writeCtx.Err(); err != nil {
		t.Fatalf("write context inherited source cancellation: %v", err)
	}
	if _, ok := writeCtx.Deadline(); !ok {
		t.Fatal("write context is missing timeout deadline")
	}
	if got, _ := writeCtx.Value(constants.ContextKeyRequestID).(string); got != "req-123" {
		t.Fatalf("request id = %q, want req-123", got)
	}
	if got, _ := writeCtx.Value(constants.ContextKeyTraceparent).(string); got == "" {
		t.Fatal("traceparent was not preserved")
	}
	if got := writeCtx.Value(unsafeKey{}); got != nil {
		t.Fatalf("unsafe request-scoped value leaked into write context: %#v", got)
	}
}

func TestExternalDeliveryWriteContextNilSourceIsBounded(t *testing.T) {
	writeCtx, cancel := externalDeliveryWriteContext(nil)
	defer cancel()

	if err := writeCtx.Err(); err != nil {
		t.Fatalf("write context unexpectedly canceled: %v", err)
	}
	if _, ok := writeCtx.Deadline(); !ok {
		t.Fatal("write context is missing timeout deadline")
	}
}

// TestTruncateRunes covers Fix B helper.
func TestTruncateRunes(t *testing.T) {
	t.Run("short_unchanged", func(t *testing.T) {
		s := "hello"
		got := truncateRunes(s, 512)
		if got != s {
			t.Fatalf("short string changed: %q", got)
		}
	})

	t.Run("exact_boundary_unchanged", func(t *testing.T) {
		s := strings.Repeat("x", 512)
		got := truncateRunes(s, 512)
		if got != s {
			t.Fatalf("512-rune string changed")
		}
	})

	t.Run("long_truncated_to_512", func(t *testing.T) {
		s := strings.Repeat("a", 600)
		got := truncateRunes(s, 512)
		if len([]rune(got)) != 512 {
			t.Fatalf("got %d runes, want 512", len([]rune(got)))
		}
	})

	t.Run("multibyte_runes_truncated_correctly", func(t *testing.T) {
		// Each Chinese character is 3 bytes; 600 chars > 512 rune limit.
		s := strings.Repeat("中", 600)
		got := truncateRunes(s, 512)
		runes := []rune(got)
		if len(runes) != 512 {
			t.Fatalf("multibyte: got %d runes, want 512", len(runes))
		}
		// All chars must still be the same rune.
		for i, r := range runes {
			if r != '中' {
				t.Fatalf("rune[%d] = %q, want 中", i, r)
			}
		}
	})
}

func TestScrubHeadersForStorage(t *testing.T) {
	t.Run("nil_and_empty_input_return_nil", func(t *testing.T) {
		if got := ScrubHeadersForStorage(nil); got != nil {
			t.Fatalf("nil input: got %v, want nil", got)
		}
		if got := ScrubHeadersForStorage(map[string]string{}); got != nil {
			t.Fatalf("empty input: got %v, want nil", got)
		}
	})

	t.Run("only_content_type_keeps_real_value", func(t *testing.T) {
		in := map[string]string{
			"Content-Type":  "application/json",
			"Authorization": "Bearer SECRET",
			"X-Api-Key":     "KEY123",
		}
		got := ScrubHeadersForStorage(in)
		if got["Content-Type"] != "application/json" {
			t.Errorf("Content-Type = %q, want real value", got["Content-Type"])
		}
		for _, name := range []string{"Authorization", "X-Api-Key"} {
			if got[name] != StoredHeaderScrubSentinel {
				t.Errorf("%s = %q, want %q", name, got[name], StoredHeaderScrubSentinel)
			}
		}
		// Input map must not be mutated.
		if in["Authorization"] != "Bearer SECRET" {
			t.Errorf("input mutated: Authorization = %q", in["Authorization"])
		}
	})

	t.Run("content_type_match_is_case_insensitive", func(t *testing.T) {
		got := ScrubHeadersForStorage(map[string]string{"content-type": "text/plain"})
		if got["content-type"] != "text/plain" {
			t.Errorf("lowercase content-type = %q, want real value", got["content-type"])
		}
	})

	t.Run("header_names_survive_scrubbing", func(t *testing.T) {
		got := ScrubHeadersForStorage(map[string]string{"X-Custom-Secret": "v"})
		if _, ok := got["X-Custom-Secret"]; !ok {
			t.Error("scrubbed map must keep the header NAME as evidence")
		}
	})
}

// TestTargetSummaryForEmail verifies the email summary is the recipient-count
// shape and never contains raw addresses: TargetSummary is metadata-tier
// (exposed by the admin list/detail API), so addresses belong only in the
// BLOCKED RequestPayload.
func TestTargetSummaryForEmail(t *testing.T) {
	got := TargetSummaryForEmail(
		[]string{"secret-recipient@example.com", "second@example.com"},
		[]string{"cc-recipient@example.com"},
	)
	if got != "3 recipients" {
		t.Errorf("TargetSummaryForEmail = %q, want \"3 recipients\"", got)
	}
	if strings.Contains(got, "@") {
		t.Errorf("TargetSummaryForEmail %q leaks a raw address", got)
	}
	if got := TargetSummaryForEmail(nil, nil); got != "0 recipients" {
		t.Errorf("TargetSummaryForEmail(nil, nil) = %q, want \"0 recipients\"", got)
	}
}

// TestEmailPayloadJSONRoundtrip_StepFingerprint verifies that StepFingerprint
// survives a JSON marshal/unmarshal round-trip for EmailPayload.
func TestEmailPayloadJSONRoundtrip_StepFingerprint(t *testing.T) {
	p := &EmailPayload{
		To:              []string{"ops@example.com"},
		Subject:         "Test",
		Body:            "Hello",
		StepID:          "step-email-1",
		StepFingerprint: "abc123def456",
	}
	b, err := json.Marshal(p)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	var got EmailPayload
	if err := json.Unmarshal(b, &got); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if got.StepFingerprint != "abc123def456" {
		t.Errorf("StepFingerprint = %q, want abc123def456", got.StepFingerprint)
	}
	if got.StepID != "step-email-1" {
		t.Errorf("StepID = %q, want step-email-1", got.StepID)
	}
}

// TestWebhookPayloadJSONRoundtrip_StepFingerprint verifies that StepFingerprint
// survives a JSON marshal/unmarshal round-trip for WebhookPayload.
func TestWebhookPayloadJSONRoundtrip_StepFingerprint(t *testing.T) {
	p := &WebhookPayload{
		URL:             "https://example.com",
		Method:          "POST",
		Body:            "{}",
		StepID:          "step-wh-1",
		StepFingerprint: "deadbeef01234567",
	}
	b, err := json.Marshal(p)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	var got WebhookPayload
	if err := json.Unmarshal(b, &got); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if got.StepFingerprint != "deadbeef01234567" {
		t.Errorf("StepFingerprint = %q, want deadbeef01234567", got.StepFingerprint)
	}
	if got.StepID != "step-wh-1" {
		t.Errorf("StepID = %q, want step-wh-1", got.StepID)
	}
}

// TestEmailPayloadJSONRoundtrip_OmitEmptyStepFingerprint verifies that an
// EmailPayload with no StepFingerprint serialises without the field (omitempty).
func TestEmailPayloadJSONRoundtrip_OmitEmptyStepFingerprint(t *testing.T) {
	p := &EmailPayload{To: []string{"a@b.com"}, Subject: "hi"}
	b, err := json.Marshal(p)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	if strings.Contains(string(b), "stepFingerprint") {
		t.Errorf("JSON must not contain stepFingerprint when empty; got %s", string(b))
	}
}

// TestWebhookPayloadJSONRoundtrip_OmitEmptyStepFingerprint verifies that a
// WebhookPayload with no StepFingerprint serialises without the field (omitempty).
func TestWebhookPayloadJSONRoundtrip_OmitEmptyStepFingerprint(t *testing.T) {
	p := &WebhookPayload{URL: "https://example.com", Method: "POST"}
	b, err := json.Marshal(p)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	if strings.Contains(string(b), "stepFingerprint") {
		t.Errorf("JSON must not contain stepFingerprint when empty; got %s", string(b))
	}
}
