package mail

import (
	"strings"
	"testing"
	"time"
)

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

	subject, body := renderLoginCode("123456", 10*time.Minute)
	if !strings.Contains(subject, "123456") {
		t.Fatalf("subject must include code, got %q", subject)
	}
	if !strings.Contains(body, "123456") {
		t.Fatalf("body must include code, got %q", body)
	}
	if !strings.Contains(body, "10 minutes") {
		t.Fatalf("body must include human-readable TTL, got %q", body)
	}
}

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

	cases := map[string]struct {
		ttl     time.Duration
		expect  string
	}{
		"sub-minute": {ttl: 30 * time.Second, expect: "1 minutes"},
		"exact":      {ttl: 10 * time.Minute, expect: "10 minutes"},
		"with secs":  {ttl: 5*time.Minute + 29*time.Second, expect: "5 minutes"},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			_, body := renderLoginCode("000000", tc.ttl)
			if !strings.Contains(body, tc.expect) {
				t.Fatalf("body missing %q for ttl=%s, got %q", tc.expect, tc.ttl, body)
			}
		})
	}
}

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

	cases := map[string]struct {
		input string
		want  string
		err   bool
	}{
		"plain":            {input: "alice@example.test", want: "alice@example.test"},
		"trims":            {input: "  bob@example.test  ", want: "bob@example.test"},
		"display-stripped": {input: "Alice <alice@example.test>", want: "alice@example.test"},
		"empty":            {input: "", err: true},
		"whitespace":       {input: "   ", err: true},
		"malformed":        {input: "not-an-email", err: true},
		"with-spaces":      {input: "ali ce@example.test", err: true},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			got, err := normaliseRecipient(tc.input)
			if tc.err {
				if err == nil {
					t.Fatalf("expected error, got %q", got)
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if got != tc.want {
				t.Fatalf("got %q want %q", got, tc.want)
			}
		})
	}
}

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

	render := templateRenderers[TemplateLoginCode]
	if render == nil {
		t.Fatal("TemplateLoginCode renderer must be registered")
	}
	subject, body, err := render(map[string]any{"code": "654321", "ttl": 7 * time.Minute})
	if err != nil {
		t.Fatalf("render: %v", err)
	}
	if !strings.Contains(subject, "654321") || !strings.Contains(body, "654321") {
		t.Fatalf("subject=%q body=%q must mention code", subject, body)
	}
	if _, _, err := render(map[string]any{"ttl": 7 * time.Minute}); err == nil {
		t.Fatal("missing code must error")
	}
}

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

	// Sample many runs per attempt so jitter does not flake the
	// invariant: median of attempt N is below median of attempt N+1
	// up to the cap.
	prev := time.Duration(0)
	for n := 1; n <= 12; n++ {
		var sum time.Duration
		runs := 32
		for range runs {
			sum += nextBackoff(n)
		}
		avg := sum / time.Duration(runs)
		if avg > backoffMax+backoffMax/4 { // generous upper bound
			t.Fatalf("attempt %d avg %s exceeds capped budget", n, avg)
		}
		if avg < backoffBase/2 {
			t.Fatalf("attempt %d avg %s below base/2", n, avg)
		}
		if n > 1 && avg < prev/2 {
			t.Fatalf("backoff decreased dramatically between attempts %d and %d (%s vs %s)", n-1, n, prev, avg)
		}
		prev = avg
	}
}

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

	if IsPermanent(nil) {
		t.Fatal("nil must not be permanent")
	}
	transient := &SendError{Err: errSentinel("transient")}
	if IsPermanent(transient) {
		t.Fatal("default SendError must not be permanent")
	}
	permanent := &SendError{Err: errSentinel("permanent"), Permanent: true}
	if !IsPermanent(permanent) {
		t.Fatal("Permanent=true must report true")
	}
}

// errSentinel is a tiny sentinel error helper used only in tests.
type errSentinel string

func (e errSentinel) Error() string { return string(e) }
