package notifier

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"regexp"
	"strings"
	"testing"
	"time"

	"github.com/google/uuid"
)

var hex64Re = regexp.MustCompile(`^[0-9a-f]{64}$`)

func TestHashToken(t *testing.T) {
	// sha256("") == e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
	emptyHash := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"

	t.Run("determinism same input same output", func(t *testing.T) {
		a := hashToken("approval-token-123")
		b := hashToken("approval-token-123")
		if a != b {
			t.Fatalf("hashToken not deterministic: %q vs %q", a, b)
		}
	})

	t.Run("empty string known sha256", func(t *testing.T) {
		got := hashToken("")
		if got != emptyHash {
			t.Fatalf("hashToken(\"\") = %q, want %q", got, emptyHash)
		}
	})

	t.Run("different inputs different outputs", func(t *testing.T) {
		a := hashToken("one")
		b := hashToken("two")
		if a == b {
			t.Fatalf("hashToken collided for different inputs: %q", a)
		}
	})

	t.Run("output is valid 64-char hex", func(t *testing.T) {
		for _, in := range []string{"", "abc", "some-longer-token-value-xyz"} {
			got := hashToken(in)
			if !hex64Re.MatchString(got) {
				t.Fatalf("hashToken(%q) = %q, not 64-char lowercase hex", in, got)
			}
			// also must decode cleanly to 32 bytes
			b, err := hex.DecodeString(got)
			if err != nil {
				t.Fatalf("hashToken(%q) decode error: %v", in, err)
			}
			if len(b) != 32 {
				t.Fatalf("hashToken(%q) decoded len = %d, want 32", in, len(b))
			}
		}
	})
}

func TestGenerateApprovalToken(t *testing.T) {
	id := uuid.New()

	t.Run("output is 64-char hex", func(t *testing.T) {
		tok := generateApprovalToken(id, "super-secret")
		if !hex64Re.MatchString(tok) {
			t.Fatalf("generateApprovalToken = %q, not 64-char lowercase hex", tok)
		}
		b, err := hex.DecodeString(tok)
		if err != nil {
			t.Fatalf("decode error: %v", err)
		}
		if len(b) != 32 {
			t.Fatalf("decoded len = %d, want 32 (sha256)", len(b))
		}
	})

	t.Run("empty secret falls back to dev secret no panic", func(t *testing.T) {
		tok := generateApprovalToken(id, "")
		if tok == "" {
			t.Fatal("empty secret produced empty token")
		}
		if !hex64Re.MatchString(tok) {
			t.Fatalf("empty-secret token %q not 64-char hex", tok)
		}
	})

	// Non-determinism: time.Now().UnixNano() is embedded in the HMAC message,
	// so two calls with identical inputs produce different tokens (unless the
	// clock has nanosecond-identical reads, which we do not assert against).
	t.Run("same inputs twice produce different tokens (time-based)", func(t *testing.T) {
		a := generateApprovalToken(id, "stable-secret")
		b := generateApprovalToken(id, "stable-secret")
		if a == b {
			// Not a hard failure (clock granularity), but document expectation.
			t.Logf("note: two immediate calls returned identical token %q — clock resolution collapsed", a)
		}
	})

	t.Run("different secrets produce different tokens", func(t *testing.T) {
		a := generateApprovalToken(id, "secret-a")
		b := generateApprovalToken(id, "secret-b")
		if a == b {
			t.Fatalf("different secrets produced same token %q", a)
		}
	})

	// HMAC correctness: re-derive the token with the same secret + approvalID
	// using a freshly captured timestamp window is impossible because we don't
	// observe the embedded timestamp. Instead, verify the token is a valid
	// HMAC-SHA256 by brute-forcing a small time window around now: reconstruct
	// mac(secret, approvalID || ts) for ts in [now-N, now] and confirm one
	// matches. This proves the token genuinely is an HMAC over (approvalID, ts)
	// with the supplied secret.
	t.Run("token is HMAC-SHA256 over approvalID+timestamp with secret", func(t *testing.T) {
		secret := "hmac-verify-secret"
		before := nowNanos()
		tok := generateApprovalToken(id, secret)
		after := nowNanos()

		// The token's embedded ts is captured inside generateApprovalToken,
		// which is called after `before` was sampled — so ts ∈ [before, after].
		// Add a tiny ±band to absorb scheduler jitter on loaded runners.
		lo := before - 10_000
		hi := after + 10_000
		matched := false
		for ts := lo; ts <= hi; ts++ {
			mac := hmac.New(sha256.New, []byte(secret))
			mac.Write([]byte(id.String()))
			mac.Write([]byte(formatInt(ts)))
			cand := hex.EncodeToString(mac.Sum(nil))
			if hmac.Equal([]byte(cand), []byte(tok)) {
				matched = true
				break
			}
		}
		if !matched {
			t.Fatalf("token %q did not match any HMAC in window [%d,%d]; not a valid HMAC-SHA256 over approvalID+ts", tok, lo, hi)
		}
	})

	t.Run("empty secret HMAC uses dev fallback secret", func(t *testing.T) {
		before := nowNanos()
		tok := generateApprovalToken(id, "")
		after := nowNanos()
		dev := "dev-secret-do-not-use-in-prod"
		lo := before - 10_000
		hi := after + 10_000
		matched := false
		for ts := lo; ts <= hi; ts++ {
			mac := hmac.New(sha256.New, []byte(dev))
			mac.Write([]byte(id.String()))
			mac.Write([]byte(formatInt(ts)))
			cand := hex.EncodeToString(mac.Sum(nil))
			if hmac.Equal([]byte(cand), []byte(tok)) {
				matched = true
				break
			}
		}
		if !matched {
			t.Fatalf("empty-secret token %q did not match dev-fallback HMAC", tok)
		}
	})

	// sanity: token should not leak the secret in plaintext
	t.Run("token does not contain secret substring", func(t *testing.T) {
		secret := "leakcheck-secret-xyz"
		tok := generateApprovalToken(id, secret)
		if strings.Contains(tok, secret) {
			t.Fatalf("token %q contains secret substring %q", tok, secret)
		}
	})
}

// nowNanos returns the current nanosecond count, matching the time source
// used by generateApprovalToken (time.Now().UnixNano()).
func nowNanos() int64 {
	return time.Now().UnixNano()
}

// formatInt mirrors fmt.Sprintf("%d", ...) used by the production code so the
// re-derivation in tests is byte-identical.
func formatInt(n int64) string {
	return fmt.Sprintf("%d", n)
}
