package webhookdemo

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"
)

type receiver struct {
	mu      sync.Mutex
	seen    map[string]bool
	applied int
	now     func() time.Time
	secret  []byte
}

func sign(secret []byte, stamp string, body []byte) string {
	m := hmac.New(sha256.New, secret)
	m.Write([]byte(stamp))
	m.Write([]byte("."))
	m.Write(body)
	return hex.EncodeToString(m.Sum(nil))
}
func (s *receiver) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		w.Header().Set("Allow", "POST")
		http.Error(w, "method", 405)
		return
	}
	r.Body = http.MaxBytesReader(w, r.Body, 1024)
	defer r.Body.Close()
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "body rejected", 413)
		return
	}
	stamp := r.Header.Get("X-Timestamp")
	sec, err := strconv.ParseInt(stamp, 10, 64)
	now := s.now().Unix()
	if err != nil || sec < now-300 || sec > now+30 {
		http.Error(w, "stale timestamp", 401)
		return
	}
	got, err := hex.DecodeString(r.Header.Get("X-Signature"))
	if err != nil {
		http.Error(w, "signature", 401)
		return
	}
	want, _ := hex.DecodeString(sign(s.secret, stamp, body))
	if !hmac.Equal(got, want) {
		http.Error(w, "signature", 401)
		return
	}
	var event struct {
		ID   string `json:"id"`
		Type string `json:"type"`
	}
	if err = json.Unmarshal(body, &event); err != nil || event.ID == "" || event.Type != "created" {
		http.Error(w, "event", 400)
		return
	}
	s.mu.Lock()
	if !s.seen[event.ID] {
		s.seen[event.ID] = true
		s.applied++
	}
	s.mu.Unlock()
	w.WriteHeader(204)
}
func TestSignatureAndDuplicate(t *testing.T) {
	now := time.Unix(1900000000, 0)
	s := &receiver{seen: map[string]bool{}, now: func() time.Time { return now }, secret: []byte("public-test-secret")}
	body := `{"id":"evt-1","type":"created"}`
	stamp := strconv.FormatInt(now.Unix(), 10)
	call := func(payload, ts, sig string) int {
		r := httptest.NewRequest("POST", "/", strings.NewReader(payload))
		r.Header.Set("X-Timestamp", ts)
		r.Header.Set("X-Signature", sig)
		w := httptest.NewRecorder()
		s.ServeHTTP(w, r)
		return w.Code
	}
	signature := sign(s.secret, stamp, []byte(body))
	if got := call(body+" ", stamp, signature); got != 401 {
		t.Fatal(got)
	}
	old := strconv.FormatInt(now.Unix()-301, 10)
	if got := call(body, old, sign(s.secret, old, []byte(body))); got != 401 {
		t.Fatal(got)
	}
	if got := call(strings.Repeat("x", 1025), stamp, signature); got != 413 {
		t.Fatal(got)
	}
	var wg sync.WaitGroup
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			if got := call(body, stamp, signature); got != 204 {
				t.Errorf("%d", got)
			}
		}()
	}
	wg.Wait()
	if s.applied != 1 {
		t.Fatalf("applied=%d", s.applied)
	}
	t.Log("raw-body tampering, timestamp window, body limit, and concurrent duplicate suppression verified")
}

func TestOutOfOrderSnapshot(t *testing.T) {
	// This works for complete versioned snapshots, not for increment/decrement events.
	version, state := 0, ""
	apply := func(v int, next string) bool {
		if v <= version {
			return false
		}
		version, state = v, next
		return true
	}
	newer, older := apply(2, "closed"), apply(1, "open")
	if !newer || older || state != "closed" {
		t.Fatal(version, state)
	}
	t.Logf("apply v2=%v; apply stale v1=%v; state=%s", newer, older, state)
}
