package main

import (
	"net/http/httptest"
	"strconv"
	"strings"
	"sync"
	"testing"
)

func TestInput(t *testing.T) {
	for name, body := range map[string]string{"duplicate": `{"name":"a","name":"b"}`, "unknown": `{"name":"a","role":"admin"}`, "case": `{"NAME":"a"}`, "extra": `{"name":"a"}{}`, "empty": `{"name":""}`} {
		t.Run(name, func(t *testing.T) {
			v := &Verifier{seen: map[string]int64{}}
			if got := status(v, request(body, "aaaaaaaaaaaaaaaa")); got != 400 {
				t.Fatal(got)
			}
		})
	}
}
func TestSignatureFieldsAndTime(t *testing.T) {
	body := []byte(`{"name":"ok"}`)
	for _, field := range []string{"host", "method", "path", "query", "time", "duplicate-header"} {
		v := &Verifier{seen: map[string]int64{}}
		r := request(string(body), "aaaaaaaaaaaaaaaa")
		switch field {
		case "host":
			r.Host = "other.example"
		case "method":
			r.Method = "DELETE"
		case "path":
			r.URL.Path = "/other"
		case "query":
			r.URL.RawQuery = "x=1"
		case "time":
			r.Header.Set("X-Time", strconv.FormatInt(testTime-61, 10))
			r.Header.Set("X-Signature", signature(r, body))
		case "duplicate-header":
			r.Header.Add("X-Time", r.Header.Get("X-Time"))
		}
		if err := v.Verify(r, body, testTime); err == nil {
			t.Fatal("accepted", field)
		}
	}
}
func TestReplayAtomic(t *testing.T) {
	v := &Verifier{seen: map[string]int64{}}
	var wg sync.WaitGroup
	ch := make(chan int, 2)
	for range 2 {
		wg.Go(func() { ch <- status(v, request(`{"name":"ok"}`, "aaaaaaaaaaaaaaaa")) })
	}
	wg.Wait()
	close(ch)
	counts := map[int]int{}
	for c := range ch {
		counts[c]++
	}
	if counts[201] != 1 || counts[401] != 1 {
		t.Fatal(counts)
	}
}
func TestCORS(t *testing.T) {
	v := &Verifier{seen: map[string]int64{}}
	r := httptest.NewRequest("OPTIONS", "https://api.example/items", nil)
	r.Header.Set("Origin", "https://app.example")
	r.Header.Set("Access-Control-Request-Method", "POST")
	r.Header.Set("Access-Control-Request-Headers", "X-Evil")
	if c := status(v, r); c != 403 {
		t.Fatal(c)
	}
	r = request(`{"name":"ok"}`, "aaaaaaaaaaaaaaaa")
	r.Header.Set("Origin", "https://other.example")
	w := httptest.NewRecorder()
	v.ServeHTTP(w, r)
	if w.Code != 403 || w.Header().Get("Access-Control-Allow-Origin") != "" {
		t.Fatal(w.Code, w.Header())
	}
	if !strings.Contains(w.Header().Get("Vary"), "Origin") {
		t.Fatal("vary")
	}
}
func TestBadMACDoesNotConsumeNonce(t *testing.T) {
	v := &Verifier{seen: map[string]int64{}}
	r := request(`{"name":"ok"}`, "aaaaaaaaaaaaaaaa")
	r.Header.Set("X-Signature", "bad")
	if status(v, r) != 401 {
		t.Fatal("bad mac")
	}
	if status(v, request(`{"name":"ok"}`, "aaaaaaaaaaaaaaaa")) != 201 {
		t.Fatal("nonce poisoned")
	}
}

func TestTimeAndCapacityBoundaries(t *testing.T) {
	body := []byte(`{"name":"ok"}`)
	for _, delta := range []int64{-61, -60, 60, 61} {
		v := &Verifier{seen: map[string]int64{}}
		r := request(string(body), "aaaaaaaaaaaaaaaa")
		r.Header.Set("X-Time", strconv.FormatInt(testTime+delta, 10))
		r.Header.Set("X-Signature", signature(r, body))
		err := v.Verify(r, body, testTime)
		if (err == nil) != (delta >= -60 && delta <= 60) {
			t.Fatalf("delta %d: %v", delta, err)
		}
	}
	v := &Verifier{seen: map[string]int64{}}
	r := request(string(body), "aaaaaaaaaaaaaaaa")
	if err := v.Verify(r, body, testTime); err != nil {
		t.Fatal(err)
	}
	if err := v.Verify(r, body, testTime+60); err == nil || err.Error() != "replay" {
		t.Fatal("edge replay", err)
	}
	if err := v.Verify(r, body, testTime+61); err == nil || err.Error() != "time" {
		t.Fatal("expired request", err)
	}
	v = &Verifier{seen: map[string]int64{}}
	for i := range 1024 {
		v.seen[strconv.Itoa(i)] = testTime + 60
	}
	if err := v.Verify(request(string(body), "bbbbbbbbbbbbbbbb"), body, testTime); err == nil || err.Error() != "capacity" || len(v.seen) != 1024 {
		t.Fatal("capacity", err, len(v.seen))
	}
	v.seen["0"] = testTime - 1
	if err := v.Verify(request(string(body), "bbbbbbbbbbbbbbbb"), body, testTime); err != nil || len(v.seen) != 1024 {
		t.Fatal("cleanup", err, len(v.seen))
	}
}
